让@EnableConfigurationProperties的值注入到@Value中

本文探讨了在SpringBoot中如何正确使用@ConfigurationProperties为定时任务设置默认配置,并避免在外部化配置中重复声明。通过实现EnvironmentPostProcessor接口,将默认值注入到Environment中,确保定时任务能读取到默认配置。

需求背景

定义了一个@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@ValueSpringEl 他们之间的关系和区别及我认为的正确使用方式。

首先@ConfigurationProperties 是Spring Boot引入的,遂查询官方文档的讲解

Spring Boot -> Externalized Configuration

  1. 我们发现外部化配置中没有值的话,报错是在
    org.springframework.util.PropertyPlaceholderHelper#parseStringValue
  2. 其中org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver是解析的关键
  3. 我们只要把默认值装载到系统中,让org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver#resolvePlaceholder可以解析到就可以了
  4. 遂我们可以把
    值装载到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是优先级最低的,让我们新加入的配置优先级最低。

以上就简单的完成了我们的需求。

最终实现

  1. 配置类中的有默认值的不需要在External Configuration中再度配置
  2. 通过一个注解@EnableBindEnvironmentProperties,绑定含有@ConfigurationPropertiesClass的默认值到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
<think> 我们正在讨论Spring Boot中的注解`@EnableConfigurationProperties`。这个注解用于启用对带有`@ConfigurationProperties`注解的类的支持,并将它们注册为Spring容器中的Bean。这样,我们就可以将外部配置(如application.properties或application.yml)绑定到这些Java对象上。 ### 作用详解 首先,`@EnableConfigurationProperties`的主要作用包括: 1. **启用配置属性绑定**:开启对`@ConfigurationProperties`注解类的处理。 2. **注册配置属性Bean**:将标注了`@ConfigurationProperties`的类注册为Spring应用上下文中的Bean。 3. **验证支持**:结合JSR-303验证注解(如`@NotNull`、`@Size`等),可以在绑定配置时进行校验。 4. **宽松绑定**:支持属性名的宽松绑定(例如,配置文件中使用`my-property`,而Java字段可以使用`myProperty`、`my_property`或`myproperty`)。 ### 使用方式 通常,我们在配置类上使用`@EnableConfigurationProperties`,并指定一个或多个需要启用的配置属性类。例如: ```java @Configuration @EnableConfigurationProperties(MyAppProperties.class) public class MyAppConfig { // 其他配置 } ``` 这里,`MyAppProperties`类应该被`@ConfigurationProperties`注解标记: ```java @ConfigurationProperties(prefix = "myapp") public class MyAppProperties { private String name; private int version; // 省略getter和setter } ``` 然后,在`application.properties`文件中,我们可以这样配置: ```properties myapp.name=MyApplication myapp.version=1 ``` ### 内部工作原理 - 当使用`@EnableConfigurationProperties`时,Spring Boot会通过`ConfigurationPropertiesBindingPostProcessor`和`ConfigurationPropertiesBeanRegistrar`等内部类来处理配置属性的绑定和注册。 - 它实际上是一个组合注解,包含了`@Import(EnableConfigurationPropertiesRegistrar.class)`,该导入器负责注册配置属性Bean。 ### 替代方案 在Spring Boot应用程序中,如果我们使用了`@SpringBootApplication`,它已经包含了`@EnableConfigurationProperties`,因此我们不需要显式添加。但是,如果我们需要在特定的配置类中启用某些配置属性类,或者我们的配置类不在主应用上下文中,那么显式使用`@EnableConfigurationProperties`就很有必要。 ### 代码示例 下面是一个完整的示例: **属性类:** ```java import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "myapp") public class MyAppProperties { private String name; private int version; // getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } } ``` **配置类(可选,如果使用@Component则无需此配置类):** ```java import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties(MyAppProperties.class) public class AppConfig { } ``` **使用配置属性:** ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { private final MyAppProperties properties; @Autowired public MyController(MyAppProperties properties) { this.properties = properties; } @GetMapping("/info") public String getAppInfo() { return "App Name: " + properties.getName() + ", Version: " + properties.getVersion(); } } ``` ### 注意事项 - 在Spring Boot 2.2之前,配置属性类必须使用`@Component`或通过`@EnableConfigurationProperties`注册。从2.2开始,只要类被`@ConfigurationProperties`注解,并且被Spring扫描到(即位于`@SpringBootApplication`扫描路径下),就会被自动注册为Bean,无需显式使用`@EnableConfigurationProperties`或`@Component`。但是,如果配置属性类不在组件扫描路径下,则仍需要通过`@EnableConfigurationProperties`来注册。 ### 总结 `@EnableConfigurationProperties`注解用于显式启用和注册配置属性类,特别是在以下场景: - 需要将特定的配置属性类注册为Bean。 - 配置属性类不在组件扫描路径下。 - 在非主配置类中需要启用配置属性绑定。 §§相关问题§§: 1. `@ConfigurationProperties`和`@Value`注解有什么区别? 2. 如何在Spring Boot中实现配置属性的验证? 3. 什么是宽松绑定(Relaxed Binding)? 4. 如何从自定义的properties文件加载配置,而不是默认的application.properties? </think> `@EnableConfigurationProperties` 是 Spring Boot 中的一个核心注解,用于启用配置属性绑定功能。以下是详细解析: ### 作用本质 该注解主要实现两大核心功能: 1. **注册配置类**:将标注 `@ConfigurationProperties` 的类注册为 Spring Bean 2. **启用绑定机制**:激活外部配置(如 application.properties)到 Java 对象的自动绑定 ### 典型使用场景 ```java @SpringBootApplication @EnableConfigurationProperties(ServerConfig.class) // 启用特定配置类 public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } } // 配置属性类 @ConfigurationProperties(prefix = "server") public class ServerConfig { private String ip; private int port; private String contextPath; // getter/setter省略 } ``` ### 工作原理详解 1. **配置处理注册** - 自动注册 `ConfigurationPropertiesBindingPostProcessor` 后处理器 - 触发所有 `@ConfigurationProperties` 注解类的处理 2. **属性绑定流程** ```mermaid graph LR A[配置文件application.yml] --> B(属性读取) B --> C{@ConfigurationProperties类} C --> D[字段类型转换] D --> E[Bean属性注入] ``` 3. **宽松绑定规则**(Relaxed Binding) - 支持多种属性命名格式自动匹配: ```properties server.context-path= # 配置文件使用中线命名 server.contextPath= # 配置类字段使用驼峰命名 SERVER_CONTEXTPATH= # 环境变量格式 ``` ### 与直接使用 `@Component` 的区别 | 方式 | 优点 | 缺点 | |----------------------------|-------------------------------|--------------------------| | `@EnableConfigurationProperties` | 1. 集中管理配置类<br>2. 延迟初始化<br>3. 明确声明依赖 | 需要显式声明配置类 | | `@Component + @ConfigurationProperties` | 自动扫描注册 | 1. 早期初始化<br>2. 隐式依赖 | ### 最佳实践 1. **多环境配置绑定** ```java @EnableConfigurationProperties({ DatabaseConfig.class, SecurityConfig.class, ApiConfig.class }) ``` 2. **验证配置有效性**(结合 JSR-303) ```java @ConfigurationProperties(prefix = "email") @Validated // 启用验证 public class EmailConfig { @NotEmpty private String host; @Min(1) @Max(65535) private int port; } ``` ### 注意事项 1. **作用域限制**:只能标注在 `@Configuration` 类上 2. **绑定时机**:在 Bean 初始化后阶段执行属性注入 3. **元注解特性**:`@SpringBootApplication` 已包含该注解功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值