為何Spring無法注入靜態域?

There is no dumb question.

文章下半段更精彩

使用Spring DI過程中試圖注入靜態域或靜態方法時,Spring會報如下Warning,導致未注入:


//測試代碼
@Value("${member}")
private static String member;

//輸出信息
一月 12, 2017 3:35:15 下午 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor buildAutowiringMetadata
警告: Autowired annotation is not supported on static fields: private static java.lang.String com.springapp.mvc.statictest.StaticTestBean.member

解決方案

解決方案如下:

使用構造方法、代理的setter亦或init-method來將靜態域值注入

//1. via constructor
public class StaticTestBean {

  private static String member;
  
  public StaticTestBean(String member) {
    StaticTestBean.member = member;
  }
}

//2. via setter
public class StaticTestBean {

  private static String member;
  
  public void setMember(String member) {
    StaticTestBean.member = member;
  }
}
  
//3. via init-method
public class StaticTestBean {

  private static String member;
  
  @Value("${member}")
  private String _member;

  @PostConstruct
  public void init() {
    StaticTestBean.member = _member;
  }
}

那么問題來了:

Spring為什么不允許注入靜態域?

首先可以確定的是,靜態域注入肯定是可以,可以看下面這段比較Evil的實現:

//quote from http://stackoverflow.com/a/3301720/1395116
import java.lang.reflect.*;

//Evil
public class EverythingIsTrue {
   static void setFinalStatic(Field field, Object newValue) throws Exception {
      field.setAccessible(true);

      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

      field.set(null, newValue);
   }
   public static void main(String args[]) throws Exception {      
      setFinalStatic(Boolean.class.getField("FALSE"), true);

      System.out.format("Everything is %s", false); // "Everything is true"
   }
}

那么是Spring IoC的無法實現么?

//check org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
if (annotation != null) {
    if (Modifier.isStatic(field.getModifiers())) {
        if (logger.isWarnEnabled()) {
            logger.warn("Autowired annotation is not supported on static fields: " + field);
        }
        continue;
    }
    boolean required = determineRequiredStatus(annotation);
    currElements.add(new AutowiredFieldElement(field, required));
}

//injection org.springframework.beans.factory.annotation.InjectionMetadata
protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
    if (this.isField) {
        Field field = (Field) this.member;
        ReflectionUtils.makeAccessible(field);
        field.set(target, getResourceToInject(target, requestingBeanName));
    }
    else {
        if (checkPropertySkipping(pvs)) {
            return;
        }
        try {
            Method method = (Method) this.member;
            ReflectionUtils.makeAccessible(method);
            method.invoke(target, getResourceToInject(target, requestingBeanName));
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

注入實現時發現如果為靜態域及方法時直接操作下一個屬性或方法,觀察對于非靜態屬性或方法的注入操作時,是可以完成對靜態域的注入的,所以說這個warning并不是實現不支持,即對靜態域注入的檢查不是一個bug,而是一個feature,所以開篇舉例的解決方案其實只是一些workaround的方案,本質上企圖通過反射修改靜態域就是有問題,此問題的討論可以參考官方開發者的一段回復:

The conceptual problem here is that annotation-driven injection happens for each bean instance. So we shouldn't inject static fields or static methods there because that would happen for every instance of that class. The injection lifecycle is tied to the instance lifecycle, not to the class lifecycle. Bridging between an instance's state and static accessor - if really desired - is up to the concrete bean implementation but arguably shouldn't be done by the framework itself.

更多可參考:SPR-3845Inject bean into static method#comment

在尋找不能注入靜態域的過程中,想起了以前編寫單元測試時類似的場景:

單元測試為什么要引入Spring?

單元測試場景很簡單,要測試OrderPaymentService的save方法,其中關聯了OrderPaymentDAO,為了免去OrderPaymentDAO對測試的影響,對OrderPaymentDAO的特定行為(insert)進行mock(模擬),其中使用了mockitospringokito來完成相應bean的mock以及注入,完美解決了已注入依賴行為無法被mock的問題,最終實現如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = SpringockitoContextLoader.class,
    locations = {"classpath:spring-test.xml", "classpath:spring-mock.xml"})
public class OrderPaymentServiceTest {

  @Resource
  private OrderPaymentService orderPaymentService;

  @Resource
  private OrderPaymentDAO orderPaymentDAO;//this bean is mocked

  @Before
  public void init(){
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void saveWithNormalDataTest() throws Exception {
    //build data
    OrderPaymentDO orderPaymentDO = new OrderPaymentDO();
    
    //define mock behaviour
    when(orderPaymentDAO.insert(any(OrderPayment.class))).thenReturn(2);

    //invoke
    boolean result = orderPaymentService.save(orderPaymentDO);
    
    //assert
    Assert.assertFalse(result);
  }
}
<!--spring-test.xml-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<context:component-scan base-package="com.springapp.mvc.pay"/>
<!--spring-mock.xml-->
<mockito:mock id="orderPaymentDAO" class="com.springapp.mvc.pay.OrderPaymentDAO"/>

當時的思考路徑應該是這樣的:

  1. 因為使用了spring,變查閱spring下ut該如何編寫,參考@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration()啟動spring并執行單測
  2. 啟動spring后注入對象的行為無法mock,便檢索mock spring注入的依賴對象的行為,發現可以使用springokito

當啟動測試,Spring啟動了,實例化了OrderPaymentService,springokito也將orderPaymentDAO mock化并注入到orderPaymentService中并完成了后續測試,得到了一個綠色的成功結果條,以為得到了完美的方案。
最后得到的單元測試代碼就變得臃腫低效,雖然最后問題解決了,整個思考過程似乎并沒有什么問題。
后來參考此方式又寫了一些測試用例,后來越發感覺哪里不對。忽然有一天終于開始質疑:

單元測試為什么一定要引入Spring?

然后將單元測試調整為:

public class OrderPaymentServiceTest {

  private OrderPaymentService orderPaymentService;

  private OrderPaymentDAO orderPaymentDAO;//this bean is mocked

  @Before
  public void init(){
    orderPaymentDAO = mock(OrderPaymentDAO.class);
  }

  @Test
  public void saveWithNormalDataTest() throws Exception {
    //build data
    OrderPaymentDO orderPaymentDO = new OrderPaymentDO();
    
    //define mock behaviour
    when(orderPaymentDAO.insert(any(OrderPayment.class))).thenReturn(2);

    //inject mocked orderPaymentDAO into orderPaymentService
    orderPaymentService.setOrderPaymentDAO(orderPaymentDAO);
    //invoke
    boolean result = orderPaymentService.save(orderPaymentDO);
    
    //assert
    Assert.assertFalse(result);
  }

}

沒有多余配置項,UT執行時也無需等待Spring的IoC、Bean被mock過程,單元測試只剩下測試所需要的東西。

思考方式調整為:如何mock一個類的成員?最終的解決方案就顯而易見了。

想起耗子叔叔的一篇舊文http://coolshell.cn/articles/10804.html,在思考問題的時,會順著一些固有的思路一直走下去,走到一個節點發現過不去了,就糾結著如何解決這個問題,而很少去考慮思考路徑是否正確,甚至思考的出發點是否就錯了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容