你真的会用Spring吗?如何在单例Bean中注入原型Bean

2,296 阅读3分钟

遇到什么问题

假设单例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

介绍

docs.spring.io/spring/docs…

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代理对象

关于方法注入更多介绍

关于方法注入的更多介绍,可以参看文章:spring.io/blog/2004/0…


源码注释GITHUB地址:github.com/shenjianeng…

欢迎关注微信公众号:

Corder小黑