在設計Spring boot應用程式時,因環境不同需要設定不同的Properties Value,例如資料庫連線Server,spring.profiles.active,或其他自訂變數時,常常不小心在資料後面多了空白而產生錯誤但肉眼難發現,後來想到之前有說明 Spring Boot資料庫存取密碼保護的方法(https://ithelp.ithome.com.tw/articles/10398877),所以處理加密資料的 PropertySecureConvertero 取得Properties Value資料時, 順便將字串資料做Trim處理,避免因資料後面多個空白,而產生錯誤。主要調整 private void process(ConfigurableEnvironment environment) Method。
調整後的PropertySecureConverter如下:
package tw.lewishome.webapp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* ConfigurationPropertySourcesConverter 是一個實現 {@link EnvironmentPostProcessor}
* 和 {@link Ordered} 的類別,
* 用於處理 Spring 應用程序的環境配置屬性源。
*
* <p>
* 此類別的主要功能是遍歷 {@link ConfigurableEnvironment} 中的屬性源,尋找
* {@link org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource}
* 類型的屬性源,
* 並從中提取配置屬性。提取的屬性將被包裝在 {@link MapPropertySource} 中,並插入到環境的屬性源列表中。
* </p>
*
* <p>
* 此類別的工作流程如下:
* </p>
* <ol>
* <li>在
* {@link #postProcessEnvironment(ConfigurableEnvironment, SpringApplication)}
* 方法中調用 {@link #process(ConfigurableEnvironment)} 方法。</li>
* <li>在 {@link #process(ConfigurableEnvironment)} 方法中,遍歷環境中的所有屬性源。</li>
* <li>檢查每個屬性源是否為
* {@link org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource}
* 類型。</li>
* <li>如果找到,則嘗試提取其配置屬性並將其添加為新的 {@link MapPropertySource}。</li>
* </ol>
*
* <p>
* 此類別的優先順序由 {@link #getOrder()} 方法定義,預設為 {@link Ordered#HIGHEST_PRECEDENCE} +
* 50。
* </p>
*/
@Slf4j
public class PropertySecureConverter implements EnvironmentPostProcessor, Ordered {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new PropertySecureConverter instance.
* This is the default constructor, implicitly provided by the compiler
* and can be used to create a new instance of the class.
*
*/
public PropertySecureConverter() {
// Constructor body (can be empty)
}
private static final Logger logger = LoggerFactory.getLogger(PropertySecureConverter.class);
/**
* 處理應用程序的環境設置。
*
* 此方法在應用程序啟動過程中被調用,以便對可配置的環境進行後處理。
*
* @param environment 可配置的環境,包含應用程序的屬性和配置。
* @param application 當前的 Spring 應用程序實例。
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ postProcessEnvironment started ! │");
log.info("└────────────────────────────────────────────────────┘");
try {
process(environment);
} catch (Exception e) {
logger.debug("Failed to process ConfigurationPropertySourcesPropertySource: {}", e.toString());
}
}
/**
* 處理給定的 ConfigurableEnvironment,從中提取配置屬性源並將其添加為 MapPropertySource。
*
* <p>
* 此方法遍歷環境中的所有 PropertySource,尋找特定類型的 PropertySource
* (即 ConfigurationPropertySourcesPropertySource)。對於每個找到的配置屬性源,
* 該方法嘗試提取其底層源並將其轉換為 Map。如果成功提取到的 Map 不為空,
* 則將其作為新的 MapPropertySource 插入到環境中,並在日誌中記錄相關信息。
* </p>
*
* @param environment 要處理的 ConfigurableEnvironment 實例
*/
private void process(ConfigurableEnvironment environment) {
// 找尋所有 PropertySource 來源
for (PropertySource<?> ps : environment.getPropertySources()) {
if (ps == null)
continue;
String className = ps.getClass().getName();
// 檢查是否為 ConfigurationPropertySourcesPropertySource 類型
if ("org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource"
.equals(className)) {
logger.info("Found ConfigurationPropertySourcesPropertySource: {}", ps.getName());
try {
// 嘗試取得配置屬性來源
Object cfgPropSources = tryGetConfigurationPropertySources(ps);
// 如果成功取得,則進行 Looping 處理
if (cfgPropSources != null) {
Iterator<?> it = asIterator(cfgPropSources);
int idx = 0;
while (it != null && it.hasNext()) {
Object cfgSource = it.next();
// 嘗試取得底層來源
Object underlying = tryGetUnderlyingSource(cfgSource);
if (underlying != null) {
// 嘗試從底層來源提取 Map
Map<String, Object> extracted = tryExtractMap(underlying);
if (extracted != null && !extracted.isEmpty()) {
// 建立並插入新的 MapPropertySource
String newName = ps.getName() + "-cfg-extracted-" + idx++;
MapPropertySource mps = new MapPropertySource(newName, extracted);
// Insert before the original source
environment.getPropertySources().addBefore(ps.getName(), mps);
logger.info("Inserted extracted MapPropertySource '{}' with {} entries", newName,
extracted.size());
}
}
}
}
} catch (Throwable t) {
logger.debug("Error while extracting configuration property sources: {}", t.toString());
}
}
}
}
/**
* 將給定的物件轉換為Iterator。
*
* @param iterable 要轉換的物件,可以是任何實現了 Iterable 接口的物件,
* 或者具有 iterator() 方法的物件。
* @return 如果物件為 null,則返回 null;如果物件是 Iterable 的實例,
* 則返回其迭代器;如果物件具有 iterator() 方法,則返回該方法的返回值,
* 否則返回 null。
*/
private Iterator<?> asIterator(Object iterable) {
if (iterable == null)
return null;
if (iterable instanceof Iterable) {
return ((Iterable<?>) iterable).iterator();
}
try {
Method iterator = iterable.getClass().getMethod("iterator");
Object it = iterator.invoke(iterable);
if (it instanceof Iterator)
return (Iterator<?>) it;
} catch (Exception ignored) {
}
return null;
}
/**
* 嘗試獲取配置屬性來源。
*
* 此方法首先檢查給定的 PropertySource 物件是否具有名為
* "getConfigurationPropertySources" 的方法。如果存在,則調用該方法並返回其結果。
* 如果該方法不存在,則嘗試訪問名為 "configurationPropertySources" 的私有字段。
* 如果該字段也不存在,則最後嘗試訪問名為 "sources" 的私有字段。
*
* @param ps 要檢查的 PropertySource 物件
* @return 返回配置屬性來源的對象,如果無法獲取,則返回 null
*/
private Object tryGetConfigurationPropertySources(PropertySource<?> ps) {
try {
Method m = ps.getClass().getMethod("getConfigurationPropertySources");
return m.invoke(ps);
} catch (Exception e) {
try {
Field f = ps.getClass().getDeclaredField("configurationPropertySources");
f.setAccessible(true);
return f.get(ps);
} catch (Exception ex) {
try {
Field f2 = ps.getClass().getDeclaredField("sources");
f2.setAccessible(true);
return f2.get(ps);
} catch (Exception ignored) {
}
}
}
return null;
}
/**
* 嘗試從給定的配置來源中獲取底層來源。
*
* @param cfgSource 要檢查的配置來源對象,可能為 null。
* @return 如果找到底層來源,則返回該來源;否則返回 null。
*
* 此方法將檢查配置來源對象的多個方法和字段,以獲取其底層來源。
* 它首先嘗試調用以下方法:getUnderlyingSource、getSource、getMap 和 getProperties。
* 如果這些方法都不存在或返回 null,則將檢查名為 source 和 underlyingSource 的字段。
*
* 如果在任何步驟中找到非 null 的值,則立即返回該值。
* 如果所有檢查都未找到有效的來源,則返回 null。
*/
private Object tryGetUnderlyingSource(Object cfgSource) {
if (cfgSource == null)
return null;
try {
for (String mName : new String[] { "getUnderlyingSource", "getSource", "getMap", "getProperties" }) {
try {
Method m = cfgSource.getClass().getMethod(mName);
Object val = m.invoke(cfgSource);
if (val != null)
return val;
} catch (NoSuchMethodException ignored) {
}
}
for (String fName : new String[] { "source", "underlyingSource" }) {
try {
Field f = cfgSource.getClass().getDeclaredField(fName);
f.setAccessible(true);
Object val = f.get(cfgSource);
if (val != null)
return val;
} catch (NoSuchFieldException ignored) {
}
}
} catch (Exception ignored) {
}
return null;
}
/**
* 嘗試從給定的物件中提取屬性映射。
*
* <p>
* 此方法接受一個物件,並根據其類型提取屬性名稱和值,返回一個包含這些屬性的映射。如果物件為 {@code null},則返回 {@code null}。
* </p>
*
* <p>
* 支持的物件類型包括:
* </p>
* <ul>
* <li>{@link Map}:直接將其內容複製到返回的映射中。</li>
* <li>{@link PropertySource}:如果是
* {@link org.springframework.core.env.EnumerablePropertySource},則提取其所有屬性名稱和值。</li>
* <li>其他類型:通過反射查找 {@code getPropertyNames} 和 {@code getProperty} 方法來提取屬性。</li>
* </ul>
*
* @param underlying 要提取屬性的物件
* @return 包含屬性名稱和值的映射,如果未能提取任何屬性則返回 {@code null}
*/
@SuppressWarnings("unchecked")
private Map<String, Object> tryExtractMap(Object underlying) {
// 如果底層來源為 null,則返回 null
if (underlying == null)
return null;
Map<String, Object> map = new HashMap<>();
try {
// 檢查底層來源的類型並提取屬性
if (underlying instanceof Map) {
// 處理 Map 類型
Map<String, Object> u = (Map<String, Object>) underlying;
map.putAll(u);
return map;
}
// 嘗試處理 PropertySource 類型
if (underlying instanceof PropertySource) {
PropertySource<?> ps = (PropertySource<?>) underlying;
if (ps instanceof org.springframework.core.env.EnumerablePropertySource) {
// 處理 EnumerablePropertySource 類型
org.springframework.core.env.EnumerablePropertySource<?> eps = (org.springframework.core.env.EnumerablePropertySource<?>) ps;
for (String name : eps.getPropertyNames()) {
map.put(name, eps.getProperty(name));
}
return map;
}
}
// 嘗試通過反射調用 getPropertyNames 和 getProperty 方法
try {
Method m = underlying.getClass().getMethod("getPropertyNames");
Object names = m.invoke(underlying);
if (names instanceof String[]) {
// 處理屬性名稱陣列
for (String n : (String[]) names) {
try {
Method gm = underlying.getClass().getMethod("getProperty", String.class);
Object v = gm.invoke(underlying, n);
map.put(n, v);
} catch (Exception ignored) {
}
}
return map;
}
} catch (Exception ignored) {
}
} catch (Exception ignored) {
ignored.printStackTrace();
}
// 如果無法提取任何屬性,則返回 null ,否則返回提取的映射
return map.isEmpty() ? null : map;
}
/**
* 取得此屬性來源的優先順序。
*
* <p>
* 回傳的優先順序值為 {@link Ordered#HIGHEST_PRECEDENCE} + 50,
* 表示此屬性來源在較高優先順序位置被載入。
* 數值越小,優先順序越高。
* </p>
*
* @return 優先順序值,為 {@link Ordered#HIGHEST_PRECEDENCE} + 50
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 50;
}
}