My Spring notes
# 一、Spring 框架概述
-
Spring 是轻量级的开源的 JavaEE 框架。
-
Spring 可以解决企业应用开发的复杂
-
Spring 有两个核心部分:IOC 和 Aop
(1)IOC:控制反转,把创建对象过程交给 Spring 进行管理
(2)Aop:面向切面,不修改源代码进行功能增强
-
Spring 特点 :
(1)方便解耦,简化开发
(2)Aop 编程支持
(3)方便程序测试
(4)方便和其他框架进行整合
(5)方便进行事务操作
(6)降低 API 开发难度
# 二、IOC 容器
# 1. IOC 概念和原理
(1)概念:
- 控制反转,把_对象创建_和_对象的调用_过程交给 spring 进行管理。
- 目的:降低耦合度。
- 底层原理:xml,反射,工厂模式
# 原理讲解:
一个类里面要用到另外一个类里面的方法,我们原始方式只能 new 然后调用,这么做这两个类的耦合度太高了
所以我们可以试着用工厂模式 (不是真正的工厂模式,而是说的简单了些) 稍微解决这个耦合度
这里我们创建了个工厂类,而我们需要 userDao 的对象调用方法时,就调用这个工厂类的方法返回一个这个 userDao 对象
但是这么做耦合度还是有,并没有降到最低
所以我们使用 ioc (包含 xml 解析,工厂模式,反射)
基本概念就是还是工厂类,只不过那个工厂类里面不再是 new UserDao 然后返回了,而是通过解析我们一开始在 xml 配置好的 bean 对象然后获取到他的 class="XXX" 的 XXX 这个值,这个 XXX 就是我们 userDao (比如说) 的__包名加类名__
有了__包名加类名__我们就可以通过反射获取到这个对象,并且存储下来在这个工厂类 (的实现类) 里面,以后别的地方要是要用就直接管我们这个工厂类 (的实现类) 要想要的对象就行了
Spring 提供 IOC 容器两种实现方式(两个接口)
-
BeanFactory:Spring 内部使用的接口,不提倡开发人员使用。
特点:加载配置文件时不会创建 xml 里面配置的那些对象,获取对象时才会创建对象。
-
ApplicationContext: BeanFactory 的子接口,提供了更多更强大的功能,一般由开发人员使用。
特点:加载配置文件时会把配置文件里的对象进行创建。
这么做的好处就是服务器启动用这个就会创建好所有对象,之后用户使用起来速度就会快,而不能用户使用的时候再创建,毕竟可以慢自己 不能慢用户姥爷
所以一般都是用__ApplicationContext__
(1)BeanFactory:是 Spring 里面最底层的接口,包含了各种 Bean 的定义,读取 bean 配置文档,管理 bean 的加载、实例化,控制 bean 的生命周期,维护 bean 之间的依赖关系。ApplicationContext 接口作为 BeanFactory 的派生,除了提供 BeanFactory 所具有的功能外,还提供了更完整的框架功能:
①继承 MessageSource,因此支持国际化。
②统一的资源文件访问方式。
③提供在监听器中注册 bean 的事件。
④同时加载多个配置文件。
⑤载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层。
(2)①BeanFactroy 采用的是延迟加载 (懒加载) 形式来注入 Bean 的,即只有在使用到某个 Bean 时 (调用 getBean ()),才对该 Bean 进行加载实例化。这样,我们就不能发现一些存在的 Spring 的配置问题。如果 Bean 的某一个属性没有注入,BeanFacotry 加载后,直至第一次使用调用 getBean 方法才会抛出异常。
②ApplicationContext,它是在容器启动时,一次性创建了所有的 Bean。这样,在容器启动时,我们就可以发现 Spring 中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext 启动后预载入所有的单实例 Bean,通过预载入单实例 bean , 确保当你需要的时候,你就不用等待,因为它们已经创建好了。它还可以为 Bean 配置 lazy-init=true 来让 Bean 延迟实例化;
③相对于基本的 BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置 Bean 较多时,程序启动较慢。
(3)BeanFactory 通常以编程的方式被创建,ApplicationContext 还能以声明的方式创建,如使用 ContextLoader。
(4)BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用,但两者之间的区别是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。
-
ApplicationContext 两个常用实现类:
- FileSystemXmlApplicationContext:绝对路径,从盘符开始算起
- ClassPathXmlApplicationContext:相对路径,从 src 开始算起
- 什么是 Bean 管理?
- Bean 管理是指两个操作:Spring 创建对象 和 Spring 注入属性
- Bean 管理有两种操作方式:基于 xml 配置文件方式实现 和 基于注解方式实现
# 2. IOC 操作 Bean 管理(基于 xml)
# xml 实现 Bean 管理:
# (1)基于 xml 方式创建对象:
如果使用 maven, 创建完一个普通项目后,在 main 和 test 里面分别加上一个名叫 resources 的 directories, 记得右键选择 mark as… 对应的
然后在 main 下面的 resources 里面再新建一个 xml 文件,用 bean 标签写上这个对象
-
在 Spring 配置文件中使用 bean 标签来创建对象
-
bean 标签有很多属性,常用属性:
- id:唯一标识
- class:类路径 (那个类的包名和类名)
-
创建对象时,默认执行无参构造函数⭐️
这是因为拿反射做出来这个类的对象,所以会默认执行无参的,所以那个类里面记得写个无参的构造函数
# (2)基于 xml 方式注入属性:
# 第一种方法:使用 set 方法进行注入:
首先先为类的属性提供 set 方法:
public class User {
private String userName;
private String userAge;
public void setUserName(String userName) {
this.userName = userName;
}
public void setUserAge(String userAge) {
this.userAge = userAge;
}
public String getUserName() {
return userName;
}
public String getUserAge() {
return userAge;
}
}
然后在 xml 配置文件中通过 property 标签进行属性注入
<!--配置User对象-->
<bean id="user" class="com.oymn.spring5.User">
<property name="userName" value="haha"></property>
<property name="userAge" value="18"></property>
</bean>
这样就完成了
// 获取上下文对象, 传进去的参数就是我们的那个配置文件(maven默认会从resources里面去找,所以不需要其他路径信息)
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean1.xml");
// 获取到保存在ioc容器(之前在xml文件)中配置的对象, 注意这个第二个参数"User.class"可以不传, 这里传了的话返回的就是这个类而不是Object
User user = applicationContext.getBean("user", User.class);
System.out.println(user.getUserName() + " " + user.getUserAge());
ApplicationContext对象.getBean(你在xml文件写的那个bean的id值[,转换成什么类型])
, 这样就能获取到我们在 xml 文件里面配置的 com.oymn.spring5.User__的实例化对象__, 返回的和这个实例化对象也是 com.oymn.spring5.User 这个类型的,因为我们传了第二个参数- 我们就通过 spring 获取到了一个类的实例化对象,然后就可以调用这个对象的各种方法
# 第二种方法:使用有参构造函数进行注入
首先提供有参构造方法
public class User {
private String userName;
private String userAge;
public User(String userName, String userAge){
this.userName = userName;
this.userAge = userAge;
}
}
然后再 xml 配置文件中通过 constructor-arg 标签进行属性注入
<!--配置User对象-->
<bean id="user" class="com.oymn.spring5.User">
<constructor-arg name="userName" value="haha"></constructor-arg>
<constructor-arg name="userAge" value="18"></constructor-arg>
</bean>
# 第三种方法:p 名称空间注入(了解即可)
首先在 xml 配置文件中添加 p 名称空间,并且在 bean 标签中进行操作
然后提供 set 方法
public class User {
private String userName;
private String userAge;
public User() {
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setUserAge(String userAge) {
this.userAge = userAge;
}
}
# (3)xml 注入其他属性
# null 值
<!--配置User对象-->
<bean id="user" class="com.oymn.spring5.User">
<property name="userName"> <null/> </property>
</bean>
# 属性值包含特殊符号
假设现在 userName 属性需要赋值为 <haha>
如果像上面那样直接在 value 中声明的话会报错,因为包含特殊符号 <>
需要通过 <![CDATA[值]]>
来表示
# 注入属性 —— 外部 bean
有两个类:UserService 和 UserDaoImpl,其中 UserDaoImpl 实现 UserDao 接口
public class UserService {
private UserDao userDao; //注意需要这个成员,并且需要constructor里面设置或者setter设置.这样才可以注入依赖
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
public void add(){
System.out.println("add");
}
}
通过 ref 来指定创建 userDaoImpl
<bean id="userDaoImpl" class="com.oymn.spring5.UserDaoImpl"></bean>
<bean id="userService" class="com.oymn.spring5.UserService">
<property name="userDao" ref="userDaoImpl"></property> //是ref不是value!!!
</bean>
# 注入属性 —— 内部 bean
不通过 ref 属性,而是通过嵌套一个 bean 标签实现
<!--内部 bean-->
<bean id="emp" class="com.atguigu.spring5.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property>
<property name="gender" value="女"></property>
<!--设置对象类型属性-->
<property name="dept">
<bean id="dept" class="com.atguigu.spring5.bean.Dept">
<property name="dname" value="安保部"></property>
</bean>
</property>
</bean>
# 注入属性 —— 级联赋值
写法一:也就是上面所说的外部 bean,通过 ref 属性来获取外部 bean
写法二:emp 类中有 ename 和 dept 两个属性,其中 dept 有 dname 属性,写法二需要 emp 提供 dept 属性的 get 方法。
<!--级联赋值-->
<bean id="emp" class="com.atguigu.spring5.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property> <property name="gender" value="女"></property>
<!--写法一-->
<property name="dept" ref="dept"></property>
<!--写法二(感觉没必要)-->
<property name="dept.dname" value="技术部"></property> //这个需要Emp类里面的dept属性有get 方法
</bean>
<bean id="dept" class="com.atguigu.spring5.bean.Dept">
<property name="dname" value="财务部"></property>
</bean>
对于第二种写法的说明:
用 set 可以仅仅修改被直接 set 的成员,但不能修改这个成员的内部属性
但如果 xml get 到那个属性,就会得到一个默认值 "对象", 若该对象某属性也可以被 set 注入,该属性就会被按照我们 xml 设置的所被 set 到想要的值
# 注入集合属性(数组,List,Map)
假设有一个 Stu 类
public class Stu {
private String[] courses;
private List<String> list;
private Map<String,String> map;
private Set<String> set;
public void setCourses(String[] courses) {
this.courses = courses;
}
public void setList(List<String> list) {
this.list = list;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
public void setSet(Set<String> set) {
this.set = set;
}
}
在 xml 配置文件中对这些集合属性进行注入
<bean id="stu" class="com.oymn.spring5.Stu">
<!--数组类型属性注入-->
<property name="courses">
<array>
<value>java课程</value>
<value>数据库课程</value>
</array>
</property>
<!--List类型属性注入-->
<property name="list">
<list>
<value>张三</value>
<value>李四</value>
</list>
</property>
<!--Map类型属性注入-->
<property name="map">
<map>
<entry key="JAVA" value="java"></entry>
<entry key="PHP" value="php"></entry>
</map>
</property>
<!--Set类型属性注入-->
<property name="set">
<set>
<value>Mysql</value>
<value>Redis</value>
</set>
</property>
</bean>
上面的集合值都是字符串,如果是对象的话,如下:
写法: 集合 + 外部 bean
<!--创建多个 course 对象-->
<bean id="course1" class="com.atguigu.spring5.collectiontype.Course">
<property name="cname" value="Spring5 框架"></property>
</bean>
<bean id="course2" class="com.atguigu.spring5.collectiontype.Course">
<property name="cname" value="MyBatis 框架"></property>
</bean>
<!--注入 list 集合类型,值是对象-->
<property name="courseList">
<list>
<ref bean="course1"></ref>
<ref bean="course2"></ref>
</list>
</property>
# 把集合注入部分提取出来
使用 util 标签,这样不同的 bean 都可以使用相同的集合注入部分了。
(其实就是县创建出来那个 list 或者什么什么结合,然后就把他看做是之前那种 bean 的一个对象,直接 ref 外部 bean 方式注入完事)
<!--将集合注入部分提取出来-->
<util:list id="booklist">
<value>易筋经</value>
<value>九阳神功</value>
</util:list>
<bean id="book" class="com.oymn.spring5.Book">
<property name="list" ref="booklist"></property>
</bean>
# FactoryBean
Spring 有两种 Bean,一种是普通 Bean,另一种是工厂 Bean(FactoryBean)
- 普通 bean:
普通 bean 就是我们上面做的那些都是
- 工厂 bean:
然后你在别的地方用 ApplicationContext (的实现类) 然后 getBean (‘myBean ’,Course.class) 获取到的就是 Course 对象而不是 MyBean 对象
(myBean 还是要写的因为在 xml 里面给这个 bean 的 id 就是 myBean)
通过 factorybean 来配置 bean 的实例,但是实际返回的是实例确实是 factorybean 的 getobject () 方法返回的实例
# Bean 的作用域:
在 Spring 中,默认情况下 bean 是单实例对象
执行结果是相同的:
通过 bean 标签的 scope 属性 来设置单实例还是多实例。
Scope 属性值:
- singleton: 默认值,表示单实例对象。加载配置文件时就会创建单实例对象。
- prototype: 表示多实例对象。不是在加载配置文件时创建对象,在调用 getBean 方法时创建多实例对象。
执行结果不同了:
所以就是你每次 getBean 这个 User 类的对象,获取的都是这个类的不同实例化对象
# Bean 的生命周期:
- bean 的生命周期:
(1)通过构造器创建 bean 实例(无参数构造)
(2)为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
(3)把 bean 实例传递 bean 后置处理器的方法 postProcessBeforeInitialization
(4)调用 bean 的初始化的方法(需要进行配置初始化的方法)
(5)把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization
(6)bean 可以使用了(对象获取到了)
(7)当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)
- 演示 bean 的生命周期
public class Orders {
private String orderName;
public Orders() {
System.out.println("第一步:执行无参构造方法创建bean实例");
}
public void setOrderName(String orderName) {
this.orderName = orderName;
System.out.println("第二步:调用set方法设置属性值");
}
//初始化方法
public void initMethod(){
System.out.println("第四步:执行初始化方法");
}
//销毁方法
public void destroyMethod(){
System.out.println("第七步:执行销毁方法");
}
}
//实现后置处理器,需要实现BeanPostProcessor接口
public class MyBeanPost implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("第三步:将bean实例传递给bean后置处理器的postProcessBeforeInitialization方法");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("第五步:将bean实例传递给bean后置处理器的postProcessAfterInitialization方法");
return bean;
}
}
<bean id="orders" class="com.oymn.spring5.Orders" init-method="initMethod" destroy-method="destroyMethod">
<property name="orderName" value="hahah"></property>
</bean>
<!--配置bean后置处理器,这样配置后整个xml里面的bean用的都是这个后置处理器-->
<bean id="myBeanPost" class="com.oymn.spring5.MyBeanPost"></bean>
@Test
public void testOrders(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
Orders orders = context.getBean("orders", Orders.class);
System.out.println("第六步:获取bean实例对象");
System.out.println(orders);
//手动让bean实例销毁
context.close();
}
注意要是
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
的话,那么在关闭的时候需要把 context 强转成 ClassPathXmlApplicationContext 类型的引用,这是因为 ApplicationContext 这个接口貌似没有声明这个 close 方法
执行结果:
# xml 自动装配:
- 根据指定的装配规则(属性名称或者属性类型),Spring 自动将匹配的属性值进行注入
- 根据属性名称自动装配:要求__emp 中属性 (成员) 的名称 dept 和 bean 标签的 id 值 dept__一样,才能识别
<!--指定autowire属性值为byName-->
<bean id="emp" class="com.oymn.spring5.Emp" autowire="byName"></bean>
<bean id="dept" class="com.oymn.spring5.Dept"></bean>
- 根据属性类型自动装配:要求同一个 xml 文件中不能有两个相同类型的 bean,否则无法识别是哪一个
<!--指定autowire属性值为byType-->
<bean id="emp" class="com.oymn.spring5.Emp" autowire="byType"></bean>
<bean id="dept" class="com.oymn.spring5.Dept"></bean>
这种方式就不用我们在里面手动写 property 标签什么的了
(当然那个对应的 type 或者 name 的那个对象 bean 在 xml 里写好了)
# 通过外部属性文件来操作 bean:
例如配置数据库信息:
-
导入德鲁伊连接池 jar 包
-
创建外部属性文件,properties 格式文件,写数据库信息
-
引入 context 名称空间,并通过 context 标签引入外部属性文件,使用 “${}” 来获取文件中对应的值
# 3. IOC 操作 Bean 管理(基于注解)
-
格式:@注解名称(属性名 = 属性值,属性名 = 属性值,……)
-
注解可以作用在类,属性,方法。
-
使用注解的目的:简化 xml 配置
# (1)基于注解创建对象:
spring 提供了四种创建对象的注解:
- @Component
- @Service:一般用于 Service 层
- @Controller:一般用于 web 层
- @Repository:一般用于 Dao 层
其实哪里都可以用随便的只不过推荐比如说 dao 层用 @Repository
注意是给实现类 (那个可以被实例化而且你想放入 IOC 容器帮你管理的,别把这个给了注解!!!(注解又实例化不了) 虽然那个属性的类型可能是接口类型的但我们想注入的是实现那个接口的实现类)
流程:
-
引入依赖:
-
开启组件扫描:扫描 base-package 包下所有有注解的类并为其创建对象
如果有多个包可以放他们的父亲包,或者都写下来中间拿逗号分开
<context:component-scan base-package="com.oymn"></context:component-scan>
这里就是 (靠反射) 扫描看你这个包哪个类上面有那些注解,然后 (靠反射) 创造那个带有注解的那个类的对象,根据注解创造对象
-
com.oymn.spring5.Service 有一个 stuService 类
//这里通过@Component注解来创建对象,括号中value的值等同于之前xml创建对象使用的id,为了后面使用时通过id来获取对象 //括号中的内容也可以省略,默认是类名并且首字母小写!!!!!!!! //可以用其他三个注解 @Component(value="stuService") public class StuService { public void add(){ System.out.println("addService"); } }
那个注解后面的值或者是默认的其实就是之前我们给 id 写的那个
所以之后要是 getBean, 里面第一个参数就是 "stuService" 来获取 StuService 类的对象
-
这样就可以通过 getBean 方法来获取 stuService 对象了
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml"); StuService stuService = context.getBean("stuService", StuService.class); System.out.println(stuService); stuService.add();
开启组件扫描的细节配置:
1. use-default-fileters 设置为 false 表示不使用默认过滤器,通过 include-filter 来设置只扫描 com.oymn 包下的所有 @Controller 修饰的类。
<context:component-scan base-package="com.oymn" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- use-default-fileters 没有设置为 false (代表没有关掉默认的 filter),exclude-filter 设置哪些注解不被扫描,例子中为 @Controller 修饰的类不被扫描
<context:component-scan base-package="com.oymn">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
# (2)基于注解进行属性注入:
-
@Autowired
:根据__属性类型__自动装配 (注入依赖)创建 StuDao 接口和 StuDaoImpl 实现类,为 StuDaoImpl 添加创建对象注解
public interface StuDao { public void add(); }
@Repository public class StuDaoImpl implements StuDao { @Override public void add() { System.out.println("StuDaoImpl"); } }
StuService 类中添加 StuDao 属性,为其添加 @Autowire 注解,spring 会自动为 stuDao 属性创建 StuDaoImpl 对象
@Component(value="stuService") public class StuService { @Autowired public StuDao stuDao; public void add(){ System.out.println("addService"); stuDao.add(); } }
@Test public void test1(){ ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml"); StuService stuService = context.getBean("stuService", StuService.class); System.out.println(stuService); stuService.add(); }
-
上面 StuDaoImpl 通过
@Repository
注解交给 IOC 容器管理创建对象 -
StuService 通过
@Component(value="stuService")
注解交给 IOC 容器管理创建对象 -
然后 StuService 里面的属性
public StuDao stuDao;
上面又通过@Autowired
注解让根据数据类型自动装配 (注入依赖) -
所以这个属性的数据类型就是 StuDao, 就会给 IOC 容器创建的 StuService 类的对象的身上的 stuDao 属性都装配 IOC 容器中生成 (上面) 的 StuDaoImpl 类的那个对象 (这里不是很清楚是不是通过找对应的 id 才找到那个对象还是直接用这个属性的名字反射还是啥的)
注意!这个 StuDao 是接口,真正注入给 StuService 类的对象的身上的 stuDao 属性的值是我们之前通过
@Repository
注解交给 IOC 容器管理创建的 StuDaoImpl 对象如果设置为了多例模式可能会不太一样…
测试结果:
-
-
@Qualifier
:根据__属性名__称自动装配当遇到一个接口有很多实现类时,只通过 @Autowire 是无法完成自动装配的,所以需要再使用 @Qualifier 通过名称来锁定某个类
比如说上面的 StuDao 类型的 stuDao, 这个 StuDao 是个接口,然后实现类有 StuDaoImpl 和 StuDaoImpl2 都用
@Repository
注解交给 IOC 容器管理创建对象.要是我们使用
@Autowired
按照属性类型就不知道是注入 StuDaoImpl 对象还是 StuDaoImpl2 对象所以这时候可以把用 ** 结合
@Autowired
和@Qualifier(value="stuDaoImpl")
这俩一块使用 ** 注解然后传值想要给这个属性注入的是哪个对象就行了(会去 IOC 容器中保管的__id 为 stuDaoImpl (所以小写没关系,因为之前给这个类的注解 @Repository 没传值,默认就是第一个字母小写的,所以能对上那个 id)__的那个类的对象)
@Component(value="stuService") public class StuService { @Autowired @Qualifier(value="stuDaoImpl") //这样就能显式指定stuDaoImpl这个实现类 public StuDao stuDao; public void add(){ System.out.println("addService"); stuDao.add(); } }
这里
@Autowired
代表去找 StuDao 接口类型的实现类到的对象,然后我们需要加上@Qualifier(value="stuDaoImpl")
代表找的是 stuDaoImpl 这个实现类的对象,而不是别的实现类的对象 -
@Resource
:可以根据类型注入,也可以根据名称注入这个注解本身是 javax (java 的) 扩展包,不是属于 Spring 的,不建议用这个 (JDK11 版本及以上完全移除了 javax 扩展,因此不能使用 @Resource 注解)
注意!!!!@Resource 默认应该是按照属性名来注入,按照属性类型注入要写
@Resource(type=UserDaoImpl.class)
@Component(value="stuService") public class StuService { //@Resource //根据名称进行注入 @Resource(name="stuDaoImpl") //根据名称进行注入 public StuDao stuDao; public void add(){ System.out.println("addService"); stuDao.add(); } }
-
@Value
:注入普通类型属性, 上面那三个都是针对于注入的是个对象且由 IOC 容器保管一般都是开发中会用到 @value 去动态读取服务器端口号,或者是配置环境是生产环境还是上线环境等等
下面示范的是直接写死的
@Value(value = "abc") private String name;
注意上面注解后面括号里给的值,一般都是小字母开头 (有的时候不给默认的也是小字母开头), 没有影响,这还是因为能找到匹配的 id 值 (或者其他…)
注意用注解方式注入依赖就不需要给那些属性设置 setter 也可以注入了
# (3)完全注解开发:
创建配置类,替代 xml 配置文件
@Configuration //表明为一个配置类
@ComponentScan(basePackages = "com.oymn") //开启组件扫描
public class SpringConfig {
}
测试类:
@Test
public void test2(){
//创建AnnotationConfigApplicationContext对象(不再是之前那个ClassPathXmlApplicationContext对象了)
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); //加载那个配置类
StuService stuService = context.getBean("stuService", StuService.class);
System.out.println(stuService);
stuService.add();
}
# 三、AOP
# 先说说静态代理和动态代理
# 静态代理
- 首先有个接口,这个接口有方法
- 然后有个类 (代理类) 实现这个接口,并且这个代理类有成员类型就是这个接口
接口:
代理类:
注意这里的代理类是用他那个 (接口类型的) 成员,来调用同一个方法,也就是接口的那一个方法
被代理类:
接着我们可以创建个类用来测试上面的:
结果为:
- 这里我们先是实例化了一个被代理对象
- 接着我们把这个实例化对象作为参数传进我们代理对象的构造函数里面
- 因为代理对象有一个 (接口类型的) 成员接收到了这个传进来的被代理实例化对象
- 接着我们用哪个创造出来的代理实例化对象的 produceCloth 方法
- 这个代理实例化对象的 produceCloth 方法实现方式其实就是调用了他那个 (接收到传进去被代理实例化对象的) 成员的实现方法
- 因为这两个类都实现了同一个接口所以实现的方法都是一样的
静态代理就是编译期间就已经确定了代理类和被代理类是什么类.
静态代理作用:
某个对象提供一个代理,代理角色固定,以控制对这个对象的访问。 代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。代理类负责请求的预处理、过滤、将请求分派给委托类处理、以及委托类执行完请求后的后续处理。
我们那个被代理对象存在意义就是调用里面实现接口的方法,要是有时候因为种种原因我们不想直接调用里面的实现方法,我们可以通过上面说的方式创造他的代理类,然后因为实现同一接口,我们只需要调用这个创建出来的代理类的对象实现那个接口的方法,然后在这个代理类的实现方法里面可能会有各种判断啊等等等,然后在合适的时候调用我们之前传给他的那个被代理类的实例化对象对应的方法,可能在这之前或者在这之后还会调用自己的一些其他方法,而这些逻辑我们又不想直接写在我们的被代理类里面实现的那个方法里,会太乱
但是要是我们需要动态的决定代理类和对应的动态代理是什么类,因为我们可能会有很多接口,所以会有很多对应的代理类和被代理类,难不成我们要像静态代理的方式一个一个都写出来?当然不,我们可以通过反射来实现
注意我们一般那些接口和实现类都会有,所以我们需要创建的是那个实现类的代理类
# 动态代理:
接口:
被代理类:
解决问题 1 和问题 2, 我们需要两个类:
这行代码应该在上面的类里面,也就是调用了 handler 变量存的对象的 bind 方法,并把那个要实现的被代理类的实例对象传了过去
测试到底什么样:
结果:
这里的逻辑:
- 首先比如说我们有个实现类 A 这个实现类实现了接口 B, 接口 B 有方法 x 和方法 y, 所以 A 有方法 x 和 y 的实现
- 现在我们有个实现类 A 的实例化对象,然后想生成这个实现类 A 的实例化对象的代理类对象
- 我们可以把实现类 A 的实例化对象传进我们弄的代理工厂的 static 方法 getProxyInstance 里面
- 在这个方法里面先是实例化了我们创建的一个类
MyInvocationHandler
, 这个类实现了InvocationHandler接口
然后实现了这个接口的invoke
方法- 我们接着给那个 handler (看上面图) 调用 bind 方法把实现类 A 的实例化对象传了过去
- 接着在我们的我们弄的代理工厂的 static 方法 getProxyInstance 里面,我们调用了 java 内部的静态方法
Proxy.newProxyInstance
并把那个实现类 A 实例对象的加载器 和 实现类 A 实现的接口 B 和 我们上面MyInvocationHandler
的实例化对象,并把这个整个方法的返回值返回出去,说明这个返回值就是我们想要的代理类对象- 其实这个时候实现类 A 对应的代理类对象就创建好了,就是这个返回值 (上面我们用 "proxyInstance" 变量接收了,但类型还是 Human 接口的)
- 这里我们就已经创建出来那个实现类 A 对应的代理类对象了,我们所要做的操作只是把那个实现类传进我们做的静态方法即可然后接收返回值即可
- 接着我们调用接口 B 里面的任意方法,比如说方法 x, 这个就会调用那个 MyInvocationHandler 类的里面的 invoke 方法–>__每当调用代理类 (对象) 的方法 (也就是我们接口 B),这个调用就会打包传递给 invacation handler 的 invoke 方法__而这里我们创建的
MyInvocationHandler
实现了 InvocationHandler 接口,所以调用的时候会调用他实现的 invoke 方法 (这里我认为是传的第三个参数 (那个变量为 "handler" 的 MyInvocationHandler 实例化对象), 在 java 内部通过反射调用他的 invoke 方法)- 而在那个 MyInvocationHandler 类里面首先有个 bind 方法接收到了传进来的那个实现类 A 的实例化对象
- 注意这里 MyInvocationHandler 类的实例化对象已经创建出来了 (“handler”), 所以哪个 bind 方法是已经把那个实现类 A 的实例化对象传给了 handler 对象的 obj 成员了,而且之后都是 in context of that “handler” instance
- 然后在这个 handler (
MyInvocationHandler
类的实例化对象) 调用 invoke 方法里面参数 java 内部给传的三个参数分别是通过Proxy.newProxyInstance
创造出来的对应实现类 A 的实例化对象的代理类对象,接口 B 被调用的方法 (这里说的就是那个方法 x) 和方法的参数 (if any)- 然后在这个 invoke 方法里面你可以像静态代理一样做各种操作判断,然后最后 其实就是 invoke (跟我们当前所在的 invoke 不是一回事) 那个传进来的方法也就是方法 x, 然后用谁 invoke 呢?当然是那个对于 handler 来说有的成员 obj 存的就是通过 bind 获取到的那个实现类 A 的实例对象,然后紧跟参数.
- 这跟静态代理的概念一样,在代理类里面有个成员存的是被代理对象,也就是那个实现类 A 的实例对象 (不管怎么样传进来的), 然后里面做一对操作 (属于代理类里面做的各种判断等等等), 然后在你觉得合适的时候调用那真正被代理对象的实现的对应的方法,只不过这里是用到反射实现的
所以从始至终我们只需要传进去一个我们想要对应代理类对象的一个被代理对象 (实现类,实现了任何接口)(这个可以是任何! 只要是实现了某个接口的都行!(动态代理的好处)), 然后我们就获得一个对应的代理类的实例对象,我们就可以拿接口类型的变量接收他
然后我们就可以拿那个变量来调用接口里面的任何的方法,都会通过那个代理类对象我们设置的逻辑或者等等等操作,然后照样执行被代理类的对应的实现方法
这可比静态代理好多了,静态代理我们还需要每个被代理类创建一个对应的代理类.
现在有另外一个接口和实现这个接口的类:
直接通过我们创建的那两个类,把那个实现类对象一放进去就能获得对应的代理类对象了
提醒一声,invoke 方法第一个参接收 Object 类型的
- 如果实例方法 -> 需要传进去你想用哪个对象来 invoke 这个方法,直接传那个对象作为第一参数就行
- 如果静态方法 -> 一般是那个
类.class
作为第一参数,一般都这么做,指明是哪个类的静态方法,其实好像传 null 也行,因为你那个 Method 类型的方法就是从你通过反射从那个类的 Class 对象获取到的,也知道你是哪个类的静态方法都可以反正接收类型是 Object, 传什么都会自动向上转型的
动态代理和静态代理的区别:静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
这样就不需要在__每一个需要那个通用方法的地方去调用这个方法__-> 要是有 100 个方法都会用到这个通用方法,那全部 100 个就都要加上调用这个通用方法?
我们完全可以就是把这些通用方法放进代理类里面的 invoke 方法里面,然后每次我们有个实现类,想要调用里面的 x 方法,然后 x 方法需要用到通用方法,我们就可以动态地获取到这个实现类的代理类对象,然后再用这个代理类对象调用 x 方法,然后这个代理类就会执行那个通用方法,然后再执行实现类里面对应的实现方法里面属于他自己的独特方法
这时候我们又有个实现类里面方法 (或者是同一个实现类另外一个方法) 也需要这个通用代码,一样操作 -> 获取他的代理类对象然后调用那个方法 -> 通用代码执行 -> 独有方法执行 -> 通用代码执行 -> 返回值 (如果有)
# 1. 底层原理
- 面向切面编程,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。通俗来说就是在不修改代码的情况下添加新的功能。
-
底层通过动态代理来实现:
- 第一种:有接口的情况,使用 JDK 动态代理:创建接口实现类的代理对象。
- 第二种:无接口的情况,使用 CGLIB 动态代理:创建当前类子类 (如果有接口的话我们想增强的就是那个实现类,要是没接口我们就是想增强这个当前类,然后我们动态代理产出一个他子类的代理对象 (他子类不需要被创建!!!)) 的代理对象。(无接口的情况也可以让一个类 (的子类) 有着他的代理类)
JDK 动态代理举例:
- 通过 java.lang.reflect.Proxy 类 的 newProxyInstance 方法 创建代理类。
newProxyInstance 方法:
参数一:类加载器
参数二:所增强方法所在的类,这个类实现的接口,支持多个接口
参数三:实现 InvocationHandle 接口,重写 invoke 方法来添加新的功能
代码举例:
public interface UserDao {
public int add(int a, int b);
public int multi(int a, int b);
}
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int multi(int a, int b) {
return a*b;
}
}
public class Main {
@Test
public void test1(){
//所需代理的类实现的接口,支持多个接口
Class[] interfaces = {UserDao.class};
UserDao userDao = new UserDaoImpl();
//调用newProxyInstance方法来创建代理类
UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(Main.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
int result = userDaoProxy.add(1, 2);
System.out.println(result);
}
//创建内部类,实现InvocationHandler接口,重写invoke方法,添加新功能
class UserDaoProxy implements InvocationHandler {
Object obj;
//通过有参构造函数将所需代理的类传过来
public UserDaoProxy(Object obj){
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("进入" + method.getName() + "方法,这是新增的代码,参数有" + Arrays.toString(args));
//执行原有的代码
Object invoke = method.invoke(obj, args);
System.out.println("方法原先的内容执行完了");
return invoke;
}
}
}
运行结果:
# 2. 基于 AspectJ 实现 AOP 操作
(1)AOP 相关术语:
- 连接点:类中__所有可以被增强__的方法,都被称为连接点。
- 切入点 (PointCut):类中__实际被增强__的方法,称为切入点。
- 通知:增强的那一部分逻辑代码。通知有多种类型:
- 前置通知:增强部分代码在原代码前面。
- 后置通知:增强部分代码在原代码后面。
- 环绕通知:增强部分代码既有在原代码前面,也有在原代码后面。
- 异常通知:原代码发生异常后才会执行。
- 最终通知:类似与 finally 那一部分
- 切面:指把通知应用到切入点这一个动作。
(2)基于 AspectJ 实现 AOP 有两种方式:
- 基于 xml 配置文件
- 基于注解方法
在使用 spring 框架配置 AOP 的时候,不管是通过 XML 配置文件还是注解的方式都需要定义 pointcut"切入点"
例如定义切入点表达式 execution (* com.sample.service.impl….(…))
(3)切入点表达式
- 语法:execution([权限修饰符] [返回类型] [类全路径] [方法名称] [参数列表])
其中:返回类型模式、方法名模式、参数模式是必选项,其他的可以不写。
-
举例 1:对 com.atguigu.dao.BookDao 类里面的 add 进行增强
execution(* com.auguigu.dao.BookDao.add(..))
-
举例 2:对 com.atguigu.dao.BookDao 类里面的所有的方法进行增强
execution(* com.atguigu.dao.BookDao.*(..))
-
举例 3:对 com.atguigu.dao 包里面所有类,类里面所有方法进行增强
execution(* com.atguigu.dao.*.* (..))
# (1)基于注解方式
@Component
public class User {
public void add(){
System.out.println("User.add()");
}
}
@Component
@Aspect //使用Aspect注解
public class UserProxy {
//前置通知
@Before(value="execution(* com.oymn.spring5.User.add(..))")
public void before(){
System.out.println("UserProxy.before()");
}
//后置通知
@AfterReturning(value="execution(* com.oymn.spring5.User.add(..))")
public void afterReturning(){
System.out.println("UserProxy.afterReturning()");
}
//最终通知
@After(value="execution(* com.oymn.spring5.User.add(..))")
public void After(){
System.out.println("UserProxy.After()");
}
//异常通知
@AfterThrowing(value="execution(* com.oymn.spring5.User.add(..))")
public void AfterThrowing(){
System.out.println("UserProxy.AfterThrowing()");
}
//环绕通知
@Around(value="execution(* com.oymn.spring5.User.add(..))")
public void Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("UserProxy.Around() _1");
//调用proceed方法执行原先部分的代码
proceedingJoinPoint.proceed(); //这个比较特殊!!!!!!!
System.out.println("UserProxy.Around() _2");
}
}
配置 xml 文件:
<!--开启组件扫描-->
<context:component-scan base-package="com.oymn"></context:component-scan>
<!--开启AspectJ生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试类:
@Test
public void test2(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
User user = context.getBean("user", User.class);
user.add();
}
运行结果:
运行结果中没有出现异常通知,在 add 方法中添加 int i = 1/0;
public void add(){
int i = 1/0;
System.out.println("User.add()");
}
上面就是我们有个被增强类 (没有实现任何接口) 里面的 add 方法想要通过代理方式增强
我们也有一个代理类 (我们自己创建好的)
然后我们 xml 里面开启注解扫描 (命名空间啥啥啥的,或者用配置类也可以)
接着我们把被增强类和增强类 (也就是代理类) 上面放注解 @Component 让 Spring IOC 容器帮忙管理这两个类的对象
我们把
@Aspect
注解放到我们创建的增强类 (也就是代理类) 上面然后我们在 xml 里面配置开启 AspectJ 生成代理对象,这个就会扫描到我们增强类上面的
@Aspect
注解,就让 Spring 知道那是个代理类然后, 比如说:
- 这里我们就是给我们代理类里面写的方法给了个 @Before 注解且传了个切入点表达式
- 这个切入点表达式那个类的那个方法 (可能是好多个类的好多个方法), 反正任何匹配的方法再执行前 (因为这里是 @Before 注解) 都会触发这个代理类里面的被 @Before 注解的方法,这个代理类的这个方法执行完了就会去执行原本你要执行的那个方法里面的东西
- 所以上方我们只需要直接 getBean 那个被增强类的实例化对象然后调用里面的 add 方法,Spring 会帮我们查看他管理的代理类里面有没有方法的切入面表达式跟我们 add 方法匹配,如果有就会按照那个注解的通知类型,在对应的时候调用那个被注解的代理类里面的那个方法
- 可能助理类里面多个方法的注解 (不同通知,比如说 @Before 一个,@After 一个等等等) 的切入表达式都跟我们 add 方法匹配,那么那些方法都会在他们的注解的通知类型对应的时机触发对应的被注解的方法
运行结果:从这里也可以看到,但出现异常时,After 最终通知有执行,而 AfterReturning (有结果才通知) 后置通知并没有执行。
@AfterThrowing 注解的方法只有在那个被增强类的方法抛出异常才会执行
对于上面的例子,有很多通知的切入点都是相同的方法,因此,可以将该切入点进行抽取:通过 @Pointcut 注解
@Pointcut(value="execution(* com.oymn.spring5.User.add(..))")
public void pointDemo(){
}
//前置通知
@Before(value="pointDemo()")
public void before(){
System.out.println("UserProxy.before()");
}
就是写个方法上面给个 @Pointcut 注解并且赋值那个切入点表达式
然后之后注解要需要这个切面点表达式就直接拿那个方法调用当做是切入点表达式就 OK
设置增强类优先级:
当有多个增强类 (每个类里面有一个或多个方法有着注解,注解的切入点表达式跟) 对我们被增强类的同一方法进行增强 (匹配) 时,可以通过 **
@Order(数字值)
** 来设置增强类的优先级,数字越小优先级越高。注意那些增强方法都会执行,只是顺序问题
@Component
@Aspect
@Order(1)
public class PersonProxy
完全注解开发 :
可以通过配置类来彻底摆脱 xml 配置文件:
@Configuration
@ComponentScan(basePackages = "com.oymn.spring5")
//@EnableAspectJAutoProxy注解相当于上面xml文件中配置的 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Config {
}
# (2)基于 xml 方式
这种方式开发中不怎么用,了解即可。
创建 Book 和 BookProxy 类
public class Book {
public void buy(){
System.out.println("buy()");
}
}
public class BookProxy {
public void before(){
System.out.println("before()");
}
}
配置 xml 文件:
<!--创建对象-->
<bean id="book" class="com.oymn.spring5.Book"></bean>
<bean id="bookProxy" class="com.oymn.spring5.BookProxy"></bean>
<aop:config>
<!--切入点-->
<aop:pointcut id="p" expression="execution(* com.oymn.spring5.Book.buy(..))"/>
<!--配置切面-->
<aop:aspect ref="bookProxy">
<aop:before method="before" pointcut-ref="p"/> <!--将bookProxy中的before方法配置为切入点的前置通知-->
</aop:aspect>
</aop:config>
# 从别处拿来的关于 AOP 笔记
# 面向切面的基本原理
什么是面向切面编程
横切关注点:影响应用多处的功能(安全、事务、日志)
切面:
横切关注点被模块化为特殊的类,这些类称为切面
优点:
每个关注点现在都集中于一处,而不是分散到多处代码中
服务模块更简洁,服务模块只需关注核心代码。
AOP 术语
- 通知:
- 定义:切面也需要完成工作。在 AOP 术语中,切面的工作被称为通知。
- 工作内容:通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决何时执行这个工作。
- Spring 切面可应用的 5 种通知类型:
- Before—— 在方法调用之前调用通知
- After—— 在方法完成之后调用通知,无论方法执行成功与否
- After-returning—— 在方法执行成功之后调用通知
- After-throwing—— 在方法抛出异常后进行通知
- Around—— 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
- 连接点:
- 定义:连接点是一个应用执行过程中能够插入一个切面的点。
- 连接点可以是调用方法时、抛出异常时、甚至修改字段时、
- 切面代码可以利用这些点插入到应用的正规流程中
- 程序执行过程中能够应用通知的所有点。
- 切点:
- 定义:如果通知定义了 “什么” 和 “何时”。那么切点就定义了 “何处”。切点会匹配通知所要织入的一个或者多个连接点。
- 通常使用明确的类或者方法来指定这些切点。
- 作用:定义通知被应用的位置(在哪些连接点)
- 切面:
- 定义:切面是通知和切点的集合,通知和切点共同定义了切面的全部功能 —— 它是什么,在何时何处完成其功能。
- 引入:
- 引入允许我们向现有的类中添加方法或属性
- 织入:
- 织入是将切面应用到目标对象来创建的代理对象过程。
- 切面在指定的连接点被织入到目标对象中,在目标对象的生命周期中有多个点可以织入
- 编译期 —— 切面在目标类编译时期被织入,这种方式需要特殊编译器。AspectJ 的织入编译器就是以这种方式织入切面。
- 类加载期 —— 切面在类加载到
- JVM ,这种方式需要特殊的类加载器,他可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的 LTW 就支持这种织入方式
- 运行期 —— 切面在应用运行期间的某个时刻被织入。一般情况下,在织入切面时候,AOP 容器会为目标对象动态的创建代理对象。Spring AOP 就是以这种方式织入切面。
# Spring 对 AOP 的支持
- 并不是所有的 AOP 框架都是一样的,他们在连接点模型上可能有强弱之分。
- 有些允许对字段修饰符级别应用通知
- 有些支持方法调用连接点
- Spring 提供的 4 种各具特色的 AOP 支持
- 基于代理的经典 AOP;
- @AspectJ 注解驱动的切面;
- 纯 POJO 切面;
- 注入式 AspectJ 切面; Spring
-
在运行期间通知对象
-
通过在代理类中织入包裹切面,Spring 在运行期间将切面织入到 Spring 管理的 Bean 中。
代理类封装了目标类,并拦截被通知的方法调用,再将调用转发给真正的目标 Bean
当拦截到方法调用时,在调用目标 Bean 方法之前,代理会执行切面逻辑。
当真正应用需要被代理的 Bean 时,Spring 才创建代理对象。如果使用 ApplicationContext,在 ApplicationContext 从 BeanFactory 中加载所有 Bean 时,Spring 创建代理对象,因为 Spring 在运行时候创建代理对象,所以我们不需要特殊的编译器来织入 Spring AOP 的切面。
-
-
Spring 支持方法创建连接点
- 因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。
- Spring 缺失对字段连接点的支持,无法让我们更加细粒度的通知,例如拦截对象字段的修改
- Spring 缺失对构造器连接点支持,我发在 Bean 创建时候进行通知。
# 使用切点选择连接点
- 切点用于准确定位,确定在什么地方应用切面通知。
- Spring 定义切点
- 在 Spring AOP 中,需要使用 AspectJ 的切点表达式来定义切点。
AspectJ 指示器 | 描述 |
---|---|
arg () | 限制连接点的指定参数为指定类型的执行方法 |
@args () | 限制连接点匹配参数由指定注解标注的执行方法 |
execution () | 用于匹配连接点的执行方法 |
this () | 限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类 |
target () | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解 |
within() | 限制连接点匹配指定类型 |
@within() | 限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注释的连接点 |
1. 创建自己的切点
- execution () 指示器选择 Instrument 的 play () 方法。
方法表达式是以 * 号开头,标识了我们不关心的方法返回值的类型。
* 后我们指定了权限定类名和方法名。
对于方法的参数列表,使用(…)标识切点选择任意的 play ( ) 方法,无论入参是什么。
- 假设我们需要匹配切点仅匹配 com.Springinaction.springidol 包。可以使用 within()
注意 && 是将 execution () 和 within () 连接起来,形成的 and 关系。同理也可以使用 || 或关系、!非关系
- 创建 Spring 的 bean ( ) 指示器
Spring 2.5 引入一个新的 bean ( ) 指示器,该指示器允许我们在切点表达式中使用 Bean ID 来标识 Bean
bean ( ) 使用 Bean ID 或 Bean 名称作为参数来限制切点只匹配特定 Bean。
如下,我们希望执行 Instrument 的 play ( ) 方法时候应用通知,但限定 Bean 的 ID 为 eddie
还可以使用非操作作为除了指定 ID 的 Bean 以外的其他 Bean 应用通知
在此场景下,切面会通知被编织到所有 ID 不为 eddie 的 Bean 中
# 在 XML 中声明切面
Spring 的 AOP 配置元素简化了基于 POJO 切面声明
AOP 配置元素 | 描述 |
---|---|
aop : advisor | 定义 AOP 通知器 |
aop : after | 定义 AOP 后置通知(不管被通知方法是否执行成功) |
aop : after-returing | 定义 AOP after-returing 通知 |
aop : after-throwing | 定义 AOP after-throwing 通知 |
aop : around | 定义 AOP 环绕通知 |
aop : aspect | 定义切面 |
aop : aspectj-autoproxy | 启动 @AspectJ 注解驱动的切面 |
aop : before | 定义 AOP 前置通知 |
aop : config | 顶层的 AOP 配置元素,大多数 aop : * 元素必须包含在 元素内 |
aop : declare-parents | 为被通知的对象引入额外接口,并透明的实现 |
aop : pointcut | 定义切点 |
# 四、JdbcTemplate
- Spring 对 JDBC 进行封装,使用 JdbcTemplate 方便对数据库的操作。
(1)增删改操作:
int update(String sql, Object... args);
(2)查询:返回某个值
T queryForObject(String sql,Class<T> requiredType);
(3)查询:返回某个对象
T queryForObject(String sql,RowMapper<T> rowMapper,Object ... args);
(4)查询:返回集合
List<T> query(String sql,RowMapper<T> rowMapper,Object... args);
(5)批量增删改:
int[] batchUpdate(String sql,List<Object[]> batchArgs);
举例:
-
引入相关 jar 包
-
配置数据库连接池;配置 JdbcTemplate 对象
<context:component-scan base-package="com.oymn"></context:component-scan> <!--配置数据库连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/book" /> <property name="username" value="root" /> <property name="password" value="000000" /> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> </bean> <!--创建JdbcTemplate对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--注入数据库连接池--> <property name="dataSource" ref="dataSource"></property> </bean>
-
创建 Service 类和 Dao 类,在 Dao 类中注入 JdbcTemplate 对象
public interface BookDao { public void add(Book book); //添加图书 public void update(Book book); //修改图书 public void delete(int id); //删除图书 public int queryCount(); //查询数量 public Book queryBookById(int id); //查询某本书 public List<Book> queryBooks(); //查询所有书 public void batchAddBook(List<Object[]> books); //批量添加图书 public void batchUpdateBook(List<Object[]> books); //批量修改图书 public void batchDeleteBook(List<Object[]> args); //批量删除图书 }
@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public void add(Book book) { String sql = "insert into t_book set name=?,price=?"; Object[] args = {book.getBookName(),book.getBookPrice()}; int update = jdbcTemplate.update(sql, args); System.out.println(update); } @Override public void update(Book book) { String sql = "update t_book set name=?,price=? where id=?"; Object[] args = {book.getBookName(),book.getBookPrice(),book.getBookId()}; int update = jdbcTemplate.update(sql, args); System.out.println(update); } @Override public void delete(int id) { String sql = "delete from t_book where id=?"; int update = jdbcTemplate.update(sql, id); System.out.println(update); } @Override public int queryCount() { String sql = "select count(*) from t_book"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); return count; } @Override public Book queryBookById(int id) { String sql = "select id bookId,name bookName,price bookPrice from t_book where id=?"; Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Book>(Book.class), id); return book; } @Override public List<Book> queryBooks() { String sql = "select id bookId,name bookName,price bookPrice from t_book"; List<Book> bookList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class)); return bookList; } @Override public void batchAddBook(List<Object[]> books) { String sql = "insert into t_book set id=?,name=?,price=?"; int[] ints = jdbcTemplate.batchUpdate(sql, books); System.out.println(ints); } @Override public void batchUpdateBook(List<Object[]> books) { String sql = "update t_book set name=?,price=? where id=?"; int[] ints = jdbcTemplate.batchUpdate(sql, books); System.out.println(ints); } @Override public void batchDeleteBook(List<Object[]> args) { String sql = "delete from t_book where id=?"; int[] ints = jdbcTemplate.batchUpdate(sql, args); System.out.println(ints); } }
@Service public class BookService { @Autowired private BookDao bookDao; //添加图书 public void add(Book book){ bookDao.add(book); } //修改图书 public void update(Book book){ bookDao.update(book); } //删除图书 public void delete(Integer id){ bookDao.delete(id); } //查询数量 public int queryCount(){ return bookDao.queryCount(); } //查询图书 public Book queryBookById(Integer id){ return bookDao.queryBookById(id); } //查询所有图书 public List<Book> queryBooks(){ return bookDao.queryBooks(); } //批量添加图书 public void batchAddBook(List<Object[]> books){ bookDao.batchAddBook(books); } //批量修改图书 public void batchUpdateBook(List<Object[]> books){ bookDao.batchUpdateBook(books); } //批量删除图书 public void batchDeleteBook(List<Object[]> args){ bookDao.batchDeleteBook(args); } }
# 五、事务管理
-
事务是数据库操作最基本单位,要么都成功,要么都失败。
-
典型场景:转账
-
事务四个特性 ACID:原子性,一致性,隔离性,持久性。
-
Spring 事务管理有两种方式:编程式事务管理 和 声明式事务管理,一般使用声明式事务管理,底层使用 AOP 原理。
-
声明式事务管理有两种方式:基于 xml 配置方式 和 基于注解方式,一般使用注解方式。
-
Spring 事务管理提供了一个接口,叫做事务管理器,这个接口针对不同的框架提供不同的实现类。
对于使用 JdbcTemplate 进行数据库交互,则使用 DataSourceTransactionManager 实现类,如果整合 Hibernate 框架则使用 HibernateTransactionManager 实现类,具体情况具体使用。
# (1)注解实现声明式事务管理:
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/book" />
<property name="username" value="root" />
<property name="password" value="000000" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
</bean>
<!--创建JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/> // 需要上面的数据库连接池对象的注入
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
在 service 类上面或者 service 类的方法上面添加事务注解 @Transactional
- 如果把 @Transactional 添加在类上面,这个类里面所有方法都添加事务。
- 如果只是添加在方法上面,则只为这个方法添加事务。
@Service
@Transactional
public class UserService {
}
然后就行了,很方便!!!
这个样子比如 userService 层有方法是调用 dao 层两个方法第二个 dao 层方法抛出异常,那么就会帮我们回滚
我们只需要上面 (xml) 配置好然后用给个注解完事!!!
一般事务管理都是放到 service 层的
声明式事务管理的参数配置:
-
propagation:事务传播行为,总共有 7 种
就比如说一个事务 (被 @Transaction 注解标注的方法) 里面还调用了另外一个方法,这另外一个方法可能也是事务 (被 @Transaction 注解标注的方法) 可能不是,反正就是不同事务之间的交集该怎么处理把应该
具体讲解看下方!
-
isolation:事务隔离级别
有三个读问题:脏读,不可重复读,虚读(幻读)。
设置隔离级别,解决读问题:
脏读 不可重复读 虚读 EAD UNCOMMITED(读未提交) 有 有 有 READ COMMITED(读已提交) 无 有 有 EPEATABLE READ(可重复读) 无 无 有 SERIALIZABLE(串行化) 无 无 无 -
timeout:超时时间
- 事务需要在一定时间内进行提交,超过时间后回滚。
- 默认值是 - 1,设置时间以秒为单位。
- readOnly:是否只读
- 默认值为 false,表示可以查询,也可以增删改。
- 设置为 true,只能查询。
- rollbackFor:回滚,设置出现哪些异常进行事务回滚。
- noRollbackFor:不回滚,设置出现哪些异常不进行事务回滚。
@Service
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public class AccountService {
}
完全注解实现声明式事务管理:
配置类:
@Configuration //配置类
@ComponentScan(basePackages = "com.oymn.spring5") //开启组件扫描
@EnableTransactionManagement //开启事务
public class Config {
//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/book");
druidDataSource.setUsername("root");
druidDataSource.setPassword("000000");
return druidDataSource;
}
//创建JdbcTemplate对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
@Service
public class AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
public void accountMoney(){
accountDao.add();
//int i=1/0; //用来模拟转账失败
accountDao.reduce();
}
}
# (2)xml 实现声明式事务管理:
`<context:component-scan base-package="com.oymn"></context:component-scan>
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/book" />
<property name="username" value="root" />
<property name="password" value="000000" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
</bean>
<!--创建JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务通知-->
<tx:advice id="txadvice">
<!--配置事务参数-->
<tx:attributes>
<tx:method name="accountMoney" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<!--配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pt" expression="execution(* com.oymn.spring5.Service.*.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>
# 事务传播行为说明白
# 1. 什么是事务传播行为?
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
用伪代码说明:
public void methodA(){
methodB();
//doSomething
}
@Transaction(Propagation=XXX)
public void methodB(){
//doSomething
}
代码中 methodA()
方法嵌套调用了 methodB()
方法, methodB()
的事务传播行为由 @Transaction(Propagation=XXX)
设置决定。这里需要注意的是 methodA()
并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。
# 2. Spring 中七种事务传播行为
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。 |
定义非常简单,也很好理解,下面我们就进入代码测试部分,验证我们的理解是否正确。
# 代码验证
文中代码以传统三层结构中两层呈现,即 Service 和 Dao 层,由 Spring 负责依赖注入和注解式事务管理,DAO 层由 Mybatis 实现,你也可以使用任何喜欢的方式,例如,Hibernate,JPA,JDBCTemplate 等。数据库使用的是 MySQL 数据库,你也可以使用任何支持事务的数据库,并不会影响验证结果。
首先我们在数据库中创建两张表:
user1
CREATE TABLE `user1` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
)
ENGINE = InnoDB;
user2
CREATE TABLE `user2` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
)
ENGINE = InnoDB;
然后编写相应的 Bean 和 DAO 层代码:
User1
public class User1 {
private Integer id;
private String name;
//get和set方法省略...
}
User2
public class User2 {
private Integer id;
private String name;
//get和set方法省略...
}
User1Mapper
public interface User1Mapper {
int insert(User1 record);
User1 selectByPrimaryKey(Integer id);
//其他方法省略...
}
User2Mapper
public interface User2Mapper {
int insert(User2 record);
User2 selectByPrimaryKey(Integer id);
//其他方法省略...
}
最后也是具体验证的代码由 service 层实现,下面我们分情况列举。
# 1.PROPAGATION_REQUIRED
我们为 User1Service 和 User2Service 相应方法加上 Propagation.REQUIRED
属性。
User1Service 方法:
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
}
User2Service 方法:
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User2 user){
user2Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequiredException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}
# 1.1 场景一
此场景外围方法没有开启事务。
验证方法 1:
@Override
public void notransaction_exception_required_required(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
验证方法 2:
@Override
public void notransaction_required_required_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三”、“李四” 均插入。 | 外围方法未开启事务,插入 “张三”、“李四” 方法在自己的事务中独立运行,外围方法异常不影响内部插入 “张三”、“李四” 方法独立的事务。 |
2 | “张三” 插入,“李四” 未插入。 | 外围方法没有事务,插入 “张三”、“李四” 方法都在自己的事务中独立运行,所以插入 “李四” 方法抛出异常只会回滚插入 “李四” 方法,插入 “张三” 方法不受影响。 |
结论:通过这两个方法我们证明了在外围方法未开启事务的情况下 Propagation.REQUIRED
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
# 1.2 场景二
外围方法开启事务,这个是使用率比较高的场景。
验证方法 1:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_required(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
验证方法 2:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_required_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
}
验证方法 3:
@Transactional
@Override
public void transaction_required_required_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三”、“李四” 均未插入。 | 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚。 |
2 | “张三”、“李四” 均未插入。 | 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,外围方法感知异常致使整体事务回滚。 |
3 | “张三”、“李四” 均未插入。 | 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,即使方法被 catch 不被外围方法感知,整个事务依然回滚。⭐️ |
结论:以上试验结果我们证明在外围方法开启事务的情况下 Propagation.REQUIRED
修饰的内部方法会加入到外围方法的事务中,所有 Propagation.REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
# 2.PROPAGATION_REQUIRES_NEW
我们为 User1Service 和 User2Service 相应方法加上 Propagation.REQUIRES_NEW
属性。
User1Service 方法:
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User1 user){
user1Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
}
User2Service 方法:
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User2 user){
user2Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNewException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}
# 2.1 场景一
外围方法没有开启事务。
验证方法 1:
@Override
public void notransaction_exception_requiresNew_requiresNew(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
throw new RuntimeException();
}
验证方法 2:
@Override
public void notransaction_requiresNew_requiresNew_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNewException(user2);
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三” 插入,“李四” 插入。 | 外围方法没有事务,插入 “张三”、“李四” 方法都在自己的事务中独立运行,外围方法抛出异常回滚不会影响内部方法。 |
2 | “张三” 插入,“李四” 未插入 | 外围方法没有开启事务,插入 “张三” 方法和插入 “李四” 方法分别开启自己的事务,插入 “李四” 方法抛出异常回滚,其他事务不受影响。 |
结论:通过这两个方法我们证明了在外围方法未开启事务的情况下 Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
# 2.2 场景二
外围方法开启事务。
验证方法 1:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_requiresNew_requiresNew(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
User2 user3=new User2();
user3.setName("王五");
user2Service.addRequiresNew(user3);
throw new RuntimeException();
}
验证方法 2:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
User2 user3=new User2();
user3.setName("王五");
user2Service.addRequiresNewException(user3);
}
验证方法 3:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
User2 user3=new User2();
user3.setName("王五");
try {
user2Service.addRequiresNewException(user3);
} catch (Exception e) {
System.out.println("回滚");
}
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三” 未插入,“李四” 插入,“王五” 插入。 | 外围方法开启事务,插入 “张三” 方法和外围方法一个事务,插入 “李四” 方法、插入 “王五” 方法分别在独立的新建事务中,外围方法抛出异常只回滚和外围方法同一事务的方法,故插入 “张三” 的方法回滚。 |
2 | “张三” 未插入,“李四” 插入,“王五” 未插入。 | 外围方法开启事务,插入 “张三” 方法和外围方法一个事务,插入 “李四” 方法、插入 “王五” 方法分别在独立的新建事务中。插入 “王五” 方法抛出异常,首先插入 “王五” 方法的事务被回滚,异常继续抛出被外围方法感知,外围方法事务亦被回滚,故插入 “张三” 方法也被回滚。 |
3 | “张三” 插入,“李四” 插入,“王五” 未插入。 | 外围方法开启事务,插入 “张三” 方法和外围方法一个事务,插入 “李四” 方法、插入 “王五” 方法分别在独立的新建事务中。插入 “王五” 方法抛出异常,首先插入 “王五” 方法的事务被回滚,异常被 catch 不会被外围方法感知,外围方法事务不回滚,故插入 “张三” 方法插入成功。 |
结论:在外围方法开启事务的情况下 Propagation.REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
# 3.PROPAGATION_NESTED
我们为 User1Service 和 User2Service 相应方法加上 Propagation.NESTED
属性。
User1Service 方法:
@Service
public class User1ServiceImpl implements User1Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.NESTED)
public void addNested(User1 user){
user1Mapper.insert(user);
}
}
User2Service 方法:
@Service
public class User2ServiceImpl implements User2Service {
//省略其他...
@Override
@Transactional(propagation = Propagation.NESTED)
public void addNested(User2 user){
user2Mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void addNestedException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}
# 3.1 场景一
此场景外围方法没有开启事务。
验证方法 1:
@Override
public void notransaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
验证方法 2:
@Override
public void notransaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三”、“李四” 均插入。 | 外围方法未开启事务,插入 “张三”、“李四” 方法在自己的事务中独立运行,外围方法异常不影响内部插入 “张三”、“李四” 方法独立的事务。 |
2 | “张三” 插入,“李四” 未插入。 | 外围方法没有事务,插入 “张三”、“李四” 方法都在自己的事务中独立运行,所以插入 “李四” 方法抛出异常只会回滚插入 “李四” 方法,插入 “张三” 方法不受影响。 |
结论:通过这两个方法我们证明了在外围方法未开启事务的情况下 Propagation.NESTED
和 Propagation.REQUIRED
作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
# 3.2 场景二
外围方法开启事务。
验证方法 1:
@Transactional
@Override
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
验证方法 2:
@Transactional
@Override
public void transaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}
验证方法 3:
@Transactional
@Override
public void transaction_nested_nested_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addNestedException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
分别执行验证方法,结果:
验证方法序号 | 数据库结果 | 结果分析 |
---|---|---|
1 | “张三”、“李四” 均未插入。 | 外围方法开启事务,内部事务为外围事务的子事务,外围方法回滚,内部方法也要回滚。 |
2 | “张三”、“李四” 均未插入。 | 外围方法开启事务,内部事务为外围事务的子事务,内部方法抛出异常回滚,且外围方法感知异常致使整体事务回滚。 |
3 | “张三” 插入、“李四” 未插入。 | 外围方法开启事务,内部事务为外围事务的子事务,插入 “李四” 内部方法抛出异常,可以单独对子事务回滚。 |
结论:以上试验结果我们证明在外围方法开启事务的情况下 Propagation.NESTED
修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务
# 4. REQUIRED,REQUIRES_NEW,NESTED 异同
由 “1.2 场景二” 和 “3.2 场景二” 对比,我们可知:
NESTED 和 REQUIRED 修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是 REQUIRED 是加入外围方法事务,所以和外围事务同属于一个事务,一旦 REQUIRED 事务抛出异常被回滚,外围方法事务也将被回滚。而 NESTED 是外围方法的子事务,有单独的保存点,所以 NESTED 方法抛出异常被回滚,不会影响到外围方法的事务。
由 “2.2 场景二” 和 “3.2 场景二” 对比,我们可知:
NESTED 和 REQUIRES_NEW 都可以做到内部方法事务回滚而不影响外围方法事务。但是因为 NESTED 是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而 REQUIRES_NEW 是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
# 5. 其他事务传播行为
鉴于文章篇幅问题,其他事务传播行为的测试就不在此一一描述了,感兴趣的读者可以去源码中自己寻找相应测试代码和结果解释。传送门:https://github.com/TmTse/tran…
# 模拟用例
介绍了这么多事务传播行为,我们在实际工作中如何应用呢?下面我来举一个示例:
假设我们有一个注册的方法,方法中调用添加积分的方法,如果我们希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),我们会这样写:
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void register(User user){
try {
membershipPointService.addPoint(Point point);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}
我们还规定注册失败要影响 addPoint()
方法(注册方法回滚添加积分方法也需要回滚),那么 addPoint()
方法就需要这样实现:
@Service
public class MembershipPointServiceImpl implements MembershipPointService{
@Transactional(propagation = Propagation.NESTED)
public void addPoint(Point point){
try {
recordService.addRecord(Record record);
} catch (Exception e) {
//省略...
}
//省略...
}
//省略...
}
我们注意到了在 addPoint()
中还调用了 addRecord()
方法,这个方法用来记录日志。他的实现如下:
@Service
public class RecordServiceImpl implements RecordService{
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void addRecord(Record record){
//省略...
}
//省略...
}
我们注意到 addRecord()
方法中 propagation = Propagation.NOT_SUPPORTED
,因为对于日志无所谓精确,可以多一条也可以少一条,所以 addRecord()
方法本身和外围 addPoint()
方法抛出异常都不会使 addRecord()
方法回滚,并且 addRecord()
方法抛出异常也不会影响外围 addPoint()
方法的执行。
通过这个例子相信大家对事务传播行为的使用有了更加直观的认识,通过各种属性的组合确实能让我们的业务实现更加灵活多样。
# 六、Spring5 新特性
# 1. 自带了日志封装
- Spring5 移除了 Log4jConfigListener,官方建议使用 Log4j2
Spring5 整合 Log4j2:
第一步:引入 jar 包
第二步:创建 log4j2.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,可以看到log4j2内部各种详细输出-->
<configuration status="INFO">
<!--先定义所有的appender-->
<appenders>
<!--输出日志信息到控制台-->
<console name="Console" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</console>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出-->
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
# 2. @Nullable 注解
@Nullable 注解可以用在方法上,属性上,参数上,表示方法返回值可以为空,属性可以为空,参数可以为空。
@Nullable //表示方法返回值可以为空
public int getId();
@Nullable //表示参数可以为空
public void setId(@Nullable int Id);
@Nullable //表示属性可以为空
public int id;
# 3. 支持函数式风格编程
这是因为 java8 新增了 lamda 表达式
@Test
public void test() {
//1 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2 调用 context 的方法对象注册
context.refresh();
context.registerBean("user1",User.class,() -> new User());
//3 获取在 spring 注册的对象
// User user = (User)context.getBean("com.atguigu.spring5.test.User");
User user = (User)context.getBean("user1");
System.out.println(user);
}
# 4. 支持整合 JUnit5
(1)整合 JUnit4:
第一步:引入 jar 包
第二步:创建测试类,使用注解方式完成
@RunWith(SpringJUnit4ClassRunner.class) //单元测试框架
@ContextConfiguration("classpath:bean4.xml") //加载配置文件
public class JUnitTest {
@Autowired
public User user;
@Test
public void test(){
System.out.println(user);
}
}
bean4.xml:
<context:component-scan base-package="com.oymn"></context:component-scan>
通过使用 @ContextConfiguration 注解,测试方法中就不用每次都通过 context 来获取对象了,比较方便。
ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
BookService bookService = context.getBean("bookService",BookService.class);
(2)整合 JUnit5: