假设我们有这样一个场景,为了保证线程安全的使用一个类的方法,我们需要在每个线程中都需要创建该类(这里记作B类)的实例,这个时候我们又是在单例bean(这里记作A类)通过@Autowired注解注入的,这个时候每次获取A类的实例并不是每次都是不同的,而是相同的,这就违背了我们的意愿了,那么在spring中有哪些解决方案呢?
这里我给出了3个方案
方案1:使用注解@Lookup
方案2:由于注解@Lookup不能与@Bean协作,使用注解@Autowired+ObjectProvider
方案3:使用ApplicationContext
简单原理:
上面3个方案其实都是直接或者间接的通过BeanFactory的getBean方法返回原型bean实例来保证每次获取的bean实例都是不一样的,这也是抛开spring提供的解决方案的最终统一实现。
1.使用注解@Lookup
代码演示
单例User:
package com.duxd.prototype;
@Component
public class User {
@Lookup
public Car getCar() {
//这里完全可以输出null,这里返回不为null完全为了测试
return new Car("保时捷");
}
}
普通类Car:
重写了toString方法输出name和实例的hashCode
package com.duxd.prototype;
public class Car {
private String name;
public Car(String name) {
this.name = name;
}
//输出对象的name和hashcode
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
"hashCode='" + this.hashCode() + '\'' +
'}';
}
}
配置类:
输出原型实例的hashcode
@Configuration(proxyBeanMethods = false)
@ComponentScan("com.duxd.prototype")
public class LookupTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(LookupTest.class);
context.refresh();
System.out.println("user对象class类型:" + context.getBean(User.class));
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
}
//向容器注入car,并且scope是原型
@Bean("car")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Car car(){
return new Car("法拉利");
}
}
输出结果:
user对象class类型:com.duxd.prototype.User$$EnhancerBySpringCGLIB$$f761903c@57c758ac
car对象:Car{name='法拉利'hashCode='178049969'}
car对象:Car{name='法拉利'hashCode='333683827'}
根据结果输出的hashCode不一样验证,使用注解@Lookup达到了效果。而且输出的name不是“保时捷”,所以我们的getCar方法逻辑并没有执行,所以我们在getCar方法中直接返回null值是可以的。
提醒:使用注解@Lookup注解来获取实例标注的方法签名必须满足一下条件:
[abstract] theMethodName(no-arguments);
因为实现原理是通过CGLIB代理创建User实例的,所以这里的getCar方法必须满足被重写的条件,必须不能是private的,不能是final的,其实不管你这里返回什么,这个方法已经被CGLIB代理重写了,而且不会调用super.getCar方法,所以这里返回什么都没有任何作用。
根据上面代码来看User类感觉是个工具类,而且并没有使用@Autowired注解注入Car实例,其实加上去也不会用任何作用,因为我们是通过getCar方法获取的,spring会将带有lookup注解的类使用CGLIB代理创建实例的,所以我们没有必要在User类里提供Car的属性了。
注意:@Lookup使用陷阱:
根据官方文档描述:
Recommendations for typical Spring configuration scenarios: When a concrete class may be needed in certain scenarios, consider providing stub implementations of your lookup methods. And please remember that lookup methods won't work on beans returned from @Bean methods in configuration classes; you'll have to resort to @Inject Provider or the like instead.
文档中提示了,@Lookup注解下的当前类不能通过@Bean方式注入,这样@Lookup不起作用。必须通过@Component注解标注类然后通过扫描的方式注入。
最后一句话还给我们提示了替代方案,下面有代码会分析到。
错误代码示例:将user通过@Bean的方式注入
User类将@Component注解去掉,并且提供一个静态对象来验证getCar里面的逻辑被执行了,也就是User类没有被代理。
public class User {
static final Car car = new Car("保时捷");
@Lookup
public Car getCar() {
//这里返回一个静态对象验证这个方法的逻辑被执行了
return User.car;
}
}
配置类添加@Bean注入User实例
@Configuration(proxyBeanMethods = false)
@ComponentScan("com.duxd.prototype")
public class LookupTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(LookupTest.class);
context.refresh();
System.out.println("user对象class类型:" + context.getBean(User.class));
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
}
//向容器注入car,并且scope是原型
@Bean("car")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Car car(){
return new Car("法拉利");
}
@Bean
public User user(){
return new User();
}
}
输出结果:
user对象class类型:com.duxd.prototype.User@258e2e41
car对象:Car{name='保时捷'hashCode='64133603'}
car对象:Car{name='保时捷'hashCode='64133603'}
输出结果可以看出,User类并没有被代理,所以getCar的逻辑会被执行,返回的是我们在User类中的静态实例,通过IOC容器创建的。
上面提到过根据官方文档最后还有一句话:
you'll have to resort to @Inject Provider or the like instead.
也就是通过依赖注入一个Provider对象(这里的Provider在spring里是ObjecrProvider或者是ObectFactory),那么我们来试试,我们只需要修改User类和配置类即可
public class User {
@Autowired
private ObjectProvider car;
public Car getCar(){
return car.getIfAvailable();
}
}
配置类:
@Configuration(proxyBeanMethods = false)
@ComponentScan("com.duxd.prototype")
public class LookupTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(LookupTest.class);
context.refresh();
System.out.println("user对象class类型:" + context.getBean(User.class));
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
}
//向容器注入car,并且scope是原型
@Bean("car")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Car car(){
return new Car("法拉利");
}
@Bean
public User user(){
return new User();
}
}
输出结果:
user对象class类型:com.duxd.prototype.User@5906ebcb
car对象:Car{name='法拉利'hashCode='1436901839'}
car对象:Car{name='法拉利'hashCode='1866161430'}
结论:
1.使用ObjectProvider的方式注入原型Bean不是通过动态代理实现的
2.根据hashcode不一样判断确实是返回了两个对象,根据name是“法拉利”而不是“保时捷”判断,我们的getCar方法逻辑没有执行,而是通过IOC注入的。
3.方案1和方案2输出的结果都是我们想要的。
这一块我单独一篇文章讲解,分析@Lookup注解有效和无效的实现原理,也会分析ObjectProvider有效的原因。
1:既然可以使用ObjectProvider,那么可以使用jdk的Optional吗?
这里先给出答案:不可以,这块分析也在下篇文章一起源码分析。
Optional测试示例:
@Component
public class User {
static final Car DEFAULT_CAR = new Car("保时捷");
@Autowired
private Optional car;
public Car getCar(){
return car.orElse(User.DEFAULT_CAR);
}
}
@Configuration(proxyBeanMethods = false)
@ComponentScan("com.duxd.prototype")
public class LookupTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(LookupTest.class);
context.refresh();
System.out.println("user对象class类型:" + context.getBean(User.class));
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
System.out.println("car对象:" + context.getBean(User.class).getCar().toString());
}
//向容器注入car,并且scope是原型
@Bean("car")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Car car(){
return new Car("法拉利");
}
}
输出结果:
user对象class类型:com.duxd.prototype.User@7920ba90
car对象:Car{name='法拉利'hashCode='112466394'}
car对象:Car{name='法拉利'hashCode='112466394'}