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-3845及Inject bean into static method#comment
在尋找不能注入靜態域的過程中,想起了以前編寫單元測試時類似的場景:
單元測試為什么要引入Spring?
單元測試場景很簡單,要測試OrderPaymentService
的save方法,其中關聯了OrderPaymentDAO
,為了免去OrderPaymentDAO
對測試的影響,對OrderPaymentDAO
的特定行為(insert)進行mock
(模擬),其中使用了mockito
及springokito
來完成相應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"/>
當時的思考路徑應該是這樣的:
- 因為使用了spring,變查閱spring下ut該如何編寫,參考
@RunWith(SpringJUnit4ClassRunner.class)
及@ContextConfiguration()
啟動spring并執行單測 - 啟動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,在思考問題的時,會順著一些固有的思路一直走下去,走到一個節點發現過不去了,就糾結著如何解決這個問題,而很少去考慮思考路徑是否正確,甚至思考的出發點是否就錯了。