你真的會用Spring嗎?如何在單例Bean中注入原型Bean

  • 2019 年 12 月 15 日
  • 筆記

遇到什麼問題

假設單例 BeanA 需要使用原型 BeanB(BeanB 可能是 BeanA 的一個屬性值)。可是容器僅創建一次單例 BeanA,因此只有一次機會來設置屬性 BeanB。

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  @Service  public class OrderService {  }    @Service  public class UserService {    	@Autowired  	private OrderService orderService;    	public OrderService getOrderService() {  		return orderService;  	}  }    @Configuration  @ComponentScan  public class Main {  	public static void main(String[] args) {  		AnnotationConfigApplicationContext context =  				new AnnotationConfigApplicationContext(Main.class);  		UserService userService = context.getBean(UserService.class);  		OrderService orderService = userService.getOrderService();  		OrderService orderService1 = userService.getOrderService();  		//ture  		System.out.println(orderService == orderService1);  	}  }

如果直接使用@Autowired注入,容器僅創建一次單例UserService,因此只有一次機會來設置OrderService

那麼,如何在單例 Bean 中注入原型 Bean 呢?

解決方案 1:實現 ApplicationContextAware

第一種解決方案,可以讓UserService實現ApplicationContextAware接口,然後在每次需要使用原型 BeanOrderService時通過調用容器的getBean方法。

@Service  public class UserService implements ApplicationContextAware {    	private ApplicationContext context;    	public OrderService getOrderService() {  		return context.getBean(OrderService.class);  	}    	@Override  	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  		context = applicationContext;  	}  }

Spring 官方並不建議使用這種方式:

The preceding is not desirable, because the business code is aware of and coupled to the Spring Framework. Method Injection, a somewhat advanced feature of the Spring IoC container, lets you handle this use case cleanly. 前面的內容是不理想的,因為業務代碼知道並耦合到 Spring 框架。方法注入是 Spring IoC 容器的一項高級功能,使您可以乾淨地處理此用例。

解決方案 2:使用@Lookup,實現方法注入

@Lookup

先來看一下@Lookup源碼

@Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface Lookup {    	/**  	 * This annotation attribute may suggest a target bean name to look up.  	 * If not specified, the target bean will be resolved based on the  	 * annotated method's return type declaration.  	 */  	String value() default "";    }

@Lookup默認是通過方法的返回類型聲明來解析目標 Bean,也可以通過 value 來指定需要查找的目標 BeanName

介紹

https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html#beans-factory-lookup-method-injection Lookup method injection is the ability of the container to override methods on container-managed beans and return the lookup result for another named bean in the container. The lookup typically involves a prototype bean, as in the scenario described in the preceding section. The Spring Framework implements this method injection by using bytecode generation from the CGLIB library to dynamically generate a subclass that overrides the method. 機器翻譯:查找方法注入是容器覆蓋容器管理的 Bean 上的方法並返回容器中另一個命名 Bean 的查找結果的能力。查找通常涉及原型 bean,如上一節中所述。Spring 框架通過使用從 CGLIB 庫生成位元組碼來動態生成覆蓋該方法的子類來實現此方法注入。

使用限制

For this dynamic subclassing to work, the class that the Spring bean container subclasses cannot be final, and the method to be overridden cannot be final, either. 為了使此動態子類起作用,Spring Bean 容器子類的類也不能是 final,而要覆蓋的方法也不能是 final。

Unit-testing a class that has an abstract method requires you to subclass the class yourself and to supply a stub implementation of the abstract method. 對具有抽象方法的類進行單元測試需要您自己對該類進行子類化,並提供該抽象方法的存根實現。

A further key limitation is that lookup methods do not work with factory methods and in particular not with @Bean methods in configuration classes, since, in that case, the container is not in charge of creating the instance and therefore cannot create a runtime-generated subclass on the fly. 另一個關鍵限制是,查找方法不適用於工廠方法,尤其不適用於配置類中的@Bean 方法,因為在這種情況下,容器不負責創建實例,因此無法創建運行時生成的子類。

根據 Spring 官方文檔,我們可以知道:

  1. 方法注入是通過 CGLIB 生成位元組碼來動態生成覆蓋該方法的子類來實現此方法注入
  2. 因為是用 CGLIB 來實現的,所以當前類和當前方法是不能為 final 的
  3. Spring 中使用@Lookup來實現方法注入

使用@Lookup 實現單例 Bean 中注入原型 Bean

@Service  public abstract class UserService {  	@Lookup  	public abstract OrderService getOrderServiceUsingLookup();  }

雖然這個類是抽象的,但是還可以被實例化到 Spring 容器中,因為 Spring 會對當前類生成子類來實現方法注入。至於具體是怎麼生成的增強對象,讀者可以自行 debug 源碼學習。

UserService代理對象