There is no dumb question.
文章下半段更精彩
使用Spring DI過程中試圖注入靜態(tài)域或靜態(tài)方法時(shí),Spring會(huì)報(bào)如下Warning
,導(dǎo)致未注入:
//測試代碼
@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
解決方案
解決方案如下:
使用構(gòu)造方法、代理的setter亦或init-method來將靜態(tài)域值注入
//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為什么不允許注入靜態(tài)域?
首先可以確定的是,靜態(tài)域注入肯定是可以,可以看下面這段比較Evil的實(shí)現(xiàn):
//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的無法實(shí)現(xiàn)么?
//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();
}
}
}
注入實(shí)現(xiàn)時(shí)發(fā)現(xiàn)如果為靜態(tài)域及方法時(shí)直接操作下一個(gè)屬性或方法,觀察對(duì)于非靜態(tài)屬性或方法的注入操作時(shí),是可以完成對(duì)靜態(tài)域的注入的,所以說這個(gè)warning
并不是實(shí)現(xiàn)不支持,即對(duì)靜態(tài)域注入的檢查不是一個(gè)bug,而是一個(gè)feature,所以開篇舉例的解決方案其實(shí)只是一些workaround的方案,本質(zhì)上企圖通過反射修改靜態(tài)域就是有問題,此問題的討論可以參考官方開發(fā)者的一段回復(fù):
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
在尋找不能注入靜態(tài)域的過程中,想起了以前編寫單元測試時(shí)類似的場景:
單元測試為什么要引入Spring?
單元測試場景很簡單,要測試OrderPaymentService
的save方法,其中關(guān)聯(lián)了OrderPaymentDAO
,為了免去OrderPaymentDAO
對(duì)測試的影響,對(duì)OrderPaymentDAO
的特定行為(insert)進(jìn)行mock
(模擬),其中使用了mockito
及springokito
來完成相應(yīng)bean的mock以及注入,完美解決了已注入依賴行為無法被mock的問題,最終實(shí)現(xiàn)如下:
@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"/>
當(dāng)時(shí)的思考路徑應(yīng)該是這樣的:
- 因?yàn)槭褂昧藄pring,變查閱spring下ut該如何編寫,參考
@RunWith(SpringJUnit4ClassRunner.class)
及@ContextConfiguration()
啟動(dòng)spring并執(zhí)行單測 - 啟動(dòng)spring后注入對(duì)象的行為無法mock,便檢索mock spring注入的依賴對(duì)象的行為,發(fā)現(xiàn)可以使用
springokito
當(dāng)啟動(dòng)測試,Spring啟動(dòng)了,實(shí)例化了OrderPaymentService,springokito也將orderPaymentDAO mock化并注入到orderPaymentService中并完成了后續(xù)測試,得到了一個(gè)綠色的成功結(jié)果條,以為得到了完美的方案。
最后得到的單元測試代碼就變得臃腫低效,雖然最后問題解決了,整個(gè)思考過程似乎并沒有什么問題。
后來參考此方式又寫了一些測試用例,后來越發(fā)感覺哪里不對(duì)。忽然有一天終于開始質(zhì)疑:
單元測試為什么一定要引入Spring?
然后將單元測試調(diào)整為:
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);
}
}
沒有多余配置項(xiàng),UT執(zhí)行時(shí)也無需等待Spring的IoC、Bean被mock過程,單元測試只剩下測試所需要的東西。
思考方式調(diào)整為:如何mock一個(gè)類的成員?最終的解決方案就顯而易見了。
想起耗子叔叔的一篇舊文http://coolshell.cn/articles/10804.html,在思考問題的時(shí),會(huì)順著一些固有的思路一直走下去,走到一個(gè)節(jié)點(diǎn)發(fā)現(xiàn)過不去了,就糾結(jié)著如何解決這個(gè)問題,而很少去考慮思考路徑是否正確,甚至思考的出發(fā)點(diǎn)是否就錯(cuò)了。