一、Spring IoC 容器概念
1. 组件和组件管理概念
-
1.1 什么是组件?
常规的三层架构处理请求流程:
-
整个项目就是由各种组件搭建而成的: -
-
1.2 我们的期待?
-
有人替我们创建组件的对象
-
有人帮我们保存组件的对象
-
有人帮助我们自动组装
-
有人替我们管理事务
-
有人协助我们整合其他框架
-
......
-
-
1.3 Spring充当组件管理角色(IoC)!
那么谁帮我们完成我们的期待,帮我们管理组件呢?
当然是Spring 框架了!
组件可以完全交给Spring 框架进行管理,Spring框架替代了程序员原有的new对象和对象属性赋值动作等!
Spring具体的组件管理动作包含:
-
组件对象实例化
-
组件属性属性赋值
-
组件对象之间引用
-
组件对象存活周期管理
-
...... 我们只需要编写元数据(配置文件)告知Spring 管理哪些类组件和他们的管理即可! 注意:组件是映射到应用程序中所有可重用组件的Java对象,应该是可复用的功能对象!
-
组件一定是对象
-
对象不一定是组件 综上所述,Spring 充当一个组件容器,创建、管理、存储组件,减少了我们的编码压力,让我们更加专注进行业务编写!
-
-
1.4 组件交给Spring管理优势!
-
降低了组件之间的耦合性:Spring IoC容器通过依赖注入机制,将组件之间的依赖关系削弱,减少了程序组件之间的耦合性,使得组件更加松散地耦合。
-
提高了代码的可重用性和可维护性:将组件的实例化过程、依赖关系的管理等功能交给Spring IoC容器处理,使得组件代码更加模块化、可重用、更易于维护。
-
方便了配置和管理:Spring IoC容器通过XML文件或者注解,轻松的对组件进行配置和管理,使得组件的切换、替换等操作更加的方便和快捷。
-
交给Spring管理的对象(组件),方可享受Spring框架的其他功能(AOP,声明式事务管理)等
-
2. Spring IoC容器和容器实现
组件管理都是交给Spring的IoC容器实现,接下来我们介绍SpringIoC容器!
-
2.1 普通和复杂容器
普通容器
生活中的普通容器
普通容器只能用来存储,没有更多功能。 程序中的普通容器
-
数组
-
集合:List
-
集合:Set 复杂容器 生活中的复杂容器
政府管理我们的一生,生老病死都和政府有关。 程序中的复杂容器 Servlet 容器能够管理 Servlet、Filter、Listener 这样的组件的一生,所以它是一个复杂容器。我们即将要学习的Spring IoC 容器也是一个复杂容器。它们不仅要负责创建组件的对象、存储组件的对象,还要负责调用组件的方法让它们工作,最终在特定情况下销毁组件。
名称 时机 次数 创建对象 默认情况:接收到第一次请求 修改启动顺序后:Web应用启动过程中 一次 初始化操作 创建对象之后 一次 处理请求 接收到请求 多次 销毁操作 Web应用卸载之前 一次 总结:Spring管理组件的容器,就是一个复杂容器,不仅存储组件,也可以管理组件之间依赖关系,并且创建和销毁组件等! -
-
2.2 SpringIoC容器介绍
Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。配置元数据以 XML、Java 注释或** Java 代码形式**表现。它允许表达组成应用程序的组件以及这些组件之间丰富的相互依赖关系。
-
上图显示了 Spring 容器工作原理的高级视图。应用程序类与配置元数据相结合,您拥有完全配置且可执行的系统或应用程序。
-
2.3 SpringIoC容器接口和实现
SpringIoc容器接口:
org.springframework.beans
和org.springframework.context
包是 Spring Framework 的 IoC 容器的基础包。BeanFactory
接口提供了一种高级配置机制,能够管理任何类型的对象,它是SpringIoC容器标准化超接口!ApplicationContext
是BeanFactory
的子接口。它补充说:-
更容易与 Spring 的 AOP 功能集成
-
消息资源处理(用于国际化)
-
特定于应用程序给予此接口实现,例如Web 应用程序的
WebApplicationContext
简而言之,BeanFactory
提供了配置框架和基本功能,而ApplicationContext
添加了更多特定于企业的功能。ApplicationContext
是BeanFactory
的完整超集! ApplicationContext容器实现类:
类型名 简介 ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 AnnotationConfigApplicationContext 通过读取Java配置类创建 IOC 容器对象 WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 -
-
2.4 SpringIoC容器管理配置方式
Spring IoC 容器使用多种形式的配置元数据。此配置元数据表示您作为应用程序开发人员如何告诉 Spring 容器实例化、配置和组装应用程序中的对象。
-
XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系。该方式从Spring框架的第一版开始提供支持。
-
注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系。
-
Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置。 配置方式的使用场景不同,为了更多体验每种方式,SSM期间,我们使用XML+注解方式为主。SpringBoot期间,我们使用配置类+注解方式为主!
-
- Spring框架提供了多种配置方式:XML配置方式、注解方式和Java配置类方式
3. Spring IoC / DI概念总结
-
IoC容器
Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。
-
IoC(Inversion of Control)控制反转
IoC 主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是“反转”了控制权。这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。
-
DI (Dependency Injection) 依赖注入
DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。在 Spring 中,DI 是通过 XML 配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter 方法注入和接口注入。
二、Spring IoC / DI 实现
1.添加依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<context.version>6.0.7</context.version>
<jupiter.versoon>5.10.1</jupiter.versoon>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${context.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${jupiter.versoon}</version>
<scope>test</scope>
</dependency>
</dependencies>
2.新建Person类
package com.cuihub.ioc;
public class Person {
public void sayHello(){
System.out.println("Hello spring");
}
}
3.新建spring的IOC配置文件
:resources/applicationContext.xml(文件名随意) ,在其中描述Person
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="p01" class="com.cuihub.ioc.Person"/>
</beans>
4.测试
从IOC容器当中获取person实例对象
package com.cuihub.ioc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class PersonTest {
@Test
public void test01() {
//1.获取IOC容器
BeanFactory beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.从IOC容器当中取出Person,获取到的是object需要强转
Person person = (Person) beanFactory.getBean("p01");
person.sayHello();
}
}
三、获取bean的方式
1.Object getBean(id)-------------------------根据id获取
2.T getBean(Class<T>)----------------------根据类型获取
3.T getBean(id,Class<T>)------------------根据id和类型获取
注意点
-根据id获取,可能获取不到,获取不到报错
-根据类型获取,有且唯一(一个个name只能匹配一个class),则获取成功,否则报错。
因为ioc容器可以创建多个bean实例
package com.cuihub.ioc;
import com.cuihub.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class PersonTest {
@Test
public void test01() {
//1.获取IOCrongqi
BeanFactory beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.从ioc容器当中获取Person
Person person = (Person) beanFactory.getBean("p01");
person.sayHello();
}
//演示获取bean的方式
@Test
public void test02() {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
//方式1
//Person person = (Person)beanFactory.getBean("p01");
//方式2
//Person person = beanFactory.getBean(Person.class);
//person.sayHello();
//方式3
Person person = beanFactory.getBean("p01", Person.class);
person.sayHello();
}
}
四、Bean属性赋值:setter注入
1.组件类添加属性
package com.cuihub.spring;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-16 11:12
*/
public class HappyComponent {
// 定义一个名为 componentName 的属性,类型为 String
private String componentName;
public String getComponentName() {
return componentName;
}
//必须配置set方法,属性设置,ioc容器是通过set方法调用,进行属性赋值!!!
public void setComponentName(String componentName) {
this.componentName = componentName;
}
// 定义一个名为 happy 的方法,无参数,无返回值
public void happy() {
// 输出字符串 "happyComponent happy" 到控制台
System.out.println("happyComponent happy");
}
}
2.配置时给属性指定值
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--2.[重要]给bean的属性赋值:setter注入 -->
<!--property标签:通过组件类的setXxx()方法给组件对象设置属性-->
<!--name属性:指定属性名(这个属性是getXxx(),setXxx()方法定义的,和成员变量无关)-->
<!--value属性:指定属性值-->
<bean id="HappyComponent" class="com.cuihub.spring.HappyComponent">
<property name="componentName" value="cuihub"/>
</bean>
</beans>
3.测试属性输出
package com.cuihub.spring;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-16 11:14
*/
public class IoTest {
//2.创建IOC容器并配置文件
@Test
public void test01() {
// 创建一个ClassPathXmlApplicationContext对象,用于加载Spring配置文件
// 这里的"spring-bean-01.xml"是配置文件的名字,位于类路径下
ClassPathXmlApplicationContext iocContain =
new ClassPathXmlApplicationContext("spring-bean-01.xml");
//3.从容器中获取bean
// 通过getBean方法从IOC容器中获取名为"HappyComponent"的bean,并将其转换为HappyComponent类型
HappyComponent happyComponent = iocContain.getBean("HappyComponent", HappyComponent.class);
// 调用HappyComponent对象的happy方法
happyComponent.happy();
//获取组件当中的
System.out.println(happyComponent.getComponentName());//cuihub
}
}
五、Bean属性赋值:引用其他Bean
1.声明新组件
package com.cuihub.spring;
public class HappyMachine {
private String machineName;
public String getMachineName() {
return machineName;
}
public void setMachineName(String machineName) {
this.machineName = machineName;
}
}
2.原组件引用新组件
package com.cuihub.spring;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-16 11:12
*/
public class HappyComponent {
// 定义一个名为 componentName 的属性,类型为 String
private String componentName;
public String getComponentName() {
return componentName;
}
//必须配置set方法,属性设置,ioc容器是通过set方法调用,进行属性赋值!!!
public void setComponentName(String componentName) {
this.componentName = componentName;
}
// 定义一个名为 happy 的方法,无参数,无返回值
public void happy() {
// 输出字符串 "happyComponent happy" 到控制台
System.out.println("happyComponent happy");
}
//引用新组件
private HappyMachine happyMachine;
public HappyMachine getHappyMachine() {
return happyMachine;
}
public void setHappyMachine(HappyMachine happyMachine) {
this.happyMachine = happyMachine;
}
}
3.配置新组件
<!--配置新组件-->
<bean id="HappyMachine" class="com.cuihub.spring.HappyMachine">
<property name="machineName" value="why"/>
</bean>
4.组件之间引用配置
<!--配置关系-->
<bean id="HappyComponent" class="com.cuihub.spring.HappyComponent">
<property name="componentName" value="cuihub"/>
<property name="happyMachine" ref="HappyMachine"/>
</bean>
5.测试
package com.cuihub.spring;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-16 11:14
*/
public class IoTest {
//2.创建IOC容器并配置文件
@Test
public void test01() {
// 创建一个ClassPathXmlApplicationContext对象,用于加载Spring配置文件
// 这里的"spring-bean-01.xml"是配置文件的名字,位于类路径下
ClassPathXmlApplicationContext iocContain =
new ClassPathXmlApplicationContext("spring-bean-01.xml");
//3.从容器中获取bean
// 通过getBean方法从IOC容器中获取名为"HappyComponent"的bean,并将其转换为HappyComponent类型
HappyComponent happyComponent = iocContain.getBean("HappyComponent", HappyComponent.class);
// 调用HappyComponent对象的happy方法
happyComponent.happy();
//获取组件当中的
System.out.println(happyComponent.getComponentName());//cuihub
//获取另一个bean
System.out.println(happyComponent.getHappyMachine().getMachineName());//why
}
}
6.注意事项
6.1声明bean,不分先后顺序,spring容器内部有缓存机制,先实例化后属性赋值!
6.2ref 容易错写成value,会抛出Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 异常!
6.3只有声明到ioc容器,方可被其他bean引用!
六: Bean 属性赋值:内部Bean声明(了解)
1.声明内部bean配置
在bean里面配置的bean就是内部bean,内部bean只能在当前bean内部使用,在其他地方不能使用。
不会在ioc容器中,实例和存储内部bean,只会缓存类信息,每次获取的时候再实例化!!
<!-- 实验五 [重要]给bean的属性赋值:内部bean -->
<bean id="happyComponent5" class="com.atguigu.ioc.HappyComponent">
<property name="happyMachine">
<!-- 在一个 bean 中再声明一个 bean 就是内部 bean -->
<!-- 内部 bean 可以直接用于给属性赋值,可以省略 id 属性 -->
<bean class="com.atguigu.ioc.HappyMachine">
<property name="machineName" value="makeHappy"/>
</bean>
</property>
</bean>
2.测试读取
@Test
public void testExperiment05() {
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-05.xml");
HappyComponent happyComponent = iocContainer.getBean("happyComponent5", HappyComponent.class);
//通过外部bean,可以获取专属内部bean
System.out.println(happyComponent.getHappyMachine().getMachineName());
//直接获取内部bean,输出! [[报错]]
//NoSuchBeanDefinitionException: No qualifying bean of type 'com.atguigu.ioc.HappyMachine' available
HappyMachine happyMachine = iocContainer.getBean(HappyMachine.class);
System.out.println("happyMachine = " + happyMachine);
}
七: Bean 属性赋值:引入外部Properties配置参数
-
实现目标
将Druid连接池对象交给SpringIoC容器管理!
-
加入数据库依赖
<!-- 数据库驱动 和 连接池-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
3.创建外部属性文件
文件位置:resources/jdbc.properties
# 配置成你的数据信息
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://主机名:端口号/数据库名
jdbc.driver=com.mysql.cj.jdbc.Driver
4.引入属性文件
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
在 IDEA 中引入 Spring 配置文件中名称空间的两种操作方式:
在打字标签名的过程中根据提示选择一个正确的名称空间 对于直接复制过来的完整标签,可以在名称空间上点击,然后根据提示引入
5.配置连接池信息
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 实验六 [重要]给bean的属性赋值:引入外部属性文件 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
6.读取测试
@Test
public void testExperiment06() throws SQLException {
ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-06.xml");
DataSource dataSource = iocContainer.getBean(DataSource.class);
Connection connection = dataSource.getConnection();
System.out.println("connection = " + connection);
}
八、实验七: 高级特性:FactoryBean特性
1. FactoryBean简介
FactoryBean 接口是Spring IoC容器实例化逻辑的可插拔特性。 用于配置复杂的Bean对象,可以将创建过程存储在 FactoryBean 的getObject方法!
FactoryBean 接口提供三种方法:
T getObject() : 返回此工厂创建的对象的实例。该返回值会被存储到IoC容器!
boolean isSingleton() : 如果此 FactoryBean 返回单例,则返回 true ,否则返回 false 。此方法的默认实现 返回 true (注意,lombok插件使用,可能影响效果)。
Class getObjectType() : 返回 getObject() 方法返回的对象类型,如果事先不知 道类型,则返回 null 。
2. FactoryBean使用场景
1. 代理类的创建
2. 第三方框架整合
3. 复杂对象实例化等
3. Factorybean应用
1. 准备FactoryBean实现类
package com.cuihub.spring;
import org.springframework.beans.factory.FactoryBean;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-21 12:24
*/
// 实现FactoryBean接口时需要指定泛型
// 泛型类型就是当前工厂要生产的对象的类型
public class HappyFactoryBean implements FactoryBean<HappyMachine> {
private String machineName;
public String getMachineName() {
return machineName;
}
public void setMachineName(String machineName) {
this.machineName = machineName;
}
@Override
public HappyMachine getObject() throws Exception {
//方法内部模拟创建,设置一个对象的复杂过程
HappyMachine happyMachine = new HappyMachine();
happyMachine.setMachineName(machineName);
return happyMachine;
}
@Override
public Class<?> getObjectType() {
return HappyMachine.class;
}
}
2. 配置FactoryBean实现类
<!-- FactoryBean机制 -->
<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是
HappyMachine对象 -->
<bean id="happyMachine01" class="com.cuihub.spring.HappyFactoryBean">
<!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
<property name="machineName" value="Cui"/>
</bean>
3. 测试读取FactoryBean和FactoryBean.getObject对象
//获取FactoryBean创建的对象
@Test
public void testFactoryBean() {
ClassPathXmlApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-01.xml");
//注意: 直接根据声明FactoryBean的id,获取的是getObject方法返回的对象
HappyMachine happyMachine = iocContainer.getBean("happyMachine01", HappyMachine.class);
System.out.println("happyMachine = " + happyMachine.getMachineName());//happyMachine = Cui
//如果想要获取FactoryBean对象, 直接在id前添加&符号即可! &happyMachine7 这是一
//种固定的约束
Object bean = iocContainer.getBean("&happyMachine01");
System.out.println("bean = " + bean);//bean = com.cuihub.spring.HappyFactoryBean@1df8da7a
}
4. FactoryBean和BeanFactory区别
FactoryBean是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建Bean! 是一种能够生产其他 Bean 的 Bean。FactoryBean 在容器启动时被创建,而在实际使用时则是通过 调用 getObject() 方法来得到其所生产的 Bean。因此,FactoryBean 可以自定义任何所需的初始化逻 辑,生产出一些定制化的 bean。
一般情况下,整合第三方框架,都是通过定义FactoryBean实现!!!
BeanFactory 是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据 库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时,BeanFactory 还 包含很多子类(例如,ApplicationContext 接口)提供了额外的强大功能。
总的来说,FactoryBean 和 BeanFactory 的区别主要在于前者是用于创建 bean 的接口,它提供了更 加灵活的初始化定制功能,而后者是用于管理 bean 的框架基础接口,提供了基本的容器功能和 bean 生命周期管理。
补充:
1. 构造器注入
package com.cuihub.spring;
/**
* @version: java version 17
* @Author: coder Cui
* @description:
* @date: 2025-04-21 16:16
*/
public class Tiger {
private String name;
private Integer age;
private String breed;
public Tiger(String name, Integer age, String breed) {
this.name = name;
this.age = age;
this.breed = breed;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
}
<!--1.构造器注入-->
<bean id="Tiger01" class = "com.cuihub.spring.Tiger">
<constructor-arg value="妞妞"/>
<constructor-arg value="2"/>
<constructor-arg value="东北虎"/>
</bean>
<!--2.构造器注入:有三个属性:index,type,name 配合使用,达到不产生歧义的目的即可-->
<bean id="Tiger02" class = "com.cuihub.spring.Tiger">
<constructor-arg value="3" index="1" type="java.lang.Integer" name="age"/>
<constructor-arg value="妞妞"/>
<constructor-arg value="东北虎"/>
</bean>
@Test
//1.获取构造器注入的对象
public void ConstructorTest(){
ClassPathXmlApplicationContext iocCOntainer = new ClassPathXmlApplicationContext("spring-bean-01.xml");
Tiger tiger01 = (Tiger)iocCOntainer.getBean("Tiger01");
System.out.println("tiger01 = " + tiger01);//tiger01 = com.cuihub.spring.Tiger@3b6f7b3c
System.out.println("tiger的名字:"+tiger01.getName());//tiger的名字:妞妞
System.out.println("tiger的年龄:"+tiger01.getAge());//tiger的年龄:2
System.out.println("tiger的品种:"+tiger01.getBreed());//tiger的品种:东北虎
}
2. 级联属性赋值
<!--3.级联属性的赋值-->
<bean id = "u01" class="com.cuihub.spring.User">
<property name="name" value="cuihub"/>
<property name="age" value="18"/>
<property name="teacher" ref="teacher01"/>
<property name="teacher.tname" value="张三"/>
<property name="teacher.tage" value="35"/>
<property name="teacher.tid" value="001"/>
</bean>
<bean id = "teacher01" class="com.cuihub.spring.Teacher"/>
3. P名称空间
<!-- 首先需要导入schema: xmlns:p="http://www.springframework.org/schema/p"-->
<!--快捷键 Alt + ↩︎-->
<!-- P名称空间 -->
<bean id = "u02" class="com.cuihub.spring.User" p:name="cuihub" p:age="18"/>
<bean id = "u03" class="com.cuihub.spring.User" p:name="cuihub" p:age="18" p:teacher-ref="t03" />
<bean id = "t03" class="com.cuihub.spring.Teacher" p:tname="Coder" p:tage="35" p:tid="002"/>
4. 属性是数组或集合类型
<!-- 数组类型的属性 -->
<bean id="s02" class="com.atguigu.ioc.Stu" p:sname="钟灵">
<property name="bookArr">
<array>
<ref bean="b01"/>
<ref bean="b02"/>
</array>
</property>
</bean>
<!-- List类型的属性 -->
<property name="bookList">
<list>
<ref bean="b01"/>
<ref bean="b02"/>
</list>
</property>
<!-- Set类型的属性 -->
<property name="bookSet">
<set>
<value>MySQL从删库到跑路</value>
<value>PHP是世界上最好的语言</value>
<value>21天精通C++</value>
</set>
</property>
<!-- Map类型的属性-->
<property name="bookMap">
<map>
<entry key="b01" value-ref="b01"/>
<entry key="b02" value-ref="b02"/>
</map>
</property>
5. 集合类型的bean
private List<User> bookList;
<!-- 集合类型的bean -->
<util:list id="list01">
<value>张三</value>
<value>李四</value>
<value>王五</value>
</util:list>
九、高级特性:Bean的作用域
1. Bean作用域概念
<bean> 标签声明Bean,只是将Bean的信息配置给SpringIoC容器!
在IoC容器中,这些 <bean>标签对应的信息转成Spring内部 BeanDefinition 对象,
BeanDefinition 对象内,包含定义的信息(id,class,属性等等)!
这意味着, BeanDefinition 与 类 概念一样,SpringIoC容器可以可以根据 BeanDefinition 对
象反射创建多个Bean对象实例。
具体创建多少个Bean的实例对象,由Bean的作用域Scope属性指定!
默认情况:我们全局只需要实例化一个Bean对象,绝大情况我们也仅需创建一个对象!
2.常用作用域
取值 | 含义 | 创建对象的时机 | 默认值 |
---|---|---|---|
singleton | 在 IOC 容器中,这个 bean 的对象始终为单实例 | IOC 容器初始化时 | 是 |
prototype | 这个 bean 在 IOC 容器中有多个实例 | 获取 bean 时 | 否 |
如果是在 WebApplicationContext 环境下还会有另外两个作用域(但不常用): | |||
request | 请求范围内有效的实例 | 每次请求 | 否 |
session | 会话范围内有效的实例 | 每次会话 | 否 |
3. 作用域配置和测试
配置scope范围
<!--bean的作用域 --> <!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对 象 --> <!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 --> <!-- scope属性默认值是:singleton 表示ioc容器当中的实例默认都是单例的,prototype多例的-->
<bean id="u04" class="com.cuihub.spring.User" scope="prototype">
<property name="name" value="cuihub"/>
<property name="age" value="18"/>
</bean>
测试读取
@Test
public void test05(){
ClassPathXmlApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-01.xml");
User bean1 = iocContainer.getBean("u04",User.class);
User bean2 = iocContainer.getBean("u04",User.class);
System.out.println(bean1 == bean2);//false
}
十、Bean的生命周期
1. 理解Bean的生命周期作用
Spring Framework的Bean生命周期是指一个Bean对象从它的创建、初始化到销毁的整个过程。
理解Spring Bean的生命周期可以帮助开发者更好地管理Bean,可以实现以下目的:
1. 避免重复初始化Bean,提高Bean实例化的效率;
2. 在Bean初始化前后做些额外的处理,如日志记录、权限检查等;
3. 实现自定义的操作,如在Bean销毁时释放资源、设置缓存等;
4. 理解Bean的整个生命周期有助于排查问题,提高应用程序的可维护性;
5. 理解Spring Aop 等功能的实现原理,并参与定制过程;
2. Bean生命周期清单和步骤内容
Spring Bean 的生命周期指从 Spring 容器创建 Bean 实例开始,到 Bean 销毁的整个过程,可以按照 以下流程分为以下几个阶段:
1. 实例化Bean实例:Spring 容器使用指定的实例化策略创建 bean,该策略可以是无参构造、 工厂方法等。当 Spring 加载 Bean 的配置文件并且 Bean 标签被解析后,这个类(Bean)就 会被实例化。
2. Bean实例属性设置:Spring 通过调用 setter 方法或直接设置字段的方式来注入 Bean 的属 性。
3. Aware 相关接口的回调:Spring 通过 Aware 接口来把一些 Bean 相关的资源注入到 Bean 中。 例如,BeanNameAware 接口可获取到 Bean 的名称;ApplicationContextAware 接口可获取到 ApplicationContext 对象实例;BeanFactoryAware 接口可获取到 BeanFactory 对象实例等。
4. Bean初始化前的操作:在 Bean 的初始化之前,Spring 允许用户自定义 Bean 实例化后的一 些操作。
如果有BeanPostProcessor注册,先执行beforeInitialization()方法;
如果Bean实现了InitializingBean接口,则执行afterPropertiesSet()方法
5. Bean 的初始化方法调用:如果在配置文件中使用init-method属性声明了初始化方法,则执 行该方法;
6. Bean初始化后的操作:在 Bean 的初始化之后,如果有BeanPostProcessors注册,执行 afterInitialization()方法; 此方法中,Bean实例已经完成了实例化和初始化工作,最终会将afterInitialization()方法 修改后返回的对象存储到IoC容器中! Spring Aop的实现,通过定义BeanPostProcessor(AbstractAutoProxyCreator),在后置方法中添 加动态代理技术,进行Bean的动态代理对象生成!
7. 使用 Bean:即在 IoC 容器中调用 getBean() 方法获取 Bean 实例,使用 Bean 的过程。
8. 销毁 Bean:当 Bean 不再被使用时,Spring 容器会自动释放 Bean 占有的资源,关闭 IoC 容 器。 开发人员可以自己实现 DisposableBean 接口或者为 Bean 配置一个指定的 destroymethod 方法来实现自定义销毁的逻辑。
9. 关闭IoC容器 在整个生命周期过程中,Spring 提供了各种监听器和钩子函数,允许开发人员在不同的 Bean 生命周期阶段添加自己的处理逻辑以实现更加灵活和智能的控制。
3. 参与Bean生命周期定义
十一、基于 注解 方式管理 Bean
实验一: Bean注解标记和扫描(IoC)
1. 注解理解
和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架 检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
本质上:所有一切的操作都是 Java 代码来完成的,XML 和注解只是告诉框架中的 Java 代码如何执行。
举例:元旦联欢会要布置教室,蓝色的地方贴上元旦快乐四个字,红色的地方贴上拉花,黄色的地 方贴上气球。
班长做了所有标记,同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解,后面同 学们做的工作,相当于框架的具体操作。
2. 扫描理解
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根 据注解进行后续操作。
3. 准备Spring项目和组件
1. 准备项目pom.xml
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
2. 准备组件类
@Component
public class CommonComponent {
}
@Service
public class XxxService {
}
@Repository
public class XxxDao {
}
@Controller
public class XxxController {
}
4.组件添加标记注解
1.组件标记注解和区别
Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。
注解 | 作用描述 |
---|---|
@Component | 用于描述 Spring 中的 Bean,是一个泛化概念,代表容器中的一个组件(Bean),可作用于应用任何层次,如 Service 层、Dao 层等 |
@Repository | 用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,功能与 @Component 相同 |
@Service | 通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,功能与 @Component 相同 |
@Controller | 通常作用在控制层(如 SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,功能与 @Component 相同 |
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注 解的基础上起了三个新的名字。
对于Spring使用IOC容器管理这些组件来说没有区别,也就是语法层面没有区别。所以@Controller、 @Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。 注意:虽然它们本质上一样,但是为了代码的可读性、程序结构严谨!我们肯定不能随便胡乱标记。
2. 使用注解标记
@Component
public class CommonComponent {
}
@Service
public class XxxService {
}
@Repository
public class XxxDao {
}
@Controller
public class XxxController {
}
5. 配置文件确定扫描范围
情况1:基本扫描配置
<!--配置包扫描路径-->
<!-- 1.包要精准,提高性能!
2.会扫描指定的包和子包内容
3.多个包可以使用,分割 例如: com.atguigu.controller,com.atguigu.service等
-->
<context:component-scan base-package="com.cuihub.spring"/>
情况2:指定排除组件
<!--指定排除规则-->
<context:component-scan base-package="com.cuihub.spring">
<!-- context:exclude-filter标签:指定排除规则 -->
<!-- type属性:指定根据什么来进行排除,annotation取值表示根据注解来排除 -->
<!-- expression属性:指定排除规则的表达式,对于注解来说指定全类名即可 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
情况3:指定扫描组件
<!-- 仅扫描指定的组件 -->
<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.cuihub.spring" use-default-filters="false">
<!-- context:include-filter标签:指定扫描规则 -->
<!-- type属性:指定根据什么来进行扫描,annotation取值表示根据注解来扫描 -->
<!-- expression属性:指定扫描规则的表达式,对于注解来说指定全类名即可 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
6. 组件BeanName问题
在我们使用 XML 方式管理 bean 的时候,每个 bean 都有一个唯一标识——id 属性的值,便于在其 他地方引用。现在使用注解后,每个组件仍然应该有一个唯一标识。
默认情况:
类名首字母小写就是 bean 的 id。例如:SoldierController 类对应的 bean 的 id 就是 soldierController。
使用value属性指定:
@Repository(value = "Cui")
public class XxxDao {
}
当注解中只设置一个属性时,value属性的属性名可以省略:
@Repository("Cui")
public class XxxDao {
}
7. 总结
1. 注解方式IoC只是标记哪些类要被Spring管理
2. 最终,我们还需要XML方式或者后面讲解Java配置类方式指定注解生效的包
3. 现阶段配置方式为 注解 (标记)+ XML(扫描)
实验二: Bean属性赋值:引用类型自动装配(DI)
1. 设定场景
SoldierController 需要 SoldierService
SoldierService 需要 SoldierDao 同时在各个组件中声明要调用的方法。
2. 自动装配实现
1. 前提
参与自动装配的组件(需要装配、被装配)全部都必须在IoC容器中。
2. @Autowired注解
在成员变量上直接标记@Autowired注解即可,不需要提供setXxx()方法。以后我们在项目中的正式 用法就是这样。
3. 给Controller装配Service
3. @Autowired注解细节
标记位置
1.成员变量(这是最主要的使用方式!)
@Controller
public class SoldierController {
@Autowired
private SoldierService soldierService;
public void getMessage(){
soldierService.getMessage();
}
}
2.构造器
@Controller
public class SoldierController {
private SoldierService soldierService;
@Autowired
public SoldierController(SoldierService soldierService) {
this.soldierService = soldierService;
}
public void getMessage(){
soldierService.getMessage();
}
}
3.setXxx()方法
@Controller
public class SoldierController {
private SoldierService soldierService;
@Autowired
public void setSoldierService(SoldierService soldierService) {
this.soldierService = soldierService;
}
public void getMessage(){
soldierService.getMessage();
}
}
4. 工作流程
- 首先根据所需要的组件类型到 IOC 容器中查找
- 能够找到唯一的 bean:直接执行装配
- 如果完全找不到匹配这个类型的 bean:装配失败
- 所需类型匹配的 bean 不止一个
- 没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配
- 能够找到:执行装配
- 找不到:装配失败
- 使用 @Qualifier 注解:根据 @Qualifier 注解中指定的名称作为 bean 的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
5. 佛系装配
给 @Autowired 注解设置 required = false 属性表示:能装就装,装不上就不装。但是实际开发时, 基本上所有需要装配组件的地方都是必须装配的,用不上这个属性
6. 扩展JSR-250注解@Resource 了解
理解JSR系列注解
JSR-250 @Resource注解
@Resource使用
实验三: Bean属性赋值:基本类型属性赋值(DI)
声明外部配置
application.properties
XML引入外部配置
<!--引入外部配置文件-->
<context:property-placeholder location="classpath:application.properties"/>
@Value注解读取配置设置默认值
public class ValueTest {
// 如果 app.name 在配置文件中不存在,使用默认值 "Default App Name"
@Value("${app.name:Default App Name}")
private String name;
}
十二、基于 配置类 方式管理 Bean
1. 完全注解开发理解
Spring 完全注解配置(Fully Annotation-based Configuration)是指通过 Java配置类 代码来配置 Spring 应 用程序,使用注解来替代原本在 XML 配置文件中的配置。相对于 XML 配置,完全注解配置具有更强的类 型安全性和更好的可读性。
两种方式思维转化:
2. 实验一:配置类和扫描注解
xml+注解方式
配置文件application.xml
配置类+注解方式(完全注解方式)
配置类
使用 @Configuration 注解将一个普通的类标记为 Spring 的配置类。
总结:
@Configuration指定一个类为配置类,可以添加配置注解,替代配置xml文件 @ComponentScan(basePackages = {"包","包"}) 替代<context:component-scan>标签实现注解扫描
@PropertySource("classpath:配置文件地址") 替代<context:property-placeholder>标签
配合IoC/DI注解,可以进行完整注解开发!
3. 实验二:@Bean定义组件
场景需求:将Druid连接池对象存储到IoC容器
需求分析:第三方jar包的类,添加到ioc容器,无法使用@Component等相关注解!因为源码jar包内容为 只读模式!
xml方式实现:
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 实验六 [重要]给bean的属性赋值:引入外部属性文件 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
配置类方式实现:
@Bean 注释用于指示方法实例化、配置和初始化要由 Spring IoC 容器管理的新对象。对于那些熟悉 Spring 的 XML 配置的人来说, @Bean 注释与 元素起着相同的作用。
4 实验三:高级特性:@Bean注解细节
1. @Bean生成BeanName问题
应用场景
避免 Bean 冲突
当存在多个同类型的 Bean 时,可能会引发自动装配冲突。这时,你可以把某些 Bean 的 autowireCandidate
属性设为 false
,从而避免这些 Bean 参与自动装配,进而解决冲突。
指定@Bean的名称:
@Bean 注释注释方法。使用此方法在指定为方法返回值的类型的 ApplicationContext 中注册 Bean 定义。缺省情况下,Bean 名称与方法名称相同。下面的示例演示 @Bean 方法声明:
前面的配置完全等同于下面的Spring XML:
2. @Bean 初始化和销毁方法指定
@Bean 注解支持指定任意初始化和销毁回调方法,非常类似于 Spring XML 在 bean 元素上的 init-method 和 destroy-method 属性,如以下示例所示:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// BeanOne类,包含初始化逻辑方法init
class BeanOne {
// 初始化逻辑方法,这里可以编写对象创建后需要执行的初始化操作
public void init() {
// initialization logic
}
}
// BeanTwo类,包含销毁逻辑方法cleanup
class BeanTwo {
// 销毁逻辑方法,这里可以编写对象销毁前需要执行的清理操作
public void cleanup() {
// destruction logic
}
}
// 配置类,用于配置Spring容器中的Bean
@Configuration
public class AppConfig {
// 使用@Bean注解将beanOne方法返回的BeanOne实例注册到Spring容器中
// initMethod指定在BeanOne实例创建后,调用其init方法进行初始化
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
// 使用@Bean注解将beanTwo方法返回的BeanTwo实例注册到Spring容器中
// destroyMethod指定在BeanTwo实例销毁前,调用其cleanup方法进行清理
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
3. @Bean Scope作用域
可以指定使用 @Bean 注释定义的 bean 应具有特定范围。您可以使用在 Bean 作用域部分中指定 的任何标准作用域。 默认作用域为 singleton ,但您可以使用 @Scope 注释覆盖此范围,如以下示例所示:
4. @Bean方法之间依赖
准备组件
Java配置类实现:
方案1:
直接调用方法返回 Bean 实例:在一个 @Bean 方法中直接调用其他 @Bean 方法来获取 Bean 实 例,虽然是方法调用,也是通过IoC容器获取对应的Bean,例如:
- 缺点:可能会导致在某些 AOP(面向切面编程)场景下,代理对象的调用出现问题。因为这种调用方式可能绕过了 Spring 的代理机制,导致切面逻辑无法正确织入。
方案2:
参数引用法:通过方法参数传递 Bean 实例的引用来解决 Bean 实例之间的依赖关系,例如:
- 优点:更符合 Spring 依赖注入的设计理念,能更好地与 Spring 的 AOP、事务管理等功能协同工作。因为是通过参数注入,Spring 能够正确处理代理对象的注入,保证切面逻辑正常生效 。在处理复杂的 Bean 依赖关系时,代码结构更加清晰、可维护性更好。
在 Spring 中,代理机制主要用于实现 AOP(面向切面编程)、事务管理等功能。当你使用代理时,Spring 会创建一个代理对象来包装目标 Bean,在调用目标 Bean 的方法前后执行额外的逻辑(如事务开启、关闭,日志记录等)。下面详细解释为何在@Bean
方法中直接调用其他@Bean
方法可能绕过 Spring 的代理机制。
1. Spring 代理机制的工作原理
Spring 使用代理模式创建代理对象,有 JDK 动态代理和 CGLIB 代理两种方式。当你为一个 Bean 配置了切面(如定义了事务管理、日志记录等切面逻辑),Spring 会自动为该 Bean 创建代理对象。当客户端调用该 Bean 的方法时,实际上是调用代理对象的方法,代理对象会在调用目标 Bean 的方法前后执行额外的逻辑。
2. 直接调用@Bean
方法绕过代理机制的原因
在@Configuration
类里,@Bean
方法默认是被 CGLIB 增强的。当你直接在一个@Bean
方法中调用另一个@Bean
方法时,Spring 为了避免创建多个相同类型的 Bean 实例,会直接调用目标方法来获取 Bean 实例,而不是通过代理对象来调用。这样一来,就会绕过 Spring 的代理机制,使得原本应该在代理对象中执行的额外逻辑(如 AOP 切面逻辑)无法被执行。
3. 示例代码说明
下面通过一个示例来展示这种情况:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
// 定义一个接口
interface MyService {
void doSomething();
}
// 实现接口
class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}
// 定义切面
@Aspect
class MyAspect {
@Pointcut("execution(* com.example.MyService.doSomething())")
public void pointcut() {}
@After("pointcut()")
public void afterAdvice() {
System.out.println("After advice executed");
}
}
// 配置类
@Configuration
@EnableAspectJAutoProxy
class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
@Bean
public MyService anotherService() {
// 直接调用 myService() 方法
MyService service = myService();
return service;
}
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean("anotherService", MyService.class);
service.doSomething();
}
}
4. 代码分析
在上述代码中:
MyAspect
是一个切面,它定义了一个@After
通知,在MyService
的doSomething()
方法执行后执行。AppConfig
是配置类,anotherService()
方法直接调用了myService()
方法来获取MyService
实例。- 在
Main
类的main
方法中,获取anotherService
Bean 并调用doSomething()
方法。由于anotherService()
方法直接调用myService()
方法,绕过了 Spring 的代理机制,所以MyAspect
的afterAdvice()
方法不会被执行。
直接调用@Bean
方法绕过代理的原因
当在@Configuration
类的一个@Bean
方法(如anotherService()
)里直接调用另一个@Bean
方法(如myService()
)时,由于@Configuration
类被 CGLIB 增强,Spring 为了保证@Bean
方法只创建一个 Bean 实例,会直接调用目标方法来获取 Bean 实例,而不是通过代理对象调用。
从底层来看,Spring 增强后的@Configuration
类会对@Bean
方法进行特殊处理。当直接调用@Bean
方法时,它会跳过代理对象的拦截逻辑,直接执行目标方法体,这样就绕过了 Spring 的代理机制,使得原本应该在代理对象中执行的额外逻辑(如 AOP 切面逻辑)无法执行。
调用过程分析
未增强时的调用情况
若 @Configuration
类未被 CGLIB 增强,anotherService()
方法调用 myService()
方法时,就是普通的 Java 方法调用。每次调用 myService()
方法,都会执行 new MyService()
语句,进而创建一个新的 MyService
实例。
增强后的调用情况
当 @Configuration
类被 CGLIB 增强后,Spring 生成的子类会重写 @Bean
方法。在重写的 @Bean
方法中,会添加额外的逻辑来保证只创建一个 Bean 实例。具体而言,Spring 会维护一个 Bean 实例的缓存,当调用 @Bean
方法时,会先检查缓存中是否已经存在该 Bean 实例。如果存在,就直接返回缓存中的实例;若不存在,则创建一个新的实例并将其放入缓存中。
当在 anotherService()
方法中调用 myService()
方法时,实际上调用的是增强类中重写的 myService()
方法。重写的 myService()
方法会检查缓存,若缓存中有 MyService
实例,就直接返回该实例,这个过程中并没有通过代理对象调用(这里的代理对象通常指的是为实现 AOP 等功能而创建的代理,和 CGLIB 增强 @Configuration
类不是同一概念),而是直接调用了重写后包含缓存逻辑的目标方法 myService()
。
总结
使用 CGLIB 增强后,调用时直接调用目标方法(即 @Configuration
类中被 @Bean
注解标注的方法)来获取 Bean 实例,这是因为重写的目标方法包含了单例管理的逻辑(如检查缓存),能够保证只创建一个 Bean 实例,而不是通过普通的代理对象调用。这样做的目的是确保 Spring 对 Bean 生命周期的正确管理,特别是单例 Bean 的唯一性。
5. 解决方案
使用参数引用法,将依赖的 Bean 作为方法参数注入,这样 Spring 会通过代理对象来注入依赖,确保代理机制正常工作。修改后的AppConfig
类如下:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
// 定义一个接口
interface MyService {
void doSomething();
}
// 实现接口
class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}
// 定义切面
@Aspect
class MyAspect {
@Pointcut("execution(* com.example.MyService.doSomething())")
public void pointcut() {}
@After("pointcut()")
public void afterAdvice() {
System.out.println("After advice executed");
}
}
// 配置类
@Configuration
@EnableAspectJAutoProxy
class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
@Bean
public MyService anotherService(MyService myService) {
return myService;
}
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean("anotherService", MyService.class);
service.doSomething();
}
}
通过这种方式,Spring 会确保注入的MyService
是代理对象,从而保证MyAspect
的afterAdvice()
方法在doSomething()
方法执行后被调用。