使用Spring實現(xiàn)@Value注入靜態(tài)字段
1. 前言
在開發(fā) spring 應(yīng)用時,不可避免會有讀取配置文件,注入到靜態(tài)變量或者常量字段的場景。
我們最常用的是 @Value 注解,但是 @Value 不支持靜態(tài)字段的注入。
本文搜索了常見的解決方案,發(fā)現(xiàn)或多或少都有一定的限制。
于是結(jié)合自己對 spring 的了解,增強(qiáng) @Value 的功能,實現(xiàn)靜態(tài)字段的直接注入。
代碼實現(xiàn)沒有經(jīng)過嚴(yán)格測試,有問題請批評指正。
2. 注入靜態(tài)變量常規(guī)方案
2.1. @Value 標(biāo)記 set 方法
示例代碼如下:
- 類必須是 spring bean
- @Value 標(biāo)記在 set 方法上,方法名沒有要求
@Component
public class TestConfig {
private static String name;
@Value("${test.name}")
public void inject(String s) {
name = s;
}
}
2.2. @ConfigurationProperties 結(jié)合 set 方法
示例代碼如下:
- 類必須是 spring bean
- set 方法名必須符合 spring 命名規(guī)范
@ConfigurationProperties(prefix = "test")
@Component
public class TestConfig {
private static String name;
public void setName(String n) {
name = n;
}
}
2.3. @Value 結(jié)合 @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. 擴(kuò)展 @Value 實現(xiàn)靜態(tài)字段的注入
前面幾種常規(guī)方案,都不太方便,而且有一定的限制。
所以筆者考慮擴(kuò)展 @Value 注解,實現(xiàn)以下功能:
- 可以注入靜態(tài)字段,包括變量和常量
- 所在類不一定是 spring bean,沒有限制
- 仍然支持 spel 表達(dá)式
- 作用的類范圍是指定包及其子包下的所有類
比如可以直接這么使用
public class Foo {
@Value("${test.string}")
private static String s;
}
接下來介紹如何實現(xiàn)
3.1. 自定義標(biāo)識注解
首先自定義一個注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectStaticField {
}
這個注解沒有實際功能,只是給需要注入靜態(tài)字段的類加上標(biāo)識,使用示例如下:
@InjectStaticField
public class Foo {
@Value("${test.string}")
private static String s;
}
為什么要聲明一個沒有實際功能的注解呢?這里簡單解釋一下。
要實現(xiàn) @Value 注入靜態(tài)變量,不可避免要加載 class,然后遍歷所有靜態(tài)字段
這種方式會導(dǎo)致項目在初始化時,就把所有的 class 都加載一遍,不管你實際有沒有用到。
加載類的時候,靜態(tài)代碼段、靜態(tài)屬性初始化都會被執(zhí)行,這可能會導(dǎo)致意料不到的后果。
但這還是存在一樣的問題,判斷一個類是否標(biāo)識該自定義注解,還是得加載這個類再進(jìn)行判斷,這不是多此一舉嗎?
這里有個小細(xì)節(jié),Spring 提供了一種機(jī)制,可以在不加載類的前提下,讀取類的元信息,包括注解信息。
所以我們可以利用這個機(jī)制,避免加載不必要的類
3.2. 在 Spring 應(yīng)用啟動時實現(xiàn)注入
這里通過實現(xiàn) SpringApplicationRunListener 接口
在 Spring 應(yīng)用啟動時實現(xiàn)注入靜態(tài)變量的邏輯
@Slf4j
public class RunListener implements SpringApplicationRunListener {
private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
/**
* 根據(jù) SpringApplicationRunListener 規(guī)范,必須要定義以下構(gòu)造方法
*/
public RunListener(SpringApplication application, String[] args) {
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
initInjectStaticField(context);
}
/**
* 注入靜態(tài)字段
*/
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) {
// 獲取類的元信息,這里不會觸發(fā)類的加載
MetadataReader reader = readerFactory.getMetadataReader(resource);
AnnotationMetadata metadata = reader.getAnnotationMetadata();
// 只有聲明指定注解的類,才支持靜態(tài)注入
if (!metadata.isAnnotated(InjectStaticField.class.getName())) {
continue;
}
// 加載類
Class<?> clazz = Class.forName(metadata.getClassName());
for (Field field : clazz.getDeclaredFields()) {
// 只注入靜態(tài)字段
if (!Modifier.isStatic(field.getModifiers())) {
continue;
}
// 獲取 Value 注解
Value anno = field.getDeclaredAnnotation(Value.class);
if (anno == null) {
continue;
}
// 讀取配置文件數(shù)據(jù)
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);
// 類型轉(zhuǎn)換
Object result = converter.convertIfNecessary(value, type, descriptor);
// 確保 private 和 final 修飾的靜態(tài)字段也可以注入
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 的實現(xiàn)類,否則不會生效
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);
}
}
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決Maven項目pom.xml導(dǎo)入了Junit包還是用不了@Test注解問題
在Maven項目中,如果在非test目錄下使用@Test注解,可能會因為pom.xml中<scope>test</scope>的設(shè)置而無法使用,正確做法是將測試代碼放在src/test/java目錄下,或去除<scope>test</scope>限制,這樣可以確保Junit依賴正確加載并應(yīng)用于適當(dāng)?shù)拇a部分2024-10-10
解決遇到Cannot resolve ch.qos.logback:logback-classic:
當(dāng)使用Maven配置項目依賴時,可能會遇到無法解析特定版本的錯誤,例如,logback-classic版本1.2.3可能無法在配置的倉庫中找到,解決方法包括檢查倉庫是否包含所需版本,或更新到其他可用版本,可通過Maven官網(wǎng)搜索并找到適用的版本,替換依賴配置中的版本信息2024-09-09
基于 SpringBoot 實現(xiàn) MySQL 讀寫分離的問題
這篇文章主要介紹了基于 SpringBoot 實現(xiàn) MySQL 讀寫分離的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
Druid簡單實現(xiàn)數(shù)據(jù)庫的增刪改查方式
這篇文章主要介紹了Druid簡單實現(xiàn)數(shù)據(jù)庫的增刪改查方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07

