前言
我们平时会写一些工具类, 为了方便调用, 一般将工具类中的方法定义为静态方法, 同时又可能需要读取配置文件中某些配置项的值, 由于方法是静态的, 这些变量也必须是静态的, 但是@Value
注解又不支持对静态变量的注入, 至少我用的Spring Boot 2.4.2版本不行, 当然, 有很多方法可以规避这个限制, 比如把@Value
打在setter方法上, 或者利用@PostConstruct
在类实例化之后通过中间变量赋值给静态变量, 然而这样代码可能略显繁琐:
/**
* 通过setter注入
*/
private static String STATIC_VALUE;
@Value("${static.prop}")
public void setStaticValue(String staticValue) {
TestStaticValueInject.STATIC_VALUE = staticValue;
}
/**
* 通过@PostConstruct注入
*/
private static String STATIC_VALUE;
@Value("${static.prop}")
private String tempStaticValue;
@PostConstruct
public void init() {
TestStaticValueInject.STATIC_VALUE = this.tempStaticValue;
}
因此我希望实现静态属性直接用@Value
注入, 如:
@Value("${static.prop}")
private static String STATIC_VALUE;
实现
我们可以模仿@MapperScan
实现一个注解打在启动类上让它去自动扫描然后注入.
1. 创建注册器类
首先创建一个类, 需要实现三个接口ImportBeanDefinitionRegistrar
, EnvironmentAware
, ResourceLoaderAware
ImportBeanDefinitionRegistrar
实现此接口的类有注册bean的能力, 由于静态属性不属于bean, 而是属于这个bean的类, 因此实际上不用注册扫描到的bean; 需要@Import
引用才能生效.EnvironmentAware
,ResourceLoaderAware
实现这两个接口可以获取应用的运行环境和资源路径加载器, 这里主要用来获取配置微间的属性, 扫描和加载类, 实现时就是实现一下setter方法.
2. 实现方法
实现registerBeanDefinitions
方法, 可以在这里面扫描bean, 获取它的类; 获取到类就好操作了, 拿到类的Field[]
后直接set
值, environment.resolvePlaceholders()
方法可以处理el表达式, 完整代码如下:
@Component
public class StaticValueInjector implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware {
private Environment environment;
private ResourceLoader resourceLoader;
private ClassLoader classLoader;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
this.classLoader = Objects.requireNonNull(resourceLoader.getClassLoader());
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 暂时使用默认过滤器, 扫描启动类下所有包
new ClassPathBeanDefinitionScanner(registry, true, environment, resourceLoader)
.findCandidateComponents(ClassUtils.getPackageName(importingClassMetadata.getClassName()))
.forEach(this::inject);
}
/**
* 向该bean中的静态变量注入值
*
* @param beanDefinition bean
*/
public void inject(BeanDefinition beanDefinition) {
try {
// 获取bean中的属性, 过滤非静态变量以及不需要注入的属性
Arrays.stream(classLoader.loadClass(beanDefinition.getBeanClassName()).getDeclaredFields())
.filter(f -> Modifier.isStatic(f.getModifiers()) && f.isAnnotationPresent(Value.class))
.forEach(v -> {
try {
v.setAccessible(true);
// 处理el表达式
v.set(null, environment.resolvePlaceholders(v.getAnnotation(Value.class).value()));
} catch (IllegalAccessException e) {
throw new BusinessException(e.getMessage());
}
});
} catch (ClassNotFoundException e) {
throw new BusinessException(e.getMessage());
}
}
}
3. 定义注解引用注册器类
要让注册器类生效需要定义一个注解, 用@Import
引入, 如:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(StaticValueInjector.class)
public @interface EnableStaticValueInjection {
}
使用和测试
首先要在启动类上打上新定义的注解:
@EnableStaticValueInjection
@SpringBootApplication
public class NotesApplication {
public static void main(String[] args) {
SpringApplication.run(NotesApplication.class, args);
}
}
然后可以直接使用@Value
打在静态属性上了, 顺便测试一下:
@Slf4j
@Component
public class TestStaticValueInject {
@Value("${static.prop}")
private static String STATIC_VALUE;
@PostConstruct
public void test() {
log.warn("测试静态注入: {}", STATIC_VALUE);
}
}
配置文件配置项:
static:
prop: 测试
测试结果及总结:
是可以注入成功的, 但是会有一条日志: 静态属性不支持自动注入, Spring确实不推荐也不支持静态变量的自动注入, 然而我们是利用反射注入的, 如果觉得这条日志烦人, 可以自定义一个注解专门用于标记, 只要在获取属性时获取该注解下面的属性就好了, 使用上和Spring的@Value
基本一样;
不知道大伙还有没有好方法, 这应该不是一个比较好的实现