@ConfigurationProperties注解原理与实战
一、@ConfigurationProperties 基本使用
在 SpringBoot 中,当想需要获取到配置文件数据时,除了可以用 Spring 自带的 @Value 注解外,SpringBoot 还提供了一种更加方便的方式:@ConfigurationProperties。只要在 Bean 上添加上了这个注解,指定好配置文件的前缀,那么对应的配置文件数据就会自动填充到 Bean 中。
比如在application.properties文件中有如下配置文件
config.username=jay.zhou
config.password=3333
那么按照如下注解配置,SpringBoot项目中使用@ConfigurationProperties的Bean,它的username与password就会被自动注入值了。就像下面展示的那样
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "config")
public class TestBean{
private String username;
private String password;
}
二、@ConfigurationProperties 源码的探索及启发
SpringBoot为什么能够获取到它的值呢?如果让我从无到有设计,我应该怎么设计呢?
首先通过源码发现,SpringBoot主要帮助我们做了两件事情。
第一件事情就是获取到使用@ConfigurationProperties的类。
第二件事就是解析配置文件,并把对应的值设置到我们的Bean中。
按照源码提供的实现思路,其核心就是对Bean的声明周期的管理。主要涉及一个叫做 BeanPostProcessor 的接口,可以在Bean初始化的时候,我们做一些文章。下面的两个方法,很简单,大致意思就是,在Spring的Bean初始化之前与之后执行。
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
好了,作为程序员,看了半天源码,扯了半天淡,不如自己实现一个类似的效果来的实在。
三、手撸代码
第一步,定义自己的注解。这个注解最后实现的功能希望与@ConfigurationProperties 类似。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
/**
* 配置属性 的前缀
*/
String prefix();
}
第二步,定义配置测试实体。用上了我们刚才定义的@Config注解,并指明其prefix属性的值为 " default "
import lombok.Data;
import org.springframework.stereotype.Component;
@Config(prefix = "default")
@Component
@Data
public class TestDataSource {
private String username;
private String password;
private int maxActiveCount;
}
第三步,编写我们的自己的配置文件。为了便于操作(实际上其它的我不会解析),使用properties这种文件类型。
我们的目标就是把 " default." 后面的对应的字段,注入到上面的TestDataSource里面。
config.properties
default.username = root
default.password = 3333
default.maxActiveCount = 10
default.maxActiveCount2 = 10
第四步,编写处理逻辑。
(1)获取使用了我们自定义注解@Config的类
(2)解析配置文件,并将对应的值使用反射技术,注入到Bean中。
下面是最主要的处理代码。实现BeanPostProcessor接口的方法,检查每一个初始化成功的Bean,如果使用了我们的自定义注解,那么就把从配置文件中解析出来的数据,使用反射技术注入进去。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;
@Component
public class ConfigPostProcess implements BeanPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigPostProcess.class);
private static final String FILE_NAME = "config.properties";
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//获取使用了我们自定义注解@Config的类
Config configAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), Config.class);
//如果这个对象使用了此注解
if (configAnnotation != null) {
LOGGER.info("当前操作的类:{}", beanName);
//解析配置文件,并将解析结果放入Map中
Map<String, String> configProperties = getConfigPropertiesFromFile(configAnnotation);
//将对应的值,使用反射技术,注入到这个bean中
bindBeanValue(bean, configProperties);
}
return bean;
}
...
}
四、实现效果
SpringBoot很多都是通过注解来实现功能的。只要我们能够学习其源码实现思路,我们也可以做出很多很多类似的功能。尽管我们的代码健壮性做不到跟大佬一样,但是在特定业务场景下,比如对某个特别重要的单例Bean进行操作,或者为某一类特定的接口实现,做一些特定的处理,可以考虑这种技术。
另外,对于反射的使用,在SpringBoot框架学习过程中也是一个非常重要的部分。
只有经常手敲代码,才能孰能生巧,切忌纸上谈兵。只有切实自己实现了特定功能,回头查看SpringBoot替我们做的事情,才能做到心中有数,否则被新手程序员问一句 " 为什么要这么配置 " 就哑口无言,实在是很尴尬。
点击动图后,效果可以更加清楚哦!!!
附录私有方法
/**
* 将对应的值,使用反射技术,注入到这个bean中
*/
private void bindBeanValue(Object bean, Map<String, String> configProperties) {
if (configProperties.size() > 0) {
configProperties.forEach((key, value) -> {
setFieldValueByFieldName(key, bean, value);
});
}
}
/**
* 从配置文件中读取配置好的键值对,并放入到Map中
*/
private Map<String, String> getConfigPropertiesFromFile(Config configAnnotation) {
//get prefix from annotation
String prefix = configAnnotation.prefix();
//read value from resource file
Properties properties = getClassNameFromResource(FILE_NAME);
Map<String, String> configProperties = new HashMap<>();
Set<String> keys = properties.stringPropertyNames();
List<String> keyList = new ArrayList<>(keys);
for (String key : keyList) {
if (key.startsWith(prefix)) {
//default.password ==> password
String realKey = key.substring(key.indexOf(prefix) + prefix.length() + 1);
String value = properties.getProperty(key);
configProperties.put(realKey, value);
}
}
return configProperties;
}
/**
* 读取配置文件,返回一个流对象
*/
private Properties getClassNameFromResource(String fileName) {
Properties properties = new Properties();
ClassLoader classLoader = ConfigPostProcess.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(fileName);
try {
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}
/**
* 为指定字段使用反射技术设值(只支持String 与 int 类型)
*/
private void setFieldValueByFieldName(String fieldName, Object object, String value) {
try {
Class c = object.getClass();
if (checkFieldExists(fieldName, c)) {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
//如果不是String,那么就是int。其它类型不支持
if(f.getType().equals(String.class)){
f.set(object, value);
}else{
int number = Integer.valueOf(value);
f.set(object, number);
}
}
} catch (Exception e) {
LOGGER.error("设置" + fieldName + "出错");
}
}
/**
* 检查这个Bean是否有配置文件中配置的字段
* 没有就不设置了
*/
private boolean checkFieldExists(String fieldName, Class c) {
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals(fieldName)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
你看我都这么努力的分享知识给你了,鼓励一下又何妨O(∩_∩)O
大宇期待与你们共同进步!同时也非常感谢最近兄弟们的支持!