相信大家都遇到过一些场景,需要在项目内对某些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
接口,以便可以发现源包含的所有名称的配置。
通过使用以下代码 IterableMapConfigurationPropertySource
实现,项目内重构源用到这种方式,很容易上手。
2、Relaxed Binding的具体实现
springboot 1.5和2.0中,属性与配置值的绑定逻辑都始于ConfigurationPropertiesBindingPostProcessor类的postProcessBeforeInitialization函数。
其中1.5版本细看源码发现,postProcessBeforeInitialization函数执行时,属性值绑定的工作被委派给了PropertiesConfigurationFactory
而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 SetSIGNATURE_PARAMETER_SET = new HashSet (); 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的使用
解析头信息
// 筛选头信息 MapheaderMap = 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{ private final PropertiesConfigurationFactory factory; public RelaxedConfigurationBinder(T object) { this(new PropertiesConfigurationFactory<>(object)); } public RelaxedConfigurationBinder(Class type) { this(new PropertiesConfigurationFactory<>(type)); } public static RelaxedConfigurationBinder with(T object) { return new RelaxedConfigurationBinder<>(object); } public static RelaxedConfigurationBinder with(Class type) { return new RelaxedConfigurationBinder<>(type); } public RelaxedConfigurationBinder(PropertiesConfigurationFactory 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 setTargetName(String targetName) { factory.setTargetName(targetName); return this; } public RelaxedConfigurationBinder setPropertySources(PropertySource>... propertySources) { MutablePropertySources sources = new MutablePropertySources(); for (PropertySource> propertySource : propertySources) { sources.addLast(propertySource); } factory.setPropertySources(sources); return this; } public RelaxedConfigurationBinder setPropertySources(Environment environment) { factory.setPropertySources(((ConfigurableEnvironment) environment).getPropertySources()); return this; } public RelaxedConfigurationBinder setPropertySources(PropertySources propertySources) { factory.setPropertySources(propertySources); return this; } public RelaxedConfigurationBinder setConversionService(ConversionService conversionService) { factory.setConversionService(conversionService); return this; } public RelaxedConfigurationBinder setValidator(Validator validator) { factory.setValidator(validator); return this; } public RelaxedConfigurationBinder 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的使用
解析头信息
// 筛选头信息 MapheaderMap = 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可自动注入或上下文直接获取 Iterablesources = 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() { MapdataMap = new HashMap (); 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