一、概览
- 含义:Spring 是一款主流的 Java EE 轻量级开源框架,包含Spring MVC、Spring Boot、Spring Cloud等
- 作用:简化 Java 企业级应用的开发难度和开发周期,提供了整合其他技术和框架的能力
- 简介:核心功能是控制反转(IoC)和面向切面(AOP)
- 组成:
- Spring Core(核心容器)
- Spring AOP(面向切面编程)
- Spring Data Access(数据访问)
- Spring Web(应用程序)
- Spring Message(消息传递)
- Spring test(测试)
- Spring Framework(框架)
- 概述:Spring Framework是Spring项目的核心框架
- 作用:Spring Framework实现IOC容器和AOP技术的功能模块
- 特点:
- 非侵入式
- 控制反转
- 面向切面编程
- 容器
- 组件化
一、Spring
1、Spring是什么?
官网地址:https://spring.io/
Spring 是最受欢迎的企业级 Java 应用程序开发框架,被全球数百万开发人员使用。
Spring 框架是一个开源的 Java 平台,最初由 Rod Johnson 编写,并于 2003 年 6 月在 Apache 2.0 许可下首次发布。
Spring 是轻量级开源框架,基础版本大小约为 2 MB。
Spring 框架的核心特性是可用于开发任何 Java 应用程序,但在 Java EE 平台上构建 Web 应用程序需要扩展。
Spring 框架的目标是通过启用基于 POJO 的编程模型,使 J2EE 开发更易于使用,并促进良好的编程实践。
2、Spring 家族
项目列表:Spring | Projects
Spring Framework:Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都以 Spring Framework 为基础。
3、Spring Framework 特性
特性 | 描述 |
---|---|
非侵入式 | 使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染;对功能性组件只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能进一步简化组件结构。这使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅。 |
控制反转(IOC) | IOC(Inversion of Control):翻转资源获取方向。传统方式是自己创建资源或向环境索取资源,而 IOC 则是环境将资源准备好,我们享受资源注入。 |
面向切面编程(AOP) | AOP(Aspect Oriented Programming):在不修改源代码的基础上增强代码功能。AOP 用来封装多个类的公共行为,将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,减少系统的重复代码,降低模块间的耦合度。另外,AOP 还解决一些系统层面上的问题,比如日志、事务、权限等。 |
容器 | Spring IOC 是一个容器,包含并管理组件对象的生命周期。组件享受容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大地降低了使用门槛,提高了开发效率。 |
组件化 | Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。这使得我们可以基于功能明确、边界清晰的组件有条不紊地搭建超大型复杂应用系统。 |
声明式 | 很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。 |
一站式 | 在 IOC 和 AOP 的基础上,Spring 可以整合各种企业应用的开源框架和优秀的第三方类库。Spring 旗下的项目已经覆盖了广泛领域,很多功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。 |
4、Spring Framework 五大功能模块
功能模块 | 功能介绍 |
---|---|
Core Container(核心容器) | 在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP & Aspects(面向切面编程) | 提供面向切面编程的支持。 |
Testing(测试) | 提供了对 JUnit 或 TestNG 测试框架的整合。 |
Data Access/Integration(数据访问/集成) | 提供了对数据访问/集成的功能。 |
Spring MVC | 提供了面向 Web 应用程序的集成功能。 |
二、Spring IOC
1、IOC
IoC(Inversion of Control,控制反转) 是一种设计原则,将对象的创建、配置和生命周期管理交给框架或容器,而不是由开发者直接控制。
- 传统方式:开发者手动通过 new 关键字创建对象并管理依赖。
- IoC方式:由Spring容器(如 ApplicationContext)负责创建对象,并在需要时注入依赖。
IOC容器的作用:
- 实例化对象。
- 配置对象及依赖关系。
- 管理对象生命周期(如单例、原型作用域)。
2、DI(依赖注入)
DI(Dependency Injection) 是 实现IOC的具体技术手段,通过外部(通常是容器)将依赖对象注入到目标对象中,而不是由目标对象自己创建依赖。
依赖注入的三种方式:
- 构造函数注入
public class UserService { private final UserRepository userRepository; // 构造函数注入 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
- Setter方法注入
public class UserService { private UserRepository userRepository; // Setter注入 public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } }
- 字段注入(通过注解,如 @Autowired 或 @Resource 直接注入字段)
public class UserService { @Autowired private UserRepository userRepository; }
Spring IoC 与 DI 的关系:
- IoC是目标:将控制权交给容器。
- DI是手段:通过依赖注入实现控制反转。
2.1 自动装配 vs 依赖注入
- 依赖注入(Dependency Injection) 是一种设计模式,旨在通过将依赖关系从一个对象传递给另一个对象,来实现对象之间的解耦。在Spring中,依赖注入通过容器来管理和传递对象之间的依赖关系,而不是由对象自身来创建或管理它们的依赖。这可以通过构造函数注入、Setter方法注入或字段注入等方式来实现
- 自动装配(Autowired) 是Spring Framework提供的一种依赖注入的方式,用于自动将合适的依赖注入到相应的位置,无需手动指定每个依赖的注入方式。通过使用@Autowired注解,Spring会自动在容器中查找匹配的依赖,并将其注入到需要的位置
3、IOC 容器在 Spring 中的实现
Spring 的 IOC 容器:
- Spring 的 IOC 容器是 IOC 思想的一个落地产品实现。
- IOC 容器中管理的组件也叫做 bean。
- 在创建 bean 之前,首先需要创建 IOC 容器。
IOC 容器的两种实现方式:
- BeanFactory:这是 IOC 容器的基本实现,是 Spring 内部使用的接口,面向 Spring 本身,不提供给开发人员使用。
- ApplicationContext:BeanFactory 的子接口,提供了更多高级特性,面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。
ApplicationContext 的主要实现类:
类型名 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象。 |
FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象。 |
ConfigurableApplicationContext | ApplicationContext 的子接口,包含一些扩展方法,如 refresh() 和 close(),让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 |
WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 |
4、基于XML管理Bean
4.1 获取Bean的方式
- 根据ID:
public class UserTest { @Test void newTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); User user = (User) applicationContext.getBean("user"); user.add(); // add…… } }
- 根据类型:
当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个public class UserTest { @Test void newTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); User user = (User) applicationContext.getBean(User.class); user.add(); // add…… } }
- 根据Id和类型:
如果组件类实现了接口,可以根据接口类型获取Bean,但前提是Bean唯一User user = (User) applicationContext.getBean("user", User.class);
4.2 依赖注入
- 根据Setter注入:
<bean id="studentOne" class="com.atguigu.spring6.bean.Student"> <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 --> <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) --> <!-- value属性:指定属性值 --> <property name="id" value="1001"></property> <property name="name" value="张三"></property> <property name="age" value="23"></property> <property name="sex" value="男"></property> </bean>
- 根据构造器注入:
<bean id="studentTwo" class="com.atguigu.spring6.bean.Student"> <constructor-arg name="id" value="1002"></constructor-arg> <constructor-arg name="name" value="李四"></constructor-arg> <constructor-arg name="age"value="33"></constructor-arg> <constructor-arg name="sex" value="女"></constructor-arg> </bean>
4.3 特殊值处理
- 字面量赋值:
<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 --> <property name="name" value="张三"/>
- Null值:
<property name="name"> <null /> </property>
- Xml实体
<!-- 解决方案一:使用XML实体来代替 --> <property name="expression" value="a < b"/>
- CDATA节
<property name="expression"> <!-- 解决方案二:使用CDATA节 --> <value><![CDATA[a < b]]></value> </property>
4.4 注入对象类型属性值
- 引用外部Bean:属性ref
<bean id="dept" class="com.example.demo.entity.Dept"> <property name="name" value="安保部" /> </bean> <bean id="emp" class="com.example.demo.entity.Emp"> <property name="name" value="张三" /> <property name="dept" ref="dept" /> </bean>
- 引用内部Bean:bean嵌套
<bean id="emp" class="com.example.demo.entity.Emp"> <property name="name" value="张三" /> <property name="dept" > <bean id="dept" class="com.example.demo.entity.Dept"> <property name="name" value="安保部" /> </bean> </property> </bean>
- 级联属性赋值:引入对象后,级联赋值name=“clazz.clazzId”
<bean id="dept" class="com.example.demo.entity.Dept"> <property name="name" value="安保部" /> </bean> <bean id="emp" class="com.example.demo.entity.Emp"> <property name="name" value="张三" /> <property name="dept" ref="dept" /> <property name="dept.name" value="秘书部" /> </bean>
4.5 引入数组类型属性值
<bean id="student" class="com.example.demo.entity.Student">
<property name="name" value="张三" />
<property name="books" >
<array>
<value>斗破苍穹</value>
<value>玉女心经</value>
<value>金瓶梅</value>
</array>
</property>
</bean>
4.6 注入集合类型属性值
- 注入List集合类型属性值:
<list> </list>
<bean id="stu1" class="com.example.demo.entity.Student"> <property name="name" value="张三" /> </bean> <bean id="class" class="com.example.demo.entity.Class"> <property name="name" value="一班" /> <property name="students" > <list> <ref bean="stu1"/> </list> </property> <property name="course" > <list> <value>数学</value>> </list> </property> </bean>
- 注入Map集合类型属性值:
<map><entry><<key>/key><value></value></entry></map>
<bean id="teacherOne" class="com.atguigu.spring6.bean.Teacher"> <property name="teacherId" value="10010"></property> <property name="teacherName" value="大宝"></property> </bean> <bean id="teacherTwo" class="com.atguigu.spring6.bean.Teacher"> <property name="teacherId" value="10086"></property> <property name="teacherName" value="二宝"></property> </bean> <bean id="studentFour" class="com.atguigu.spring6.bean.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 --> <property name="clazz" ref="clazzOne"></property> <property name="hobbies"> <array> <value>抽烟</value> <value>喝酒</value> <value>烫头</value> </array> </property> <property name="teacherMap"> <map> <entry> <key> <value>10010</value> </key> <ref bean="teacherOne"></ref> </entry> <entry> <key> <value>10086</value> </key> <ref bean="teacherTwo"></ref> </entry> </map> </property> </bean>
- 注入引用集合类型属性值:
<util></util>
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--list集合类型的bean--> <util:list id="students"> <ref bean="studentOne"></ref> <ref bean="studentTwo"></ref> <ref bean="studentThree"></ref> </util:list> <!--map集合类型的bean--> <util:map id="teacherMap"> <entry> <key> <value>10010</value> </key> <ref bean="teacherOne"></ref> </entry> <entry> <key> <value>10086</value> </key> <ref bean="teacherTwo"></ref> </entry> </util:map> <bean id="clazzTwo" class="com.atguigu.spring6.bean.Clazz"> <property name="clazzId" value="4444"></property> <property name="clazzName" value="Javaee0222"></property> <property name="students" ref="students"></property> </bean> <bean id="studentFour" class="com.atguigu.spring6.bean.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 --> <property name="clazz" ref="clazzOne"></property> <property name="hobbies"> <array> <value>抽烟</value> <value>喝酒</value> <value>烫头</value> </array> </property> <property name="teacherMap" ref="teacherMap"></property> </bean>
4.7 引入P命名空间属性值
通过p:成员属性,即可完成各个属性的赋值.
- 引入P命名空间
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
- 通过p命名空间方式为bean的各个属性赋值
<bean id="studentSix" class="com.atguigu.spring6.bean.Student" p:id="1006" p:name="小明" p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap"></bean>
4.8 引入外部属性文件
- 添加依赖
<!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency>
- 创建外部属性文件:在resources文件下创建jdbc.properties文件
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
- 引入属性文件
在使用 context:property-placeholder 元素加载外包配置文件功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 引入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> </beans>
- 配置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>
- 演示
@Test public void testDataSource() throws SQLException { ApplicationContext ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); DataSource dataSource = ac.getBean(DruidDataSource.class); Connection connection = dataSource.getConnection(); System.out.println(connection); }
4.9 引入Bean的作用域
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean class="com.atguigu.spring6.bean.User" scope="prototype"></bean>
4.10 Bean的生命周期
<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean class="com.atguigu.spring6.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod">
<property name="id" value="1001"></property>
<property name="username" value="admin"></property>
<property name="password" value="123456"></property>
<property name="age" value="23"></property>
</bean>
<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myBeanProcessor" class="com.atguigu.spring6.process.MyBeanProcessor"/>
- bean对象创建(调用无参构造器)
- bean对象设置属性
- bean的后置处理器(初始化之前):
BeanPostProcessor.postProcessBeforeInitialization
- bean对象初始化(需在配置bean时指定初始化方法):
User.initMethod()
- bean的后置处理器(初始化之后):
BeanPostProcessor.postProcessAfterInitialization
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法):
User.destroyMethod()
- IOC容器关闭
Bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行。
4.11 引入FactoryBean属性值
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
- 创建类UserFactoryBean
package com.atguigu.spring6.bean; public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { return new User(); } @Override public Class<?> getObjectType() { return User.class; } }
- 配置bean
<bean id="user" class="com.atguigu.spring6.bean.UserFactoryBean"></bean>
- 演示
此时获取到的容器对象,并不是UserFactoryBean,而是User@Test public void testUserFactoryBean(){ //获取IOC容器 ApplicationContext ac = new ClassPathXmlApplicationContext("spring-factorybean.xml"); User user = (User) ac.getBean("user"); System.out.println(user); }
4.12 引入自动装配
自动装配方式:
- byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
- 若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值null
- 若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
- byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
示例:
- Bean 配置
<bean id="userController" class="com.example.demo.controller.UserController" autowire="byType" /> <bean id="userService" class="com.example.demo.service.impl.UserServiceImpl" autowire="byType" /> <bean id="userDao" class="com.atguigu.spring6.autowire.dao.impl.UserDaoImpl" />
- 演示
@Test public void testAutoWireByXML(){ ApplicationContext ac = new ClassPathXmlApplicationContext("autowire-xml.xml"); UserController userController = ac.getBean(UserController.class); userController.saveUser(); }
5、基于注解管理Bean
5.1 概述
Java 增加了对注解(Annotation)的支持,它是代码中的一种特殊标记,可以在编译、类加载和运行时被读取,执行相应的处理。
注解 | 说明 |
---|---|
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
5.2 步骤
- 引入依赖
- 开启组件扫描
- 使用注解定义 Bean
- 依赖注入
5.3 开启组件扫描
Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 <context:component-scan>
元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。
- 添加约束
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> </beans>
- 开启扫描
<context:component-scan base-package="com.atguigu.spring6" />
- 指定要排除的组件
<context:component-scan base-package="com.atguigu.spring6"> <!-- context:exclude-filter标签:指定排除规则 --> <!-- type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名 expression属性:设置要排除的注解或类型的全类名 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <!--<context:exclude-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>--> </context:component-scan>
- 仅扫描指定组件
<!-- use-default-filters="false",表示关闭默认扫描规则 --> <context:component-scan base-package="com.atguigu" use-default-filters="false"> <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 --> <!-- use-default-filters属性:取值false表示关闭默认扫描规则 --> <!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 --> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <!--<context:include-filter type="assignable" expression="com.atguigu.spring6.controller.UserController"/>--> </context:component-scan>
5.4 使用注解定义Bean
一般不推荐使用 @Autowired、@Resource:Field injection is not recommended
5.4.1 使用@Autowired注入
- 默认按类型(ByType)注入
- 若存在多个同类型Bean,需结合@Qualifier(“beanName”)指定名称。
@Autowired @Qualifier("userServiceA") private UserService userService;
- 支持构造器注入(推荐用于强制依赖)。
private final UserService userService; @Autowired public MyController(@Qualifier("userServiceA") UserService userService) { this.userService = userService; }
- required属性:
- True:表示注入的时候要求被注入的Bean必须是存在的,如果不存在则报错
- False:表示注入的时候要求被注入的Bean不一定是存在的,如果存在的话就注入,不存在的话,也不报错
- 属于Spring框架(
org.springframework.beans.factory.annotation
包) - 需要Spring环境支持。
5.4.2 使用@Resource注入
- 默认按名称(ByName)注入,优先匹配字段名或name属性。
- 若名称未找到,则回退到按类型匹配。
- 不支持构造器注入,通常用于字段或Setter方法。
private UserService userService; @Resource public void setUserService(UserService userService) { this.userService = userService; }
- 属于Java EE标准(javax.annotation包),遵循JSR-250规范。
- 不依赖Spring,但需引入
javax.annotation-api
依赖(JDK 9+需手动添加)。<dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency>
6、手搓IOC
简化版的 手动实现IOC容器 实现以下功能:
- Bean的注册与存储
- 依赖查找与注入
- 单例与原型作用域支持
6.1 核心接口与类定义
- Bean定义类(BeanDefinition)
public class BeanDefinition { private Class<?> type; // Bean的类型 private String scope = "singleton"; // 作用域(singleton/prototype) private boolean lazy = false; // 是否懒加载 // 构造方法、Getter/Setter省略 }
- IOC容器(SimpleIOCContainer)
import java.util.HashMap; import java.util.Map; public class SimpleIOCContainer { // 存储Bean定义 private final Map<String, BeanDefinition> beanDefinitions = new HashMap<>(); // 存储单例Bean实例 private final Map<String, Object> singletonBeans = new HashMap<>(); // 注册Bean定义 public void registerBean(String beanName, BeanDefinition beanDefinition) { beanDefinitions.put(beanName, beanDefinition); } // 获取Bean实例 public Object getBean(String beanName) { BeanDefinition definition = beanDefinitions.get(beanName); if (definition == null) { throw new RuntimeException("Bean未定义: " + beanName); } // 处理作用域 if ("singleton".equals(definition.getScope())) { Object bean = singletonBeans.get(beanName); if (bean == null) { bean = createBean(beanName, definition); singletonBeans.put(beanName, bean); } return bean; } else { return createBean(beanName, definition); } } // 创建Bean实例并注入依赖 private Object createBean(String beanName, BeanDefinition definition) { try { // 1. 通过反射实例化对象 Class<?> clazz = definition.getType(); Object instance = clazz.getDeclaredConstructor().newInstance(); // 2. 简单依赖注入(假设通过字段注入) // 此处简化:遍历所有字段,查找是否有@Autowired注解 // 实际Spring会处理更复杂的逻辑(如构造器注入、方法注入等) for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { // 获取依赖的Bean名称(简化版,按类型查找) String dependencyBeanName = field.getType().getSimpleName(); Object dependency = getBean(dependencyBeanName); field.setAccessible(true); field.set(instance, dependency); } } return instance; } catch (Exception e) { throw new RuntimeException("创建Bean失败: " + beanName, e); } } }
6.2 自定义注解
// 模拟@Autowired注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
// 模拟@Component注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
6.3 测试用例
- 定义Bean
@Component("userDao") public class UserDao { public void query() { System.out.println("UserDao查询数据..."); } } @Component("userService") public class UserService { @Autowired private UserDao userDao; public void doSomething() { userDao.query(); System.out.println("UserService执行操作..."); } }
- 初始化容器并测试
public class Main { public static void main(String[] args) { SimpleIOCContainer container = new SimpleIOCContainer(); // 注册Bean定义(模拟扫描@Component并自动注册) BeanDefinition userDaoDef = new BeanDefinition(); userDaoDef.setType(UserDao.class); userDaoDef.setScope("singleton"); container.registerBean("userDao", userDaoDef); BeanDefinition userServiceDef = new BeanDefinition(); userServiceDef.setType(UserService.class); userServiceDef.setScope("singleton"); container.registerBean("userService", userServiceDef); // 获取Bean实例 UserService userService = (UserService) container.getBean("userService"); userService.doSomething(); } }
- 输出结果
UserDao查询数据... UserService执行操作...
6.4 实现总结
- 核心流程
- 注册Bean定义:记录Bean的类名、作用域等信息。
- 创建Bean实例:通过反射实例化对象,并递归注入依赖。
- 作用域管理:单例模式下缓存实例,原型模式每次创建新实例。
- 简化点
- 依赖注入仅支持字段注入,未处理构造器/方法注入。
- 未实现循环依赖处理(实际Spring使用三级缓存解决)。
- 未实现AOP、生命周期回调等高级功能。
- 扩展方向
- 添加@ComponentScan注解实现包扫描。
- 支持构造器注入和@Qualifier。
- 实现BeanPostProcessor机制处理初始化逻辑。
三、AOP
1、概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
2、AOP 作用
- 代码重用和模块化:AOP可以将一些通用的行为(例如日志记录、安全控制、事务管理等)抽象出来,形成可重用的模块,避免在每个业务逻辑中都重复编写这些代码。
- 分离关注点:AOP将不同的关注点分离开来,使得各个模块间职责更加清晰明确,代码的可读性和可维护性也更强。
- 简化开发:AOP可以帮助开发人员将关注点从业务逻辑中分离出来,使得开发更加简单明了。
- 提高系统的可扩展性:在系统需求变化时,只需要修改AOP模块而不是修改业务逻辑,这可以使得系统更加易于扩展和维护。
- 降低代码耦合度:AOP的作用是将不同的关注点分离开来,这可以避免代码之间的紧耦合,提高代码的可复用性和可维护性。
3、AOP核心概念
概念 | 说明 |
---|---|
Aspect(切面) | 封装横切关注点的模块(如日志切面、事务切面),包含多个通知(Advice)和切点(Pointcut)。 |
Advice(通知) | 切面在特定连接点(Join Point)执行的动作,如前置通知、后置通知等。 |
Pointcut(切点) | 定义通知应用的位置(哪些方法需要被增强),通过表达式匹配目标方法。 |
Join Point(连接点) | 程序执行过程中的某个点(如方法调用、异常抛出),Spring AOP仅支持方法级别的连接点。 |
Target(目标对象) | 被代理的原始对象(即业务类)。 |
Proxy(代理) | 通过动态代理生成的增强对象,调用代理方法时会触发切面逻辑。 |
Weaving(织入) | 将切面应用到目标对象的过程,Spring AOP在运行时通过动态代理实现。 |
3.1 横切关注点(Cross-Cutting Concerns)
- 定义:横切关注点是指那些与应用程序核心功能无关但又存在于整个应用程序中的功能或关注点。这些关注点通常会横跨多个模块和层次,例如日志记录、安全性、事务管理、性能监控等。
- 特点:
- 与应用程序的核心功能相对立,它们在应用程序的各个模块中散布,而不是集中在某一个特定的模块中。
- 如果直接写在业务代码中,会导致代码重复、难以维护、可读性差。
- 重要性:将横切关注点与核心业务逻辑分离开来是很有意义的,因为这样可以提高代码的可维护性、可重用性和可测试性。AOP允许开发人员将这些横切关注点从应用程序的核心业务逻辑中抽取出来,通过特殊的技术手段(例如切面)将其模块化地应用到应用程序中。
- 示例:
- 日志记录:对方法调用进行日志记录,便于问题排查。
- 事务管理:控制方法的事务性操作,确保数据一致性。
- 权限控制:在方法执行前进行权限验证,控制访问。
- 异常处理:集中处理异常,避免在各个方法中重复写异常处理逻辑。
- 性能监控:监控方法执行时间,进行性能优化。
通过AOP技术,开发人员可以定义一个切面,将横切关注点封装在切面中,并通过切点表达式指定切面在哪些方法上应用。这样,横切关注点就可以在不修改业务逻辑代码的情况下被添加、修改或删除,从而提高了应用程序的灵活性和可扩展性。
3.2 切面(Aspect)
- 定义:切面是AOP的核心模块,指的是封装横切关注点的模块化单元。切面通常是一个类,其中包含特定的逻辑,如日志记录、事务管理等功能。
- 作用:切面定义了在何时、何地、以何种方式“切入”到业务代码中。每个切面都可以包含多个切点和通知,以决定切面在应用中的行为方式。
- 实现:在AOP框架中,切面通常通过注解或配置文件进行定义,使其可以动态地附加到目标对象上,实现解耦和代码复用。
3.3 切点(Pointcut)
- 定义:切点指的是程序执行的某个特定点,比如方法的调用、异常的抛出、字段的访问等。在AOP中,切点是潜在的切入位置,表示横切关注点可以在何处插入到应用代码中。
- 作用:切点用于定义通知应该被应用到哪些方法上。虽然理论上任何代码执行点都可以作为切点,但在实际应用中,AOP通常围绕方法调用来定义切点。
- 实现:AOP框架通常提供切点表达式,用来匹配哪些方法或类需要被增强。
切点拦截规则表达式语法:
execution(* com.service.*.*(..))
:匹配com.service包下所有类的所有方法execution(* save*(..))
:匹配所有以save开头的方法execution(* com.service.UserService.*(..))
:匹配UserService接口的所有方法@annotation(com.example.Log)
:匹配被@Log注解标记的方法
示例:
@Pointcut("@annotation(com.example.demo.common.annotation.OpLog)")
public void opLog() {}
3.4 通知(Advice)
- 定义:通知定义了切面在切点上的具体行为。通知是AOP的执行部分,表示在匹配的切点上执行的代码逻辑。
- 类型:
- 前置通知(Before Advice):在方法执行之前执行的通知。
- 后置通知(After Advice):在方法执行之后执行的通知,不管方法是否发生异常。
- 返回通知(After Returning Advice):在方法正常返回后执行的通知。
- 异常通知(After Throwing Advice):在方法抛出异常时执行的通知。
- 环绕通知(Around Advice):在方法执行的前后都执行的通知,允许在方法调用之前和之后都添加自定义逻辑。这种通知最为灵活,可以完全控制目标方法的执行流程。
示例:
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[方法开始] " + joinPoint.getSignature());
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long endTime = System.currentTimeMillis();
System.out.println("[方法结束] 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
3.5 连接点(Joinpoint)
- 定义:连接点是指程序执行过程中能够插入切面的点。在Spring AOP中,这些点通常是方法的执行点,包括方法的调用前、调用后、抛出异常时等。连接点是切面逻辑被织入到目标对象的具体位置。
- 特点:
- 具体性:连接点是程序执行中的具体点,如方法的开始、结束或异常抛出等。
- 可拦截性:AOP框架可以在这些点上拦截方法的执行,并插入切面逻辑。
- 普遍性:在Spring AOP中,几乎每个方法调用都可以被视为一个连接点。
- 作用:连接点的作用是为切面提供了插入点,使得切面逻辑可以在不修改目标对象代码的情况下被动态地织入到目标对象中。通过连接点,AOP实现了对横切关注点的模块化处理,使得横切关注点可以在多个模块或类中共享和重用。
- 与切点的关系:虽然每个方法调用都可以被视为一个连接点,但并非所有的连接点都需要被切面逻辑拦截。切点(Pointcut)是连接点的子集,它定义了哪些连接点将被切面逻辑拦截。切点通过特定的表达式来匹配连接点,从而确定哪些方法调用将被增强。
3.6 目标对象(Target)
- 定义:目标对象是应用AOP切面的对象,即包含业务逻辑的实际对象。
- 作用:AOP通过对目标对象的增强来实现切面功能。在AOP中,目标对象是被代理的对象,切面逻辑会在目标对象的方法执行之前、之后或环绕执行。
3.7 代理(Proxy)
- 定义:代理是AOP在目标对象上的“包装”,负责实现对目标对象的增强。
- 类型:
- JDK动态代理:基于接口创建的代理,适用于目标对象实现接口的情况。
- CGLIB代理:基于子类的代理,适用于目标对象没有实现接口的情况。
- 作用:通过代理对象间接调用目标对象的方法,并在调用之前或之后插入切面逻辑。
3.8 织入(Weaving)
- 定义:织入是将切面逻辑与目标对象结合的过程,也就是将切面应用到目标对象上,使得增强的代码在目标对象的方法中生效。
- 方式:
- 编译时织入:在编译时将切面织入到目标代码中,生成增强后的字节码。这种方式需要专门的编译器支持。
- 类加载时织入:在类加载到JVM时,通过字节码操作将切面织入到目标类中。
- 运行时织入:在程序运行期间,通过动态代理或其他方式将切面织入到目标对象中。这是Spring AOP的主要方式。
4、动态代理
在 Java 开发中,动态代理是实现面向切面编程(AOP)的核心技术之一,主要用于在不修改目标对象代码的前提下,增强其功能(如日志、事务、权限控制等)。常见的动态代理实现方式有两种:JDK 动态代理和 CGLIB 代理。
4.1 JDK 动态代理
4.1.1 核心机制
- 基于接口:JDK 动态代理要求目标对象必须实现至少一个接口,代理类会动态实现这些接口。
- 反射生成:通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口动态生成代理对象。
- 运行时增强:代理逻辑在运行时动态生成,无需源码或编译期处理。
4.1.2 实现步骤
- 定义接口:目标对象必须实现接口。
public interface UserService { void save(); }
- 实现目标类:
public class UserServiceImpl implements UserService { @Override public void save() { System.out.println("保存用户"); } }
- 实现 InvocationHandler:定义代理逻辑。
public class MyInvocationHandler implements InvocationHandler { private final Object target; // 目标对象 public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("前置增强"); Object result = method.invoke(target, args); // 调用目标方法 System.out.println("后置增强"); return result; } }
- 生成代理对象:
UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 目标接口 new MyInvocationHandler(target) // 代理逻辑处理器 ); proxy.save(); // 调用代理方法
4.1.3 优点与缺点
(1)优点
- JDK 原生支持,无需引入第三方库。
- 生成代理对象速度快。
(2)缺点
- 只能代理接口,无法代理无接口的类。
- 反射调用方法性能略低(但 JVM 优化后差距可忽略)。
4.2 CGLIB 代理
4.2.1 核心机制
- 基于继承:CGLIB(Code Generation Library)通过继承目标类生成子类,重写父类方法实现代理。
- 字节码操作:使用 ASM 框架直接修改字节码生成代理类。
- FastClass机制:通过生成索引表,绕过反射调用,直接访问目标方法。
- 无需接口:可直接代理普通类(但无法代理 final 类或 final 方法)。
4.2.1 实现步骤
- 定义目标类(无需接口):
public class UserService { public void save() { System.out.println("保存用户"); } }
- 实现 MethodInterceptor:定义代理逻辑。
public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置增强"); Object result = proxy.invokeSuper(obj, args); // 调用父类方法 System.out.println("后置增强"); return result; } }
- 生成代理对象:
Enhancer enhancer = new Enhancer(); // Enhancer 是 CGLIB 库中用于生成代理类的工具类,通过继承目标类并重写方法实现代理 enhancer.setSuperclass(UserService.class); // 设置父类 enhancer.setCallback(new MyMethodInterceptor()); // 设置回调逻辑 UserService proxy = (UserService) enhancer.create(); proxy.save(); // 调用代理方法
4.2.1 优点与缺点
(1)优点
- 可代理无接口的类。
- 方法调用通过 FastClass 机制直接定位,性能优于反射。
(2)缺点
- 需要引入 CGLIB 依赖(Spring 核心包已内置)。
- 无法代理 final 类或方法。
- 生成代理对象速度较慢(首次生成需操作字节码)。
4.3 JDK 动态代理 vs CGLIB 代理
特性 | JDK 动态代理 | CGLIB 代理 |
---|---|---|
代理方式 | 基于接口 | 基于继承 |
目标对象要求 | 必须实现接口 | 类不能是 final,方法不能是 final |
性能 | 生成代理快,调用稍慢(反射) | 生成代理慢,调用快(FastClass) |
依赖 | JDK 原生支持 | 需引入 CGLIB 库 |
适用场景 | 有接口的类 | 无接口的类或需要高性能的场景 |
- JDK 动态代理:适合代理接口,轻量级且无需额外依赖。
- CGLIB 代理:适合代理无接口的类,性能更优但需权衡生成速度。
4.4 Spring 框架中的代理选择
- 默认策略:如果目标对象实现了接口,Spring AOP 优先使用 JDK 动态代理;否则使用 CGLIB 代理。
- 强制使用 CGLIB:通过配置 @EnableAspectJAutoProxy(proxyTargetClass = true),强制所有代理使用 CGLIB。
4.5 代码示例(Spring Boot 中强制 CGLIB)
- 添加 CGLIB 依赖(Spring Boot 2.x+ 已内置):
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
- 启用 CGLIB 代理:
@Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用 CGLIB public class AppConfig { }
4.6 性能优化建议
- 高频调用场景:优先选择 CGLIB 代理(FastClass 机制减少反射开销)。
- 代理生成开销:对代理类进行缓存(如 Spring 的单例 Bean)。
- 避免过度代理:代理会增加调用链路深度,影响调试和性能。
5、基于注解的AOP
5.1 概述
基于注解的AOP是一种AOP的实现方式,它通过在Java类、方法、参数等上添加注解的方式来实现切面的定义和应用,相比于传统的XML配置方式更加便捷和灵活
5.2 基本用例-注解实现AOP
基于注解的AOP的实现:
- 将目标对象和切面交给IOC容器管理(注解+扫描)
- 开启AspectJ的自动代理,为目标对象自动生成代理
- 将切面类通过注解@Aspect标识
示例:
- 导入依赖
<!--spring aop依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>6.0.2</version> </dependency> <!--spring aspects依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.2</version> </dependency>
- 创建接口
public interface Calculator { int add(int i, int j); }
- 实现类
@Component public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } }
- 创建切面类
@Aspect @Component public class LogAspect { // 前置通知 // 异常通知 // 返回通知 // 后置通知 // 环绕通知 …… }
- 配置Spring配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.example.demo" /> <aop:aspectj-autoproxy /> </beans>
- 演示
@Test public void testAdd(){ ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); Calculator calculator = ac.getBean( Calculator.class); int add = calculator.add(1, 1); }
5.3 基于XML的AOP
<context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan>
<aop:config>
<!--配置切面类-->
<aop:aspect ref="loggerAspect">
<aop:pointcut id="pointCut"
expression="execution(* com.atguigu.aop.xml.CalculatorImpl.*(..))"/>
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
<aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
<aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
四、Spring AOP的局限性
- 仅支持方法级别的连接点:无法拦截字段访问或构造器调用。
- 仅拦截Spring管理的Bean:非Spring容器管理的对象无法被增强。
- 同类内部方法调用不触发AOP:若类内部方法A调用方法B,方法B的切面逻辑不会生效(需通过代理对象调用)。
- 性能开销:动态代理会引入额外性能损耗,但通常可忽略不计。
五、使用Spring AOP的步骤
- 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 定义切面类
@Aspect @Component public class TransactionAspect { @Before("execution(* com.example.service.*.*(..))") public void startTransaction() { System.out.println("开启事务"); } @AfterReturning("execution(* com.example.service.*.*(..))") public void commitTransaction() { System.out.println("提交事务"); } @AfterThrowing("execution(* com.example.service.*.*(..))") public void rollbackTransaction() { System.out.println("回滚事务"); } }
- 启用AOP支持:在配置类中添加@EnableAspectJAutoProxy:
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
六、Spring AOP vs AspectJ
特性 | Spring AOP | AspectJ |
---|---|---|
实现方式 | 基于动态代理(运行时织入) | 基于字节码增强(编译时/加载时织入) |
性能 | 较低(代理调用有额外开销) | 更高(直接修改字节码) |
功能范围 | 仅支持方法级别的切面 | 支持字段、构造器、静态代码块等 |
依赖 | 无需额外依赖(Spring集成) | 需独立安装编译器或使用LTW(加载时织入) |
适用场景 | 简单切面需求(如日志、事务) | 复杂切面需求(如性能监控、安全检查) |
七、最佳实践
- 优先使用注解配置:@Aspect + @Component更简洁。
- 避免过度使用AOP:仅对横切关注点(如日志、事务)使用,业务逻辑保持清晰。
- 注意代理对象的类型:若需获取目标类而非接口,需设置proxyTargetClass=true。
- 处理同类内部调用:通过AopContext.currentProxy()获取代理对象。
- 通过Spring AOP,开发者可以以非侵入式的方式增强代码功能,提升模块化和可维护性。