使用Spring實(shí)現(xiàn)@Value注入靜態(tài)字段
1. 前言
在開(kāi)發(fā) spring 應(yīng)用時(shí),不可避免會(huì)有讀取配置文件,注入到靜態(tài)變量或者常量字段的場(chǎng)景。
我們最常用的是 @Value 注解,但是 @Value 不支持靜態(tài)字段的注入。
本文搜索了常見(jiàn)的解決方案,發(fā)現(xiàn)或多或少都有一定的限制。
于是結(jié)合自己對(duì) spring 的了解,增強(qiáng) @Value 的功能,實(shí)現(xiàn)靜態(tài)字段的直接注入。
代碼實(shí)現(xiàn)沒(méi)有經(jīng)過(guò)嚴(yán)格測(cè)試,有問(wèn)題請(qǐng)批評(píng)指正。
2. 注入靜態(tài)變量常規(guī)方案
2.1. @Value 標(biāo)記 set 方法
示例代碼如下:
- 類(lèi)必須是 spring bean
- @Value 標(biāo)記在 set 方法上,方法名沒(méi)有要求
@Component public class TestConfig { private static String name; @Value("${test.name}") public void inject(String s) { name = s; } }
2.2. @ConfigurationProperties 結(jié)合 set 方法
示例代碼如下:
- 類(lèi)必須是 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 實(shí)現(xiàn)靜態(tài)字段的注入
前面幾種常規(guī)方案,都不太方便,而且有一定的限制。
所以筆者考慮擴(kuò)展 @Value 注解,實(shí)現(xiàn)以下功能:
- 可以注入靜態(tài)字段,包括變量和常量
- 所在類(lèi)不一定是 spring bean,沒(méi)有限制
- 仍然支持 spel 表達(dá)式
- 作用的類(lèi)范圍是指定包及其子包下的所有類(lèi)
比如可以直接這么使用
public class Foo { @Value("${test.string}") private static String s; }
接下來(lái)介紹如何實(shí)現(xiàn)
3.1. 自定義標(biāo)識(shí)注解
首先自定義一個(gè)注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface InjectStaticField { }
這個(gè)注解沒(méi)有實(shí)際功能,只是給需要注入靜態(tài)字段的類(lèi)加上標(biāo)識(shí),使用示例如下:
@InjectStaticField public class Foo { @Value("${test.string}") private static String s; }
為什么要聲明一個(gè)沒(méi)有實(shí)際功能的注解呢?這里簡(jiǎn)單解釋一下。
要實(shí)現(xiàn) @Value
注入靜態(tài)變量,不可避免要加載 class,然后遍歷所有靜態(tài)字段
這種方式會(huì)導(dǎo)致項(xiàng)目在初始化時(shí),就把所有的 class 都加載一遍,不管你實(shí)際有沒(méi)有用到。
加載類(lèi)的時(shí)候,靜態(tài)代碼段、靜態(tài)屬性初始化都會(huì)被執(zhí)行,這可能會(huì)導(dǎo)致意料不到的后果。
但這還是存在一樣的問(wèn)題,判斷一個(gè)類(lèi)是否標(biāo)識(shí)該自定義注解,還是得加載這個(gè)類(lèi)再進(jìn)行判斷,這不是多此一舉嗎?
這里有個(gè)小細(xì)節(jié),Spring 提供了一種機(jī)制,可以在不加載類(lèi)的前提下,讀取類(lèi)的元信息,包括注解信息。
所以我們可以利用這個(gè)機(jī)制,避免加載不必要的類(lèi)
3.2. 在 Spring 應(yīng)用啟動(dòng)時(shí)實(shí)現(xiàn)注入
這里通過(guò)實(shí)現(xiàn) SpringApplicationRunListener 接口
在 Spring 應(yīng)用啟動(dòng)時(shí)實(shí)現(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); // 獲取啟動(dòng)類(lèi)所在包路徑 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) { // 獲取類(lèi)的元信息,這里不會(huì)觸發(fā)類(lèi)的加載 MetadataReader reader = readerFactory.getMetadataReader(resource); AnnotationMetadata metadata = reader.getAnnotationMetadata(); // 只有聲明指定注解的類(lèi),才支持靜態(tài)注入 if (!metadata.isAnnotated(InjectStaticField.class.getName())) { continue; } // 加載類(lèi) 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); // 類(lèi)型轉(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 的實(shí)現(xiàn)類(lèi),否則不會(huì)生效
org.springframework.boot.SpringApplicationRunListener=demo.spring.listener.RunListener
3.3. 測(cè)試
要注入的類(lèi)
@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'}
測(cè)試打印
@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é)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決Maven項(xiàng)目pom.xml導(dǎo)入了Junit包還是用不了@Test注解問(wèn)題
在Maven項(xiàng)目中,如果在非test目錄下使用@Test注解,可能會(huì)因?yàn)閜om.xml中<scope>test</scope>的設(shè)置而無(wú)法使用,正確做法是將測(cè)試代碼放在src/test/java目錄下,或去除<scope>test</scope>限制,這樣可以確保Junit依賴(lài)正確加載并應(yīng)用于適當(dāng)?shù)拇a部分2024-10-10解決遇到Cannot resolve ch.qos.logback:logback-classic:
當(dāng)使用Maven配置項(xiàng)目依賴(lài)時(shí),可能會(huì)遇到無(wú)法解析特定版本的錯(cuò)誤,例如,logback-classic版本1.2.3可能無(wú)法在配置的倉(cāng)庫(kù)中找到,解決方法包括檢查倉(cāng)庫(kù)是否包含所需版本,或更新到其他可用版本,可通過(guò)Maven官網(wǎng)搜索并找到適用的版本,替換依賴(lài)配置中的版本信息2024-09-09java編程實(shí)現(xiàn)郵件定時(shí)發(fā)送的方法
這篇文章主要介紹了java編程實(shí)現(xiàn)郵件定時(shí)發(fā)送的方法,涉及Java基于定時(shí)器實(shí)現(xiàn)計(jì)劃任務(wù)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11SpringCloud OpenFeign超時(shí)控制示例詳解
在Spring Cloud中使用OpenFeign時(shí),可以通過(guò)配置來(lái)控制請(qǐng)求的超時(shí)時(shí)間,這篇文章主要介紹了SpringCloud OpenFeign超時(shí)控制,需要的朋友可以參考下2024-05-05基于 SpringBoot 實(shí)現(xiàn) MySQL 讀寫(xiě)分離的問(wèn)題
這篇文章主要介紹了基于 SpringBoot 實(shí)現(xiàn) MySQL 讀寫(xiě)分離的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Spring基于xml實(shí)現(xiàn)自動(dòng)裝配流程詳解
自動(dòng)裝配是使用spring滿(mǎn)足bean依賴(lài)的一種方法,spring會(huì)在應(yīng)用上下文中為某個(gè)bean尋找其依賴(lài)的bean,Spring中bean有三種裝配機(jī)制,分別是:在xml中顯式配置、在java中顯式配置、隱式的bean發(fā)現(xiàn)機(jī)制和自動(dòng)裝配2023-01-01Druid簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)庫(kù)的增刪改查方式
這篇文章主要介紹了Druid簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)庫(kù)的增刪改查方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07