你真的会用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代理对象