不能执行autowired_想用@Autowired注入static静态成员?官方不推荐你却还偏要这么做...
前言
各位小伙伴大家好,我是A哥。通過本專欄前兩篇的學(xué)習(xí),相信你對static關(guān)鍵字在Spring/Spring Boot里的應(yīng)用有了全新的認識,能夠解釋工作中遇到的大多數(shù)問題/疑問了。本文繼續(xù)來聊聊static關(guān)鍵字更為常見的一種case:使用@Autowired依賴注入靜態(tài)成員(屬性)。
在Java中,針對static靜態(tài)成員,我們有一些最基本的常識:靜態(tài)變量(成員)它是屬于類的,而非屬于實例對象的屬性;同樣的靜態(tài)方法也是屬于類的,普通方法(實例方法)才屬于對象。而Spring容器管理的都是實例對象,包括它的@Autowired依賴注入的均是容器內(nèi)的對象實例,所以對于static成員是不能直接使用@Autowired注入的。
這很容易理解:類成員的初始化較早,并不需要依賴實例的創(chuàng)建,所以這個時候Spring容器可能都還沒“出生”,談何依賴注入呢?這個示例,你或許似曾相識:
@Component public class SonHolder {@Autowiredprivate static Son son;public static Son getSon() {return son;} }然后“正常使用”這個組件:
@Autowired private SonHolder sonHolder;@Transaction public void method1(){...sonHolder.getSon().toString(); }運行程序,結(jié)果拋錯:
Exception in thread "main" java.lang.NullPointerException...很明顯,getSon()得到的是一個null,所以給你扔了個NPE。
版本約定
本文內(nèi)容若沒做特殊說明,均基于以下版本: - JDK:1.8 - Spring Framework:5.2.2.RELEASE
正文
說起@Autowired注解的作用,沒有人不熟悉,自動裝配嘛。根據(jù)此注解的定義,它似乎能使用在很多地方:
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired {boolean required() default true; }本文我們重點關(guān)注它使用在FIELD成員屬性上的case,標(biāo)注在static靜態(tài)屬性上是本文討論的中心。
說明:雖然Spring官方現(xiàn)在并不推薦字段/屬性注入的方式,但它的便捷性仍無可取代,因此在做業(yè)務(wù)開發(fā)時它仍舊是主流的使用方式場景描述
假如有這樣一個場景需求:創(chuàng)建一個教室(Room),需要傳入一批學(xué)生和一個老師,此時我需要對這些用戶按照規(guī)則(如名字中含有test字樣的示為測試帳號)進行數(shù)據(jù)合法性校驗和過濾,然后才能正常走創(chuàng)建邏輯。此case還有以下特點: - 用戶名字/詳細信息,需要遠程調(diào)用(如FeignClient方式)從UC中心獲取 - 因此很需要做橋接,提供防腐層 - 該過濾規(guī)則功能性很強,工程內(nèi)很多地方都有用到 - 有點工具的意思有木有
閱讀完“題目”感覺還是蠻簡單的,很normal的一個業(yè)務(wù)需求case嘛,下面我來模擬一下它的實現(xiàn)。
從UC用戶中心獲取用戶數(shù)據(jù)(使用本地數(shù)據(jù)模擬遠程訪問):
/*** 模擬去遠端用戶中心,根據(jù)ids批量獲取用戶數(shù)據(jù)** @author yourbatman* @date 2020/6/5 7:16*/ @Component public class UCClient {/*** 模擬遠程調(diào)用的結(jié)果返回(有正常的,也有測試數(shù)據(jù))*/public List<User> getByIds(List<Long> 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嘍因為過濾測試用戶的功能過于通用,并且規(guī)則也需要收口,須對它進行封裝,因此有了我們的內(nèi)部幫助類UserHelper:
/*** 工具方法:根據(jù)用戶ids,按照一定的規(guī)則過濾掉測試用戶后返回結(jié)果** @author yourbatman* @date 2020/6/5 7:43*/ @Component public class UserHelper {@AutowiredUCClient ucClient;public List<User> getAndFilterTest(List<Long> userIds) {List<User> 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());}}很明顯,它內(nèi)部需依賴于UCClient這個遠程調(diào)用的結(jié)果。封裝好后,我們的業(yè)務(wù)Service層任何組件就可以盡情的“享用”該工具啦,形如這樣:
/*** 業(yè)務(wù)服務(wù):教室服務(wù)** @author yourbatman* @date 2020/6/5 7:29*/ @Service public class RoomService {@AutowiredUserHelper userHelper;public void create(List<Long> studentIds, Long teacherId) {// 因為學(xué)生和老師統(tǒng)稱為user 所以可以放在一起校驗List<Long> userIds = new ArrayList<>(studentIds);userIds.add(teacherId);List<User> users = userHelper.getAndFilterTest(userIds);// ... 排除掉測試數(shù)據(jù)后,執(zhí)行創(chuàng)建邏輯System.out.println("教室創(chuàng)建成功");}}書寫個測試程序來模擬Service業(yè)務(wù)調(diào)用:
@ComponentScan public class DemoTest {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(DemoTest.class);// 模擬接口調(diào)用/單元測試RoomService roomService = context.getBean(RoomService.class);roomService.create(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L), 101L);} }運行程序,結(jié)果輸出:
id=2 name=YourBatman_test是測試用戶,已過濾 id=4 name=YourBatman_test是測試用戶,已過濾 id=6 name=YourBatman_test是測試用戶,已過濾 教室創(chuàng)建成功一切都這么美好,相安無事的,那為何還會有本文指出的問題存在呢?正所謂“不作死不會死”,總有那么一些“追求極致”的選手就喜歡玩花,下面姑且讓我猜猜你為何想要依賴注入static成員屬性呢?
幫你猜猜你為何有如此需求?
從上面示例類的命名中,我或許能猜出你的用意。UserHelper它被命名為一個工具類,而一般我們對工具類的理解是: 1. 方法均為static工具方法 2. 使用越便捷越好 1. 很明顯,static方法使用是最便捷的嘛
現(xiàn)狀是:使用UserHelper去處理用戶信息還得先@Autowired注入它的實例,實屬不便。因此你想方設(shè)法的想把getAndFilterTest()這個方法變?yōu)殪o態(tài)方法,這樣通過類名便可直接調(diào)用而并不再依賴于注入UserHelper實例了,so你想當(dāng)然的這么“優(yōu)化”:
@Component public class UserHelper {@Autowiredstatic UCClient ucClient;public static List<User> getAndFilterTest(List<Long> userIds) {... // 處理邏輯完全同上} }屬性和方法都添加上static修飾,這樣使用方通過類名便可直接訪問(無需注入):
@Service public class RoomService {public void create(List<Long> studentIds, Long teacherId) {...// 通過類名直接調(diào)用其靜態(tài)方法List<User> users = UserHelper.getAndFilterTest(userIds);...} }運行程序,結(jié)果輸出:
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.NullPointerExceptionat 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)以為天衣無縫,可結(jié)果并不完美,拋異常了。我特意多粘貼了兩句info日志,它們告訴了你為何拋出NPE異常的原因:@Autowired不支持標(biāo)注在static字段/屬性上。
為什么@Autowired不能注入static成員屬性
靜態(tài)變量是屬于類本身的信息,當(dāng)類加載器加載靜態(tài)變量時,Spring的上下文環(huán)境還沒有被加載,所以不可能為靜態(tài)變量綁定值(這只是最表象原因,并不準(zhǔn)確)。同時,Spring也不鼓勵為靜態(tài)變量注入值(言外之意:并不是不能注入),因為它認為這會增加了耦合度,對測試不友好。
這些都是表象,那么實際上Spring是如何“操作”的呢?我們沿著AutowiredAnnotationBeanPostProcessor輸出的這句info日志,倒著找原因,這句日志的輸出在這:
AutowiredAnnotationBeanPostProcessor:// 構(gòu)建@Autowired注入元數(shù)據(jù)方法 // 簡單的說就是找到該Class類下有哪些是需要做依賴注入的 private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {...// 循環(huán)遞歸,因為父類的也要管上do {// 遍歷所有的字段(包括靜態(tài)字段)ReflectionUtils.doWithLocalFields(targetClass, field -> {if (Modifier.isStatic(field.getModifiers())) {logger.info("Autowired annotation is not supported on static fields: " + field);}return;...});// 遍歷所有的方法(包括靜態(tài)方法)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靜態(tài)字段/靜態(tài)方法執(zhí)行@Autowired注入的最真實原因:掃描Class類需要注入的元數(shù)據(jù)的時候,直接選擇忽略掉了static成員(包括屬性和方法)。
那么這個處理的入口在哪兒呢?是否在這個階段時Spring真的無法給static成員完成賦值而選擇忽略掉它呢,我們繼續(xù)最終此方法的調(diào)用處。此方法唯一調(diào)用處是findAutowiringMetadata()方法,而它被調(diào)用的地方有三個:
調(diào)用處一:執(zhí)行時機較早,在MergedBeanDefinitionPostProcessor處理bd合并期間就會解析出需要注入的元數(shù)據(jù),然后做check。它會作用于每個bd身上,所以上例中的2句info日志第一句就是從這輸出的
AutowiredAnnotationBeanPostProcessor:@Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);metadata.checkConfigMembers(beanDefinition); }調(diào)用處二:在InstantiationAwareBeanPostProcessor也就是實例創(chuàng)建好后,給屬性賦值階段(也就是populateBean()階段)執(zhí)行。所以它也是會作用于每個bd的,上例中2句info日志的第二句就是從這輸出的
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; }調(diào)用處三:這個方法比較特殊,它表示對于帶有任意目標(biāo)實例(已經(jīng)不僅是Class,而是實例本身)直接調(diào)用的“本地”處理方法實行注入。這是Spring提供給“外部”使用/注入的一個public公共方法,比如給容器外的實例注入屬性,還是比較實用的,本文下面會介紹它的使用辦法
說明:此方法Spring自己并不會主動調(diào)用,所以不會自動輸出日志(這也是為何調(diào)用處有3處,但日志只有2條的原因)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成員的原因。既然這樣,難道就沒有辦法滿足我的“訴求”了嗎?答案是有的,接著往下看。
間接實現(xiàn)static成員注入的N種方式
雖然Spring會忽略掉你直接使用@Autowired + static成員注入,但還是有很多方法來繞過這些限制,實現(xiàn)對靜態(tài)變量注入值。下面A哥介紹2種方式,供以參考:
方式一:以set方法作為跳板,在里面實現(xiàn)對static靜態(tài)成員的賦值
@Component public class UserHelper {static UCClient ucClient;@Autowiredpublic void setUcClient(UCClient ucClient) {UserHelper.ucClient = ucClient;} }方式二:使用@PostConstruct注解,在里面為static靜態(tài)成員賦值
@Component public class UserHelper {static UCClient ucClient;@AutowiredApplicationContext applicationContext;@PostConstructpublic void init() {UserHelper.ucClient = applicationContext.getBean(UCClient.class);} }雖然稱作是2種方式,但其實我認為思想只是一個:延遲為static成員屬性賦值。因此,基于此思想確切的說會有N種實現(xiàn)方案(只需要保證你在使用它之前給其賦值上即可),各位可自行思考,A哥就沒必要一一舉例了。
高級實現(xiàn)方式
作為福利,A哥在這里提供一種更為高(zhuang)級(bi)的實現(xiàn)方式供以你學(xué)習(xí)和參考:
@Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton {@Autowiredprivate AutowireCapableBeanFactory beanFactory;/*** 當(dāng)所有的單例Bena初始化完成后,對static靜態(tài)成員進行賦值*/@Overridepublic void afterSingletonsInstantiated() {// 因為是給static靜態(tài)屬性賦值,因此這里new一個實例做注入是可行的beanFactory.autowireBean(new UserHelper());} }UserHelper類不再需要標(biāo)注@Component注解,也就是說它不再需要被Spirng容器管理(static工具類確實不需要交給容器管理嘛,畢竟我們不需要用到它的實例),這從某種程度上也是節(jié)約開銷的表現(xiàn)。
public class UserHelper {@Autowiredstatic UCClient ucClient;... }運行程序,結(jié)果輸出:
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.NullPointerExceptionat 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)報錯。當(dāng)然嘍,這是我故意的,雖然拋異常了,但是看到我們的進步了沒:info日志只打印一句了(自行想想啥原因哈)。不賣關(guān)子了,正確的姿勢還得這么寫:
public class UserHelper {static UCClient ucClient;@Autowiredpublic void setUcClient(UCClient ucClient) {UserHelper.ucClient = ucClient;} }再次運行程序,一切正常(info日志也不會輸出嘍)。這么處理的好處我覺得有如下三點: 1. 手動管理這種case的依賴注入,更可控。而非交給Spring容器去自動處理 2. 工具類本身并不需要加入到Spring容器內(nèi),這對于有大量這種case的話,是可以節(jié)約開銷的 3. 略顯高級,裝x神器(可別小看裝x,這是個中意詞,你的加薪往往來來自于裝x成功)
當(dāng)然,你也可以這么玩:
@Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton {@Autowiredprivate AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor;@Overridepublic void afterSingletonsInstantiated() {autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper());} }依舊可以正常work。這不正是上面介紹的調(diào)用處三麼,馬上就學(xué)以致用了有木有,開心吧 。
使用建議
有這種使用需求的小伙伴需要明晰什么才叫真正的util工具類?若你的工具類存在外部依賴,依賴于Spring容器內(nèi)的實例,那么它就稱不上是工具類,就請不要把它當(dāng)做static來用,容易玩壞的。你現(xiàn)在能夠這么用恰好是得益于Spring管理的實例默認都是單例,所以你賦值一次即可,倘若某天真變成多例了呢(即使可能性極小)?
強行這么擼,是有隱患的。同時也打破了優(yōu)先級關(guān)系、生命周期關(guān)系,容易讓“初學(xué)者”感到迷糊。當(dāng)然若你堅持這么使用也未嘗不可,那么請做好相關(guān)規(guī)范/歸約,比如使用上面我推薦的高(zhuang)級(bi)使用方式是一種較好的選擇,這個時候手動管理往往比自動來得更安全,降低后期可能的維護成本。
思考題
總結(jié)
本文介紹了Spring依賴注入和static的關(guān)系,從使用背景到原因分析都做了相應(yīng)的闡述,A哥覺得還是蠻香的,對你幫助應(yīng)該不小吧。
最后,我想對小伙伴說:依賴注入的主要目的,是讓容器去產(chǎn)生一個對象的實例然后管理它的生命周期,然后在生命周期中使用他們,這會讓單元測試工作更加容易(什么?不寫單元測試,那你應(yīng)該關(guān)注我嘍,下下下個專欄會專門講單元測試)。而如果你使用靜態(tài)變量/類變量就擴大了使用范圍,使得不可控了。這種static field是隱含共享的,并且是一種global全局狀態(tài),Spring并不推薦你去這么做,因此使用起來務(wù)必當(dāng)心~
關(guān)注A哥
Author | A哥(YourBatman) -------- | ----- 個人站點 | www.yourbatman.cn E-mail | yourbatman@qq.com 微 信 | fsx641385712 公眾號 | BAT的烏托邦(ID:BAT-utopia) 知識星球 | BAT的烏托邦 每日文章推薦 | 每日文章推薦
總結(jié)
以上是生活随笔為你收集整理的不能执行autowired_想用@Autowired注入static静态成员?官方不推荐你却还偏要这么做...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 门锁了开不了_智能门锁不会突然没电 门锁
- 下一篇: 程序怎么启动vasp_构建可扩展的GPU