需求背景
定义了一个@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