iT邦幫忙

0

應用系統建置前準備工具 - SystemEnvReader 系統環境讀取工具

  • 分享至 

  • xImage
  •  

概述

SystemEnvReader 是一個 Spring @Component,用來統一從 Spring Environment 取得系統層級與組態屬性值,並可列出所有可枚舉的屬性(包含 environment / property sources 中的 key/value)。此工具常用於診斷、匯出環境資訊或在程式中以統一方式讀取設定值。

此類別的設計重點:

  • Environment 為來源(支援 profiles、application.properties、系統環境變數、JVM 參數等)。
  • 提供單一點查詢:getProperty(key) / getProperty(key, defaultValue)
  • 可列出所有可枚舉屬性(getAllEnvironmentVariables()),方便除錯或匯出。
  • getProperty(String) 使用 Spring Cache (@Cacheable) 加速查詢(預設使用名為 catchSysEnvVariables 的快取與 redisCacheManager)。

相依與前提

  • Spring Framework / Spring Boot
  • org.apache.commons:commons-lang3(使用 StringUtils
  • 若要使 @Cacheable 起作用,需在專案中設定對應的 Cache Manager(此類別使用 redisCacheManager 名稱)。

主要功能

  1. getProperty(String key)

    • 回傳指定 key 的屬性值(等同呼叫 getProperty(key, null))。
    • 此方法有 @Cacheable,查詢結果會快取,cache 名稱為 catchSysEnvVariables
  2. getProperty(String key, String defaultValue)

    • 嘗試從 Environment 取得屬性,若值為空或發生例外則回傳 defaultValue
    • 會對取得的字串做 trim() 與空白判斷(使用 StringUtils.isNotBlank)。
  3. getAllEnvironmentVariables()

    • 取得目前 Environment 中可列舉的所有屬性名稱與值,回傳 Map<String,String>
    • keys 會被轉為大寫並移除空白與雙引號(便於輸出與比較),並加上一個 ACTIVE.PROFILES 的條目顯示當前啟用的 profile。
    • 會遍歷 AbstractEnvironment.getPropertySources() 中的 EnumerablePropertySource,彙整各個 property source 的屬性名稱。

使用範例

基本注入(Spring)

@Autowired
private SystemEnvReader sysEnvReader;

String value = sysEnvReader.getProperty("server.port");
String valueWithDefault = sysEnvReader.getProperty("custom.key", "defaultVal");

列出所有環境屬性(用於診斷)

Map<String,String> all = sysEnvReader.getAllEnvironmentVariables();
all.forEach((k,v) -> System.out.println(k + " = " + v));

注意:若在容器外(或非 Spring 管理的情況)使用,需先有 Environment 的 bean 可注入,否則此類別需透過手動建立或以 Environment 參數呼叫其他靜態方法封裝。

設計與實作要點

  • @Cacheable(cacheNames = "catchSysEnvVariables", key = "#keyValue", cacheManager = "redisCacheManager")

    • 表示 getProperty(String) 的結果會被快取在名為 catchSysEnvVariables 的快取空間下,快取管理器名稱為 redisCacheManager。使用此功能前請確認專案快取設定已正確配置。
  • 屬性名稱標準化

    • getAllEnvironmentVariables() 會把屬性名稱轉為大寫,並移除空白與雙引號,這讓輸出更容易比對(但會失去原始大小寫資訊)。
  • 點評:列出全部屬性會暴露許多設定(例如密鑰或機密資訊),在生產環境輸出或儲存時請務必避免將敏感值(如密碼、私鑰)明文外洩。

範例單元測試(建議)

下面是可放在 src/test/java 的示意單元測試範例,使用 Spring Boot Test 與 Mockito,並針對行為進行驗證。

  1. 測試 getProperty(key, default) 在存在與不存在情況
@SpringBootTest
class SystemEnvReaderTest {
    @Autowired
    private SystemEnvReader sysEnvReader;

    @Test
    void testGetPropertyWithDefault() {
        String exists = sysEnvReader.getProperty("spring.application.name", "no-app");
        assertNotNull(exists);

        String notExists = sysEnvReader.getProperty("non.existing.key", "fallback");
        assertEquals("fallback", notExists);
    }
}
  1. 測試 getAllEnvironmentVariables() 能夠回傳包含 ACTIVE.PROFILES
@SpringBootTest
class SystemEnvReaderAllTest {
    @Autowired
    private SystemEnvReader sysEnvReader;

    @Test
    void testGetAllEnvironmentVariables() {
        Map<String,String> map = sysEnvReader.getAllEnvironmentVariables();
        assertTrue(map.containsKey("ACTIVE.PROFILES"));
        // 指定環境中可以再檢查其它 known key
    }
}
  1. 建議在測試中關閉快取或使用測試專用的 cache manager,避免快取影響測試結果。

重要注意事項

  • 環境變數命名差異:在作業系統環境變數中常以 _ 代替 .(例如 SPRING_PROFILES_ACTIVE),在某些情況下 Spring 會做對應,使用者應酌量處理。
  • 不要在日誌或公開輸出中直接列印敏感設定(如密碼、憑證),getAllEnvironmentVariables() 回傳的 map 可能包含此類值。
  • 若啟用了 @Cacheable,請確認 cache 設定(例如 Redis)與快取失效策略(TTL)符合需求,以避免舊值殘留。

程式碼 SystemEnvReader.java

package tw.lewishome.webapp.base.utility.common;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.StreamSupport;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component;

/**
 * 讀取順序由上而下依序為,。
 * //
 * https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config
 * 注意: 若使用 系統環境變數 ,則將 "." 改用 "_" 取代 。例如: set SPRING_PROFILES_ACTIVE=production
 * <ul>
 * <li>Java 命令列參數:最優先,例如使用 --server.port=9090。</li>
 * <li>JVM 系統屬性:透過 System.getProperties() 擷取的系統參數,例如使用 -Dserver.port=9090。</li>
 * <li>JVM 系統屬性兩個重複 : 後面的值會覆蓋前面的值。
 * <li>若在容器中運行,容器的環境變數設定(例如,在 $CATALINA_BASE/conf/setenv.sh 中設定),因為它是 JVM
 * 啟動時的環境變數。
 * <li>作業系統環境變數:環境變數,例如設定 SERVER_PORT=9090</li>
 * <li>application.properties 的屬性:例如來自 my.secret=${random.value}</li>
 * <li>application.properties 檔案中的屬性:這是預設值</li>
 * <li>application-xxx.properties 檔案:根據 Spring profiles 載入的設定檔</li>
 * <li>命令行參數(java --spring.profile.active=profile -jar app.jar)</li>
 * <li>測試程式中的properties @TestPropertySource</li>
 * <li>Devtools啟用時在$HOME/.config/spring-boot的全域設定</li>
 * </ul>
 */

@Component
public class SystemEnvReader {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * 
     * Constructs a new SystemEnvironmentReader instance.
     * This is the default constructor, implicitly provided by the compiler
     */
    public SystemEnvReader() {
        // Constructor body (can be empty)
    }

    @Autowired
    private Environment environment;

    /**
     * getProperty.
     * 
     * @param keyValue  keyValue
     * @return a String object
     */
    @Cacheable(cacheNames = "catchSysEnvVariables", key = "#keyValue", cacheManager = "redisCacheManager")
    public String getProperty(String keyValue) {
        return getProperty(keyValue, null);
    }

    /**
     * getProperty.
     * 
     * @param key          key
     * @param defaultValue defaultValue
     * @return a String object
     */ 
    @SuppressWarnings("null")
    public String getProperty(String key, String defaultValue) {
        try {
            String propertyValue = environment.getProperty(key);
            if (StringUtils.isNotBlank(propertyValue)) {
                return propertyValue.trim();
            }
            return defaultValue;
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * getAllEnvironmentVariables.
     * 
     * @return a Map object
     */
    public Map<String, String> getAllEnvironmentVariables() {
        Map<String, String> mapAllEnvironmentVariables = new HashMap<>();

        // System.out.println("===========================================");
        // System.out.println("Active profiles: " +
        // Arrays.toString(environment.getActiveProfiles()));
        mapAllEnvironmentVariables.put("active.profiles".trim().toUpperCase(),
                Arrays.toString(environment.getActiveProfiles()));

        MutablePropertySources sources = ((AbstractEnvironment) environment).getPropertySources();
        StreamSupport.stream(sources.spliterator(), false).filter(ps -> ps instanceof EnumerablePropertySource)
                .map(ps -> ((EnumerablePropertySource<?>) ps).getPropertyNames()).flatMap(Arrays::stream).distinct()
                .forEach(prop -> {
                    String propKey = prop.trim().toUpperCase().replaceAll("\"", "").replaceAll("\\s", "");
                    String propValue = environment.getProperty(prop);
                    if (propValue != null) {
                        propValue = propValue.trim().replaceAll("\"", "");
                    }
                    mapAllEnvironmentVariables.put(propKey, propValue);
                    // System.out.println(propKey + ": " + propValue);
                });
        // System.out.println("===========================================");
        return mapAllEnvironmentVariables;
    }

}

單元測試程式碼 SystemEnvReader.java

package tw.lewishome.webapp.base.utility.common;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;

public class SystemEnvReaderTest {

    private void injectEnvironment(SystemEnvReader reader, AbstractEnvironment env) throws Exception {
        Field f = SystemEnvReader.class.getDeclaredField("environment");
        f.setAccessible(true);
        f.set(reader, env);
    }

    @Test
    public void testGetPropertyReturnsTrimmedValue() throws Exception {
        AbstractEnvironment env = mock(AbstractEnvironment.class);
        when(env.getProperty("my.key")).thenReturn("  trimmedValue  ");

        SystemEnvReader reader = new SystemEnvReader();
        injectEnvironment(reader, env);

        String result = reader.getProperty("my.key", "default");
        Assertions.assertEquals("trimmedValue", result);
    }

    @Test
    public void testGetPropertyReturnsDefaultForBlankAndOnException() throws Exception {
        AbstractEnvironment env = mock(AbstractEnvironment.class);
        when(env.getProperty("blank.key")).thenReturn("   ");
        when(env.getProperty("null.key")).thenReturn(null);
        when(env.getProperty("throw.key")).thenThrow(new RuntimeException("boom"));

        SystemEnvReader reader = new SystemEnvReader();
        injectEnvironment(reader, env);

        Assertions.assertEquals("def", reader.getProperty("blank.key", "def"));
        Assertions.assertEquals("def", reader.getProperty("null.key", "def"));
        Assertions.assertEquals("def", reader.getProperty("throw.key", "def"));
    }

    @SuppressWarnings("null")
    @Test
    public void testGetAllEnvironmentVariablesCollectsAndNormalizes() throws Exception {
        // prepare environment and property sources
        AbstractEnvironment env = mock(AbstractEnvironment.class);
        Map<String, Object> props = new HashMap<>();
        props.put("prop.one", " value ");
        props.put("prop two", " \"quoted\" ");
        props.put("null.prop", null);

        MapPropertySource mps = new MapPropertySource("testSource", props);
        MutablePropertySources sources = new MutablePropertySources();
        sources.addLast(mps);

        when(env.getActiveProfiles()).thenReturn(new String[] { "prod" });
        when(env.getPropertySources()).thenReturn(sources);
        when(env.getProperty(anyString())).thenAnswer(invocation -> props.get(invocation.getArgument(0)));

        SystemEnvReader reader = new SystemEnvReader();
        injectEnvironment(reader, env);

        Map<String, String> result = reader.getAllEnvironmentVariables();

        // active profiles entry
        Assertions.assertEquals(Arrays.toString(new String[] { "prod" }), result.get("ACTIVE.PROFILES"));

        // prop.one -> key becomes PROP.ONE, value trimmed
        Assertions.assertEquals("value", result.get("PROP.ONE"));

        // prop two -> key becomes PROPTWO (spaces removed), value quotes removed and trimmed
        Assertions.assertEquals("quoted", result.get("PROPTWO"));

        // null.prop -> key becomes NULL.PROP with null value
        Assertions.assertTrue(result.containsKey("NULL.PROP"));
        Assertions.assertNull(result.get("NULL.PROP"));
    }
}

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言