如何让@EnableConfigurationProperties的值注入到@Value中
作者:Boom_Man
需求背景
定义了一个@ConfigurationProperties
的配置类,然后在其中定义了一些定时任务的配置,如cron表达式,因为项目会有默认配置,遂配置中有默认值
大体如下:
@Data @Validated @ConfigurationProperties(value = "task") public class TaskConfigProperties { /** * 任务A在每天的0点5分0秒进行执行 */ @NotBlank private String taskA = "0 5 0 * * ? "; }
定时任务配置:
@Scheduled(cron = "${task.task-a}") public void finalCaseReportGenerate(){ log.info("taskA定时任务开始执行"); //具体的任务 log.info("taskA定时任务完成执行"); }
但是如上直接使用是有问题的${task.taskA}
是没有值的,必须要在外部化配置中再写一遍,这样我们相当于默认值就没有用了,这怎么行呢,我们来搞定他。
探究其原理
@ConfigurationProperties
、@Value
、SpringEl
他们之间的关系和区别及我认为的正确使用方式。
首先@ConfigurationProperties
是Spring Boot引入的,遂查询官方文档的讲解
Spring Boot -> Externalized Configuration
我们发现外部化配置中没有值的话,报错是在org.springframework.util.PropertyPlaceholderHelper#parseStringValue
其中org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver
是解析的关键
我们只要把默认值装载到系统中,让org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver#resolvePlaceholder
可以解析到就可以了
遂我们可以把值装载到Environment中
/** * @author wangqimeng * @date 2020/3/4 0:04 */ @Data @Slf4j @Validated @ConfigurationProperties(prefix = "task") public class TaskConfigProperties implements InitializingBean , EnvironmentPostProcessor { /** * 任务A在每天的0点5分0秒进行执行 */ @NotBlank private String taskA = "0 5 0 * * ? "; @Value("${task.task-a}") public String taskAValue; @Autowired private Environment environment; @Override public void afterPropertiesSet() throws Exception { log.info("taskAValue:{}",taskAValue); } @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { log.info("TaskConfigProperties-> postProcessEnvironment 开始执行"); //取到当前配置类上的信息 MutablePropertySources propertySources = environment.getPropertySources(); Properties properties = new Properties(); if (taskA != null) { properties.put("task.task-a", this.taskA); } PropertySource propertySource = new PropertiesPropertySource("task", properties); //即优先级低 propertySources.addLast(propertySource); } }
需要在META-INF -> spring.factories
中配置
org.springframework.boot.env.EnvironmentPostProcessor=\ cn.boommanpro.config.TaskConfigProperties
所以addLast是优先级最低的,让我们新加入的配置优先级最低。
以上就简单的完成了我们的需求。
最终实现
配置类中的有默认值的不需要在External Configuration中再度配置
通过一个注解@EnableBindEnvironmentProperties
,绑定含有@ConfigurationProperties
Class的默认值到Environment
@EnableBindEnvironmentProperties
/** * @author wangqimeng * @date 2020/3/4 1:21 */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EnableBindEnvironmentProperties { Class<?>[] value() default {}; }
@EnableBindEnvironmentPropertiesRegister
import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Properties; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.env.PropertySource; /** * @author wangqimeng * @date 2020/3/4 15:11 */ @Slf4j public class EnableBindEnvironmentPropertiesRegister implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { MutablePropertySources propertySources = environment.getPropertySources(); EnableBindEnvironmentProperties annotation = application.getMainApplicationClass().getAnnotation(EnableBindEnvironmentProperties.class); Arrays.stream(annotation.value()) .forEach(aClass -> registerToEnvironment(propertySources, aClass)); } public void registerToEnvironment(MutablePropertySources propertySources, Class<?> clazz) { ConfigurationProperties annotation = clazz.getAnnotation(ConfigurationProperties.class); if (annotation == null) { return; } String prefix = annotation.prefix(); String name = String.format("%s-%s", prefix, clazz.getName()); try { Properties properties = toProperties(prefix, clazz.newInstance()); PropertySource propertySource = new PropertiesPropertySource(name, properties); propertySources.addLast(propertySource); } catch (Exception e) { log.error("Exception:", e); throw new RuntimeException(); } } public Properties toProperties(String prefix, Object o) throws Exception { Properties properties = new Properties(); Map<String, Object> map = objectToMap(o); map.forEach((s, o1) -> { properties.put(String.format("%s.%s", prefix, camelToUnderline(s)), o1); }); return properties; } public static String camelToUnderline(String param) { if (param == null || "".equals(param.trim())) { return ""; } int len = param.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = param.charAt(i); if (Character.isUpperCase(c)) { sb.append("-"); sb.append(Character.toLowerCase(c)); } else { sb.append(c); } } return sb.toString(); } public static Map<String, Object> objectToMap(Object obj) throws Exception { if (obj == null) { return null; } Map<String, Object> map = new HashMap<>(10); BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass()); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor property : propertyDescriptors) { String key = property.getName(); if (key.compareToIgnoreCase("class") == 0) { continue; } Method getter = property.getReadMethod(); Object value = getter != null ? getter.invoke(obj) : null; if (value == null) { continue; } map.put(key, value); } return map; } }
配置到META-INF/spring.factories
# Application Listeners org.springframework.boot.env.EnvironmentPostProcessor=\ cn.boommanpro.annotation.EnableBindEnvironmentPropertiesRegister
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。