Spring的@Lookup方法注入黑魔法 🪄

52 阅读9分钟

一、开篇故事:单例老板与临时工的困境 👔

想象这样一个场景:

你是一家公司的老板(Singleton Bean),负责分配任务。公司有很多临时工(Prototype Bean),每个任务都需要一个新的临时工来完成。

问题来了:

@Component
@Scope("singleton") // 你是单例老板
public class Boss {
    @Autowired
    private Worker worker; // 注入临时工
    
    public void assignTask() {
        worker.doWork(); // 每次都是同一个临时工!😱
    }
}

@Component
@Scope("prototype") // 临时工应该是多例的
public class Worker {
    public void doWork() {
        System.out.println("工人ID: " + this.hashCode());
    }
}

期望: 每次assignTask()都用新的临时工
现实: 因为Boss是单例,注入的Worker也是同一个!

怎么办? 🤔


二、问题分析:单例依赖多例的悖论 💔

2.1 核心矛盾

单例Bean(Singleton)
  ├─ 只创建一次
  ├─ 依赖注入也只执行一次
  └─ 注入的Prototype Bean"固定"了!
  
多例Bean(Prototype)
  ├─ 每次获取都应该创建新实例
  └─ 但被单例Bean持有,变成了"伪单例"

2.2 图解问题

时刻1: Boss创建
Boss (单例)
  └─ worker: Worker@123 (第一次注入)

时刻2: assignTask()
Boss (单例)
  └─ worker: Worker@123 (还是同一个!)

时刻3: assignTask()
Boss (单例)
  └─ worker: Worker@123 (还是同一个!!)
  
❌ 期望每次都是新的Worker,但一直是Worker@123

2.3 生活类比

场景: 你雇了一个管家(单例),管家需要每天找不同的快递员(多例)送货。

问题:

  • 管家被雇佣时(注入依赖),找了一个快递员A
  • 以后每次送货,管家都让快递员A去送
  • 其他快递员B、C、D都失业了!😢

期望:

  • 每次送货都换一个新快递员

三、传统解决方案(笨拙版)😅

方案1:注入ApplicationContext

@Component
@Scope("singleton")
public class Boss {
    @Autowired
    private ApplicationContext context;
    
    public void assignTask() {
        // 每次手动从容器获取新实例
        Worker worker = context.getBean(Worker.class);
        worker.doWork();
    }
}

缺点:

  • ❌ 侵入性强,代码耦合Spring容器
  • ❌ 不优雅,违反依赖注入原则
  • ❌ 测试困难

方案2:注入ObjectProvider

@Component
@Scope("singleton")
public class Boss {
    @Autowired
    private ObjectProvider<Worker> workerProvider;
    
    public void assignTask() {
        // 每次从Provider获取新实例
        Worker worker = workerProvider.getObject();
        worker.doWork();
    }
}

优点:

  • ✅ 比ApplicationContext优雅一些
  • ✅ 解耦容器

缺点:

  • ❌ 还是需要手动调用getObject()

四、@Lookup闪亮登场!⭐

4.1 什么是@Lookup?

@Lookup 是Spring提供的方法注入注解,可以让Spring动态重写你的方法,每次调用都从容器获取新的Bean实例。

4.2 基本用法

@Component
@Scope("singleton")
public class Boss {
    
    // 抽象方法或空方法,Spring会自动实现
    @Lookup
    public Worker getWorker() {
        return null; // 这里返回什么都无所谓,Spring会重写
    }
    
    public void assignTask() {
        Worker worker = getWorker(); // 每次都是新实例!
        worker.doWork();
    }
}

@Component
@Scope("prototype")
public class Worker {
    public void doWork() {
        System.out.println("工人ID: " + this.hashCode());
    }
}

输出:

工人ID: 123456
工人ID: 789012  ← 不同的实例!
工人ID: 345678  ← 每次都是新的!

4.3 生活类比

@Lookup就像给管家装了个"自动找人App":

管家:"需要快递员。"
App(Spring):"好的,给你找一个新的!"
  → 第1次:分配快递员A
  → 第2次:分配快递员B(新的)
  → 第3次:分配快递员C(新的)

五、@Lookup的实现原理 🔍

5.1 CGLIB动态代理

核心技术: Spring使用CGLIB生成Boss的子类,重写@Lookup方法。

原始类:Boss
  └─ getWorker() { return null; }

CGLIB生成子类:Boss$$EnhancerBySpringCGLIB$$12345678
  └─ getWorker() {
       return beanFactory.getBean(Worker.class); // Spring实现的!
     }

5.2 字节码生成

// 原始代码
public abstract class Boss {
    @Lookup
    public abstract Worker getWorker();
}

// CGLIB生成的子类(伪代码)
public class Boss$$CGLIB extends Boss {
    private BeanFactory beanFactory; // Spring注入
    
    @Override
    public Worker getWorker() {
        // 每次调用都从容器获取新实例
        return (Worker) this.beanFactory.getBean(Worker.class);
    }
}

5.3 图解过程

1. Spring扫描到@Lookup
   ↓
2. 使用CGLIB创建Boss的子类
   ↓
3. 重写getWorker()方法
   ↓
4. 方法内部调用beanFactory.getBean()
   ↓
5. 每次调用getWorker()都返回新的Worker实例

5.4 源码分析

// CglibSubclassingInstantiationStrategy.java
public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) {
    
    // 检查是否有@Lookup方法
    if (bd.hasMethodOverrides()) {
        // 使用CGLIB创建子类
        return instantiateWithMethodInjection(bd, beanName, owner);
    } else {
        // 普通Bean,直接实例化
        return super.instantiate(bd, beanName, owner);
    }
}

// LookupOverride.java
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) {
    // 拦截@Lookup方法
    LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
    
    // 从容器获取Bean
    Object bean = this.owner.getBean(lo.getBeanName());
    
    return bean; // 返回新实例
}

六、@Lookup的多种用法 🛠️

6.1 抽象方法(推荐)

@Component
public abstract class CommandManager {
    
    @Lookup
    public abstract Command createCommand(); // 抽象方法
    
    public void process() {
        Command command = createCommand();
        command.execute();
    }
}

@Component
@Scope("prototype")
public class Command {
    public void execute() {
        System.out.println("执行命令:" + this.hashCode());
    }
}

优点:

  • ✅ 语义清晰,一看就知道是动态方法
  • ✅ 强制子类实现(虽然Spring会代理)

6.2 具体方法(也可以)

@Component
public class CommandManager {
    
    @Lookup
    public Command createCommand() {
        return null; // Spring会忽略这个返回值
    }
    
    public void process() {
        Command command = createCommand();
        command.execute();
    }
}

6.3 指定Bean名称

@Component
public abstract class NotificationService {
    
    @Lookup("emailSender") // 指定Bean名称
    public abstract MessageSender getSender();
    
    public void sendNotification(String message) {
        MessageSender sender = getSender();
        sender.send(message);
    }
}

@Component("emailSender")
@Scope("prototype")
public class EmailSender implements MessageSender {
    public void send(String message) {
        System.out.println("发送邮件:" + message);
    }
}

6.4 带参数的方法

@Component
public abstract class TaskExecutor {
    
    @Lookup
    public abstract Task createTask(String taskName); // 带参数
    
    public void execute(String taskName) {
        Task task = createTask(taskName);
        task.run();
    }
}

@Component
@Scope("prototype")
public class Task {
    private String name;
    
    // Spring会自动处理参数
    public Task(String name) {
        this.name = name;
    }
    
    public void run() {
        System.out.println("执行任务:" + name);
    }
}

七、实战案例:订单处理系统 📦

需求

每个订单需要一个独立的处理器(Processor),处理器包含订单的上下文信息,不能共享。

实现

// 订单服务(单例)
@Service
public abstract class OrderService {
    
    @Lookup
    protected abstract OrderProcessor createProcessor();
    
    public void processOrder(Order order) {
        // 每个订单都用新的处理器
        OrderProcessor processor = createProcessor();
        processor.setOrder(order);
        processor.process();
    }
}

// 订单处理器(多例)
@Component
@Scope("prototype")
public class OrderProcessor {
    private Order order;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
    
    public void setOrder(Order order) {
        this.order = order;
    }
    
    public void process() {
        System.out.println("处理订单:" + order.getId() + 
                          ",处理器ID:" + this.hashCode());
        
        // 1. 扣减库存
        inventoryService.deduct(order);
        
        // 2. 处理支付
        paymentService.pay(order);
        
        // 3. 更新订单状态
        order.setStatus("PAID");
    }
}

// Controller
@RestController
@RequestMapping("/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping
    public String createOrder(@RequestBody Order order) {
        orderService.processOrder(order);
        return "订单处理成功";
    }
}

测试

@SpringBootTest
public class OrderServiceTest {
    
    @Autowired
    private OrderService orderService;
    
    @Test
    public void testMultipleProcessors() {
        Order order1 = new Order(1L);
        Order order2 = new Order(2L);
        Order order3 = new Order(3L);
        
        orderService.processOrder(order1);
        orderService.processOrder(order2);
        orderService.processOrder(order3);
    }
}

输出:

处理订单:1,处理器ID:123456
处理订单:2,处理器ID:789012  ← 不同的处理器
处理订单:3,处理器ID:345678  ← 每个订单都是新处理器

八、@Lookup vs 其他方案对比 📊

方案优雅度侵入性性能推荐度
@Lookup⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
ObjectProvider⭐⭐⭐⭐⭐⭐⭐⭐
ApplicationContext⭐⭐⭐⭐
手动工厂类⭐⭐⭐⭐⭐⭐

代码对比

// 方案1:@Lookup(最优雅)
@Lookup
public abstract Worker getWorker();

public void assignTask() {
    Worker worker = getWorker(); // 简洁!
    worker.doWork();
}

// 方案2:ObjectProvider(次优)
@Autowired
private ObjectProvider<Worker> workerProvider;

public void assignTask() {
    Worker worker = workerProvider.getObject(); // 稍微啰嗦
    worker.doWork();
}

// 方案3:ApplicationContext(不推荐)
@Autowired
private ApplicationContext context;

public void assignTask() {
    Worker worker = context.getBean(Worker.class); // 耦合容器
    worker.doWork();
}

九、注意事项与坑 ⚠️

坑1:类不能是final

// ❌ 错误:final类无法被CGLIB继承
@Component
public final class Boss { // final!
    @Lookup
    public Worker getWorker() {
        return null;
    }
}

// 报错:Cannot create CGLIB subclass of final class

解决: 去掉final关键字

坑2:方法不能是private

// ❌ 错误:private方法无法被重写
@Component
public class Boss {
    @Lookup
    private Worker getWorker() { // private!
        return null;
    }
}

// @Lookup不生效,每次都返回null

解决: 改成public或protected

坑3:方法不能是final

// ❌ 错误:final方法无法被重写
@Component
public class Boss {
    @Lookup
    public final Worker getWorker() { // final!
        return null;
    }
}

解决: 去掉final关键字

坑4:必须是Spring管理的Bean

// ❌ 错误:Boss不是Spring Bean
public class Boss { // 没有@Component
    @Lookup
    public Worker getWorker() {
        return null;
    }
}

// @Lookup不生效

解决: 加上@Component注解

坑5:返回类型必须匹配

@Component
public abstract class Boss {
    
    @Lookup
    public abstract Worker getWorker();
}

@Component
@Scope("prototype")
public class SpecialWorker extends Worker {
    // ...
}

// 使用
Worker worker = getWorker(); // OK,返回SpecialWorker
SpecialWorker specialWorker = getWorker(); // ❌ 类型不匹配!

十、高级用法 🚀

10.1 结合@Scope实现请求级Bean

@Component
public abstract class UserService {
    
    @Lookup
    public abstract RequestContext getRequestContext();
    
    public void processRequest() {
        RequestContext context = getRequestContext();
        context.setAttribute("user", getCurrentUser());
    }
}

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private Map<String, Object> attributes = new HashMap<>();
    
    public void setAttribute(String key, Object value) {
        attributes.put(key, value);
    }
}

10.2 策略模式实现

@Component
public abstract class PaymentService {
    
    @Lookup("alipayStrategy")
    public abstract PaymentStrategy getAlipayStrategy();
    
    @Lookup("wechatStrategy")
    public abstract PaymentStrategy getWechatStrategy();
    
    public void pay(Order order, String paymentType) {
        PaymentStrategy strategy;
        if ("alipay".equals(paymentType)) {
            strategy = getAlipayStrategy();
        } else {
            strategy = getWechatStrategy();
        }
        strategy.pay(order);
    }
}

@Component("alipayStrategy")
@Scope("prototype")
public class AlipayStrategy implements PaymentStrategy {
    public void pay(Order order) {
        System.out.println("支付宝支付:" + order.getAmount());
    }
}

@Component("wechatStrategy")
@Scope("prototype")
public class WechatStrategy implements PaymentStrategy {
    public void pay(Order order) {
        System.out.println("微信支付:" + order.getAmount());
    }
}

10.3 对象池模式

@Component
public abstract class ConnectionPool {
    
    @Lookup
    protected abstract Connection createConnection();
    
    private Queue<Connection> pool = new ConcurrentLinkedQueue<>();
    private AtomicInteger count = new AtomicInteger(0);
    
    public Connection getConnection() {
        Connection conn = pool.poll();
        if (conn == null && count.get() < 10) {
            conn = createConnection(); // 创建新连接
            count.incrementAndGet();
        }
        return conn;
    }
    
    public void releaseConnection(Connection conn) {
        pool.offer(conn); // 归还连接池
    }
}

@Component
@Scope("prototype")
public class Connection {
    private final String id = UUID.randomUUID().toString();
    
    public void execute(String sql) {
        System.out.println("连接" + id + "执行SQL:" + sql);
    }
}

十一、面试高频问题 🎤

Q1: @Lookup的作用是什么?

答: @Lookup用于解决单例Bean依赖多例Bean的问题。它通过CGLIB动态代理,重写标注的方法,使每次调用都从Spring容器获取新的Bean实例。

Q2: @Lookup的实现原理?

答: Spring使用CGLIB生成当前类的子类,重写@Lookup标注的方法。重写后的方法内部调用beanFactory.getBean()获取Bean实例,从而保证每次调用都返回新对象。

Q3: @Lookup有哪些限制?

答:

  1. 类不能是final(无法被CGLIB继承)
  2. 方法不能是private(无法被重写)
  3. 方法不能是final(无法被重写)
  4. 必须是Spring管理的Bean

Q4: @Lookup和ObjectProvider的区别?

答:

  • @Lookup:更优雅,无需手动调用getObject(),像普通方法一样使用
  • ObjectProvider:需要注入Provider,手动调用getObject()获取实例
  • 推荐优先使用@Lookup

Q5: 什么场景适合用@Lookup?

答:

  1. 单例Bean需要获取多例Bean
  2. 命令模式(每次执行都创建新命令对象)
  3. 策略模式(每次使用都创建新策略实例)
  4. 对象池(按需创建新对象)

十二、最佳实践 💡

1. 使用抽象方法

// ✅ 推荐
public abstract class Boss {
    @Lookup
    public abstract Worker getWorker();
}

// ❌ 不推荐(虽然可以)
public class Boss {
    @Lookup
    public Worker getWorker() {
        return null; // 多余的返回值
    }
}

2. 方法命名清晰

// ✅ 好的命名
@Lookup
public abstract Task createTask();

@Lookup
public abstract Command newCommand();

// ❌ 不好的命名
@Lookup
public abstract Task get(); // 太笼统

@Lookup
public abstract Task task(); // 不清楚是获取还是创建

3. 配合接口使用

public interface TaskFactory {
    Task createTask();
}

@Component
public abstract class TaskFactoryImpl implements TaskFactory {
    @Override
    @Lookup
    public abstract Task createTask();
}

十三、总结口诀 📝

单例依赖多例难,
@Lookup来帮忙。
CGLIB生成子类代,
方法重写藏玄机。

抽象方法最优雅,
每次调用新实例。
类不能final要记清,
方法private也不行。

命令策略对象池,
@Lookup都能搞定。
Spring黑魔法虽酷炫,
合理使用才是王道!

十四、完整示例项目 📁

// 1. 抽象命令
@Component
@Scope("prototype")
public class PrintCommand {
    private String message;
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public void execute() {
        System.out.println("打印:" + message + 
                          ",命令ID:" + this.hashCode());
    }
}

// 2. 命令管理器
@Component
public abstract class CommandManager {
    
    @Lookup
    public abstract PrintCommand createPrintCommand();
    
    public void print(String message) {
        PrintCommand command = createPrintCommand();
        command.setMessage(message);
        command.execute();
    }
}

// 3. 控制器
@RestController
@RequestMapping("/commands")
public class CommandController {
    
    @Autowired
    private CommandManager commandManager;
    
    @GetMapping("/print")
    public String print(@RequestParam String message) {
        commandManager.print(message);
        return "打印成功";
    }
}

// 4. 测试
@Test
public void testLookup() {
    commandManager.print("Hello");  // 命令ID:123456
    commandManager.print("World");  // 命令ID:789012
    commandManager.print("Spring"); // 命令ID:345678
    
    // 每次都是不同的命令实例!✅
}

参考资料 📚


系列完结! 🎉
从131到135,五大知识点全部搞定!


编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0

愿你的代码像魔法一样优雅! 🪄✨