文章目录
想法
开发环境:SpringBoot + IDEA
比如说,我们在代码中,有一些环境相关的变量配置到了 properties 配置文件中,使用时用 @Value 注解获取。
现在由于某种奇葩原因,有部分配置项不能直接配在配置文件properties中,需要以其他方式获取。
我们有以下几种方式可以来实现,先讲方案,后续再讲下原理。
方案一 自定义Value注解
自定义一个用法类似 @Value 的注解,取值逻辑自定义(实际原理与 @Value 并不相同),步骤如下:
1. 自定义一个取值注解 @CustomValue
比如我这里例子,定义一个名称为 CustomValue 的注解,注解只支持传入一个值 value,类似 @Value, 传入"${xxx}" 格式时表示取 key 为 xxx 的配置项。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface CustomValue {
String value();
}
2. 自定义 BeanPostProcessor 处理注解
BeanPostProcessor 会在SpringApplication启动过程尾声被逐个调用,我们可以自定义一个 BeanPostProcessor 并用 @Component 注解交给 Spring 框架管理。在这个处理类中我们检查每一个 Spring 创建的对象,判断其中如果有被 CustomValue 注解的字段,则进行解析赋值。
@Component
public class CustomValueBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean == null){
return bean;
}
//过滤出自定义注解@CustomValue标记的字段
Field[] declaredFields = bean.getClass().getDeclaredFields();
for(Field declaredField : declaredFields){
CustomValue annotation = declaredField.getAnnotation(CustomValue.class);
if(annotation == null){
continue;
}
ReflectionUtils.makeAccessible(declaredField);
try{
//自定义解析注解字段获得值
String value = getValue(annotation);
//将值注入到目标字段中
declaredField.set(bean, value);
} catch (Exception e){
e.printStackTrace();;
}
}
return bean;
}
/** 用自定义方式获取值 */
private String getValue(CustomValue annotation) {
return "";
}
}
3. 应用自定义的注解并测试
在Service层应用自定义的注解注入变量:
@Service
public class TestServiceImpl implements TestService{
//使用官方注解
@Value("${qc.test.value}")
private String something;
//使用自定义注解
@CustomValue("${Levi}")
private String name;
//使用自定义注解
@CustomValue(": How are you?")
private String words;
@Override
public String saySomething() {
return something + " " + name + " " + words;
}
}
在控制层返回获取到的值:
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@RequestMapping(path = "/say", method = RequestMethod.GET)
public String say(){
return testService.saySomething();
}
}
4. 配置取值
到这里整个代码基本框架已经完成,但是显然我们现在还没能获取到值,一是@Value中配置的变量还没配置到 properties 中,二是 CustomValueBeanPostProcessor 的 getValue() 方法还没有实现。
考虑到实际情况下我们会有开发、测试、生产环境的区别,故我们需要在配置文件目录下建两个配置文:
一个是 application.properties 文件,其中指定当前环境是dev开发环境:
spring.profiles.active=dev
再建一个 application-dev.properties 文件,其中配置代码用到的配置项:
qc.test.value=hello!
然后再来完善我们的自定义注解取值逻辑。
我们这里举例用比较简单的方式,从枚举中获取值,自定义一个枚举,支持传入 key 和 环境标识 来获取 value。
public enum CustomValueEnum {
ELEN("Elen","dev-Elen","stg-Elen","prd-Elen"),
LEVI("Levi","dev-Levi","stg-Levi","prd-Levi"),
MIKASA("Mikasa","dev-Mikasa","stg-Mikasa","prd-Mikasa"),
ZEKE("Zeke","dev-Zeke","stg-Zeke","prd-Zeke");
/** 键 */
private String key;
/** dev环境的值 */
private String dev;
/** stg环境的值 */
private String stg;
/** prd环境的值 */
private String prd;
CustomValueEnum(String key, String dev, String stg, String prd){
this.key = key;
this.dev = dev;
this.stg = stg;
this.prd = prd;
}
private static HashMap<String, CustomValueEnum> map;
static{
map = new HashMap<>();
Arrays.stream(values()).forEach(item->map.put(item.key, item));
}
/** 根据key获取枚举 */
public static CustomValueEnum getEnumByKey(String key){
return map.get(key);
}
/**根据 key 和 环境标识 获取值**/
public static String getValByKeyEnv(String key, String env){
if(key==null || key =="" || env == null || env ==""){
return null;
}
CustomValueEnum valEnum = CustomValueEnum.getEnumByKey(key);
if(valEnum==null ){
return null;
}
switch (env){
case "dev":
return valEnum.dev;
case "stg":
return valEnum.stg;
case "prd":
return valEnum.prd;
default:
return null;
}
}
}
在 CustomValueBeanPostProcessor 的 getValue() 方法中通过枚举获取值:
@Component
public class CustomValueBeanPostProcessor implements BeanPostProcessor {
@Value("${spring.profiles.active}")
private String env;
//匹配 ${xxx}格式
private static final String patternStr = "^\\$\\{\\w+\\}$";
private static final Pattern pattern = Pattern.compile(patternStr);
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean == null){
return bean;
}
//过滤出自定义注解@CustomValue标记的字段
Field[] declaredFields = bean.getClass().getDeclaredFields();
for(Field declaredField : declaredFields){
CustomValue annotation = declaredField.getAnnotation(CustomValue.class);
if(annotation == null){
continue;
}
ReflectionUtils.makeAccessible(declaredField);
try{
//自定义解析注解字段获得值
String value = getValue(annotation);
//将值注入到目标字段中
declaredField.set(bean, value);
} catch (Exception e){
e.printStackTrace();;
}
}
return bean;
}
/** 用自定义方式获取值 */
private String getValue(CustomValue annotation) {
String key = annotation.value();
if(pattern.matcher(key).find()){
key = key.substring(2,key.length()-1);
return CustomValueEnum.getValByKeyEnv(key, env);
}else{
return key;
}
}
}
然后编写启动类,启动应用,访问接口地址,可以看到成功取到了值。
方案二 扩展 @Value 解析范围
除了自定义一个注解,我们还可以直接让 @Value 加载自己的值,不过可能出现一个问题,如果我们的key命名和其他组件的配置key冲突了,应该取谁的?这样可能会导致我们系统出现奇奇怪怪预想不到的问题。
所以要采用这种方案的话,建议通过生成一个随机串当自定义key前缀之类的方案,来避免key冲突。
1. 编写自定义的PropetySourcesPlaceHolderConfigurer
复用方案一的代码,再编写一个自定义的 PropetySourcesPlaceHolderConfigurer 并用 @Component 交给 Spring 管理,重写 mergeProperties() 方法,将自己的配置项和系统的配置项合并。
当 Spring 解析 @Value 的时候就会从合并后的结果中取到我们的自定义配置项。
@Component
public class CustomPropertySourcesPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer {
/** 当前环境标识 */
private static String env;
@Override
public void setEnvironment(Environment environment){
super.setEnvironment(environment);
//这里可以做一些自定义取值的初始化操作
env = environment.getProperty("spring.profiles.active");
}
@Override
protected Properties mergeProperties() throws IOException {
Properties props = super.mergeProperties();
if(props == null){
props = new Properties();
}
//将枚举中的变量配置合并到系统环境变量 props 中,这样 @Value 就能在 props 中取到值了。
//实际情况下建议自定义配置的key加一个随机串当前缀,避免和其他系统变量冲突导致奇奇怪怪的问题。
for(CustomValueEnum item: CustomValueEnum.values()){
if(!props.containsKey(item.getKey())){
props.setProperty(item.getKey(), CustomValueEnum.getValByKeyEnv(item.getKey(), env));
}
}
return props;
}
}
2. 用@Value加载自定义配置项
改写Service,用 @Value 加载枚举类中的值,像下面示例代码中增加的extValue变量。
@Service
public class TestServiceImpl implements TestService{
//使用官方注解,注入properties的值
@Value("${qc.test.value}")
private String something;
//使用官方注解,注入自定义取值
@Value("${Zeke}")
private String extValue;
//使用自定义注解
@CustomValue("${Levi}")
private String name;
//使用自定义注解
@CustomValue(": How are you?")
private String words;
@Override
public String saySomething() {
return something + " " + name + " " + words + " -- " + extValue;
}
}
启动应用,访问接口,可以看到 extValue 的值被成功读取到了。
示例代码将附加到文末