spring注解@Lookup使用原理和注意点以及其他替换实现方案

假设场景

假设我们有这样一个场景,为了保证线程安全的使用一个类的方法,我们需要在每个线程中都需要创建该类(这里记作B类)的实例,这个时候我们又是在单例bean(这里记作A类)通过@Autowired注解注入的,这个时候每次获取A类的实例并不是每次都是不同的,而是相同的,这就违背了我们的意愿了,那么在spring中有哪些解决方案呢?

 

 

Spring解决方案

这里我给出了3个方案

方案1:使用注解@Lookup

方案2:由于注解@Lookup不能与@Bean协作,使用注解@Autowired+ObjectProvider<T>类(或者ObjectFactory<T>)+@Bean

方案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注解来获取实例标注的方法签名必须满足一下条件:

<public|protected> [abstract] <return-type> 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<TargetBean> 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<TargetBean> or the like instead.

也就是通过依赖注入一个Provider对象(这里的Provider在spring里是ObjecrProvider或者是ObectFactory),那么我们来试试,我们只需要修改User类和配置类即可

public class User {
    @Autowired
    private ObjectProvider<Car> 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> 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'}

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值