問題描述
-
在定義 Bean 時,有時候我們會使用原型 Bean,例如定義如下:
@Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class ServiceImpl { }
-
然后我們按照下面的方式去使用它:
@RestController public class HelloWorldController {@Autowiredprivate ServiceImpl serviceImpl;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + serviceImpl;}; }
-
結果,我們會發現,不管我們訪問多少次http://localhost:8080/hi,訪問的結果都是不變的,如下:
helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af
-
很明顯,這很可能和我們定義 ServiceImpl 為原型 Bean 的初衷背道而馳,如何理解這個現象呢?
案例分析
-
當一個屬性成員 serviceImpl 聲明為 @Autowired 后,那么在創建 HelloWorldController 這個 Bean 時,會先使用構造器反射出實例,然后來裝配各個標記為 @Autowired 的屬性成員(裝配方法參考 AbstractAutowireCapableBeanFactory#populateBean)。
-
具體到執行過程,它會使用很多 BeanPostProcessor 來做完成工作,其中一種是 AutowiredAnnotationBeanPostProcessor,它會通過 DefaultListableBeanFactory#findAutowireCandidates 尋找到 ServiceImpl 類型的 Bean,然后設置給對應的屬性(即 serviceImpl 成員)。
-
關鍵執行步驟可參考 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject:
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Field field = (Field) this.member;Object value;//尋找“bean”if (this.cached) {value = resolvedCachedArgument(beanName, this.cachedFieldValue);}else {//省略其他非關鍵代碼value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);}if (value != null) {//將bean設置給成員字段ReflectionUtils.makeAccessible(field);field.set(bean, value);} }
-
待我們尋找到要自動注入的 Bean 后,即可通過反射設置給對應的 field。這個 field 的執行只發生了一次,所以后續就固定起來了,它并不會因為 ServiceImpl 標記了 SCOPE_PROTOTYPE 而改變。
-
所以,當一個單例的 Bean,使用 autowired 注解標記其屬性時,你一定要注意這個屬性值會被固定下來。
問題修正
- 通過上述源碼分析,我們可以知道要修正這個問題,肯定是不能將 ServiceImpl 的 Bean 固定到屬性上的,而應該是每次使用時都會重新獲取一次。所以這里我提供了兩種修正方式:
1. 自動注入 Context
-
即自動注入 ApplicationContext,然后定義 getServiceImpl() 方法,在方法中獲取一個新的 ServiceImpl 類型實例。修正代碼如下:
@RestController public class HelloWorldController {@Autowiredprivate ApplicationContext applicationContext;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};public ServiceImpl getServiceImpl(){return applicationContext.getBean(ServiceImpl.class);}}
2. 使用 Lookup 注解
-
類似修正方法 1,也添加一個 getServiceImpl 方法,不過這個方法是被 Lookup 標記的。修正代碼如下:
@RestController public class HelloWorldController {@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};@Lookuppublic ServiceImpl getServiceImpl(){return null;} }
- 通過這兩種修正方式,再次測試程序,我們會發現結果已經符合預期(每次訪問這個接口,都會創建新的 Bean)。
-
這里我們不妨再拓展下,討論下 Lookup 是如何生效的。畢竟在修正代碼中,我們看到 getServiceImpl 方法的實現返回值是 null,這或許很難說服自己。
-
首先,我們可以通過調試方式看下方法的執行,參考下圖
-
從上圖我們可以看出,我們最終的執行因為標記了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,這個方法的關鍵實現參考 LookupOverrideMethodInterceptor#intercept:
private final BeanFactory owner;public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);Assert.state(lo != null, "LookupOverride not found");Object[] argsToUse = (args.length > 0 ? args : null); // if no-arg, don't insist on args at allif (StringUtils.hasText(lo.getBeanName())) {return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :this.owner.getBean(lo.getBeanName()));}else {return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :this.owner.getBean(method.getReturnType()));} }
-
我們的方法調用最終并沒有走入案例代碼實現的 return null 語句,而是通過 BeanFactory 來獲取 Bean。所以從這點也可以看出,其實在我們的 getServiceImpl 方法實現中,隨便怎么寫都行,這不太重要。
-
例如,我們可以使用下面的實現來測試下這個結論:
@Lookup public ServiceImpl getServiceImpl(){//下面的日志會輸出么?log.info("executing this method");return null; }
-
以上代碼,添加了一行代碼輸出日志。測試后,我們會發現并沒有日志輸出。這也驗證了,當使用 Lookup 注解一個方法時,這個方法的具體實現已并不重要。
-
再回溯下前面的分析,為什么我們走入了 CGLIB 搞出的類,這是因為我們有方法標記了 Lookup。我們可以從下面的這段代碼得到驗證,參考 SimpleInstantiationStrategy#instantiate:
@Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {// Don't override the class with CGLIB if no overrides.if (!bd.hasMethodOverrides()) {//return BeanUtils.instantiateClass(constructorToUse);}else {// Must generate CGLIB subclass.return instantiateWithMethodInjection(bd, beanName, owner);} }
-
在上述代碼中,當 hasMethodOverrides 為 true 時,則使用 CGLIB。而在本案例中,這個條件的成立在于解析 HelloWorldController 這個 Bean 時,我們會發現有方法標記了 Lookup,此時就會添加相應方法到屬性 methodOverrides 里面去(此過程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)。
-
添加后效果圖如下: