1. 程式人生 > >想用@Autowired注入static靜態成員?官方不推薦你卻還偏要這麼做

想用@Autowired注入static靜態成員?官方不推薦你卻還偏要這麼做

> 生命太短暫,不要去做一些根本沒有人想要的東西。本文已被 [**https://www.yourbatman.cn**](https://www.yourbatman.cn) 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的**專欄**供以免費學習。關注公眾號【**BAT的烏托邦**】逐個擊破,深入掌握,拒絕淺嘗輒止。 [TOC] ![](https://img-blog.csdnimg.cn/20200717111541960.png) # 前言 各位小夥伴大家好,我是A哥。通過本專欄前兩篇的學習,相信你對static關鍵字在Spring/Spring Boot裡的應用有了全新的認識,能夠解釋工作中遇到的大多數問題/疑問了。本文繼續來聊聊static關鍵字更為常見的一種case:使用`@Autowired`依賴注入靜態成員(屬性)。 在Java中,針對static靜態成員,我們有一些最基本的常識:靜態變數(成員)它是**屬於類**的,而非屬於例項物件的屬性;同樣的靜態方法也是屬於類的,普通方法(例項方法)才屬於物件。而Spring容器管理的都是**例項物件**,包括它的`@Autowired`依賴注入的均是容器內的物件例項,所以對於static成員是不能直接使用`@Autowired`注入的。 > 這很容易理解:類成員的初始化較早,並不需要依賴例項的建立,所以這個時候Spring容器可能都還沒“出生”,談何依賴注入呢? 這個示例,你或許似曾相識: ```java @Component public class SonHolder { @Autowired private static Son son; public static Son getSon() { return son; } } ``` 然後“正常使用”這個元件: ```java @Autowired private SonHolder sonHolder; @Transaction public void method1(){ ... sonHolder.getSon().toString(); } ``` 執行程式,結果拋錯: ```java Exception in thread "main" java.lang.NullPointerException ... ``` 很明顯,`getSon()`得到的是一個null,所以給你扔了個NPE。 ![](https://img-blog.csdnimg.cn/20200606065710511.png) --- ## 版本約定 本文內容若沒做特殊說明,均基於以下版本: - JDK:`1.8` - Spring Framework:`5.2.2.RELEASE` --- # 正文 說起`@Autowired`註解的作用,沒有人不熟悉,**自動裝配**嘛。根據此註解的定義,它似乎能使用在很多地方: ```java @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required() default true; } ``` 本文我們重點關注它使用在FIELD成員屬性上的case,標註在static靜態屬性上是本文討論的中心。 > 說明:雖然Spring官方現在並不推薦欄位/屬性注入的方式,但它的便捷性仍無可取代,因此在做**業務開發**時它仍舊是主流的使用方式 --- ## 場景描述 假如有這樣一個場景需求:建立一個教室(Room),需要傳入一批學生和一個老師,此時我需要對這些**使用者**按照規則(如名字中含有test字樣的示為測試帳號)進行資料合法性校驗和過濾,然後才能正常走建立邏輯。此case還有以下特點: - 使用者名稱字/詳細資訊,需要遠端呼叫(如FeignClient方式)從UC中心獲取 - 因此很需要做橋接,提供防腐層 - 該過濾規則功能性很強,工程內很多地方都有用到 - 有點工具的意思有木有 閱讀完“題目”感覺還是蠻簡單的,很normal的一個業務需求case嘛,下面我來模擬一下它的實現。 從UC使用者中心獲取使用者資料(使用本地資料模擬遠端訪問): ```java /** * 模擬去遠端使用者中心,根據ids批量獲取使用者資料 * * @author yourbatman * @date 2020/6/5 7:16 */ @Component public class UCClient { /** * 模擬遠端呼叫的結果返回(有正常的,也有測試資料) */ public List getByIds(List userIds) { return userIds.stream().map(uId -> { User user = new User(); user.setId(uId); user.setName("YourBatman"); if (uId % 2 == 0) { user.setName(user.getName() + "_test"); } return user; }).collect(Collectors.toList()); } } ``` > 說明:實際情況這裡可能只是一個`@FeignClient`介面而已,本例就使用它mock嘍 因為過濾測試使用者的功能過於**通用**,並且規則也需要收口,須對它進行封裝,因此有了我們的**內部**幫助類`UserHelper`: ```java /** * 工具方法:根據使用者ids,按照一定的規則過濾掉測試使用者後返回結果 * * @author yourbatman * @date 2020/6/5 7:43 */ @Component public class UserHelper { @Autowired UCClient ucClient; public List getAndFilterTest(List userIds) { List users = ucClient.getByIds(userIds); return users.stream().filter(u -> { Long id = u.getId(); String name = u.getName(); if (name.contains("test")) { System.out.printf("id=%s name=%s是測試使用者,已過濾\n", id, name); return false; } return true; }).collect(Collectors.toList()); } } ``` 很明顯,它內部需依賴於`UCClient`這個遠端呼叫的結果。封裝好後,我們的業務Service層任何元件就可以盡情的“享用”該工具啦,形如這樣: ```java /** * 業務服務:教室服務 * * @author yourbatman * @date 2020/6/5 7:29 */ @Service public class RoomService { @Autowired UserHelper userHelper; public void create(List studentIds, Long teacherId) { // 因為學生和老師統稱為user 所以可以放在一起校驗 List userIds = new ArrayList<>(studentIds); userIds.add(teacherId); List users = userHelper.getAndFilterTest(userIds); // ... 排除掉測試資料後,執行建立邏輯 System.out.println("教室建立成功"); } } ``` 書寫個測試程式來模擬Service業務呼叫: ```java @ComponentScan public class DemoTest { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(DemoTest.class); // 模擬介面呼叫/單元測試 RoomService roomService = context.getBean(RoomService.class); roomService.create(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L), 101L); } } ``` 執行程式,結果輸出: ```java id=2 name=YourBatman_test是測試使用者,已過濾 id=4 name=YourBatman_test是測試使用者,已過濾 id=6 name=YourBatman_test是測試使用者,已過濾 教室建立成功 ``` 一切都這麼美好,相安無事的,那為何還會有本文指出的問題存在呢?正所謂“不作死不會死”,總有那麼一些“追求極致”的選手就喜歡玩花,下面姑且讓我猜猜你為何想要依賴注入static成員屬性呢? ![](https://img-blog.csdnimg.cn/20200607071214299.png) --- ### 幫你猜猜你為何有如此需求? 從上面示例類的命名中,我或許能猜出你的用意。`UserHelper`它被命名為一個工具類,而一般我們對工具類的理解是: 1. 方法均為static工具方法 2. 使用越便捷越好 1. 很明顯,static方法使用是最便捷的嘛 現狀是:使用`UserHelper`去處理使用者資訊還得先`@Autowired`注入它的例項,實屬不便。因此你想方設法的想把`getAndFilterTest()`這個方法變為靜態方法,這樣通過類名便可直接呼叫而並不再依賴於注入UserHelper例項了,so你想當然的這麼“優化”: ```java @Component public class UserHelper { @Autowired static UCClient ucClient; public static List getAndFilterTest(List userIds) { ... // 處理邏輯完全同上 } } ``` 屬性和方法都新增上static修飾,這樣使用方通過類名便可直接訪問(無需注入): ```java @Service public class RoomService { public void create(List studentIds, Long teacherId) { ... // 通過類名直接呼叫其靜態方法 List users = UserHelper.getAndFilterTest(userIds); ... } } ``` 執行程式,結果輸出: ```java 07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient 07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient ... Exception in thread "main" java.lang.NullPointerException at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:23) at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26) at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19) ``` 以為天衣無縫,可結果並不完美,拋異常了。我特意多貼上了兩句info日誌,它們告訴了你為何丟擲NPE異常的原因:**@Autowired不支援標註在static欄位/屬性上**。 ![](https://img-blog.csdnimg.cn/20200607072738933.png) --- ## 為什麼@Autowired不能注入static成員屬性 靜態變數是屬於**類本身**的資訊,當類載入器載入靜態變數時,Spring的上下文環境**還沒有**被載入,所以不可能為靜態變數繫結值(這只是最表象原因,並不準確)。同時,Spring也不鼓勵為靜態變數注入值(言外之意:並不是不能注入),因為它認為這會增加了耦合度,對測試不友好。 這些都是表象,那麼實際上Spring是如何“操作”的呢?我們沿著`AutowiredAnnotationBeanPostProcessor`輸出的這句info日誌,倒著找原因,這句日誌的輸出在這: ```java AutowiredAnnotationBeanPostProcessor: // 構建@Autowired注入元資料方法 // 簡單的說就是找到該Class類下有哪些是需要做依賴注入的 private InjectionMetadata buildAutowiringMetadata(final Class clazz) { ... // 迴圈遞迴,因為父類的也要管上 do { // 遍歷所有的欄位(包括靜態欄位) ReflectionUtils.doWithLocalFields(targetClass, field -> { if (Modifier.isStatic(field.getModifiers())) { logger.info("Autowired annotation is not supported on static fields: " + field); } return; ... }); // 遍歷所有的方法(包括靜態方法) ReflectionUtils.doWithLocalMethods(targetClass, method -> { if (Modifier.isStatic(method.getModifiers())) { logger.info("Autowired annotation is not supported on static methods: " + method); } return; ... }); ... targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); ... } ``` 這幾句程式碼道出了Spring為何不給static靜態欄位/靜態方法執行`@Autowired`注入的**最真實原因**:掃描Class類需要注入的元資料的時候,直接選擇忽略掉了static成員(包括屬性和方法)。 那麼這個處理的入口在哪兒呢?是否在這個階段時Spring真的無法給static成員完成賦值而選擇忽略掉它呢,我們繼續最終此方法的呼叫處。此方法唯一呼叫處是`findAutowiringMetadata()`方法,而它被呼叫的地方有三個: 呼叫處一:執行時機較早,在`MergedBeanDefinitionPostProcessor`處理bd合併期間就會解析出需要注入的元資料,然後做check。它會作用於每個bd身上,所以上例中的2句info日誌第一句就是從這輸出的 ```java AutowiredAnnotationBeanPostProcessor: @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); } ``` 呼叫處二:在`InstantiationAwareBeanPostProcessor`也就是**例項建立好後**,給屬性賦值階段(也就是`populateBean()`階段)執行。所以它也是會作用於每個bd的,上例中2句info日誌的第二句就是從這輸出的 ```java AutowiredAnnotationBeanPostProcessor: @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } ... return pvs; } ``` 呼叫處三:這個方法比較特殊,它表示對於帶有任意**目標例項**(已經不僅是Class,而是例項本身)直接呼叫的“本地”處理方法實行注入。這是Spring提供給“外部”使用/注入的一個public公共方法,比如給容器外的例項注入屬性,還是比較實用的,本文下面會介紹它的使用辦法 > 說明:此方法Spring自己並不會主動呼叫,所以不會自動輸出日誌(這也是為何呼叫處有3處,但日誌只有2條的原因) ```java AutowiredAnnotationBeanPostProcessor: public void processInjection(Object bean) throws BeanCreationException { Class clazz = bean.getClass(); InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null); try { metadata.inject(bean, null, null); } ... } ``` 通過這部分原始碼,從底層詮釋了Spring為何不讓你`@Autowired`注入static成員的原因。既然這樣,難道就沒有辦法滿足我的“訴求”了嗎?答案是有的,接著往下看。 --- ## 間接實現static成員注入的N種方式 雖然Spring會忽略掉你直接使用**@Autowired + static成員**注入,但還是有很多方法來**繞過**這些限制,實現對靜態變數注入值。下面A哥介紹2種方式,供以參考: 方式一:以set方法作為跳板,在裡面實現對static靜態成員的賦值 ```java @Component public class UserHelper { static UCClient ucClient; @Autowired public void setUcClient(UCClient ucClient) { UserHelper.ucClient = ucClient; } } ``` 方式二:使用`@PostConstruct`註解,在裡面為static靜態成員賦值 ```java @Component public class UserHelper { static UCClient ucClient; @Autowired ApplicationContext applicationContext; @PostConstruct public void init() { UserHelper.ucClient = applicationContext.getBean(UCClient.class); } } ``` 雖然稱作是2種方式,但其實我認為思想只是一個:**延遲為static成員屬性賦值**。因此,基於此思想**確切的說**會有N種實現方案(只需要保證你在使用它之前給其賦值上即可),各位可自行思考,A哥就沒必要一一舉例了。 --- ### 高階實現方式 作為**福利**,A哥在這裡提供一種更為高(zhuang)級(bi)的實現方式供以你學習和參考: ```java @Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired private AutowireCapableBeanFactory beanFactory; /** * 當所有的單例Bena初始化完成後,對static靜態成員進行賦值 */ @Override public void afterSingletonsInstantiated() { // 因為是給static靜態屬性賦值,因此這裡new一個例項做注入是可行的 beanFactory.autowireBean(new UserHelper()); } } ``` UserHelper類**不再需要**標註`@Component`註解,也就是說它不再需要被Spirng容器管理(static工具類確實不需要交給容器管理嘛,畢竟我們不需要用到它的例項),這從某種程度上也是節約開銷的表現。 ```java public class UserHelper { @Autowired static UCClient ucClient; ... } ``` 執行程式,結果輸出: ```java 08:50:15.765 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient Exception in thread "main" java.lang.NullPointerException at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:26) at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26) at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19) ``` 報錯。當然嘍,這是我故意的,雖然拋異常了,但是看到我們的進步了沒:**info日誌只打印一句了**(自行想想啥原因哈)。不賣關子了,正確的姿勢還得這麼寫: ```java public class UserHelper { static UCClient ucClient; @Autowired public void setUcClient(UCClient ucClient) { UserHelper.ucClient = ucClient; } } ``` 再次執行程式,**一切正常**(info日誌也不會輸出嘍)。這麼處理的好處我覺得有如下三點: 1. 手動管理這種case的依賴注入,更可控。而非交給Spring容器去自動處理 2. 工具類**本身**並不需要加入到Spring容器內,這對於有大量這種case的話,是可以節約開銷的 3. 略顯高階,裝x神器(可別小看裝x,這是個中意詞,你的加薪往往來來自於裝x成功) 當然,你也可以這麼玩: ```java @Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor; @Override public void afterSingletonsInstantiated() { autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper()); } } ``` 依舊可以正常work。這不正是上面介紹的**呼叫處三**麼,馬上就學以致用了有木有,開心吧