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'}

 

Spring Data MongoDB中,可以使用MongoDB的聚合管道(Aggregation)和lookup操作将两个集合关联起来。 假设我们有两个集合:orders和customers,orders集合包含每个订单的信息,而customers集合包含每个客户的信息。我们希望在查询订单时,将每个订单的客户信息也一起查询出来。 可以使用聚合管道中的$lookup操作实现这个需求。具体步骤如下: 1. 创建一个Orders类来表示订单,其中包含客户ID字段: ``` @Document(collection = "orders") public class Order { @Id private String id; private String customerId; private double amount; // getters and setters } ``` 2. 创建一个Customers类来表示客户,其中包含客户ID和客户名字字段: ``` @Document(collection = "customers") public class Customer { @Id private String id; private String name; // getters and setters } ``` 3. 创建一个OrderWithCustomer类来表示订单和客户信息,其中包含Order和Customer对象: ``` public class OrderWithCustomer { private Order order; private Customer customer; // getters and setters } ``` 4. 使用聚合管道查询订单和客户信息: ``` @Autowired private MongoTemplate mongoTemplate; public List<OrderWithCustomer> findOrdersWithCustomers() { LookupOperation lookupOperation = LookupOperation.newLookup() .from("customers") .localField("customerId") .foreignField("id") .as("customer"); Aggregation aggregation = Aggregation.newAggregation( lookupOperation, Aggregation.unwind("customer"), Aggregation.project() .and("customer._id").as("customer.id") .and("customer.name").as("customer.name") .and("amount").as("order.amount") .andExclude("_id"), Aggregation.replaceRoot().withValueOf(ObjectOperators.valueOf("order")) ); AggregationResults<OrderWithCustomer> results = mongoTemplate.aggregate(aggregation, "orders", OrderWithCustomer.class); return results.getMappedResults(); } ``` 在聚合管道中,我们首先使用$lookup操作将orders集合中的customerId字段和customers集合中的_id字段进行关联,得到每个订单对应的客户信息。然后使用$unwind操作将客户信息拆分成多个文档,方便后续处理。接着使用$project操作将客户信息和订单信息合并到一个文档中。最后使用$replaceRoot操作将合并后的文档替换为原来的订单文档,得到最终的查询结果。 这样,我们就可以通过聚合管道和lookup操作实现两个集合的关联查询。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值