@Autowired可以注入static靜態成員?官方不推薦

前言

各位小夥伴大家好,本文來聊聊static關鍵字更爲常見的一種case:使用@Autowired依賴注入靜態成員(屬性)。

在Java中,針對static靜態成員,我們有一些最基本的常識:靜態變量(成員)它是屬於類的,而非屬於實例對象的屬性;同樣的靜態方法也是屬於類的,普通方法(實例方法)才屬於對象。而Spring容器管理的都是實例對象,包括它的@Autowired依賴注入的均是容器內的對象實例,所以對於static成員是不能直接使用@Autowired注入的。

這很容易理解:類成員的初始化較早,並不需要依賴實例的創建,所以這個時候Spring容器可能都還沒“出生”,談何依賴注入呢?

這個示例,你或許似曾相識:

@Component
public class SonHolder {

    @Autowired
    private static Son son;

    public static Son getSon() {
        return son;
    }
}

然後“正常使用”這個組件:

@Autowired
private SonHolder sonHolder;

@Transaction
public void method1(){
    ...
    sonHolder.getSon().toString();
}

運行程序,結果拋錯:

Exception in thread "main" java.lang.NullPointerException
    ...

很明顯,getSon()得到的是一個null,所以給你扔了個NPE。

 

 


版本約定

本文內容若沒做特殊說明,均基於以下版本:

•JDK:1.8

•Spring Framework:5.2.2.RELEASE


正文

說起@Autowired註解的作用,沒有人不熟悉,自動裝配嘛。根據此註解的定義,它似乎能使用在很多地方:

@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用戶中心獲取用戶數據(使用本地數據模擬遠程訪問):

/**
 * 模擬去遠端用戶中心,根據ids批量獲取用戶數據
 *
 * @author yourbatman
 * @date 2020/6/5 7:16
 */
@Component
public class UCClient {

    /**
     * 模擬遠程調用的結果返回(有正常的,也有測試數據)
     */
    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嘍

因爲過濾測試用戶的功能過於通用,並且規則也需要收口,須對它進行封裝,因此有了我們的內部幫助類UserHelper

/**
 * 工具方法:根據用戶ids,按照一定的規則過濾掉測試用戶後返回結果
 *
 * @author yourbatman
 * @date 2020/6/5 7:43
 */
@Component
public class UserHelper {

    @Autowired
    UCClient 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());
    }

}

 

很明顯,它內部需依賴於UCClient這個遠程調用的結果。封裝好後,我們的業務Service層任何組件就可以盡情的“享用”該工具啦,形如這樣:

/**
 * 業務服務:教室服務
 *
 * @author yourbatman
 * @date 2020/6/5 7:29
 */
@Service
public class RoomService {

    @Autowired
    UserHelper userHelper;

    public void create(List<Long> studentIds, Long teacherId) {
        // 因爲學生和老師統稱爲user 所以可以放在一起校驗
        List<Long> userIds = new ArrayList<>(studentIds);
        userIds.add(teacherId);
        List<User> users = userHelper.getAndFilterTest(userIds);

        // ...  排除掉測試數據後,執行創建邏輯
        System.out.println("教室創建成功");
    }

}

書寫個測試程序來模擬Service業務調用:

@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);
    }
}

運行程序,結果輸出:

id=2 name=YourBatman_test是測試用戶,已過濾id=4 name=YourBatman_test是測試用戶,已過濾id=6 name=YourBatman_test是測試用戶,已過濾教室創建成功

一切都這麼美好,相安無事的,那爲何還會有本文指出的問題存在呢?正所謂“不作死不會死”,總有那麼一些“追求極致”的選手就喜歡玩花,下面姑且讓我猜猜你爲何想要依賴注入static成員屬性呢?

 


幫你猜猜你爲何有如此需求?

從上面示例類的命名中,我或許能猜出你的用意。UserHelper它被命名爲一個工具類,而一般我們對工具類的理解是:

1.方法均爲static工具方法2.使用越便捷越好1.很明顯,static方法使用是最便捷的嘛

現狀是:使用UserHelper去處理用戶信息還得先@Autowired注入它的實例,實屬不便。因此你想方設法的想把getAndFilterTest()這個方法變爲靜態方法,這樣通過類名便可直接調用而並不再依賴於注入UserHelper實例了,so你想當然的這麼“優化”:

@Component
public class UserHelper {

    @Autowired
    static UCClient ucClient;

    public static List<User> getAndFilterTest(List<Long> userIds) {
        ... // 處理邏輯完全同上
    }
}

屬性和方法都添加上static修飾,這樣使用方通過類名便可直接訪問(無需注入):

@Service
public class RoomService {

    public void create(List<Long> studentIds, Long teacherId) {
        ...
        // 通過類名直接調用其靜態方法
        List<User> users = UserHelper.getAndFilterTest(userIds);
        ...
    }
}

運行程序,結果輸出:

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字段/屬性上


爲什麼@Autowired不能注入static成員屬性

靜態變量是屬於類本身的信息,當類加載器加載靜態變量時,Spring的上下文環境還沒有被加載,所以不可能爲靜態變量綁定值(這只是最表象原因,並不準確)。同時,Spring也不鼓勵爲靜態變量注入值(言外之意:並不是不能注入),因爲它認爲這會增加了耦合度,對測試不友好。

這些都是表象,那麼實際上Spring是如何“操作”的呢?我們沿着AutowiredAnnotationBeanPostProcessor輸出的這句info日誌,倒着找原因,這句日誌的輸出在這:

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日誌第一句就是從這輸出的

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日誌的第二句就是從這輸出的

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條的原因)

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靜態成員的賦值

@Component
public class UserHelper {

    static UCClient ucClient;

    @Autowired
    public void setUcClient(UCClient ucClient) {
        UserHelper.ucClient = ucClient;
    }
}

方式二:使用@PostConstruct註解,在裏面爲static靜態成員賦值

@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)的實現方式供以你學習和參考:

@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工具類確實不需要交給容器管理嘛,畢竟我們不需要用到它的實例),這從某種程度上也是節約開銷的表現。

public class UserHelper {

    @Autowired
    static UCClient ucClient;
    ...
}

運行程序,結果輸出:

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日誌只打印一句了(自行想想啥原因哈)。不賣關子了,正確的姿勢還得這麼寫:

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成功)

當然,你也可以這麼玩:​​​​​​​

@Component
public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton {

    @Autowired
    private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor;
    @Override
    public void afterSingletonsInstantiated() {
        autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper());
    }
}

依舊可以正常work。這不正是上面介紹的調用處三麼,馬上就學以致用了有木有,開心吧😄。

 


使用建議

有這種使用需求的小夥伴需要明晰什麼才叫真正的util工具類?若你的工具類存在外部依賴,依賴於Spring容器內的實例,那麼它就稱不上是工具類,就請不要把它當做static來用,容易玩壞的。你現在能夠這麼用恰好是得益於Spring管理的實例默認都是單例,所以你賦值一次即可,倘若某天真變成多例了呢(即使可能性極小)?

強行這麼擼,是有隱患的。同時也打破了優先級關係、生命週期關係,容易讓“初學者”感到迷糊。當然若你堅持這麼使用也未嘗不可,那麼請做好相關規範/歸約,比如使用上面我推薦的高(zhuang)級(bi)使用方式是一種較好的選擇,這個時候手動管理往往比自動來得更安全,降低後期可能的維護成本。


思考題

1.在解析類的@Autowired注入元數據的時候,Spring工廠/容器明明已經準備好了,理論上已經完全具備幫你完成注入/賦值的能力,既然這樣,爲何Spring還偏要“拒絕”這麼幹呢?可直接注入static成員不香嗎?2.既然@Autowired不能注入static屬性,那麼static方法呢?@Value註解呢?


總結

本文介紹了Spring依賴注入和static的關係,從使用背景到原因分析都做了相應的闡述,A哥覺得還是蠻香的,對你幫助應該不小吧。

最後,我想對小夥伴說:依賴注入的主要目的,是讓容器去產生一個對象的實例然後管理它的生命週期,然後在生命週期中使用他們,這會讓單元測試工作更加容易(什麼?不寫單元測試,那你應該關注我嘍,下下下個專欄會專門講單元測試)。而如果你使用靜態變量/類變量就擴大了使用範圍,使得不可控了。這種static field是隱含共享的,並且是一種global全局狀態,Spring並不推薦你去這麼做,因此使用起來務必當心~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章