Springboot1.x和2.x 通過@ConfigurationProperties對bean刷新自定義屬性的實現和使用差別


  相信大家都遇到過一些場景,需要在項目內對某些bean自定義屬性值進行刷新,這里我們用到的propertySource源數據可能並不是來自於外部,而是某段程序運行的中間過程產生的結果集。諸如此類的場景,比如可能是某些項目啟動后的數據預處理,簽名請求字段數據的預處理等,這些場景的共同點是屬性值比較固定,為了減少不必要的硬代碼,所以想到了用@ConfigurationProperties實現對bean刷新自定義屬性。

  另springboot從外部加載配置信息,外部可以是屬性文件、yaml文件、環境變量以及平台化的配置中心等,此類的加載方式有很多,這里不過多贅述,有疑問可以參考這篇文章:https://www.cnblogs.com/onlymate/p/10110642.html

踩坑

  回到話題,項目內bean自定義屬性刷新的實現,由於springboot 1.x和2.x版本使用上差異還是比較大的,所以踩了一個框架版本升級帶來的坑。本來1.x跑的很溜代碼在2.x里各種爆紅,究其原因是在springboot 2.x里很多包已不再使用,其中就有org.springframework.boot.bind包,而我正好用到該包下的PropertiesConfigurationFactory,所以不爆紅才怪。

一點理解

  對於springboot 1.x和2.x配置綁定部分源碼原理上的理解,簡單提一下。springboot自發布以來就提供@ConfigurationProperties注解操作配置類進行寬松綁定(Relaxed Binding),有趣的是兩個大版本中Relaxed Binding的具體實現是不一樣的,看過部分文檔后覺得springboot 2.0是想為使用者提供更嚴謹的API,所以重新設計了綁定發生的方式。2.0為我們添加了幾個新的抽象,並且開發了一個全新的綁定API,而部分舊包舊代碼不再使用。主要以下幾點

1、PropertySources和ConfigurationPropertySources

  對於PropertySource你一定不陌生,結合接口Environment,這個接口是一個PropertyResolver,它可以讓你從一些底層的PropertySource實現中解析屬性。Spring框架為常見的配置提供PropertySource實現,例如系統屬性,命令行標志和屬性文件。 Spring Boot 會以對大多數應用程序有意義的方式自動配置這些實現(例如,加載application.properties)。

  在Spring Boot 2.0不再直接使用現有的PropertySource接口進行綁定,而是引入了一個新的ConfigurationPropertySource接口。同時提供了一個合理的方式來實施放松綁定規則,這些規則以前是活頁夾的一部分。該接口的主要API非常簡單: ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);還有一個IterableConfigurationPropertySource變相的實現了Iterable接口,以便可以發現源包含的所有名稱的配置。

通過使用以下代碼 Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment); 可以獲取外部源數據;或者根據需要,還提供一個簡單的MapConfigurationPropertySource實現,項目內重構源用到這種方式,很容易上手。

2、Relaxed Binding的具體實現

   springboot 1.5和2.0中,屬性與配置值的綁定邏輯都始於ConfigurationPropertiesBindingPostProcessor類的postProcessBeforeInitialization函數。

  其中1.5版本細看源碼發現,postProcessBeforeInitialization函數執行時,屬性值綁定的工作被委派給了PropertiesConfigurationFactory<T>類(這哥們是我們在2.0壓根找不到的貨,所以其之下細節不展開講了);

  而2.0版本postProcessBeforeInitialization函數調用時,屬性值綁定的工作則被委派給了ConfigurationPropertiesBinder類,調用了bind函數,但ConfigurationPropertiesBinder類並不是一個public類,實際上它只相當於ConfigurationPropertiesBindingPostProcessor的一個內部靜態類,表面上負責處理@ConfigurationProperties注解的綁定任務。從源碼中可以看出,具體的工作委派給了另一個Binder類的對象。Binder類是SpringBoot 2.0版本后加入的類,它是負責處理對象與多個ConfigurationPropertySource之間的綁定的執行者,后面的代碼示例中我們會見到。

至此基本springboot 1.x和2.x版本在屬性配置綁定上的差異簡單說明了個七七八八,后面我們開始從使用上開始填坑:

場景:簽名請求,服務端需要解析header信息中的簽名字段的過程。此類字段的key一定是服務端事先定義好的,解析過程需要反復使用的。

簽名頭信息類:

@Data
@ToString
@ConfigurationProperties(prefix="openapi.validate")
public class SignatureHeaders {
    private static final String SIGNATURE_HEADERS_PREFIX = "openapi-validate-";
    
    public static final Set<String> SIGNATURE_PARAMETER_SET = new HashSet<String>();
    private static String HEADER_APPID = SIGNATURE_HEADERS_PREFIX + "appid";
    private static String HEADER_TIMESTAMP = SIGNATURE_HEADERS_PREFIX + "timestamp";
    private static String HEADER_NONCE = SIGNATURE_HEADERS_PREFIX + "nonce";
    private static String HEADER_SIGNATURE = SIGNATURE_HEADERS_PREFIX + "signature";
    
    
    static {
        SIGNATURE_PARAMETER_SET.add(HEADER_APPID);
        SIGNATURE_PARAMETER_SET.add(HEADER_TIMESTAMP);
        SIGNATURE_PARAMETER_SET.add(HEADER_NONCE);
        SIGNATURE_PARAMETER_SET.add(HEADER_SIGNATURE);
    }
    
    /** 分配appid */
    private String appid;
    /** 分配appsecret */
    private String appsecret;
    /** 時間戳:ms */
    private String timestamp;
    /** 流水號/隨機串:至少16位,有效期內防重復提交 */
    private String nonce;
    /** 簽名 */
    private String signature;
    
    
}

 

一、1.x的使用

解析頭信息

// 篩選頭信息
Map<String, Object> headerMap = Collections.list(request.getHeaderNames())
                .stream()
                .filter(headerName -> SignatureHeaders.HEADER_NAME_SET.contains(headerName))
                .collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), headerName -> request.getHeader(headerName)));
PropertySource propertySource = new MapPropertySource("signatureHeaders", headerMap);
SignatureHeaders signatureHeaders = RelaxedConfigurationBinder.with(SignatureHeaders.class).setPropertySources(propertySource).doBind();

綁定輔助類

public class RelaxedConfigurationBinder<T> {
    private final PropertiesConfigurationFactory<T> factory;

    public RelaxedConfigurationBinder(T object) {
        this(new PropertiesConfigurationFactory<>(object));
    }

    public RelaxedConfigurationBinder(Class<T> type) {
        this(new PropertiesConfigurationFactory<>(type));
    }

    public static <T> RelaxedConfigurationBinder<T> with(T object) {
        return new RelaxedConfigurationBinder<>(object);
    }

    public static <T> RelaxedConfigurationBinder<T> with(Class<T> type) {
        return new RelaxedConfigurationBinder<>(type);
    }

    public RelaxedConfigurationBinder(PropertiesConfigurationFactory<T> factory) {
        this.factory = factory;
        ConfigurationProperties properties = getMergedAnnotation(factory.getObjectType(), ConfigurationProperties.class);
        javax.validation.Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        factory.setValidator(new SpringValidatorAdapter(validator));
        factory.setConversionService(new DefaultConversionService());
        if (null != properties) {
            factory.setIgnoreNestedProperties(properties.ignoreNestedProperties());
            factory.setIgnoreInvalidFields(properties.ignoreInvalidFields());
            factory.setIgnoreUnknownFields(properties.ignoreUnknownFields());
            factory.setTargetName(properties.prefix());
            factory.setExceptionIfInvalid(properties.exceptionIfInvalid());
        }
    }

    public RelaxedConfigurationBinder<T> setTargetName(String targetName) {
        factory.setTargetName(targetName);
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(PropertySource<?>... propertySources) {
        MutablePropertySources sources = new MutablePropertySources();
        for (PropertySource<?> propertySource : propertySources) {
            sources.addLast(propertySource);
        }
        factory.setPropertySources(sources);
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(Environment environment) {
        factory.setPropertySources(((ConfigurableEnvironment) environment).getPropertySources());
        return this;
    }

    public RelaxedConfigurationBinder<T> setPropertySources(PropertySources propertySources) {
        factory.setPropertySources(propertySources);
        return this;
    }

    public RelaxedConfigurationBinder<T> setConversionService(ConversionService conversionService) {
        factory.setConversionService(conversionService);
        return this;
    }

    public RelaxedConfigurationBinder<T> setValidator(Validator validator) {
        factory.setValidator(validator);
        return this;
    }

    public RelaxedConfigurationBinder<T> setResolvePlaceholders(boolean resolvePlaceholders) {
        factory.setResolvePlaceholders(resolvePlaceholders);
        return this;
    }

    public T doBind() throws GeneralException {
        try {
            return factory.getObject();
        } catch (Exception ex) {
            throw new GeneralException("配置綁定失敗!", ex);
        }
    }
}

坑點前面提到了,在輔助類中需要用到PropertiesConfigurationFactory來指定configurationPropertySource等設置、完成綁定動作等,而PropertiesConfigurationFactory在2.x中是不存在的。

二、2.x的使用

解析頭信息

 // 篩選頭信息
  Map<String, Object> headerMap = Collections.list(request.getHeaderNames())
                .stream()
                .filter(headerName -> SignatureHeaders.SIGNATURE_PARAMETER_SET.contains(headerName))
                .collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), headerName -> request.getHeader(headerName)));
  // 自定義ConfigurationProperty源信息
  ConfigurationPropertySource sources = new MapConfigurationPropertySource(headerMap);
  // 創建Binder綁定類
  Binder binder = new Binder(sources);
  // 綁定屬性
  SignatureHeaders signatureHeaders = binder.bind("openapi.validate", Bindable.of(SignatureHeaders.class)).get();

2.x的使用拋開了構建屬性配置工廠,我們自己通過MapConfigurationPropertySource實現了自定義屬性配置源,然后直接通過新加的綁定類Binder加載源信息,做識別后直接綁定到bean屬性,從代碼實現上看省去大量初始化代碼。

2.x加載外部屬性配置實現:

// 讀取自配置文件/配置中心 // environment可自動注入或上下文直接獲取
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);// 設置Binder
Binder binder = new Binder(sources);
// 屬性綁定
SignatureHeaders signatureHeaders = binder.bind("openapi.validate", Bindable.of(SignatureHeaders.class)).get();

 

demo示例:將自定義Map的配置屬性數據加載到頭信息類中去

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SignatureApp.class)
@Slf4j
public class ConfigurationPropertyTest {
    
    @Test
    public void testConfigurationPropertySources() {
        Map<String, Object> dataMap = new HashMap<String, Object>();
        dataMap.put("openapi.validate.appid", "123456789");
        dataMap.put("openapi.validate.timestamp", "1565062140111");
        dataMap.put("openapi.validate.nonce", "20190805180100102030");
        dataMap.put("openapi.validate.signature", "vDMbihw6uaxlhoBCBJAY9xnejJXNCAA0QCc+I5X9EYYwAdccjNSB4L4mPZXymbH+fwm3ulkuY7UBNZclV1OBoELCSUMn7VRLAVqBS4bKrTA=");
        
        ConfigurationPropertySource sources = new MapConfigurationPropertySource(dataMap);
        Binder binder = new Binder(sources);
        SignatureHeaders signatureHeaders = binder.bind("openapi.validate", Bindable.of(SignatureHeaders.class)).get(); 
        
        log.info("###Parse Result: {}", signatureHeaders);
    }
}

 

以上主要是我在使用過程遇到問題的剖析,其實還是框架版本差異造成的影響,這也再次告訴我們不能老思維處理新問題,老代碼不是一拷就能用的。另外介於篇幅問題,很多比較基礎的知識點、使用方法,沒有過多鋪開的講,不然就不是這個篇幅了,后面有時間會一點點完善。ps:閑話比較多,因為自己也是看着別人博文一步一步蹚過來的,那種沒有任何描述,上來就是一通代碼,還沒頭沒尾的技術博文,真的很讓人頭疼。

 

附錄:1.x和2.x原理源碼分析 Spring Boot中Relaxed Binding機制的不同實現 - 簡書  https://www.jianshu.com/p/a1fbfc4f9e12


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM