三天急速通关Spring6
- 0 文章介绍
- 1 介绍
- 1.1 背景
- 1.2 Spring
- 2 Spring6入门程序
- 2.1 准备环境
- 2.2 配置文件
- 2.3 Tips
- 3 IoC通过XML注入
- 3.1 介绍
- 3.2 Set注入
- 3.2.1 简单类型注入
- 3.2.2 注入外部bean与内部bean
- 3.2.3 其他类型注入
- 3.3 Constructor注入,P注入,C注入,Util注入
- 3.4 XML自动化注入
- 4 Bean的作用域与GoF的工厂模式
- 4.1 作用域
- 4.2 工厂模式
- 5 Bean的实例化方式、生命周期、循环依赖问题
- 5.1 实例化方式
- 5.2 生命周期
- 5.3 循环依赖问题
- 6 IoC通过注解注入
- 6.1 入门程序
- 6.2 声明Bean的标签
- 6.3 负责注入的标签
- 6.4 全注解开发
- 7 面向切面编程AOP
- 7.1 代理模式
- 7.1.1 代理模式介绍
- 7.1.2 静态代理
- 7.1.3 动态代理
- 7.2 面向切面编程AOP
- 7.2.1 AOP七大术语
- 7.2.2 切点表达式
- 7.2.3 使用Spring的AOP
- 8 Spring6中的事务
- 8.1 注解事务的使用
- 8.2 事务的属性
0 文章介绍
在倍速观看动力节点杜老师的Spring6
教程之后,根据视频内容以及课程笔记进行实践,经过自己的理解并总结后形成这篇学习笔记。文章总共分为九个章节,包括了原教程的十九个章节的大部分知识,学习本文的前置知识需要:JavaSE
,JDBC
,MySQL
,XML
,MyBatis
。本文所提供的信息和内容仅供参考,作者和发布者不保证其准确性和完整性。
1 介绍
1.1 背景
观察之前在学习MyBatis
的MVC
架构实践的时候,会发现在Controller
中使用了ServiceImpl
,ServiceImpl
使用了DaoImpl
,这个时候如果换了数据库,会发现需要在程序中需要新增另一个DaoImpl
并修改ServiceImpl
,增加可以,但修改不行,违背了OCP
原则。同时Controller
到DaoImpl
这样上层逐层依赖下层的结构也违背了DIP
(Dependence
Inversion
Principle
,依赖倒置原则)。
-
OCP
(Open
-Closed
Principle
,开闭原则)的核心思想是对扩展开放,修改关闭。 -
DIP
(Dependence
Inversion
Principle
,依赖倒置原则),核心思想包括两点。- 高层模块不应该依赖低层模块,二者都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
理解:
ServiceImpl
不实例化具体的DaoImpl
,只声明一个DaoImpl
接口。
IoC
(Inversion of Control
,控制反转)本质是一种面向对象的编程思想,利用这种编程思想可以降低程序耦合度,符合依赖倒置原则。
- 核心思想:将对象的创建权交出去,将对象与对象之间关系的管理权交出去,由第三方容器来负责创建与维护。
- 实现方式:
DI
(Dependency
Injection
,依赖注入)。- 依赖:一种关系,当某个类
A
需要使用到其他类B
来完成任务时称A
依赖B
。 - 注入:一个过程,依赖关系传递给需要它的类的过程。
- 实现方式:
set
方法注入,构造方法注入。
- 依赖:一种关系,当某个类
1.2 Spring
Spring
是一个轻量级的控制反转(IoC
)和面向切面(AOP
)的容器框架,为了解决EJB
臃肿的设计,以及难以测试等问题,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。
-
Spring的的八大模块:
模块名称
功能描述
主要用途
Spring Core
提供Spring框架的基础功能,包括依赖注入( DI
)、控制反转(IoC
)容器、Bean
管理等。管理应用程序的依赖关系,实现松耦合的组件开发。 Spring Context
扩展了 Spring Core
,提供更高级的容器功能,如事件传播、国际化支持、资源加载等。提供应用程序的上下文环境,支持更复杂的场景,如 Web
应用。Spring AOP
提供面向切面编程( AOP
)的支持,允许将横切关注点(如日志、事务管理、安全)与业务逻辑分离。实现日志记录、事务管理、权限验证等功能,减少代码重复 Spring DAO
提供数据访问对象( DAO
)的支持,包括异常转换和模板方法,用于简化数据访问层的开发。统一数据访问异常处理,支持多种数据访问技术(如 JDBC
、Hibernate
、JPA
等)。Spring ORM
集成各种对象关系映射( ORM
)框架,如Hibernate
、JPA
等,提供统一的ORM
支持。简化 ORM
框架的使用,支持多种ORM
工具,方便开发持久层代码。Spring Web MVC
提供基于 Servlet
的Web
开发框架,支持MVC
模式,用于构建传统的同步Web
应用程序。构建 Web
应用程序,处理HTTP
请求和响应,支持表单处理、视图解析等功能。Spring WebFlux
提供响应式编程支持,用于构建高性能的非阻塞 Web
应用程序。构建基于 Reactive
编程模型的Web
应用,提高并发处理能力,支持响应式流规范。 -
Spring的的五大特点:
- 轻量:完整的
Spring
框架只有1MB
多的JAR
,所需的处理开销也微不足道。 - 控制反转:对象依赖的其它对象通过被动的方式传递进来,而不是对象自己创建或查找。
- 面向切面:通过分离横切关注点(
cross-cutting concerns
)来提高代码的模块化程度。横切关注点是指那些与业务逻辑无关,但又需要在多个地方重复使用的功能,难以通过传统的面向对象编程(OOP
)方式很好地分离。 - 容器:管理应用对象的配置和生命周期,在这个意义上它是一种容器。
- 框架:将简单的组件配置、组合成为复杂的应用。
- 轻量:完整的
2 Spring6入门程序
跟着做就好,0 创建Maven Java Web
工程 -> 1 依赖导入 -> 2 配置log4j2.xml
-> 3 配置Spring6
的beans.xml
-> 4 编写Java
代码测试IoC
容器。
2.1 准备环境
暂时用不着Tomcat
,Maven
的打包方式设置为Jar
。
-
IntelliJ IDEA
:2024.1.7 -
Navicat for MySQL
:17.1.2 -
MySQL
:8.0.26 -
JDK
:17.0.2 -
Maven:3.9.1
2.2 配置文件
- pom.xml依赖导入
<dependencies><!--spring context依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.1.14</version></dependency><!--junit依赖--><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.12.0-M1</version><scope>test</scope></dependency><!--log4j2的依赖--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency>
</dependencies>
-
配置
log4j2.xml
,在类目录下创建,名字不能修改,配置之后Spring6
也会打印详细过程。<?xml version="1.0" encoding="UTF-8"?><configuration><loggers><!--level指定日志级别,从低到高的优先级:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF--><root level="DEBUG"><appender-ref ref="spring6log"/></root></loggers><appenders><!--输出日志信息到控制台--><console name="spring6log" target="SYSTEM_OUT"><!--控制日志输出的格式--><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/></console></appenders></configuration>
-
配置
beans.xml
文件<?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="user" class="com.cut.spring6.bean.User"/> </beans>
-
创建
beans.xml
中bean
标签指向的com.cut.spring6.bean.User
类java">package com.cut.spring6.bean;/*** 用户类** @author tang* @version 1.0.0* @since 1.0.0*/ public class User {public User() {System.out.println("User无参构造方法执行了");}@Overridepublic String toString() {return "User{}";} }
-
测试程序
src/test/java/com/cut/spring6/BeanTest.java
java">package com.cut.spring6;import com.cut.spring6.bean.User; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.junit.jupiter.api.Test;/*** 测试类* @author tang* @version 1.0.0* @since 1.0.0*/ public class BeanTest {@Testpublic void testBean() {ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");User user = context.getBean("user", User.class);System.out.println(user);} }
-
测试输出
2025-02-09 20:23:53 872 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@142269f2 2025-02-09 20:23:53 942 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [beans.xml] 2025-02-09 20:23:53 955 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user' User无参构造方法执行了 User{}Process finished with exit code 0
2.3 Tips
bean
标签的id
不能重复,若重复会在spring6
启动时报错。spring6
底层是通过解析xml中bean
的全限定类型,再通过反射动态创建对象。- 创建完的对象放在一个
Map
中,key
是id
,value
是对象。 beans.xml
可以创建多个,并且命名不限定,同时ClassPathXmlApplicationContext()
可以传递多个xml
文件,如果文件路径没在类路径,需要使用FileSystemXmlApplicationContext()
。spring6
除了可以创建自定义类,也可以创建JDK
中的类,只要类非抽象并提供了无参构造。getBean()
如果传递不存在的id会报错。ApplicationContext
还有个超级父接口BeanFactory
,在后续还会再提到。
3 IoC通过XML注入
3.1 介绍
IoC
(Inverse of Control
,控制反转)是一种思想,降低程序耦合度,提高扩展力,达到OCP
(Open-Closed Principle
,开闭原则),DIP
(Dependence Inversion Principle
,依赖倒置原则)。将对象的创建以及对象与对象之间的维护交给容器管理,通过DI
(Dependency Injection
,依赖注入)实现,其中DI
又分为两种:
Set
注入:通过反射机制调用set
方法来给属性赋值,让对象之间产生关系。Constructor
注入:调用构造方法来给属性赋值,让对象之间产生关系。
3.2 Set注入
3.2.1 简单类型注入
-
创建
com.cut.spring6.bean.UserComplete
类java">package com.cut.spring6.bean;import java.net.URI; import java.net.URL; import java.time.LocalDate; import java.util.Date; import java.util.Locale;/*** 包括所有基本数据类型的User类** @author tang* @version 1.0.0* @since 1.0.0*/ public class UserComplete {private byte age;private short height;private int userId;private long phoneNumber;private float balance;private double salary;private boolean married;private char gender;private Byte ageWrapper;private Short heightWrapper;private Integer userIdWrapper;private Long phoneNumberWrapper;private Float balanceWrapper;private Double salaryWrapper;private Boolean marriedWrapper;private Character genderWrapper;private String name;private Date birthDate;private Season favoriteSeason;private URI personalWebsite;private URL profilePictureUrl;private LocalDate joinDate;private Locale locale;private Class<?> userClassType;... ...
-
resource
目录下创建user.property
文件用于beans.xml
里属性赋值# Basic Information age=30 height=175 userId=1001 phoneNumber=1234567890 balance=1000.50 salary=5000.75 isMarried=true gender=M# Wrapper Classes ageWrapper=31 heightWrapper=176 userIdWrapper=1002 phoneNumberWrapper=9876543210 balanceWrapper=2000.25 salaryWrapper=6000.50 isMarriedWrapper=false genderWrapper=F# String and Date name=tang # 日期字符串格式不能随便写,必须是Date对象toString()方法执行的结果。 birthDate=Fri Sep 30 15:26:38 CST 2022 favoriteSeason=SUMMER personalWebsite=https://www.baidu.com # spring6之后,会自动检查url是否有效,如果无效会报错。 profilePictureUrl=https://www.baidu.com joinDate=EPOCH # java.util.Locale 主要在软件的本地化时使用。 locale=CHINESE userClassType=java.lang.String
-
beans.xml
的根标签中增加命名context
命名空间用于加载user.properties
文件<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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd "><context:property-placeholder location="user.properties"/>
-
注入
<!-- 通过外部属性文件给简单类型注入 --> <bean id="userCompleteBean" class="com.cut.spring6.bean.UserComplete"><property name="age" value="${age}"/><property name="height" value="${height}"/><property name="userId" value="${userId}"/><property name="phoneNumber" value="${phoneNumber}"/><property name="balance" value="${balance}"/><property name="salary" value="${salary}"/><property name="married" value="${isMarried}"/><property name="gender" value="${gender}"/><property name="ageWrapper" value="${ageWrapper}"/><property name="heightWrapper" value="${heightWrapper}"/><property name="userIdWrapper" value="${userIdWrapper}"/><property name="phoneNumberWrapper" value="${phoneNumberWrapper}"/><property name="balanceWrapper" value="${balanceWrapper}"/><property name="salaryWrapper" value="${salaryWrapper}"/><property name="marriedWrapper" value="${isMarriedWrapper}"/><property name="genderWrapper" value="${genderWrapper}"/><property name="name" value="${name}"/><property name="birthDate" value="${birthDate}"/><property name="favoriteSeason" value="${favoriteSeason}"/><property name="personalWebsite" value="${personalWebsite}"/><property name="profilePictureUrl" value="${profilePictureUrl}"/><property name="joinDate" value="${joinDate}"/><property name="locale" value="${locale}"/><property name="userClassType" value="${userClassType}"/> </bean>
-
测试代码
java">@Test public void testBeanBaseType() {System.out.println("-------------testBeanBaseType-------------");ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");UserComplete user = context.getBean("userCompleteBean", UserComplete.class);System.out.println(user); }
-
测试结果
-------------testBeanBaseType------------- ... log4j2日志输出 ... UserComplete{ age=30, height=175, userId=1001, phoneNumber=1234567890, balance=1000.5, salary=5000.75, married=true, gender=M, ageWrapper=31, heightWrapper=176, userIdWrapper=1002, phoneNumberWrapper=9876543210, balanceWrapper=2000.25, salaryWrapper=6000.5, marriedWrapper=false, genderWrapper=F, name='tang', birthDate=Sat Oct 01 05:26:38 CST 2022, favoriteSeason=SUMMER, personalWebsite=https://www.baidu.com, profilePictureUrl=https://www.baidu.com, joinDate=1970-01-01, locale=chinese, userClassType=class java.lang.String }Process finished with exit code 0
3.2.2 注入外部bean与内部bean
-
创建
com.cut.spring6.dao.UserDao
与com.cut.spring6.dao.impl.UserDaoImpl
java">package com.cut.spring6.dao;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public interface UserDao { }
java">package com.cut.spring6.dao.impl;import com.cut.spring6.dao.UserDao;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public class UserDaoImpl implements UserDao { }
-
创建
com.cut.spring6.service.UserService
与com.cut.spring6.service.impl.UserServiceImpl
java">package com.cut.spring6.service;import com.cut.spring6.dao.UserDao;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public interface UserService {}
java">package com.cut.spring6.service.impl;import com.cut.spring6.dao.UserDao; import com.cut.spring6.service.UserService;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public class UserServiceImpl implements UserService {private UserDao userDao;public UserDao getUserDao() {return userDao;}public void setUserDao(UserDao userDao) {this.userDao = userDao;}@Overridepublic String toString() {return "UserServiceImpl{" +"userDao=" + userDao +'}';} }
-
beans.xml
配置<!-- 外部注入 --> <bean id="userDaoBean" class="com.cut.spring6.dao.impl.UserDaoImpl"/> <bean id="userServiceBeanOuter" class="com.cut.spring6.service.impl.UserServiceImpl"><property name="userDao" ref="userDaoBean"/> </bean><!-- 内部注入 --> <bean id="userServiceBeanInner" class="com.cut.spring6.service.impl.UserServiceImpl"><property name="userDao"><bean class="com.cut.spring6.dao.impl.UserDaoImpl"/></property> </bean>
-
测试
java">@Test public void testBeanOuter() {System.out.println("-------------testBeanOuter-------------");ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");UserServiceImpl userServiceImpl = context.getBean("userServiceBeanOuter", UserServiceImpl.class);System.out.println(userServiceImpl); }@Test public void testBeanInner() {System.out.println("-------------testBeanInner-------------");ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");UserServiceImpl userServiceImpl = context.getBean("userServiceBeanInner", UserServiceImpl.class);System.out.println(userServiceImpl); }
-
测试结果
-------------testBeanInner------------- ... log4j输出 ... UserServiceImpl{userDao=com.cut.spring6.dao.impl.UserDaoImpl@388ffbc2} -------------testBeanOuter------------- ... log4j输出 ... UserServiceImpl{userDao=com.cut.spring6.dao.impl.UserDaoImpl@7e094740}
3.2.3 其他类型注入
-
级联属性
<bean id="student" class="com.powernode.spring6.beans.Student"><property name="name" value="张三"/><!--要点1:以下两行配置的顺序不能颠倒--><property name="clazz" ref="clazzBean"/><!--要点2:clazz属性必须有getter方法--><property name="clazz.name" value="高三一班"/> </bean>
-
数组,
List
,Set
注入,简单类型用标签,非简单类型用标签外部注入<bean id="person" class="com.powernode.spring6.beans.Person"><property name="array"><array><value>鸡排</value><value>汉堡</value></array></property><property name="list"><list><value>鸡排</value><value>汉堡</value></list></property><property name="set"><set><value>鸡排</value><value>汉堡</value><!-- <ref bean="otherBean"/> --></set></property> </bean>
-
map
注入,key
和value
如果是简单类型分别用key
,value
属性,非简单类型分别用key-ref
,value-ref
属性<bean id="peopleBean" class="com.powernode.spring6.beans.People"><property name="addrs"><map><!--如果key不是简单类型,使用 key-ref 属性--><!--如果value不是简单类型,使用 value-ref 属性--><entry key="1" value="北京大兴区"/><entry key="2" value="上海浦东区"/><entry key="3" value="深圳宝安区"/></map></property> </bean>
-
properties
注入<bean id="peopleBean" class="com.powernode.spring6.beans.People"><property name="properties"><props><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></props></property> </bean>
-
空字符串和
null
注入的处理<bean id="vipBean" class="com.powernode.spring6.beans.Vip"><!--空串的第一种方式--><!--<property name="email" value=""/>--><!--空串的第二种方式--><property name="email"><value/></property> </bean> <!-- null的第一种方式,不给属性赋值 --> <bean id="vipBean" class="com.powernode.spring6.beans.Vip" /> <!-- null的第二种方式 --> <bean id="vipBean" class="com.powernode.spring6.beans.Vip"><property name="email"><null/></property> </bean>
-
特殊字符的注入,使用<![CDATA[]]>,这个只能使用
value
标签,不能用value
属性。<bean id="mathBean" class="com.powernode.spring6.beans.Math"><property name="result"><!--只能使用value标签--><value><![CDATA[2 < 3]]></value></property> </bean>
3.3 Constructor注入,P注入,C注入,Util注入
-
Constructor
注入,可以用index
,可以用name
,也可以不指定。<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--第一个参数下标是0--><constructor-arg index="0" ref="orderDaoBean"/><!--第二个参数下标是1--><constructor-arg index="1" ref="userDaoBean"/> </bean><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--这里使用了构造方法上参数的名字--><constructor-arg name="orderDao" ref="orderDaoBean"/><constructor-arg name="userDao" ref="userDaoBean"/> </bean><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--没有指定下标,也没有指定参数名字--><constructor-arg ref="orderDaoBean"/><constructor-arg ref="userDaoBean"/> </bean>
-
P
命名空间注入,基于setter
方法,beans
标签属性增加xmlns:p="http://www.springframework.org/schema/p"
<bean id="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/>
-
C
命名空间注入,基于构造方法,beans
标签属性增加xmlns:c="http://www.springframework.org/schema/c"
<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>
-
Util
命名空间注入,让配置复用,可以结合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:util="http://www.springframework.org/schema/util"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"><util:properties id="prop"><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></util:properties><bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1"><property name="properties" ref="prop"/></bean><bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2"><property name="properties" ref="prop"/></bean> </beans>
3.4 XML自动化注入
自动化注入指的是通过Spring
容器自动注入依赖关系,XML
自动化注入可以通过名字或者类型进行自动化注入,自动注入以外的其他属性会是默认值。
-
ByName
通过名称注入,bean标签添加autowire="byName"
属性,实际为set
注入。<bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/> <bean id="userDao" class="com.powernode.spring6.dao.UserDao"/>
有
id
为userDao
的bean
,UserService
有相同名称的字段,并且有对应的se
t方法,在生成id
为userService
的bean
的时候会自动生成UserDao
对象赋值给userDao
属性。 -
ByType
通过类型注入,bean
标签添加autowire="byType"
属性,实际为set
注入。<bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byType"/> <bean id="userDao" class="com.powernode.spring6.dao.UserDao"/>
只需要
UserService
有UserDao
类型的字段即可自动注入,多个相同类型会提示报错。
4 Bean的作用域与GoF的工厂模式
这一节主要简单介绍一下bean
的作用域,以及三种工厂模式:简单工厂,工厂方法,抽象工厂。
4.1 作用域
Spring
框架中,singleton
和prototype
是两种最常见的Bean
作用域,通过设置Bean
标签的scope
属性修改作用域。
singleton
作用域:整个Spring
容器中只有一个实例,无论何时请求该Bean
,Spring
都会返回同一个实例,默认值。prototype
作用域:每次调用getBean()
方法或注入该Bean
时,Spring
都会返回一个新的实例。- 其他常见作用域:
request
:一个请求对应一个Bean
,WEB
应用限定。session
:一个会话对应一个Bean
,WEB
应用限定。global session
:portlet
应用中专用的。如果在Servlet
的WEB
应用中使用global session
的话,和session
一个效果。application
:一个应用对应一个Bean
,WEB
应用限定。websocket
:一个websocket
生命周期对应一个Bean
,WEB
应用限定。- 自定义
scope
:很少使用。
Spring
根据Bean
的作用域来选择管理方式:
singleton
:精确地知道该Bean
何时被创建,何时初始化完成,以及何时被销毁;prototype
:创建了Bean
的实例后不再跟踪其生命周期。
如果想要让spring
管理自己创建的对象
java">User user = new User();
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("userBean", user);
// 从spring容器中获取bean
User userBean = factory.getBean("userBean", User.class);
4.2 工厂模式
工厂模式通常有三种形态:
-
简单工厂模式(
Simple Factory
):不属于GOF
,又叫静态工厂方法模式,是工厂方法模式的一种特殊实现。核心思想: 工厂类可以根据参数的不同通过静态方法返回不同类的实例,被创建的实例通常都具有共同的父类。
理解: 一个工厂类,一个抽象产品类,多个具体产品类继承于抽象产品类,使用的时候传入参数到工厂类的静态方法,根据参数的不同返回不同的具体产品类对象,可以用同一个抽象产品类接收。
优点: 不管怎么创建,需要哪个对象时,只需要向工厂索要即可,生产消费分离。
缺点: 工厂类很难维护,不符合
OCP
。 -
工厂方法模式(
Factory Method
):是23种设计模式之一。核心思想: 定义一个通用的工厂接口,客户端代码依赖于工厂接口而不是具体的工厂类。这种方式支持多态性,客户端代码可以通过工厂接口调用不同的具体工厂类,从而实现灵活的扩展。
理解: 为了符合
OCP
,在简单工厂的基础上再增加一个通用工厂接口,多个具体工厂,具体工厂各自负责生产自己的具体产品,使用的时候,创建不同的具体工厂类对象用同一个工厂接口接收,同时具体工厂类创建的不同具体产品类对象可以用同一个抽象产品类接收。优点: 创建对象只需要之道类名称,扩展性高,增加产品只需要增加对应工厂类即可,屏蔽产品的具体实现,调用者只关心产品的接口。
缺点: 类的数量暴增。
java">public class Client {public static void main(String[] args) {// 使用 ConcreteFactoryA 创建产品Factory factoryA = new ConcreteFactoryA();Product productA = factoryA.factoryMethod();productA.usefulFunction();// 使用 ConcreteFactoryB 创建产品Factory factoryB = new ConcreteFactoryB();Product productB = factoryB.factoryMethod();productB.usefulFunction();} }
-
抽象工厂模式(
Abstract Factory
):是23种设计模式之一。核心思想: 一个抽象工厂接口定义了一组创建方法,用于创建多个产品族中的产品对象。具体工厂类实现了这些接口,负责创建特定主题或风格的产品族。
理解: 通过抽象工厂接口获取具体工厂类对象,再通过具体工厂类创建具体的产品对象。
优点: 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点: 产品族扩展非常困难,要增加一个系列的某一产品,既要在
AbstractFactory
里加代码,又要在具体的里面加代码。
java">public class Client {public static void main(String[] args) {// 使用 ConcreteFactory1 创建产品族AbstractFactory factory1 = new ConcreteFactory1();ProductA productA1 = factory1.createProductA();ProductB productB1 = factory1.createProductB();productA1.usefulFunctionA();productB1.usefulFunctionB();// 使用 ConcreteFactory2 创建产品族AbstractFactory factory2 = new ConcreteFactory2();ProductA productA2 = factory2.createProductA();ProductB productB2 = factory2.createProductB();productA2.usefulFunctionA();productB2.usefulFunctionB();} }
5 Bean的实例化方式、生命周期、循环依赖问题
5.1 实例化方式
Bean
提供了多种实例化方式,通常包括4种方式,构造方法,简单工厂,factory-bean
,FactoryBean
。之前一直使用的都是构造方法实例化。
-
简单工厂:声明一个工厂类,添加
get
方法,返回Vip
对象,再给bean
标签添加factory-method
属性即可。<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
-
factory-bean
:本质还是通过工厂方法模式进行实例化。<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/> <bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
-
FactoryBean
接口:类实现FactoryBean
接口之后,factory-bean
与factory-method
就不需要指定了。java">public class PersonFactoryBean implements FactoryBean<Person> {@Overridepublic Person getObject() throws Exception {return new Person();}@Overridepublic Class<?> getObjectType() {return null;}@Overridepublic boolean isSingleton() {// true表示单例// false表示原型return true;} }
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
注意
FactoryBean
和BeanFactory
的区别,前者是工厂Bean
,用于实例化其它Bean
对象的一个Bean
,后者是Bean
工厂,负责创建Bean
对象。理解:工厂
Bean
是给Bean
工厂帮忙的,工厂Bean
相当于一个特殊车间生产Bean
,可以在里面自定义生产逻辑。
5.2 生命周期
生命周期指的是对象从创建开始到最终销毁的整个过程。
-
五步生命周期
常见的是五个步骤,
其中:- 实例化:反射机制调用
Bean
的无参构造函数来创建Bean
实例,作用是分配内存并创建对象的基本结构。 - 属性注入:反射机制调用
Bean
的setter
方法完成属性值注入。 - 初始化:完成加载配置、分配资源、执行初始化逻辑等任务,确保
Bean
在被使用之前完全可用。 - 使用:
getBean()
使用。 - 销毁:
Spring
容器关闭时,会调用Bean
的销毁方法,释放资源,作用是清理资源,避免内存泄漏。
- 实例化:反射机制调用
-
七步生命周期
在初始化前和初始化后添加代码,可以实现
Bean
后处理器BeanPostProcessor
接口,重写before
和after
方法。
-
十步生命周期
属性注入后,初始化之前还会检查
Aware
相关接口以及InitializingBean
接口,同时在销毁之前还会检查DisposableBean
接口。
5.3 循环依赖问题
循环依赖是指两个或多个 Bean
之间相互依赖,形成了一个闭环,导致 Spring
容器无法正确地实例化对象。
Spring
使用三级缓存来解决循环依赖问题,具体包括:
- 一级缓存(
singletonObjects
):存储完全初始化完成的单例Bean
。 - 二级缓存(
earlySingletonObjects
):存储已经实例化但尚未完成初始化的Bean
。 - 三级缓存(
singletonFactories
):存储 Bean 的工厂对象,用于提前暴露Bean
的早期引用。
创建一个 Bean
时,会在实例化后立即将其放入三级缓存中,如果在属性注入阶段发现循环依赖,Spring
会从三级缓存中获取早期引用,从而打破循环。
Spring
只能解决setter
方法注入的单例bean
之间的循环依赖。Spring
在创建ClassA
对象后,不需要等给属性赋值,直接将其曝光到bean
缓存当中。在解析ClassA
的属性时,又发现依赖于ClassB
,再次去获取ClassB
,当解析ClassB
的属性时,又发现需要ClassA
的属性,但此时的ClassA
已经被提前曝光加入了正在创建的bean
的缓存中,则无需创建新的的ClassA
的实例,直接从缓存中获取即可。从而解决循环依赖问题。
总结:singleton
和prototype
作用域,
- 对于 构造注入,
Spring
无法解决任何作用域的循环依赖。 - 对于 属性注入,只要至少有一个
Bean
是singleton
作用域,Spring
就可以解决循环依赖问题。
6 IoC通过注解注入
注解的存在主要是为了简化XML
的配置,Spring6
倡导全注解开发。
6.1 入门程序
-
beans.xml
添加context
命名空间<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.cut.spring6"/><!-- 多个包的情况下可以用逗号隔开或者直接用公共包 --><!-- <context:component-scan base-package="com.cut.spring6.bean,com.cut.spring6.dao"/> --><!-- <context:component-scan base-package="com.cut.spring6"/> --> </beans>
-
编写
User
类java">package com.cut.spring6.bean;import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;/*** User类** @author tang* @version 1.0.0* @since 1.0.0*/ @Component("userBean") public class User {@Value("Tang")private String name;@Value("18")private Integer age;private String sex;@Value("男")public void setSex() {}public User(@Value("TangCon") String name, @Value("20") Integer age, @Value("公") String sex) {this.name = name;this.age = age;this.sex = sex;}... ...
-
执行结果
User{name='Tang', age=18, sex='公'}
会发现,先执的有参构造注入,再执行的set注入。
6.2 声明Bean的标签
常见的有四个,其中@Controller、@Service、@Repository
这三个注解都是@Component
注解的别名。
@Component
:bean
层上使用@Controller
:控制器类上使用@Service
:service
类上使用@Repository
:dao
类上使用
这么多名字是为了可读性以及控制实例化的对象:
<context:component-scan base-package="com.powernode.spring6.bean3" use-default-filters="false"><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
use-default-filters="true"
表示只要有Component、Controller、Service、Repository
中的任意一个注解标注,则进行实例化,反之则不进行实例化,true
为默认值。context:include-filter
添加例外,上面的代码表示Controller
能被实例化,如果上面改为true
,那么只有Controller
不能实例化。
6.3 负责注入的标签
Bean
属性赋值常见的有四个:
-
@Value
简单数据类型注解,可以出现在属性上、
setter
方法上、以及构造方法的形参上。但一般推荐不写setter
方法,直接在属性上注解。 -
@Autowired
和@Qualifier
可以用在属性上、构造方法上、构造方法的参数上、
setter
方法上,当带参数的构造方法只有一个的时候,这个注解可以省略,默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier
注解一起使用。java">// 当UserDao有多个实现类,而Autowired又默认是ByType,所以会查找出错 // 使用@Qualifier("userDaoForOracle")指明实现类的BeanId,即ByName @Autowired @Qualifier("userDaoForOracle") public void setUserDao(UserDao userDao) {this.userDao = userDao; }
-
@Resource
@Resource
是JDK
扩展包中标准注解,默认根据byName
装配,未指定name
时使用属性名作为name
,name
找不到则启动byType
装配。JDK8
以后使用需要引入依赖,<dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version> </dependency>
@Resource(name="beanId")
6.4 全注解开发
所谓全注解开发,就是不再需要XML
参与,声明一个类,替代XML
java">import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;@Configuration
@ComponentScan({"com.powernode.spring6.dao", "com.powernode.spring6.service"})
public class Spring6Configuration {
}
这个时候获得bean
需要使用AnnotationConfigApplicationContext()
java">ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
7 面向切面编程AOP
主要包括了代理模式的介绍以及Spring6
中利用代理实现的AOP
。
7.1 代理模式
7.1.1 代理模式介绍
GoF23种设计模式之一,属于结构型设计模式。主要的作用是为其他对象提供一种代理以控制对这个对象的访问,或保护或隐藏目标对象。
7.1.2 静态代理
很简单,声明一个代理类,实现与目标对象实现的相同接口,将目标对象作为自己的属性。
java">package com.cut.spring6.service.impl;import com.cut.spring6.service.OrderService;/*** 代理的目标对象** @author tang* @version 1.0.0* @since 1.0.0*/
public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("订单已生成");}@Overridepublic void detail() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("订单信息如下:******");}@Overridepublic String modify() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("订单信息如下:******");return "modified return";}
}
java">package com.cut.spring6.service;/*** 代理对象** @author tang* @version 1.0.0* @since 1.0.0*/
public class OrderServiceProxy implements OrderService {private final OrderService orderService;public OrderServiceProxy(OrderService orderService) {this.orderService = orderService;}@Overridepublic void generate() {long begin = System.currentTimeMillis();orderService.generate();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();orderService.detail();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");}@Overridepublic String modify() {long begin = System.currentTimeMillis();orderService.modify();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");return "modified return";}
}
订单已生成
耗时1014毫秒
订单信息如下:******
耗时1008毫秒
订单信息如下:******
耗时1014毫秒
modified returnProcess finished with exit code 0
7.1.3 动态代理
上面的方法虽然能行,但是每个类都声明一个代理类会造成类爆炸的问题,那么有没有那种运行的时候,哪个类需要代理,再创建对应的代理类的方法呢,有的兄弟,有的,像这样的方法有三个:
-
JDK
动态代理由
java.lang.reflect.Proxy
这个类实现,Proxy.newProxyInstance(类加载器,接口类型,调用处理器对象)
,三个参数:- 类加载器:在内存中生成字节码之后需要加载到内存当中的,所以要指定类加载器,一般用目标对象的类加载器。
- 接口类型:需要告诉代理对象目标对象实现了哪些接口,直接通过目标对象获取即可。
- 调用处理器对象:需要一个实现了
InvocationHandler
接口的对象,这个对象有个方法invoke
,这个方法里面可以写处理目标对象本身的逻辑之外的逻辑。
java">package com.cut.spring6.service;import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public class TimerInvocationHandler implements InvocationHandler {private final Object target;public TimerInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {long begin = System.currentTimeMillis();Object retValue = method.invoke(target, args);long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");// 这里需要返回return retValue;} }
java">public void testJDKProxy() {OrderService orderService = new OrderServiceImpl();// 三个参数TimerInvocationHandler timerInvocationHandler = new TimerInvocationHandler(orderService);ClassLoader classLoader = orderService.getClass().getClassLoader();Class<?>[] interfaces = orderService.getClass().getInterfaces();OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(classLoader, interfaces, timerInvocationHandler);// 调用动态代理类增强之后的方法orderServiceProxy.generate();orderServiceProxy.detail();System.out.println(orderServiceProxy.modify()); }
订单已生成 耗时1015毫秒 订单信息如下:****** 耗时1004毫秒 订单信息如下:****** 耗时1012毫秒 modified returnProcess finished with exit code 0
-
CGLIB
动态代理CGLIB
既可以代理接口,又可以代理类,底层采用继承的方式实现,所以被代理的目标类不能使用final
修饰。注意这里需要用到
TimerMethodInterceptor
而不是上面的TimerInvocationHandler
需要引入依赖
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version> </dependency>
没实现接口的代理目标类
java">package com.cut.spring6.service;/*** 没实现接口的代理目标对象** @author tang* @version 1.0.0* @since 1.0.0*/ public class UserService {public void login() {System.out.println("用户正在登录系统....");}public String modify() {System.out.println("用户正在修改密码....");return "modified return";}public void logout() {System.out.println("用户正在退出系统....");} }
java">package com.cut.spring6.service;import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @author tang* @version 1.0.0* @since 1.0.0*/ public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {long begin = System.currentTimeMillis();Object retValue = methodProxy.invokeSuper(target, objects);long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");// 一定要返回return retValue;} }
java">public void testCGLIBProxy() {// 创建字节码增强器Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接口enhancer.setCallback(new TimerMethodInterceptor());// 生成源码,编译class,加载到JVM,并创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();System.out.println(userServiceProxy.modify());userServiceProxy.logout(); }
用户正在登录系统.... 耗时6毫秒 用户正在修改密码.... 耗时0毫秒 modified return 用户正在退出系统.... 耗时0毫秒Process finished with exit code 0
这些
API
只能熟能生巧了。 -
Javassist
动态代理Javassist
是一个开源的分析、编辑和创建Java
字节码的类库,之前MyBatis
的学习中用到过。
7.2 面向切面编程AOP
将与核心业务无关的代码独立的抽取出来(日志、事务管理、安全等系统服务,被称为交叉业务),形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP
。Spring6
中的实现方式为:JDK
动态代理 + CGLIB
动态代理技术。Spring
在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK
动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB
。
7.2.1 AOP七大术语
-
连接点(
JoinPoint
):可以织入切面的位置,定义了可以被增强的具体位置。 -
切点(
Pointcut
):真正织入切面的方法,通过表达式匹配连接点,确定哪些连接点需要被增强。 -
通知(
Advice
):又叫增强,是具体要织入的代码,定义了在连接点上执行的具体逻辑。通知类型 定义 前置通知( BeforeAdvice
)目标方法执行之前执行。 后置通知( AfterAdvice
)目标方法执行之后执行的逻辑,无论方法是否正常返回或抛出异常,实际就是finally。 环绕通知( AroundAdvice
)包裹目标方法的执行,可以在方法执行前后添加逻辑,甚至可以控制方法是否执行。 异常通知( AfterThrowingAdvice
)目标方法抛出异常后执行的逻辑。只在方法抛出异常时触发。 返回通知( AfterReturningAdvice
)目标方法成功返回后执行的逻辑。只在方法正常返回时触发。 -
切面(
Aspect
):将切点和通知组合起来,形成一个完整的增强逻辑模块。 -
织入(
Weaving
):通知应用到目标对象上的过程,将切面逻辑织入到目标对象中,从而实现代码的动态增强。 -
代理对象(
Proxy
):目标对象被织入通知后产生的新对象,拦截对目标对象方法的调用,执行切面逻辑。 -
目标对象(
TargetObject
):被织入通知的对象,目标对象是实际执行业务逻辑的对象,AOP通过代理机制增强其行为。
7.2.2 切点表达式
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
- **访问控制权限修饰符:**可选项,没写就是四个都匹配,写了就只匹配写的。
- **返回值类型:**必填项,如
String,int
等类型,`*`代表所有类型。 - **全限定类名:**可选项,省略就匹配所有类,
com.cut..
表示匹配com.cut
包下所有的类 - **方法名:**必填项,*表示所有方法,
set\*
表示所有set
开头的方法。 - **形式参数列表:**必填项,
()
表示没有参数,(..)
参数类型和个数随意,(\*)
只有一个参数,(\*, String)
第一个参数类型随意,第二个参数是String。 - **异常:**可选项,省略时表示任意异常类型。
练习:
-
1 匹配
com.example.service
包下所有类中以find
开头的方法。java">execution(* com.example.service.*.find*(..))
-
2 匹配
com.example.service
包下所有类中返回值类型为String
的方法。java">execution(String com.example.service.*.*(..))
-
3 匹配
com.example.service
包下所有类中包含String
类型参数的方法。java">execution(* com.example.service.*.*(.., java.lang.String, ..))
-
4 匹配
com.example.service
包下所有类中可能抛出RuntimeException
的方法。java">execution(* com.example.service.*.*(..) throws java.lang.RuntimeException)
-
5 匹配
com.example.service
包下所有类中public
访问权限的方法。java">execution(public * com.example.service.*.*(..))
-
6 匹配
com.example.service
包下所有类中名为findUser
,且参数为String
和int
的方法。java">execution(* com.example.service.*.findUser(String, int))
-
匹配
com.example.service
包及其子包下所有类中以find
开头的方法,或者com.example.dao
包及其子包下所有类中以find
开头的方法。java">execution(* com.example.service..*.find*(..)) || execution(* com.example.dao..*.find*(..))
-
匹配
com.example.service
包下所有类中返回值类型为String
且可能抛出RuntimeException
的方法。java">execution(String com.example.service.*.*(..) throws java.lang.RuntimeException)
-
匹配
com.example.service
包下所有类中public
访问权限且返回值类型为String
的方法。java">execution(public String com.example.service.*.*(..))
-
匹配
com.example.service
包下所有类中public
访问权限且包含String
类型参数的方法。 -
java">execution(public * com.example.service.*.*(.., java.lang.String, ..))
7.2.3 使用Spring的AOP
主要有下面两种方式:
Spring
框架结合AspectJ
框架实现的AOP
,基于注解方式。Spring
框架结合AspectJ
框架实现的AOP
,基于XML
方式。
增加pom.xml
依赖,spring-context
已经包含了spring-aop
,所以只需再引入spring-aspects
即可。
<!--spring aspects依赖-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.1.14</version>
</dependency>
beans.xml
增加AOP
命名空间并开启自动代理,带有@Aspect
注解的bean
都会生成代理对象。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><context:component-scan base-package="com.cut.spring6"/><!-- 多个包的情况下可以用逗号隔开或者直接用公共包 --><!-- <context:component-scan base-package="com.cut.spring6.bean,com.cut.spring6.dao"/> --><!-- <context:component-scan base-package="com.cut.spring6"/> --><!--开启自动代理--><!--proxy-target-class默认值是false,表示采用jdk动态代理,当没有接口的时候,会自动选择cglib--><aop:aspectj-autoproxy proxy-target-class="false"/></beans>
当然在使用全注解开发也是可以的
java">@Configuration
@ComponentScan("com.powernode.spring6.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}
切面类的编写,用了个@Pointcut
简化了代码,不用每个方法都写一遍切点表达式,@Order()
注解中的值越小,切面的同志越优先被执行。
java">package com.cut.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.util.Arrays;/*** @author tang* @version 1.0.0* @since 1.0.0*/
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.cut.spring6.service.UserService.*(..))")public void pointcut() {}@Around("pointcut()")public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");Object proceed = proceedingJoinPoint.proceed();System.out.println("环绕通知里的方法执行结果:\t" + proceed);System.out.println("环绕通知结束");return proceed;}@Before("pointcut()")public void beforeAdvice() {System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice() {System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice() {System.out.println("异常通知");}@After("pointcut()")public void afterAdvice() {System.out.println("最终通知");}}
测试
java">public void testAOP() {ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");UserService userService = context.getBean("userService", UserService.class);System.out.println("---------login start-----------");userService.login();System.out.println("---------login end-----------");System.out.println("---------modify start-----------");System.out.println(userService.modify());System.out.println("---------modify end-----------");System.out.println("---------logout start-----------");userService.logout();System.out.println("---------logout end-----------");
}
输出
---------login start-----------
环绕通知开始
前置通知
用户正在登录系统....
后置通知
最终通知
环绕通知里的方法执行结果: null
环绕通知结束
---------login end-----------
---------modify start-----------
环绕通知开始
前置通知
用户正在修改密码....
后置通知
最终通知
环绕通知里的方法执行结果: modified return
环绕通知结束
modified return
---------modify end-----------
---------logout start-----------
环绕通知开始
前置通知
用户正在退出系统....
后置通知
最终通知
环绕通知里的方法执行结果: null
环绕通知结束
---------logout end-----------Process finished with exit code 0
8 Spring6中的事务
8.1 注解事务的使用
Spring
对事务的管理底层实现方式是基于AOP
实现的,采用AOP
的方式进行了封装,其中
PlatformTransactionManager
接口:spring
事务管理器的核心接口,spring6
中有两个实现:
DataSourceTransactionManager
:支持JdbcTemplate、MyBatis、Hibernate
等事务管理。JtaTransactionManager
:支持分布式事务管理。
引入命名空间并配置事务管理器,并配置事务注解驱动器
<?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:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"><!-- 配置事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 配置“事务注解驱动器”,开始注解的方式控制事务。 --><tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在service
类上或方法上添加@Transactional
注解,类上添加该注解,该类中所有的方法都有事务,在某个方法上添加该注解,表示只有这个方法使用事务。
8.2 事务的属性
比较重要的属性:
-
事务传播行为
事务的传播行为(
Propagation Behavior
)定义了在一个事务上下文中调用另一个事务方法时,事务应该如何进行传播。Spring6
里一共有七中传播行为。java">@Transactional(propagation = Propagation.REQUIRED)
传播行为 定义 行为 适用场景 REQUIRED
加入当前事务,无则新建事务。 - 有事务:加入当前事务。
- 无事务:新建事务。通用场景,需事务保证。 SUPPORTS
加入当前事务,无则非事务执行。 - 有事务:加入当前事务。
- 无事务:非事务执行。不需事务,但可受益于事务上下文。 MANDATORY
必须加入当前事务,无则抛异常。 - 有事务:加入当前事务。
- 无事务:抛异常。必须在事务中执行。 REQUIRES_NEW
新建事务,挂起当前事务。 - 有事务:新建事务,挂起当前事务。
- 无事务:新建事务。需独立事务,如数据一致性要求高。 NOT_SUPPORTED
非事务执行,挂起当前事务。 - 有事务:挂起当前事务,非事务执行。
- 无事务:非事务执行。不需事务,如读取配置、日志记录。 NEVER
非事务执行,有事务则抛异常。 - 有事务:抛异常。
- 无事务:非事务执行。绝对不能在事务中执行。 NESTED
嵌套事务,无则新建事务。 - 有事务:启动嵌套事务。
- 无事务:新建事务。需嵌套事务支持,如部分操作独立提交或回滚。 -
事务隔离级别
java">@Transactional(isolation = Isolation.READ_COMMITTED)
隔离级别 脏读 不可重复读 幻读 读未提交 有 有 有 读提交 无 有 有 可重复读 无 无 有 序列化 无 无 无 读未提交(
Read Uncommitted
)- 含义:这是最低的隔离级别。在一个事务读取数据时,其他事务即使未提交的修改也可以被读取到。也就是说,一个事务可以读取到另一个事务尚未提交的数据,这种数据有可能会被回滚,所以可能会导致脏读。
- 举例:比如事务 A 修改了一条数据但未提交,事务 B 在这个隔离级别下可以读取到事务 A 修改后的数据。如果事务 A 最终回滚了修改,那么事务 B 读取到的数据就是不准确的。
读提交(
Read Committed
)- 含义:一个事务只能读取到其他事务已经提交的数据。避免了脏读,但可能会出现不可重复读的情况。即在一个事务的两次查询之间,其他事务提交了对数据的修改,导致同一个事务读取到的数据前后不一致。
- 举例:事务 A 在查询某个数据后,事务 B 提交了对该数据的修改。当事务 A 再次查询同一数据时,就会得到修改后的不同结果。
可重复读(
Repeatable Read
)- 含义:保证在一个事务的多次读取过程中,看到的数据是一致的。它避免了脏读和不可重复读,但可能会出现幻读。幻读是指在一个事务中,两次执行相同的查询语句,由于其他事务的插入或删除操作,导致返回的结果集中新增或减少了行。
- 举例:事务 A 在查询某个范围的数据后,事务 B 插入了一条符合该范围条件的数据。当事务 A 再次查询同一范围的数据时,就会多出一条数据,这就是幻读。
序列化(
Serializable
)- 含义:这是最高的隔离级别。事务的执行顺序是严格按照时间顺序进行的,就像事务是依次串行执行一样,完全避免了脏读、不可重复读和幻读。但是这种隔离级别下并发性能很低,因为事务之间相互等待对方执行完毕才能执行。
- 举例:如果有多个事务需要操作同一组数据,它们会按照先后顺序排队执行,一个事务执行完所有操作后,下一个事务才能开始,就像人们排队办事一样。
脏读(
Dirty Read
)- 含义:一个事务读取了另一个事务未提交的数据,如果对方事务回滚,那么这个事务读取的数据就是无效的。这种现象在读未提交隔离级别下可能会出现。
不可重复读(
Non - Repeatable Read
)- 含义:在一个事务的两次查询之间,其他事务提交了对数据的修改,导致同一个事务读取到的数据前后不一致。这种现象在读提交隔离级别下可能会出现。
幻读(
Phantom Read
)- 含义:在一个事务中,两次执行相同的查询语句,由于其他事务的插入或删除操作,导致返回的结果集中新增或减少了行。这种现象在可重复读隔离级别下可能会出现。
-
事务超时
java">@Transactional(timeout = 10)
表示超过10秒如果该事务中所有的
DML
语句还没有执行完毕的话,最终结果会选择回滚。如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的
DML
语句。 -
只读事务
java">@Transactional(readOnly = true)
只允许
select
语句执行,delete insert update
均不可执行,启动spring
的优化策略,提高select
语句执行效率。 -
设置出现哪些异常回滚事务
java">@Transactional(rollbackFor = RuntimeException.class)
发生
RuntimeException
异常或该异常的子类异常才回滚。 -
设置出现哪些异常不回滚事务
java">@Transactional(noRollbackFor = NullPointerException.class)
发生
NullPointerException
或该异常的子类异常不回滚,其他异常则回滚。| 可重复读 | 无 | 无 | 有 |
| 序列化 | 无 | 无 | 无 |读未提交(
Read Uncommitted
)- 含义:这是最低的隔离级别。在一个事务读取数据时,其他事务即使未提交的修改也可以被读取到。也就是说,一个事务可以读取到另一个事务尚未提交的数据,这种数据有可能会被回滚,所以可能会导致脏读。
- 举例:比如事务 A 修改了一条数据但未提交,事务 B 在这个隔离级别下可以读取到事务 A 修改后的数据。如果事务 A 最终回滚了修改,那么事务 B 读取到的数据就是不准确的。
读提交(
Read Committed
)- 含义:一个事务只能读取到其他事务已经提交的数据。避免了脏读,但可能会出现不可重复读的情况。即在一个事务的两次查询之间,其他事务提交了对数据的修改,导致同一个事务读取到的数据前后不一致。
- 举例:事务 A 在查询某个数据后,事务 B 提交了对该数据的修改。当事务 A 再次查询同一数据时,就会得到修改后的不同结果。
可重复读(
Repeatable Read
)- 含义:保证在一个事务的多次读取过程中,看到的数据是一致的。它避免了脏读和不可重复读,但可能会出现幻读。幻读是指在一个事务中,两次执行相同的查询语句,由于其他事务的插入或删除操作,导致返回的结果集中新增或减少了行。
- 举例:事务 A 在查询某个范围的数据后,事务 B 插入了一条符合该范围条件的数据。当事务 A 再次查询同一范围的数据时,就会多出一条数据,这就是幻读。
序列化(
Serializable
)- 含义:这是最高的隔离级别。事务的执行顺序是严格按照时间顺序进行的,就像事务是依次串行执行一样,完全避免了脏读、不可重复读和幻读。但是这种隔离级别下并发性能很低,因为事务之间相互等待对方执行完毕才能执行。
- 举例:如果有多个事务需要操作同一组数据,它们会按照先后顺序排队执行,一个事务执行完所有操作后,下一个事务才能开始,就像人们排队办事一样。
脏读(
Dirty Read
)- 含义:一个事务读取了另一个事务未提交的数据,如果对方事务回滚,那么这个事务读取的数据就是无效的。这种现象在读未提交隔离级别下可能会出现。
不可重复读(
Non - Repeatable Read
)- 含义:在一个事务的两次查询之间,其他事务提交了对数据的修改,导致同一个事务读取到的数据前后不一致。这种现象在读提交隔离级别下可能会出现。
幻读(
Phantom Read
)- 含义:在一个事务中,两次执行相同的查询语句,由于其他事务的插入或删除操作,导致返回的结果集中新增或减少了行。这种现象在可重复读隔离级别下可能会出现。
-
事务超时
java">@Transactional(timeout = 10)
表示超过10秒如果该事务中所有的
DML
语句还没有执行完毕的话,最终结果会选择回滚。如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的
DML
语句。 -
只读事务
java">@Transactional(readOnly = true)
只允许
select
语句执行,delete insert update
均不可执行,启动spring
的优化策略,提高select
语句执行效率。 -
设置出现哪些异常回滚事务
java">@Transactional(rollbackFor = RuntimeException.class)
发生
RuntimeException
异常或该异常的子类异常才回滚。 -
设置出现哪些异常不回滚事务
java">@Transactional(noRollbackFor = NullPointerException.class)
发生
NullPointerException
或该异常的子类异常不回滚,其他异常则回滚。