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)。org.apache.commons:commons-lang3(使用 StringUtils)@Cacheable 起作用,需在專案中設定對應的 Cache Manager(此類別使用 redisCacheManager 名稱)。getProperty(String key)
getProperty(key, null))。@Cacheable,查詢結果會快取,cache 名稱為 catchSysEnvVariables。getProperty(String key, String defaultValue)
Environment 取得屬性,若值為空或發生例外則回傳 defaultValue。trim() 與空白判斷(使用 StringUtils.isNotBlank)。getAllEnvironmentVariables()
Environment 中可列舉的所有屬性名稱與值,回傳 Map<String,String>。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,並針對行為進行驗證。
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);
}
}
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
}
}
_ 代替 .(例如 SPRING_PROFILES_ACTIVE),在某些情況下 Spring 會做對應,使用者應酌量處理。getAllEnvironmentVariables() 回傳的 map 可能包含此類值。@Cacheable,請確認 cache 設定(例如 Redis)與快取失效策略(TTL)符合需求,以避免舊值殘留。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;
}
}
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"));
}
}