Sprng依赖注入(三):构造方法注入是如何工作的?

文章详细分析了Spring中构造方法注入的工作原理,通过一个示例展示了构造方法注入的过程,指出其与字段注入和setter注入的区别在于注入时机更早,且在bean实例化完成后就已经完成了依赖属性的注入。此外,文章还提到了在bean生命周期中的关键步骤,如BeanPostProcessor和InstantiationAwareBeanPostProcessor的作用。

前言

这是Spring依赖注入系列的第三篇,前两篇主要分析了Spring bean依赖属性注入的两种方式,是字段注入和setter方法注入,单独比较这两种方式,会发现其过程和工作原理非常类似,那么构造方法注入会不会也和前两种比较类似呢?本篇文章将会揭晓答案。

构造方法注入方法

以@Autowired注解为例,即把@Autowired注解标记在目标bean的构造方法上,而构造方法的入参数是引用bean类型;

构造方法注入示例

示例主要内容:1、定义Teachert类; 2、定义Student类;3、在Student类中依赖Teacher;4、使用@Autowired注解标记在Student(Teacher teacher)上,即在Student对象中以构造方法注入的方式注入Teacher对象;

@Slf4j
@Component
public class Student {
    private String name="小明";
    private Teacher teacher;
    public Student() {
        log.info("----student的无参数构造方法被执行");
    }
    @Autowired
    public Student(Teacher teacher) {
        this.teacher = teacher;
        log.info("----student的有参数构造方法(teacher)被执行");
    }
    public Student(String name, Teacher teacher) {
        this.name = name;
        this.teacher = teacher;
        log.info("----student的有参数构造方法(name,teacher)被执行");
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
        log.info("----student中的setTeacher方法被调用");
    }

}
@Slf4j
@Component
public class Teacher  {
    private String name="李老师";
    private Student student;
    public Teacher() {
        log.info("----teacher的无参数构造方法被执行");
    }
    public Teacher(Student student) {
        this.student = student;
        log.info("----teacher的有参数构造方法被执行");
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        log.info("----teacher中的setStudent方法被调用");
        this.student = student;
    }
}
@Component
@Slf4j
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("student")) {
            Student student = (Student) bean;
            log.info("----student属性注入完成,student.name:" + student.getName());
        }
        return bean;
    }
}
@Component
@Slf4j
public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        if (beanName.equals("student")||beanName.equals("teacher")) {
            log.info("----bean实例化完成,beanName:" + beanName);
        }
        return true;
    }
}
    @Test
    public void test4() {
        log.info("----单元测试执行开始");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.fanfu");
        Student bean = context.getBean(Student.class);
        log.info("----单元测试执行完毕");
    }

单元测试执行日志

从单元调试执行日志结果来看,teacher先被实例化,然后再调用student的有参数构造方法进行student对象的实例化,同时把teacher对象注入到了student对象里,这和字段注入、setter方法注入的过程明显不同,字段注入、setter方法注入都是student先实例化,然后实例化teacher对象,再把teacher对象注入到student对象里。这是一个非常明显的区别,要加深印象。

构造方法注入的工作原理

这里从工作原理上,深入分析一下构造方法注入和字段注入、setter方法注入的过程的区别在哪里?

1、通过之前的分析,已经知道bean实例化时机是在AbstractAutowireCapableBeanFactory#doCreateBean--->createBeanInstance(),那么先执行createBeanInstance()并观察其返回值,发现student对象实例化好的同时teacher对象也注入到student对象内,这和单元测试的执行日志结果是一致的;

2、那就直接着进入到AbstractAutowireCapableBeanFactory#createBeanInstance(),方法内第一行resolveBeanClass(mbd, beanName),从名字看意思是解析出bean的Class实例对象,接着往下走调用determineConstructorsFromBeanPostProcessors(beanClass, beanName),又引起了我的注意,看过我的这篇文章的小伙伴Springboot扩展点之SmartInstantiationAwareBeanPostProcessor,一定对determineCandidateConstructors()不陌生,这个方法可以决定使用哪个构造器构造 bean,执行结果就是student的有参数构造方法Student(Teacher teacher);再来看下面的autowireConstructor(beanName, mbd, ctors, args)调用,从名字就能看出来是根据构造器自动装配bean;

3、那还等什么,进入AbstractAutowireCapableBeanFactory#autowireConstructor(),里面太简单了,new一个构造方法解析器对象(ConstructorResolver),调用ConstructorResolver#autowireConstructor();

4、ConstructorResolver#autowireConstructor()内的解析过程比较复杂,各种的判断,让人眼花缭乱,但没关系,谁让我是一机灵鬼呢,我发现第一句就是BeanWrapperImpl bw = new BeanWrapperImpl(),这太熟悉了,Spring容器刚实例化好的bean都包装成一个BeanWrapper类型对象,我只需要牢牢看好这个对象,肯定能找到在什么时候注入student对象。后面果然让我抓到了,getUserDeclaredConstructor(candidate)拿到了有参数的构造方法对象,然后调用createArgumentArray(),入参数有bean的Class对象实例、bean构造方法形参名称,这个方法十有八九有我想要的结果;

5、进入到ConstructorResolver#createArgumentArray(),里面又是很多的判断,判断完之后又调用了ConstructorResolver#resolveAutowiredArgument(); 这一块判断比较多,一定要耐心;

这个方法要加印象,一会记得看一下返回值(在第10步);

6、进入到ConstructorResolver#resolveAutowiredArgument(),眼前一亮发现了 this.beanFactory.resolveDependency(...),而这里的this.beanFactory是DefaultListableBeanFactory,有看过Spring依赖注入(一):字段注入的方式是如何工作的?Sprng依赖注入(二):setter注入是如何工作的?的一对这个方法也不会陌生,从名字也能看出来是bean的依赖解析;

7、进入到DefaultListableBeanFactory#resolveDependency(),又调用了doResolveDependency();

8、doResolveDependency()中,又调用了findAutowireCandidates(beanName, type, descriptor)查找Student类依赖属性Teacher类的Class对象;

9、doResolveDependency()中,继续向下会调用descriptor.resolveCandidate(...),而这个方法就是根据上一步得到Teacher类的Class对象实例化Teacher;进入到descriptor.resolveCandidate(...),就一句beanFactory.getBean(beanName),是不是熟悉的配方熟悉的味道?

10、跳过teacher对象的创建过程,继续往下,直到ConstructorResolver#createArgumentArray()执行完,返回值是teacher对象,还记得开始是准备实例化student,但是student还没实例化,其依赖的引用属性teacher已经完成了实例化了;

11、继续往下走,一直到ConstructorResolver#autowireConstructor()的最后一步,这时第4步new的BeanWrapperImpl()对象开始往里面注入student对象了,但是这里不是直接注入,而是先调用ConstructorResolver#instantiate(String, RootBeanDefinition,Constructor, Object[])实例化Student,拿到student对象再包装进BeanWrapperImpl对象,这里着重看一下instantiate()的入参数是:构造方法对象、teacher对象;

12、至此teacher、student对象都已经实例化了,并且student对象的依赖属性teacher也注入了;继续往下走,直到AbstractAutowireCapableBeanFactory#doCreateBean--->createBeanInstance()执行完;还记得在Spring依赖注入(一):字段注入的方式是如何工作的?是如何分析的吗?如下:

Spring中bean创建核心逻辑都在AbstractAutowireCapableBeanFactory#doCreateBean()中,这个方法主要干了这几件事:第一,bean的实例化;第二,bean的后置处理方法执行;第三,把实例化完成、未完成属性注入的bean提前暴露到Spring的第三级缓存中去;第四,bean依赖属性注入;第五,bean的初始化;student对象在完成这几步后,就可以作为Spring容器中正式的bean开始被使用了。

但是事实上,createBeanInstance()执行完的时候,依赖属性注入已经完成了,第三步注入到Spring第三级缓存里的bean已经是一个完成属性注入的bean了,第四步也不会再执行注入操作了;

构造方法注入总结

Spring bean使用构造方法注入依赖属性的注入时机,要早于字段注入和、setter方法注入,而且注入到Spring第三级缓存里的bean已经是一个完成依赖属性注入的bean。得到这个结论,后面再来分析Spring的bean的循环依赖为什么有的是可以解决的?有的是解决不了的?其实答案已经明显了,下面一篇文章将重点分析。

<think>嗯,用户现在的问题是关于如何在SpringAI中为其他模型实现回调功能。用户已经提到了参考站内引用,但根据系统提示,需要更多参考之前的对话。回顾之前的对话,用户之前问的是关于快速排序的示例和问题生成,所以当前这个问题是一个新的主题。用户可能是在开发AI应用时需要集成不同的模型,并通过回调机制处理响应。考虑到用户提到“其他模型”,可能是指除了OpenAI之外的其他模型供应商,比如HuggingFace、Anthropic等。SpringAI提供的统一接口是关键点。回调功能的核心是StreamingCallback接口,之前在回答中已经详细说明了。需要进一步解释如何为不同模型配置回调。用户可能不太清楚如何具体实现,尤其是模型适配的部分。可能需要分步骤说明:依赖引入、配置APIKey、创建回调实例、注册到ChatClient等。还要注意用户可能遇到的难点,比如模型适配器的选择是否正确,回调中的重写方法是否正确实现。可能需要提醒用户检查模型供应商是否支持流式响应,因为这是回调的前提。另外,示例代码中不同模型的名字写法(如openai,anthropic等)是否和配置一致,这点容易出错,需要强调。用户可能已经查阅过SpringAI文档,但具体实现细节仍有疑惑。因此,回答需要聚焦在代码示例和配置步骤上,避免理论描述。之前的回答已经提供了OpenAI和Anthropic的示例,现在可以补充HuggingFace的例子,因为用户特别提到“其他模型”,应该覆盖更多供应商。最后,需要生成相关问题帮助用户深入理解。可能的延伸问题包括异常处理、性能优化、自定义回调逻辑等,这些都能帮助用户在实际应用中调试和扩展功能。</think> 在 Spring AI 中为其他模型(如 Anthropic、Hugging Face 等)实现回调功能,核心在于统一使用 `StreamingChatClient` 和 `StreamingCallback` 接口。以下是具体步骤和代码实现: --- ### 🛠 实现步骤 1. **添加模型依赖** 在 `pom.xml` 或 `build.gradle` 中引入目标模型的 Spring AI Starter: ```xml <!-- Anthropic 示例 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-anthropic-spring-boot-starter</artifactId> </dependency> ``` 2. **配置 API Key** 在 `application.yml` 中配置模型凭据: ```yaml spring: ai: anthropic: api-key: YOUR_API_KEY # 其他模型同理,如 huggingface.api-key ``` 3. **创建回调类** 实现 `StreamingCallback` 接口,重写 `onComplete()` 处理最终结果,`onStart()` 用于初始化: ```java public class CustomCallback implements StreamingCallback { private StringBuilder fullContent = new StringBuilder(); @Override public void onStart() { System.out.println("Stream started"); } @Override public void onComplete(ChatResponse response) { System.out.println("Full response: " + fullContent); // 可在此保存日志或触发后续逻辑 } @Override public void onNext(String token) { fullContent.append(token); System.out.print(token); // 实时输出流式token } } ``` 4. **调用模型并注册回调** 在服务层注入对应模型的 `ChatClient` 并执行调用: ```java @Service public class AIService { private final StreamingChatClient chatClient; // 自动注入当前启用的模型客户端 public AIService(StreamingChatClient chatClient) { this.chatClient = chatClient; } public void streamResponse(String prompt) { PromptMessage message = new PromptMessage(prompt); chatClient.stream(message) .registerCallback(new CustomCallback()) // 注册回调 .start(); } } ``` --- ### ⚙️ 关键技术说明 1. **多模型统一接口** Spring AI 的 `StreamingChatClient` 为不同模型提供一致的流式调用接口,开发者无需关心底层实现差异。支持的模型包括: - `OpenAiChatClient` - `AnthropicChatClient` - `HuggingfaceChatClient` - `AzureOpenAiChatClient` 2. **回调生命周期控制** | 回调方法 | 触发时机 | 典型用途 | |----------------|----------------------------|----------------------------| | `onStart()` | 流式响应开始时触发 | 初始化资源、记录请求日志 | | `onNext()` | 每收到一个token时触发 | 实时显示流式结果 | | `onComplete()` | 流式响应全部结束时触发 | 汇总结果、执行后续业务逻辑 | 3. **异步非阻塞处理** 使用 `registerCallback().start()` 触发异步调用,避免阻塞主线程,适合高并发场景。 --- ### 💡 适配注意事项 - **模型兼容性**:确认目标模型供应商支持流式API(如 Anthropic 的 `messages` API 需启用 `stream: true`)。 - **异常处理**:在回调类中添加 `onError(Throwable error)` 方法捕获超时/网络异常。 - **客户端选择**:通过 `@Qualifier` 注解注入特定客户端(如 `@Qualifier("anthropicChatClient")`)实现多模型并行调用。 --- ### 📝 示例:Hugging Face 调用 ```java @Autowired @Qualifier("huggingfaceChatClient") private StreamingChatClient hfClient; public void hfStream(String input) { hfClient.stream(new PromptMessage(input)) .registerCallback(new CustomCallback()) .start(); } ``` > **✅ 关键点**:Spring AI 的抽象层屏蔽了模型差异,**仅需更换依赖和`@Qualifier`注解**即可切换不同供应商,回调逻辑完全复用。 --- 📚 **相关问题** 1. 如何在 Spring AI 中处理流式调用的超时和重试机制? 2. Spring AI 是否支持自定义模型客户端整合私有化部署的LLM? 3. 回调中如何实现实时内容过滤或敏感词替换? 4. 多模型并行调用时如何管理资源竞争? [^1]: Spring AI 官方文档 - [Streaming API](https://docs.spring.io/spring-ai/reference/api/streaming.html) [^2]: Anthropic 流式 API 规范 - [Message Streaming](https://docs.anthropic.com/claude/reference/messages-streaming)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡夫贩夫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值