1. 前言
在开发 spring 应用时,不可避免会有读取配置文件,注入到静态变量或者常量字段的场景。
我们最常用的是 @Value 注解,但是 @Value 不支持静态字段的注入。
本文搜索了常见的解决方案,发现或多或少都有一定的限制。于是结合自己对 spring 的了解,增强 @Value 的功能,实现静态字段的直接注入。代码实现没有经过严格测试,有问题请批评指正。
2. 注入静态变量常规方案
2.1. @Value 标记 set 方法
示例代码如下:
- 类必须是 spring bean
- @Value 标记在 set 方法上,方法名没有要求
@Component
public class TestConfig {
private static String name;
@Value("${test.name}")
public void inject(String s) {
name = s;
}
}
2.2. @ConfigurationProperties 结合 set 方法
示例代码如下:
- 类必须是 spring bean
- set 方法名必须符合 spring 命名规范
@ConfigurationProperties(prefix = "test")
@Component
public class TestConfig {
private static String name;
public void setName(String n) {
name = n;
}
}
2.3. @Value 结合 @PostConstruct 间接注入
示例代码如下:
@Component
public class TestConfig {
@Value("${test.name}")
private String name;
private static String staticName;
@PostConstruct
public void init() {
staticName = name;
System.out.println("staticName = " + staticName);
}
}
3. 扩展 @Value 实现静态字段的注入
前面几种常规方案,都不太方便,而且有一定的限制。所以笔者考虑扩展 @Value 注解,实现以下功能:
- 可以注入静态字段,包括变量和常量
- 所在类不一定是 spring bean,没有限制
- 仍然支持 spel 表达式
- 作用的类范围是指定包及其子包下的所有类
比如可以直接这么使用
public class Foo {
@Value("${test.string}")
private static String s;
}
接下来介绍如何实现
3.1. 自定义标识注解
首先自定义一个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectStaticField {
}
这个注解没有实际功能,只是给需要注入静态字段的类加上标识,使用示例如下:
@InjectStaticField
public class Foo {
@Value("${test.string}")
private static String s;
}
为什么要声明一个没有实际功能的注解呢?这里简单解释一下。要实现 @Value
注入静态变量,不可避免要加载 class,然后遍历所有静态字段
这种方式会导致项目在初始化时,就把所有的 class 都加载一遍,不管你实际有没有用到。加载类的时候,静态代码段、静态属性初始化都会被执行,这可能会导致意料不到的后果。
但这还是存在一样的问题,判断一个类是否标识该自定义注解,还是得加载这个类再进行判断,这不是多此一举吗?
这里有个小细节,Spring 提供了一种机制,可以在不加载类的前提下,读取类的元信息,包括注解信息。所以我们可以利用这个机制,避免加载不必要的类
3.2. 在 Spring 应用启动时实现注入
这里通过实现 SpringApplicationRunListener 接口,在 Spring 应用启动时实现注入静态变量的逻辑
@Slf4j
public class RunListener implements SpringApplicationRunListener {
private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
/**
* 根据 SpringApplicationRunListener 规范,必须要定义以下构造方法
*/
public RunListener(SpringApplication application, String[] args) {
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
initInjectStaticField(context);
}
/**
* 注入静态字段
*/
private static void initInjectStaticField(ConfigurableApplicationContext context) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
BeanExpressionResolver expressionResolver = beanFactory.getBeanExpressionResolver();
if (expressionResolver == null) {
expressionResolver = new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader());
}
ConfigurableEnvironment env = context.getEnvironment();
TypeConverter converter = beanFactory.getTypeConverter();
CachingMetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(context);
// 获取启动类所在包路径
String packagePath = ClassUtils.classPackageAsResourcePath(App.class);
// 配置扫描包 pattern
String searchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
packagePath + '/' + DEFAULT_RESOURCE_PATTERN;
try {
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 扫描包
Resource[] resources = context.getResources(searchPattern);
for (Resource resource : resources) {
// 获取类的元信息,这里不会触发类的加载
MetadataReader reader = readerFactory.getMetadataReader(resource);
AnnotationMetadata metadata = reader.getAnnotationMetadata();
// 只有声明指定注解的类,才支持静态注入
if (!metadata.isAnnotated(InjectStaticField.class.getName())) {
continue;
}
// 加载类
Class<?> clazz = Class.forName(metadata.getClassName());
for (Field field : clazz.getDeclaredFields()) {
// 只注入静态字段
if (!Modifier.isStatic(field.getModifiers())) {
continue;
}
// 获取 Value 注解
Value anno = field.getDeclaredAnnotation(Value.class);
if (anno == null) {
continue;
}
// 读取配置文件数据
String strValue = env.resolveRequiredPlaceholders(anno.value());
// 解析 spel
Object value = expressionResolver.evaluate(strValue,
new BeanExpressionContext(beanFactory, null));
Class<?> type = field.getType();
TypeDescriptor descriptor = new TypeDescriptor(field);
// 类型转换
Object result = converter.convertIfNecessary(value, type, descriptor);
// 确保 private 和 final 修饰的静态字段也可以注入
field.setAccessible(true);
if (Modifier.isFinal(field.getModifiers())) {
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
// 注入值
field.set(null, result);
}
}
} catch (Exception ex) {
log.error("Inject static field failed", ex);
throw new RuntimeException(ex);
}
log.info("Inject static field done");
}
}
要记得在 META-INF/spring.factories
声明 SpringApplicationRunListener 的实现类,否则不会生效
org.springframework.boot.SpringApplicationRunListener=demo.spring.listener.RunListener
3.3. 测试
要注入的类
@InjectStaticField
public class Foo {
@Value("${test.string}")
public static String string;
@Value("${test.int:100}")
public static Integer i;
@Value("#{${test.map}}")
public static final Map<Object, Object> MAP = null;
}
application.properties
test.name=jack
test.map={key1: 'value1', key2: 'value2'}
测试打印
@SpringBootApplication
public class App {
@PostConstruct
public void init() {
System.out.println("Foo.string = " + Foo.string);
System.out.println("Foo.i = " + Foo.i);
System.out.println("Foo.MAP = " + Foo.MAP);
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}