Spring的基本使用
- 第一章 Spring简介
- 第二章 IOC容器
- 第三章 AOP
- 第四章 声明式事务
第一章 Spring简介
Spring官网链接:官网
Spring的五大功能
功能模块 | 功能介绍 |
---|---|
Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP&Aspects | 面向切面编程 |
Testing | 提供了对 junit 或 TestNG 测试框架的整合。 |
Data Access/Integration | 提供了对数据访问/集成的功能。 |
Spring MVC | 提供了面向Web应用程序的集成功能。 |
Spring的核心就是控制反转(IOC)和依赖注入(DI)及面向切面编程(AOP),之后会给大家介绍这些概念。
第二章 IOC容器
1.1 IOC容器概念
1.1.1 程序中的容器
- 数组
- 集合List、Set、Map
- Servlet容器能够管理 Servlet、Filter、Listener
①Servlet的生命周期
名称 | 时机 | 次数 |
---|---|---|
创建对象 | 默认情况:接收到第一次请求修改启动顺序后:Web应用启动过程中 | 一次 |
初始化 | 创建对象之后 | 一次 |
处理请求 | 接收到请求 | 多次 |
销毁 | Web应用卸载之前 | 一次 |
②Filter生命周期
名称 | 时机 | 次数 |
---|---|---|
创建对象 | Web应用启动时 | 一次 |
初始化 | 创建对象之后 | 一次 |
拦截请求 | 接收到匹配的请求 | 多次 |
销毁 | Web应用卸载之前 | 一次 |
1.1.2 IOC思想
IOC概念:Inversion of Control,中文意思是控制反转。它是软件设计模式中的一种设计原则和思想。
① 获取资源的传统方式
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,因此开发人员需要知道在具体容器中特定资源的获取方式。
②控制反转的方式获取资源
将你设计好的对象交给容器控制,而不是显式的用代码进行对象的创建。
把创建和查找依赖对象的控制权交给IOC容器,由IOC容器进行注入、组合对象。
这也就是说容器主动的将资源推送给需要的组件,而开发人员是不需要知道容器是如何创建资源的,只需要提供接收资源的方式。
③使用IOC的好处
- 资源集中管理,实现资源的可配置和易管理。
- 降低了资源之间的依赖程度,即松耦合。
- 减少对象的创建和内存消耗。
- 使得程序的整个体系结构可维护、灵活性、扩展性变高。
④DI
DI:Dependency Injection,中文意思是依赖注入。他也是IOC的另外一个说法。就是组件之间依赖关系是由容器在运行期决定。组件的创建都由容器来创建,再由容器来决定各组件之间的依赖关系。各个组件不再自己决定依赖的其他对象。换句话说,就是组件运行所依赖的对象只能是注入进来的对象,注入哪个对象,我就依赖哪个!
1.2 基于XML管理Bean
1.2.1 创建bean
由Spring的IOC容器来创建类的对象。
步骤:
①创建一个Maven Module
②在pom.xml中添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
spring-context的依赖包
③创建组件类
public class Computer {
public void work(){
System.out.println("==========电脑启动=======");
}
}
④创建Spring的配置文件
注意:这个spring配置文件的名字可以自定义
⑤编写spring.xml文件
bean标签:通过配置bean标签告诉IOC容器需要创建对象的组件是什么。
name属性:bean的唯一标识。
id属性:bean的唯一标识。
class属性:组件类的全类名。
<?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 name="computer" class="com.zhang.Computer">
</bean>
</beans>
⑥测试加载
public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
Computer computer = applicationContext.getBean("computer", Computer.class);
computer.work();
}
}
1.2.2 获取bean
有两种方式获取bean。
- 根据name属性或id属性获取
- 根据类型获取
1.2.2.1 根据name属性或id属性获取
注意:name属性和id属性的值必须是唯一的,不能重复。
在spring.xml中bean上不管是name属性还是id属性,只需要在getBean时写上他们的值就可以获取到了。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
// 如果不命名第二个参数,获取的就是Object对象,需要强转为Computer类
// Computer computer = (Computer)applicationContext.getBean("computer");
Computer computer = applicationContext.getBean("computer", Computer.class);
computer.work();
1.2.2.2 根据类型获取
注意: 如果通过类型获取,在xml文件中只能有一个该类型的bean;如果有多个该类型的bean,则会报 org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.zhang.Computer’ available: expected single matching bean but found 2: computer,computer1 这个错误。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
Computer computer = applicationContext.getBean(Computer.class);
computer.work();
1.2.3 属性注入
实体类如下:
public class CPU {
private String name;
public CPU(){}
public CPU(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Computer {
private String name;
private CPU cpu;
public Computer(){}
public Computer(String name, CPU cpu) {
this.name = name;
this.cpu = cpu;
}
public CPU getCpu() {
return cpu;
}
public void setCpu(CPU cpu) {
this.cpu = cpu;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void work(){
System.out.println("==========电脑启动=======");
}
}
1.2.3.1 setter方式
1.2.3.1.1 < property > 标签注入
property 标签:通过实体类的setXxx() 方法给实体类对象设置属性;
name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关,与其方法的参数有关);
value属性:指定属性值;
ref属性:通过bean的id引用另一个bean。
<bean id="cpu" class="com.zhang.pojo.CPU">
<property name="name" value="Intel"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer">
<property name="name" value="惠普"/>
<!-- 这个ref注入时,idea有提示,并且ref只能匹配setXxx()方法中参数的相对应的实体类类型-->
<property name="cpu" ref="cpu"/>
</bean>
注意:ref和value属性的区别:当我们使用value属性时,Spring会直接给该属性赋值,Spring不会把这个属性作为一个bean的id,也不会去找相应的bean来赋值;而ref属性会找自己相对应的bean(类型),即引用这个对象。
1.2.3.1.2 <p:prop> 命名空间注入
导入命名空间
注意:命名空间你可以先写p:,然后使用Alt+Enter,idea帮你自动导入命名空间。
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
如上面使用 property 标签注入的方式,我们可以改成 <p:prop> 命名空间注入的形式。
注意: p命名空间后边跟着属性名相当于注入值,而后边跟着属性名-ref相当于注入实体类。
<bean id="cpu" class="com.zhang.pojo.CPU" p:name="Intel"/>
<bean id="computer" class="com.zhang.pojo.Computer" p:name="惠普" p:cpu-ref="cpu"/>
1.2.3.2 构造器方式
1.2.3.2.1 位置index注入
index属性:指定参数所在位置的索引(从0开始)
<bean id="cpu" class="com.zhang.pojo.CPU">
<constructor-arg index="0" value="Inter"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer">
<constructor-arg index="0" value="惠普"/>
<constructor-arg index="1" ref="cpu"/>
</bean>
1.2.2.3.2 命名参数注入
name属性:指定属性名;
value属性:指定的属性值;
ref属性:通过bean的id引用另一个bean。
<bean id="cpu" class="com.zhang.pojo.CPU">
<constructor-arg name="name" value="Inter"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer">
<!--这个name属性可以省略-->
<constructor-arg name="name" value="惠普"/>
<constructor-arg name="cpu" ref="cpu"/>
</bean>
1.2.3.3 自动装配方式
使用bean标签的autowire属性设置自动装配效果;
1.2.3.3.1 根据类型进行装配
若autowire属性的值为byType,则表示根据类型进行装配,注意:上下文默认情况下最多只有一个类型,如果有多个类型,通过设置primary=“true” ,则此bean就是优先注入的;如果给其中多余的bean设置autowire-candidate=“false” 属性,则就不会作为自动装配的候选。
<bean id="cpu1" class="com.zhang.pojo.CPU" autowire-candidate="false">
<property name="name" value="Intel"/>
</bean>
<bean id="cpu2" class="com.zhang.pojo.CPU" primary="true">
<property name="name" value="AMD"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer" autowire="byType">
</bean>
1.2.3.3.2 根据属性名进行装配
若autowire属性的值为byName,则表示根据bean的id进行匹配。而bean的id是根据需要装配组件的属性的属性名来确定的;
<bean id="cpu" class="com.zhang.pojo.CPU">
<property name="name" value="AMD"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer" autowire="byName">
</bean>
1.2.3.3.3 根据构造器进行装配
若autowire属性的值为constructor,则表示通过构造器来匹配。
首先匹配对应参数,名字,类型。
如果有多个类型参数时,优先名字匹配,如果名字匹配成功则正常注入,否则不执行有参构造方法,不能注入
<bean id="cpu1" class="com.zhang.pojo.CPU">
<property name="name" value="Intel"/>
</bean>
<bean id="cpu" class="com.zhang.pojo.CPU">
<property name="name" value="AMD"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer" autowire="constructor">
</bean>
1.2.3.4 通过内部bean方式
<bean id="computer" class="com.zhang.pojo.Computer">
<property name="name" value="惠普"/>
<property name="cpu">
<!-- 在一个 bean 中再声明一个 bean 就是内部 bean -->
<!-- 内部 bean 可以直接用于给属性赋值,可以省略 id 和 name属性 -->
<bean class="com.zhang.pojo.CPU">
<property name="name" value="Intel"/>
</bean>
</property>
</bean>
1.2.4 特殊类型的注入以及特殊值处理
实体类如下
public class CollectionBean {
private List<String> list;
private Set<String> set;
private String[] arr;
private Map<String,String> map;
private Properties prop;
public CollectionBean(){}
public CollectionBean(List<String> list,Set<String> set){
this.list = list;
this.set = set;
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
public Set<String> getSet() {
return set;
}
public void setSet(Set<String> set) {
this.set = set;
}
public String[] getArr() {
return arr;
}
public void setArr(String[] arr) {
this.arr = arr;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
public Properties getProp() {
return prop;
}
public void setProp(Properties prop) {
this.prop = prop;
}
}
1.2.4.1 集合类型的属性注入
给List、Set、数组、Map、Properties等这些特殊类型进行注入。
<bean id="collectionBean" class="com.zhang.pojo.CollectionBean">
<!-- list标签:准备一组集合类型的数据,给集合属性赋值 -->
<property name="list">
<list>
<value>张三</value>
<value>李四</value>
<value>王麻子</value>
</list>
</property>
<!-- 使用array标签给数组属性赋值 -->
<property name="arr">
<array>
<value>张三</value>
<value>李四</value>
<value>王麻子</value>
</array>
</property>
<!-- 使用map标签给Map类型的属性赋值 -->
<property name="map">
<map>
<entry key="张三" value="123"/>
<entry key="李四" value="456"/>
<entry key="王五" value="789"/>
</map>
</property>
<!-- 使用set标签给集合属性赋值,只是附带了去重功能 -->
<property name="set">
<set>
<value>张三</value>
<value>李四</value>
<value>王麻子</value>
</set>
</property>
<!-- 可以使用props标签 -->
<property name="prop">
<props>
<prop key="张三">123</prop>
<prop key="李四">456</prop>
<prop key="王五">789</prop>
</props>
</property>
</bean>
1.2.4.2 赋null值
null标签:将一个属性值明确设置为null。
<bean id="bean" class="com.zhang.pojo.CollectionBean">
<constructor-arg index="0">
<!-- null标签:将一个属性值明确设置为null -->
<null/>
</constructor-arg>
<constructor-arg index="1">
<!-- null标签:将一个属性值明确设置为null -->
<null/>
</constructor-arg>
</bean>
1.2.4.3 赋特殊符号(<、>、>=、<=)
注意: 小于号在XML文档中用来定义标签的开始,不能随便使用,如果需要使用的话,需要使用XML实体来代替或者使用CDATA节。
CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据。XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析,因此CDATA节中写什么符号都随意。
实体类如下:
public class Expression {
private String expression;
public String getExpression() {
return expression;
}
public void setExpression(String expression) {
this.expression = expression;
}
}
<bean id="expression" class="com.zhang.pojo.Expression">
<property name="expression">
<value><![CDATA[ a < b]]></value>
</property>
</bean>
1.2.5 级联属性赋值
Spring 级联属性是当两个bean 关联时,从一个bean 给 另一个bean 赋值。
第一种写法:
<bean id="cpu" class="com.zhang.pojo.CPU">
</bean>
<bean id="computer" class="com.zhang.pojo.Computer">
<property name="name" value="惠普"/>
<property name="cpu" ref="cpu"/>
<!-- 对于Computer来说,cpu的name属性就是级联属性 -->
<property name="cpu.name" value="Inter"/>
</bean>
第二种写法:这种写法之前也提到过。
<bean id="cpu" class="com.zhang.pojo.CPU">
<property name="name" value="Inter"/>
</bean>
<bean id="computer" class="com.zhang.pojo.Computer">
<property name="name" value="惠普"/>
<property name="cpu" ref="cpu"/>
</bean>
1.2.6 引入外部属性文件
在pom.xml中加入依赖
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
创建外部属性文件
jdbc.url=jdbc:mysql://localhost:3306/testdb
jdbc.driver=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=123456
在spring.xml文件中引入外部文件,并且使用外部文件。
在这之前,先引入命名空间。
xmlns:context="http://www.springframework.org/schema/context"
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
测试
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
DataSource dataSource = applicationContext.getBean("dataSource", DataSource.class);
try {
System.out.println(dataSource.getConnection());
} catch (SQLException throwables) {
throwables.printStackTrace();
}
1.2.7 继承和抽象
可以通过继承来实现代码的复用,属性注入的复用
创建两个实体类:Person和Student
public class Person {
private String address;
private String sex;
private String school;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
}
public class Student { // 本身和Person 没有继承关系
private String address;
private String sex;
private String school;
private String name;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在spring.xml文件中配置
<!-- 模板,不希望被实例化,设置为抽象的-->
<bean id="person" class="com.zhang.pojo.Person" abstract="true">
<property name="address" value="北京"/>
<property name="school" value="北京邮电大学"/>
<property name="sex" value="男"/>
</bean>
<!-- 使用上面bean 中的属性,可以parent="person" 继承过来-->
<bean id="student" class="com.zhang.pojo.Student" parent="person">
<property name="name" value="张三"/>
</bean>
1.2.8 拆分
1.2.8.1 在 xml 中 import 导入其他的 xml
创建a.xml文件
在a.xml文件中编写bean
<bean id="cpu" class="com.zhang.pojo.CPU">
<property name="name" value="Inter"/>
</bean>
在spring.xml文件引入a.xml文件,并且使用a.xml文件中的bean。
<import resource="a.xml"/>
<bean class="com.zhang.pojo.Computer">
<property name="cpu" ref="cpu"/>
</bean>
1.2.8.2 在代码应用中,可以同时加载多个xml文件
String[] files = {"a.xml","spring.xml"};
ApplicationContext context = new ClassPathXmlApplicationContext(files);
1.2.8.3 容器之间继承
<bean name="computer" class="com.zhang.pojo.Computer">
<property name="cpu">
<!--parent:去父容器中查找-->
<ref parent="cpu"/>
</property>
</bean>
1.2.9 生命周期
1.2.9.1 bean的作用域
在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:
取值 | 含义 | 创建对象的时机 |
---|---|---|
singleton | 在IOC容器中,这个bean的对象始终为单实例,不管多少次getBean,只实例化一次 | IOC容器初始化时 |
prototype | 这个bean在IOC容器中有多个实例,每次getBean时,都会实例化 | 获取bean时 |
1.2.9.2 bean的生命周期
lazy-init="false" : 立即加载,创建上下文时,立即构造bean 对象
lazy-init="true" : 延时加载,在使用时,获取bean 时,才去实例加载
bean的生命周期清单
- bean对象创建(调用无参构造器)
- 给bean对象设置属性
- bean对象初始化之前操作(由bean的后置处理器负责)
- bean对象初始化(需在配置bean时指定初始化方法)
- bean对象初始化之后操作(由bean的后置处理器负责)
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法)
- IOC容器关闭
bean的初始化方法和销毁方法
public void init(){
System.out.println("初始化方法");
}
public void destroy(){
System.out.println("销毁方法");
}
使用init-method属性指定初始化方法
使用destroy-method属性指定销毁方法
<bean id="cpu" class="com.zhang.pojo.CPU" init-method="init" destroy-method="destroy">
<property name="name" value="Inter"/>
</bean>
bean的后置处理器
① 创建后置处理器
// 声明一个自定义的bean后置处理器
// 注意:bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
public class BeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("==== " + beanName + " = " + bean);
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("--- " + beanName + " = " + bean);
return null;
}
}
②将bean的后置处理器放入IOC容器
因为bean的后置处理器要放入IOC容器才能生效。
<bean id="beanProcessor" class="com.zhang.BeanProcessor"/>
1.3 基于注解管理Bean
注解的概念:和XML文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
但是追根本源:所有一切的操作都是由Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。
注意: 我们使用注解后,每一个组件必须保证有一个唯一的标识。
1.3.1 标记与扫描
扫描的概念:Spring要通过扫描包,才能知道开发人员在哪标记了注解,然后通过注解进行后面的操作。
1.3.1.1 创建四个常用组件类
使用@Component注解标记普通组件
import org.springframework.stereotype.Component;
@Component
public class CommonComponent {
}
使用@Repository注解标记持久化层组件
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao {
}
使用@Service注解标记业务逻辑组件
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
}
使用@Controller注解标记控制层组件
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
}
上面四个组件之间的对比:
我们可以点进去查看源码,我们会发现:@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。对于Spring的IOC容器来说,这三个组件本质上没有什么区别,而他们的的作用主要是来提高代码的可读性。
1.3.1.2 扫描的使用
最基本扫描的配置
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.zhang"/>
在配置扫描包的基础上指定匹配模式
<!-- resource-pattern属性:配置要扫描资源的正则表达式的 -->
<context:component-scan base-package="com.zhang" resource-pattern="**/User*.class"/>
在配置扫描包的基础上指定不需要扫描的包
<context:component-scan base-package="com.zhang">
<!--
context:exclude-filter标签:指定一些不需要扫描的包
type属性:指定根据什么来进行去除。例如:annotation取值是根据注解来排除。
expression属性:指定全类名,表示过滤器的表达式
-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
在配置扫描包的基础上指定其他需要的规则
<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.zhang" use-default-filters="false">
<!-- context:include-filter标签: 在配置扫描包的基础上添加其他需要的规则 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
注意:被组件标记后,默认情况下获取bean,就是类名的首字母小写。
如:UserServiceImpl类对应的bean的id或name属性就是 userServiceImpl。
当然,我们也可以自定义bean的名字。如果当注解中只有一个属性时,其value属性的属性名可以省略不写。
// @Service("service")
@Service(value = "service")
public class UserServiceImpl implements UserService {
}
1.3.2 自动装配
在成员变量上使用@Autowired注解来完成自动装配,不在需要setXxx()方法了。
注意:自动装配前必须要扫描所用到的所有组件,将其装入IOC容器。
在controller层装配Service
@Controller("controller")
public class UserController {
@Autowired
private UserService userService;
public void add(){
userService.add();
}
}
在service层装配Dao
@Service(value = "useService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void add() {
userDao.insert();
}
}
注意:@Autowired注解还可以标记在构造器和setXxx()方法上。
@Autowired注解的工作流程
注意: @Qualifier 注解和 @Autowired注解要搭配使用,可以只有@Autowired注解,但是不能只有 @Qualifier 注解。
① 首先到IOC容器中查找所需要的组件类型
如果能找到唯一的bean,则进行装配;
如果找不到这个bean,则装配失败;
如果有多个相同类型的bean的话
若有 @Qualifier 注解:则需要根据 @Qualifier 注解中指定的名称作为bean的id或name进行匹配
如果找到,则匹配成功;找不到,则匹配失败。
若没有 @Qualifier 注解:则需要按照被加了@Autowired注解的属性的名字进行匹配
如果找到,则匹配成功;找不到,则匹配失败。
提醒: @Autowired注解设置required = false属性表示:能装就装,装不上就不装。
1.3.3 完全注解开发
创建组件类:
@Component
public class CommonComponents {
}
创建配置类:
使用@Configuration注解将一个普通的类标记为Spring的配置类
@Configuration
// 配置自动扫描包
@ComponentScan("com.zhang")
public class MyConfiguration {
// @Bean注解相当于XML配置文件中的bean标签
// @Bean注解标记的方法的返回值会被放入IOC容器
@Bean(name = "abc")
public CommonComponents getComponent() {
CommonComponents commonComponent = new CommonComponents();
System.out.println("数码宝贝");
return commonComponent;
}
}
测试
public static void main(String[] args) {
// AnnotationConfigApplicationContext根据配置类创建IOC容器对象
ApplicationContext context= new AnnotationConfigApplicationContext(MyConfiguration.class);
context.getBean("abc");
}
1.3.4 整合junit4
向spring.xml文件中加入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.5</version>
</dependency>
注意:此依赖的版本号要与spring整体的依赖要保持一致,要不然会报错。针对于这个情况,我们以后可以在pom.xml文件中这样配置spring的相关依赖,可以更方便的管理spring相关的版本。
<properties>
<spring-version>5.3.5</spring-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-version}</version>
</dependency>
</dependencies>
junit的使用
// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(value = {"classpath:spring.xml"})
public class TestJunit {
@Autowired
private UserController userController;
@Test
public void test(){
userController.add();
}
}
使用junit的好处:
①:不需要之间创建IOC容器对象了。
②:任何的bean都可以在测试类中直接进行自动装配。
第三章 AOP
3.1 代理模式
3.1.1 代理模式的简介
代理模式是一种常用的Java设计模式。代理模式就是为一个对象提供一个代理类来扩展自己本身的功能。也就是说通过代理类来访问此对象中的目标方法。例如:广告商找明星拍广告需要通过经纪人。
下面是用图来描述一下。
按照代理的创建时期,代理类可以分为两种。
- 静态代理:静态代理的代理类在编译期间生成了.class文件。
- 动态代理:动态代理的代理类在Java运行时通过反射动态生成。
3.1.2 静态代理
编写接口
public interface IPlay {
void play(String gameName);
}
编写接口的实现类(被代理类)
public class Play implements IPlay{
@Override
public void play(String gameName) {
System.out.println();
System.out.println("我在玩" + gameName);
System.out.println();
}
}
编写静态代理类
public class PlayProxy implements IPlay{
// 将被代理对象声明为成员变量
private IPlay play;
public PlayProxy(IPlay play) {
this.play = play;
}
@Override
public void play(String gameName) {
System.out.println("调用被代理类方法之前做的准备工作");
// 调用被代理对象执行的目标方法
play.play(gameName);
System.out.println("调用被代理类方法完成");
}
}
测试:
public class Test {
public static void main(String[] args) {
IPlay play = new Play();
PlayProxy playProxy = new PlayProxy(play);
playProxy.play("王者");
}
}
3.1.3 动态代理
概念:根据代理类对象的接口,动态创建代理类对象
有两种方法实现动态代理:
- 通过jdk本身来实现动态代理
- 通过cglib实现动态代理
3.1.3.1 通过jdk实现动态代理
通过图来理解jdk实现的动态代理
实现步骤:
①:编写代理类,实现接口 InvocationHandler 用来实现回调
②:声明属性,用来接收所有类型对象(被代理类对象)
③:编写方法,根据被代理类对象动态创建代理类对象
④:重写 InvocationHandler 接口中的 invoke 方法,通过反射调用原有业务方法,做增强功能。
编写代理类
public class ProxyBean implements InvocationHandler {
// 声明被代理类对象
private Object biz;
public Object newProxyInstance(Object biz){
this.biz = biz;
// 创建代理类对象需要三个参数:
// ①:加载目标对象的类的类加载器
// ②:目标对象所实现的所有接口组成的数组
// ③:需要 InvocationHandler 对象,因为本类是 InvocationHandler接口的实现类,所以传入本类对象即可
return Proxy.newProxyInstance(biz.getClass().getClassLoader(), biz.getClass().getInterfaces(),this);
}
/**
*
* @param proxy 代理对象,但是这里未用到 proxy(代理对象)
* @param method 代表目标方法的Method对象
* @param args 外部调用目标方法时传入的实际参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法名为:" + method.getName());
// invoke方法中的第一个参数是调用目标方法的目标对象,即声明的成员变量。
// 第二个参数是外部调用目标方法时传入的参数。
// 声明这个变量来接收目标方法的返回值
Object targetValue = method.invoke(biz,args);
return targetValue;
}
}
测试
public class Test {
public static void main(String[] args) {
// 创建被代理的对象
IPlay plays = new Play();
// 创建代理类
ProxyBean proxyBean = new ProxyBean();
// 通过代理类来创建被代理类的对象
IPlay play = (IPlay)proxyBean.newProxyInstance(plays);
// 通过代理类的对象调用被代理类对象的方法
play.play("王者");
}
}
3.1.3.2 通过cglib实现动态代理
通过图来理解cglib实现的动态代理
注意: 在编写业务类的时候不需要实现接口了。
编写业务类
public class Play {
public void play(String gameName) {
System.out.println();
System.out.println("我在玩" + gameName);
System.out.println();
}
}
在pom.xml文化中加入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
编写代理类
public class CglibProxyBean implements MethodInterceptor {
/**
* 根据被代理对象,动态生成代理类对象(被代理类的子类)
* @param biz 传入被代理类对象
* @return
*/
public Object newProxyInstance(Object biz){
// 创建动态类对象
Enhancer enhancer = new Enhancer();
// 这个是将代理类对象设置为被代理类对象的子类
// 即代理类对象作为子类,被代理类对象作为父类
enhancer.setSuperclass(biz.getClass());
// 设置回调
enhancer.setCallback(this);
return enhancer.create();
}
/**
*
* @param o 代理对象
* @param method 被代理对象的目标方法
* @param objects 调用被代理方法时传入的参数
* @param methodProxy 生成的代理类
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("========前置增强========");
System.out.println(method.getName());
Object targetValue = methodProxy.invokeSuper(o, objects);
System.out.println("========后置增强========");
return targetValue;
}
}
测试
public class Test {
public static void main(String[] args) {
IPlay play = new Play();
CglibProxyBean cglibProxyBean = new CglibProxyBean();
play = (IPlay)cglibProxyBean.newProxyInstance(play);
play.play("王者");
}
}
3.1.3.3 jdk和cglib实现动态代理的区别
①:实现方式不同
jdk实现动态代理必须依赖接口。
cglib实现动态代理是通过类来实现,即代理类要作为被代理类的子类(代理类继承被代理类)。
②:实现原理不同
jdk的动态代理是利用反射机制生成一个实现代理接口的代理类,在调用具体方法前调用InvokeHandler方法来处理。
cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
③:性能不同
jdk1.6和1.7的时候,jdk实现动态代理要比cglib实现动态代理的速度慢。
jdk1.8的时候,jdk实现动态代理要比cglib实现动态代理的速度快。
关于使用上的注意点:
如果被代理类实现了接口,则Spring会默认使用jdk动态代理。
如果被代理类未实现接口,则Spring要采用cglib动态代理。
如果被代理类实现了接口,Spring也可以强制使用cglib动态代理。
如果被代理类被final修饰,则不能采用cglib动态代理。
3.2 AOP的核心套路
3.3 AOP术语
3.3.1 切面
概念:把所有通知方法封装到一个类中。
3.3.2 横切关注点
概念:从每个方法中抽取出来的同一类非核心业务。在未改变原有功能的基础上,额外加了几个功能,就有几个横切关注点。
下面用图来可以更好理解。
3.3.2 连接点
概念:将方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴方向,x轴和y轴的相交点就是连接点。即执行通知的方法。
下面用图表示:
3.3.3 切入点
切入点也可以这样理解:在所有的方法中都可以使用增强通知,但你只需要其中的一部分的方法使用增强通知,其余的方法不需要使用增强通知,因此这些部分方法使用了增强通知的地方叫做切入点。
注意:AOP技术是通过切入点来定位连接点。
下面用图来理解:
3.3.4 增强通知
在每一个横切关注点上要做的事情需要写一个方法来实现,这样的方法就叫增强通知。
- 前置通知:在被代理的目标方法前执行。
- 后置通知:在被代理的目标方法结束后执行。
- 异常通知:在被代理的目标方法执行过程中抛出异常时执行。
- 最终通知:在被代理的目标方法执行的过程中,不管是否抛出异常,都会执行。
- 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知。
3.3.5 织入
将通知方法(切面)应用到被代理对象(目标对象)中。
3.4 AOP的使用
在pom.xml文件中导入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
3.4.1 切入点表达式的语法
execution(public * com.zhang..*.*(..))
execution()是最常用的切点函数,其语法如下所示:
整个表达式可以分为五个部分:
①:execution():表达式主体。
②:第一个 * 号:表示返回类型, * 表示所有的类型。
③:包名: 表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包。
④:第二个 * 号:表示类名, * 号表示所有的类。
⑤:*(…):最后这个 * 号表示方法名,* 号表示所有的方法,后面括号里面表示方法的参数,两个句点表示任何参数。
注意点:
- 首先要特别注意 * 后是否有空格,有无空格的含义相差很大。
- 用 * 号代替 权限修饰符和返回值部分表示权限修饰符和返回值不限。
- 在包名的部分,一个 * 号只能代表包的层次结构中的一层,表示这一层是任意的。
- 在包名的部分,使用 . . 表示包名任意、包的层次深度任意。
- 在类名的部分,类各部分整体用 * 代替,表示类名任意。
- 在类名的部分,可以使用 * 号代替类名的一部分。
- 在方法名部分,可以用 * 号表示方法名任意。
- 在方法名部分,可以用 * 号代替方法名的一部分。
- 在方法参数列表部分,使用 (…) 表示参数列表任意。
- 在方法参数列表部分,使用 (int , …) 表示参数列表以一个int类型的参数开头。
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的。
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符。
- public * 表示权限修饰符明确,返回值任意是可以的。
例子:
①:表示匹配所有名称以Dao结尾的类或接口
public * com.zhang.*Dao..*.*(..)
②:表示匹配所有名称以Test结尾的方法
public * com.zhang.Dao..*Test.*(..)
③:表示匹配所有方法中第一个参数类型为int的方法
public * com.zhang.Dao..*.*(int,..)
④:表示匹配所有方法的返回值类型为int的方法
public int com.zhang.Dao..*.*(..)
3.4.2 通过代理实现AOP
五个通知(增强类):
通知 | 执行时间 | 实现方式 |
---|---|---|
前置通知 | 在运行目标方法之前执行的 | 实现MethodBeforeAdvice接口,并重写before()方法 |
后置通知 | 在运行目标方法之后执行的,如果运行目标方法过程中出现异常,则不会执行后置通知 | 实现AfterReturningAdvice接口,并重写afterReturning()方法 |
异常通知 | 在运行目标方法时出现异常执行的 | 实现ThrowsAdvice接口,无重写方法 |
环绕通知 | 在环绕通知中可以定义前置通知、后置通知、异常通知、最终通知 | 实现MethodInterceptor接口,并重写invoke()方法; |
最终通知 | 运行完方法后,不管是否发生异常,都会执行 | 无 |
AdviceBefore(前置通知)
public class AdviceBefore implements MethodBeforeAdvice {
private Logger logger = Logger.getLogger(AdviceBefore.class);
/**
* 功能: 日期+类+方法+参数
* @param method 将来调用的方法
* @param objects 方法的传入参数
* @param o 目标对象
* @throws Throwable
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
logger.info("---------前置通知开始----------");
logger.info(new Date() + ": " + o.getClass().getName() + "." + method.getName());
logger.info("参数有: " + Arrays.toString(objects));
logger.info("---------前置通知完成---------");
}
}
AfterReturningAdvice(后置通知)
public class AdviceAfter implements AfterReturningAdvice{
private Logger logger = Logger.getLogger(AdviceBefore.class);
/**
* 功能: 日期+类+方法+参数
* @param o 方法的返回值
* @param method 将来调用的方法
* @param objects 方法的传入参数
* @param o1 目标对象
* @throws Throwable
*/
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
logger.info("---------后置通知开始----------");
logger.info(new Date() + ": " + o1.getClass().getName() + "." + method.getName());
logger.info("返回值:" + o);
logger.info("参数有: " + Arrays.toString(objects));
logger.info("---------后置通知完成---------");
}
}
ThrowsAdvice(异常通知)
public class AdviceThrows implements ThrowsAdvice {
private Logger logger = Logger.getLogger(AdviceThrows.class);
/**
* @param method 将来调用的方法
* @param args 方法的传入参数
* @param target 目标对象
* @param e
*/
public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
logger.info("---------异常通知开始----------");
logger.info(new Date() + ": " + target.getClass().getName() + "." + method.getName());
logger.info("参数有: " + Arrays.toString(args));
logger.info("异常信息为:" + e.getMessage());
logger.info("---------异常通知完成---------");
}
}
MethodInterceptor(环绕通知)
public class AdviceAround implements MethodInterceptor {
private Logger logger = Logger.getLogger(AdviceBefore.class);
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
logger.info("---------环绕通知开始----------");
try {
logger.info("全类名为:" + methodInvocation.getThis().getClass().getName());
// 获取方法
Method method = methodInvocation.getMethod();
logger.info("方法名:" + method.getName());
// 获取参数
Object[] arguments = methodInvocation.getArguments();
logger.info("参数有:" + Arrays.toString(arguments));
// 必须要使用methodInvocation.proceed(),可以决定业务类方法执行的位置
logger.info("返回值为:" + methodInvocation.proceed());
} catch (Throwable throwable) {
logger.info("异常信息为:" + throwable.getMessage());
} finally {
logger.info("环绕通知中的最终通知");
}
logger.info("---------环绕通知完成---------");
return null;
}
}
编写业务类
// 业务的接口
public interface IPlay {
void play(String gameName);
}
// 实现业务的接口
public class Play implements IPlay{
@Override
public void play(String gameName) {
System.out.println();
// int i = 10 / 0;
System.out.println("我在玩" + gameName);
System.out.println();
}
}
配置spring.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 -->
<bean id="play" class="com.zhang.test.Play"/>
<!-- 前置通知 -->
<bean id="adviceBefore" class="com.zhang.aop.AdviceBefore"/>
<!-- 后置通知 -->
<bean id="adviceAfter" class="com.zhang.aop.AdviceAfter"/>
<!-- 异常通知 -->
<bean id="adviceThrow" class="com.zhang.aop.AdviceThrows"/>
<!-- 环绕通知 -->
<bean id="adviceAround" class="com.zhang.aop.AdviceAround"/>
<!-- 代理bean -->
<bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- target属性:指定要代理的目标类-->
<property name="target" ref="play"/>
<!-- proxyInterfaces属性:指定要代理的接口(即代理要实现的接口)-->
<property name="proxyInterfaces" value="com.zhang.test.IPlay"/>
<!-- proxyTargetClass属性:如果value为true,则使用cglib;如果value为false,则使用jdk动态代理-->
<property name="proxyTargetClass" value="true"/>
<!-- interceptorNames属性:指定要在代理的目标类中添加的功能-->
<property name="interceptorNames">
<list>
<list>
<!-- 一般情况配置了环绕通知就不需要配置前置通知、异常通知、后置通知了 -->
<value>adviceAround</value>
<value>adviceThrow</value>
<!--<value>adviceBefore</value>
<value>adviceAfter</value>-->
</list>
</list>
</property>
</bean>
</beans>
3.4.3 通过SpringAPI接口实现AOP
与通过代理实现AOP对比,它俩不同的是spring.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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 业务bean -->
<bean id="play" class="com.zhang.test.Play"/>
<!-- 前置通知 -->
<bean id="adviceBefore" class="com.zhang.aop.AdviceBefore"/>
<!-- 后置通知 -->
<bean id="adviceAfter" class="com.zhang.aop.AdviceAfter"/>
<!-- 异常通知 -->
<bean id="adviceThrow" class="com.zhang.aop.AdviceThrows"/>
<!-- 环绕通知 -->
<bean id="adviceAround" class="com.zhang.aop.AdviceAround"/>
<!-- 配置aop,并且导入aop的命名空间-->
<aop:config>
<!-- 切入点(连接点的集合) 连接点 执行通知的方法 -->
<aop:pointcut expression="execution(public * com.zhang.test..*.*(..))" id="pointcut"/>
<!--
定义AOP通知器,这是一种特殊的aspect;
其advice-ref属性是用来配置通知
pointcut-ref属性是用来配置切点
-->
<!-- <aop:advisor advice-ref="adviceBefore" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="adviceAfter" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="adviceThrow" pointcut-ref="pointcut"/>-->
<aop:advisor advice-ref="adviceAround" pointcut-ref="pointcut"/>
</aop:config>
</beans>
3.4.4 通过自定义类实现AOP
自定义的LogAdvice类:
注意:在通知方法的形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入。通过JoinPoint对象就可以获取目标方法名、实参数列表等。
public class LogAdvice {
private Logger logger = Logger.getLogger(LogAdvice.class);
/**
* 前置通知
* @param joinPoint
*/
public void before(JoinPoint joinPoint){
logger.info("****************前置增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
// getSignature()可以获取目标方法签名对象,即一个方法的全部声明信息
// 通过方法的签名对象获取目标方法的详细信息
logger.info("方法名为:" + joinPoint.getSignature().getName());
// 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
logger.info("****************前置增强处理结束**************");
}
/**
* 后置通知
* @param joinPoint
*/
public void after(JoinPoint joinPoint){
logger.info("****************最终增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
logger.info("方法名为:" + joinPoint.getSignature().getName());
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
logger.info("****************最终增强处理结束**************");
}
/**
* 最终通知
* @param joinPoint
* @param val
*/
public void afterReturning(JoinPoint joinPoint,Object val){
logger.info("****************后置增强处理开始**************");
logger.info(val);
logger.info("****************后置增强处理结束**************");
}
/**
* 异常通知
* @param joinPoint
* @param exception
*/
public void except(JoinPoint joinPoint,Exception exception){
logger.info("****************异常增强处理开始**************");
logger.info(exception.getMessage());
logger.info("****************异常增强处理结束**************");
}
/**
* 环绕通知
* @param joinPoint
*/
public void around(ProceedingJoinPoint joinPoint){
logger.info("****************环绕增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
logger.info("方法名为:" + joinPoint.getSignature().getName());
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
try {
logger.info("****************前置增强处理**************");
// 原有业务执行
Object returnVal = joinPoint.proceed();
logger.info("****************后置增强处理**************" + " 返回值:" + returnVal);
} catch (Exception e) {
e.getMessage();
logger.info("****************异常增强处理**************");
} catch (Throwable throwable) {
throwable.printStackTrace();
logger.info(throwable.getMessage());
} finally {
logger.info("****************最终增强处理**************");
}
logger.info("****************环绕增强处理结束**************");
}
}
编写spring.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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 业务bean -->
<bean id="play" class="com.zhang.test.Play"/>
<!-- 配置切面类的bean -->
<bean id="logAdvice" class="com.zhang.aop.LogAdvice"/>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pointcut" expression="execution(public * com.zhang.test..*.*(..))"/>
<!-- aop:aspect标签:配置切面 -->
<!-- ref属性:关联切面类的bean -->
<aop:aspect ref="logAdvice">
<!-- aop:before标签: 配置前置通知 -->
<!-- method属性:指定前置通知的方法名 -->
<!-- pointcut-ref属性:引用切入点表达式 -->
<aop:before method="before" pointcut-ref="pointcut"/>
<!-- aop:after标签:配置最终通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<!-- aop:after-returning标签:配置后置通知 -->
<!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="val"/>
<!-- aop:after-throwing标签:配置异常通知 -->
<!-- throwing属性:指定的通知方法中用来接收目标方法抛出异常的异常对象的参数名-->
<aop:after-throwing method="except" throwing="exception" pointcut-ref="pointcut"/>
<!-- aop:around标签:配置环绕通知-->
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
3.4.5 通过注解实现AOP
在spring.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"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.zhang.test"/>
<!-- 启用基于注解的AOP功能 -->
<aop:aspectj-autoproxy/>
</beans>
编写业务类
跟之前不同的是: 使用注解实现时不需要实现接口。
public class Play{
public void play(String gameName) {
System.out.println();
// int i = 10 / 0;
System.out.println("我在玩" + gameName);
System.out.println();
}
}
自定义通知类添加注解
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAdvice {
private Logger logger = Logger.getLogger(LogAdvice.class);
// 切入点表达式重用
@Pointcut("execution(public * com.zhang.test..*.*(..))")
public void myPoint(){}
// @Before注解:声明当前方法是前置通知方法
// value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
// 第一种方法是写切入点表达式
// @Before(value = "execution(public * com.zhang.test..*.*(..))")
// 第二种方法是使用已经定义好的切入点表达式,
// 注意:使用切入点表达式不仅可以使用本类中定义好的,也可以使用其他类中的。
@Before(value = "myPoint()")
public void before(JoinPoint joinPoint){
logger.info("****************前置增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
logger.info("方法名为:" + joinPoint.getSignature().getName());
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
logger.info("****************前置增强处理结束**************");
}
// @After注解标记最终通知方法
//@After(value = "execution(public * com.zhang.test..*.*(..))")
@Before(value = "myPoint()")
public void after(JoinPoint joinPoint){
logger.info("****************最终增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
logger.info("方法名为:" + joinPoint.getSignature().getName());
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
logger.info("****************最终增强处理结束**************");
}
// @AfterReturning注解标记后置通知方法
// 注意:要在@AfterReturning注解中通过returning属性设置形参名称,
// 并且要在通知方法中声明一个对应的形参,
// 其方法中形参的名称要与returning属性设置的名称要相对应,Spring会将目标方法的返回值从这里传给我们
@AfterReturning(value = "execution(public * com.zhang.test..*.*(..))",returning = "val")
public void afterReturning(JoinPoint joinPoint,Object val){
logger.info("****************后置增强处理开始**************");
logger.info(val);
logger.info("****************后置增强处理结束**************");
}
// @AfterThrowing注解标记异常通知方法
// 在@AfterThrowing注解中声明一个throwing属性设置形参名称
// 并且在通知方法中声明一个对应的形参,
// 其方法中的形参名称就是在throwing属性中所设置的,
// Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(value = "execution(public * com.zhang.test..*.*(..))",throwing = "exception")
public void except(JoinPoint joinPoint,Exception exception){
logger.info("****************异常增强处理开始**************");
logger.info(exception.getMessage());
logger.info("****************异常增强处理结束**************");
}
// 使用@Around注解标明环绕通知方法
// 在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型对象传给我们
// @Around(value = "execution(public * com.zhang.test..*.*(..))")
public void around(ProceedingJoinPoint joinPoint){
logger.info("****************环绕增强处理开始**************");
logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
// 通过getSignature()方法获取签名对象,然后通过签名对象获取目标方法的方法名
logger.info("方法名为:" + joinPoint.getSignature().getName());
// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
try {
logger.info("****************前置增强处理**************");
// 通过ProceedingJoinPoint对象让原有业务执行
// 声明变量用来存储目标方法的返回值
Object returnVal = joinPoint.proceed();
logger.info("****************后置增强处理**************" + " 返回值:" + returnVal);
} catch (Exception e) {
e.getMessage();
logger.info("****************异常增强处理**************");
} catch (Throwable throwable) {
throwable.printStackTrace();
logger.info(throwable.getMessage());
} finally {
logger.info("****************最终增强处理**************");
}
logger.info("****************环绕增强处理结束**************");
}
}
第四章 声明式事务
4.1 JDBCTemplate
Spring提供的数据访问模板,提供了很多方便操作数据库的方法,例如JDBCTemplate。
4.1.1 实现步骤
在pom.xml文件中添加依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
编写实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Integer bookid;
private String bookname;
private Float price;
private String author;
private Date pubDate;
}
编写Dao接口
public interface BookDao {
List<Book> queryAll();
}
编写使用Dao接口的类
@Repository
public class BookDao implements IBookDao {
@Autowired
private JdbcTemplate template;
@Override
public List<Book> queryAll() {
String sql = "select bookid,bookname,pubDate,price,author from t_book";
return template.query(sql,new BookRowMapper());
}
class BookRowMapper implements RowMapper<Book>{
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book book = new Book();
book.setBookid(rs.getInt("bookid"));
book.setBookname(rs.getString("bookname"));
book.setPrice(rs.getFloat("price"));
book.setAuthor(rs.getString("author"));
book.setPubDate(rs.getDate("pubDate"));
return book;
}
}
}
编写spring-conf.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="com.zhang"/>
<bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/testdb">
</property>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver">
</property>
</bean>
<bean name="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
编写测试类
@Test
public void test1(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
bookDao.queryAll().forEach(System.out::println);
}
4.1.2 基本使用
4.1.2.1 增删改查
IBookDao接口中
public interface IBookDao {
String get(Integer bookid);
void save(Book book);
Integer update(Book book);
Integer delete(Integer bookid);
}
IBookDao接口的实现类中
@Repository
public class BookDao implements IBookDao {
@Autowired
private JdbcTemplate template;
// 根据bookid查询图书名字
@Override
public String get(Integer bookid) {
String sql = "select bookname from t_book where bookid = ?";
return template.queryForObject(sql,String.class,1);
}
// 添加图书
@Override
public void save(Book book) {
String sql = "insert into t_book (bookname,pubDate,price,author) values (?,?,?,?)";
template.update(sql,book.getBookname(),book.getPubDate(),book.getPrice(),book.getAuthor());
}
// 更新图书
@Override
public Integer update(Book book) {
String sql = "update t_book set bookname = ? where bookid = ?";
return template.update(sql,book.getBookname(),book.getBookid());
}
// 根据id删除图书
@Override
public Integer delete(Integer bookid) {
String sql = "delete from t_book where bookid = ?";
return template.update(sql,bookid);
}
}
测试类
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
System.out.println(bookDao.get(1));
}
@Test
public void test3() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
bookDao.save(new Book(null,"C++",65f,"王五",new Date()));
}
@Test
public void test4() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
Book book = new Book();
book.setBookid(1);
book.setBookname("Spring");
bookDao.update(book);
}
@Test
public void test5() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
bookDao.delete(2);
}
4.1.2.2 使用BeanPropertyRowMapper类进行查询
在IBookDao的接口中
public interface IBookDao {
List<Book> queryAll();
}
在IBookDao接口的实现类中
@Repository
public class BookDao implements IBookDao {
@Autowired
private JdbcTemplate template;
@Override
public List<Book> queryAll() {
String sql = "select bookid,bookname,pubDate,price,author from t_book";
return template.query(sql,new BeanPropertyRowMapper<>(Book.class));
}
}
4.1.2.3 进行批量插入
在IBookDao的接口中
public interface IBookDao {
void batchInsert(List<Book> list);
}
在IBookDao接口的实现类中
@Repository
public class BookDao implements IBookDao {
@Autowired
private JdbcTemplate template;
@Override
public void batchInsert(List<Book> list) {
String sql = "insert into t_book (bookname,price,author,pubDate)values(?,?,?,?)";
List<Object[]> params = new ArrayList<>();
for (Book book : list) {
Object[] param = {book.getBookname(),book.getPrice(),book.getAuthor(),book.getPubDate()};
params.add(param);
}
template.batchUpdate(sql, params);
}
}
测试类:
@Test
public void test6() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
List<Book> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Book book = new Book();
book.setBookname("Java" + i);
book.setPubDate(new Date());
book.setAuthor("王五" + i);
book.setPrice(65 + 1f);
list.add(book);
}
bookDao.batchUpdate(list);
}
4.2 声明式事务概念
4.2.1 编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
缺陷:需要开发人员手动来完成一些细节上的操作,如果开发人员不能将一些代码有效的抽取出来,代码就不能得到高效利用,每次实现功能时都需要自己手动编写额外的代码。
4.2.2 声明式事务
由于编程式事务的缺陷,我们可以使用框架将固定模式的代码抽取出来,进行相关的封装。此时,我们只需要在配置文件中进行简单的配置即可完成操作。
好处:
①:提高了开发效率
②:消除了冗余的代码
声明式事务和编程式事务的区别:
- 编程式:自己写代码完成功能
- 声明式:通过配置让框架实现功能
4.2.3 事务管理器
我们在Spring使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,之后整合MyBatis用的也是这个类。
DataSourceTransactionManager类中的方法:
我们可以看到事务的一些主要方法:
- doBegin():开启事务
- doSuspend():挂起事务
- doResume():恢复挂起的事务
- doCommit():提交事务
- doRollback():回滚事务
4.3 基于注解的声明式事务
4.3.1 准备工作
这里我们还是用 JDBCTemplate部分 准备工作的数据,再次之外,我们还会加入业务逻辑层,在业务逻辑层中使用事务。
下来会测试Spring在默认情况下的事务是否会生效。
准备 IBookDao接口中添加方法
Integer updateMoney(Integer bookid,String money);
实现IBookDao接口,因为这里是在做测试,所以将下面的Sql语句写错,为了便于测试默认情况下事务是否会生效。
@Override
public Integer updateMoney(Integer bookid,String money) {
// 这里故意将sql语句写错。
String sql = "update t1_book set price = ? where bookid = ?";
return template.update(sql,money,bookid);
}
编写IBookService接口
void updateTwice(Integer bookid,String bookName,String money);
实现IBookService接口
@Autowired
private IBookDao bookDao;
@Override
public void updateTwice(Integer bookid, String bookName, String money) {
Book book = new Book();
book.setBookid(bookid);
book.setBookname(bookName);
// 第一次修改书名
bookDao.update(book);
// 第二次修改金额
bookDao.updateMoney(bookid,money);
}
添加日志依赖
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
配置logback.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- 指定日志输出的位置 -->
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志输出的格式 -->
<!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
</encoder>
</appender>
<!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
<!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
<root level="INFO">
<!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
<appender-ref ref="STDOUT" />
</root>
<!-- 根据特殊需求指定局部日志级别 -->
<logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="DEBUG"/>
<logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" />
</configuration>
最终结果: 名字修改成功了,但是书的价格修改失败。因此在默认情况下,事务是不会开启的。
4.3.2 添加事务
4.3.2.1 在spring-conf.xml文件中配置事务管理器
<!-- 配置事务管理器 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
<property name="dataSource" ref="dataSource"/>
</bean>
4.3.2.2 开启基于注解的声明式事务功能
<!-- 开启基于注解的声明式事务功能 -->
<!-- 使用transaction-manager属性指定当前使用是事务管理器的bean -->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
这里给大家提醒一下,导入<tx:annotation-driven />这个的命名空间时要注意一下,千万不能导错,否则就会无用。
xmlns:tx="http://www.springframework.org/schema/tx"
4.3.2.3 在需要事务的方法上使用注解
@Autowired
private IBookDao bookDao;
@Transactional
public void updateTwice(Integer bookid, String bookName, Float money) {
Book book = new Book();
book.setBookid(bookid);
book.setBookname(bookName);
// 第一次修改书名
bookDao.update(book);
// 第二次修改金额
bookDao.updateMoney(bookid,money);
}
4.3.2.4 补充:也可以在需要事务的类上使用@Transactional注解
注意: @Transactional注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的@Transactional注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了@Transactional注解。
例:在BookService类上设置了@Transactional注解 readOnly 属性为 true,那么在这个类中有查询的方法都不需要设置@Transactional注解了,而增删改方法需要设置@Transactional注解 readOnly 属性为 false。
@Transactional(readOnly = true)
@Service
public class BookService implements IBookService {
@Autowired
private IBookDao bookDao;
@Transactional(readOnly = false)
@Override
public void updateTwice(Integer bookid, String bookName, Float money) {
Book book = new Book();
book.setBookid(bookid);
book.setBookname(bookName);
// 第一次修改书名
bookDao.update(book);
// 第二次修改金额
bookDao.updateMoney(bookid,money);
}
@Override
public String getName(Integer bookid) {
return bookDao.get(bookid);
}
}
最终结果: 在配置了事务之后,如果修改数据的途中发生了错误,则会出现事务的回滚,即数据修改失败。
4.3.3 设置事务的只读属性
概述:对于一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
设置方式:在业务层所需要的方法上设置此属性。
提醒:从这里开始我们都是在方法上设置@Transactional注解,不在类上声明。
@Autowired
private IBookDao bookDao;
// readOnly = true 把当前事务设置为只读。
@Transactional(readOnly = true)
@Override
public String getName(Integer bookid) {
return bookDao.get(bookid);
}
注意:只读属性只适用于查询操作,如果是增删改操作,会报异常。
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
4.3.4 设置事务的超时属性
概述:事务在执行的过程中,会因为某些问题导致程序卡住,从而长时间占用数据库资源。那么此时出现问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
也就是说:超时回滚,释放资源。
设置方式:
在业务逻辑层中的方法上设置@Transactional注解的 timeout属性,让其中的IBookDao实现类的update方法睡眠,这里设置的休眠时间必须要比 timeout属性设置的时间要长。
@Autowired
private IBookDao bookDao;
@Transactional(timeout = 3)
@Override
public void updateTwice(Integer bookid, String bookName, Float money) {
Book book = new Book();
book.setBookid(bookid);
book.setBookname(bookName);
// 第一次修改书名
bookDao.update(book);
// 第二次修改金额
bookDao.updateMoney(bookid,money);
}
下面sleep方法必须要放在执行SQL语句之前,要不然就不会起作用
@Override
public Integer update(Book book) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
String sql = "update t_book set bookname = ? where bookid = ?";
return template.update(sql,book.getBookname(),book.getBookid());
}
测试时出现的异常信息:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Thu Aug 11 16:45:38 CST 2022
4.3.5 设置事务的回滚和不回滚的异常属性
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
4.3.5.1 默认情况
默认情况下对于运行时异常是回滚的,编译时异常不回滚。
@Override
public Integer update(Book book) throws FileNotFoundException{
String sql = "update t_book set bookname = ? where bookid = ?";
int update = template.update(sql, book.getBookname(), book.getBookid());
// 抛出编译时异常测试是否回滚 === 不回滚
new FileInputStream("abcd");
// 抛出运行时异常测试是否回滚 == 回滚
// System.out.println(10/0);
return update;
}
4.3.5.2 设置回滚的异常
设置形式:在@Transactional设置rollbackFor 属性
在出现编译时异常的情况下设置回滚属性。
它这个原本是不回滚的,设置之后是回滚的。
@Autowired
private IBookDao bookDao;
@Transactional(rollbackFor = Exception.class)
@Override
public Integer update(Book book) throws FileNotFoundException{
int update = bookDao.update(book);
new FileInputStream("abcd");
return update;
}
4.3.5.3 设置不回滚的异常
设置形式:在@Transactional设置noRollbackFor 属性
在出现运行时异常的情况下设置不回滚属性。
它这个原本是回滚的,设置之后是不回滚的。
@Autowired
private IBookDao bookDao;
@Transactional(noRollbackFor = Exception.class)
@Override
public Integer update(Book book) throws FileNotFoundException{
int update = bookDao.update(book);
System.out.println(10/0);
return update;
}
4.3.5.4 同时设置回滚和不回滚异常
①:范围不同
不管是哪个设置范围大,都是在大范围内在排除小范围的设定。
例如:rollbackFor = Exception.class ; noRollbackFor = FileNotFoundException.class ;
意思是除了 FileNotFoundException 之外,其他所有 Exception 范围的异常都回滚;但是碰到 FileNotFoundException 不回滚。此时小范围的优先级大于大范围的优先级。
②:范围一致
回滚和不回滚的异常设置了相同范围。
例如:rollbackFor = Exception.class ; noRollbackFor = Exception.class ;
那么此时Spring会采纳 rollbackFor 设置的属性,遇到此范围内的异常会回滚。
4.3.6 设置事务的隔离级别属性
准备工作:
3.6.1 测试读未提交
业务层的方法:
@Autowired
private IBookDao bookDao;
@Transactional(readOnly = true,isolation = Isolation.READ_UNCOMMITTED)
@Override
public String getName(Integer bookid) {
String name = bookDao.get(bookid);
return name; // 这里打一个断点
}
@Transactional(readOnly = false,isolation = Isolation.READ_UNCOMMITTED)
@Override
public Integer updateName(Book book) throws FileNotFoundException{
int update = bookDao.updateName(book);
System.out.println(10/0);
return update; // 这里打一个断点
}
测试方法: 这里需要启动两个debug界面。运行之后就可以看到 getName()方法取的书名。
@Test
public void test10() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookService bookService = context.getBean("bookService", IBookService.class);
Book book = new Book();
book.setBookid(1);
book.setBookname("Java9997878");
bookService.updateName(book);
}
@Test
public void test11() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookService bookService = context.getBean("bookService", IBookService.class);
bookService.getName(1);
}
测试结果:执行查询操作的事务,它读取了另一个尚未提交的数据。
3.6.2 测试读已提交
业务层的方法:
@Autowired
private IBookDao bookDao;
@Transactional(readOnly = true,isolation = Isolation.READ_COMMITTED)
@Override
public String getName(Integer bookid) {
String name = bookDao.get(bookid);
return name; // 这里打一个断点
}
@Transactional(readOnly = false,isolation = Isolation.READ_COMMITTED,,noRollbackFor = Exception.class)
@Override
public Integer updateName(Book book) throws FileNotFoundException{
int update = bookDao.updateName(book);
System.out.println(10/0);
return update; // 这里打一个断点
}
测试方法: 这里需要启动两个debug界面。运行之后就可以看到 getName()方法取的书名。
@Test
public void test10() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookService bookService = context.getBean("bookService", IBookService.class);
Book book = new Book();
book.setBookid(1);
book.setBookname("Java9997878");
bookService.updateName(book);
}
@Test
public void test11() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
IBookService bookService = context.getBean("bookService", IBookService.class);
bookService.getName(1);
}
测试结果: 执行查询操作的事务读取的是数据库中正确的数据。
4.3.7 设置事务的传播行为属性
用图可能会更好理解。
4.3.7.1 propagation属性
名称 | 含义 |
---|---|
REQUIRED 默认值 | 当前方法必须工作在事务中; 如果当前线程上有已经开启的事务可用,那么就在这个事务中运行;如果当前线程上没有已经开启的事务,那么就自己开启新事务,在新事务中运行;所以当前方法有可能和其他方法共用事务;在共用事务的情况下:当前方法会因为其他方法回滚而受连累 |
REQUIRES_NEW 建议使用 | 当前方法必须工作在事务中 ;不管当前线程上是否有已经开启的事务,都要开启新事务; 在新事务中运行;不会和其他方法共用事务,避免被其他方法连累 |
4.3.7.2 测试
4.3.7.2.1 测试REQUIRED 模式
IBookService接口
public Integer updateName(Book book);
public Integer updateMoney(Book book);
实现IBookService接口的类
@Transactional(readOnly = false,propagation = Propagation.REQUIRED)
@Override
public Integer updateName(Book book) {
int count = bookDao.updateName(book);
System.out.println(10/0);
return count;
}
@Transactional(readOnly = false,propagation = Propagation.REQUIRED)
@Override
public Integer updateMoney(Book book) {
Integer count = bookDao.updateMoney(book.getBookid(), book.getPrice());
return count;
}
编写TestService的类
@Service
public class TestService {
@Autowired
private IBookService bookService;
@Transactional
public void topTxMethod(){
Book book = new Book();
book.setBookid(4);
book.setPrice(99f);
bookService.updateMoney(book);
book.setBookname("张五");
bookService.updateName(book);
}
}
编写测试类
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
TestService bookService = context.getBean("testService", TestService.class);
bookService.topTxMethod();
}
事务的执行结果
[18:44:41.809] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Creating new transaction with name [com.zhang.service.impl.TopService.topTxMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
[18:44:44.128] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] for JDBC transaction]
[18:44:44.132] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] to manual commit]
[18:44:44.155] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateMoney]]
[18:44:44.177] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] for JDBC transaction]
[18:44:44.177] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] to manual commit]
[18:44:44.180] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[18:44:44.182] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set price = ? where bookid = ?]]
[18:44:44.235] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction commit]
[18:44:44.235] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234]]
[18:44:44.243] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] after transaction]
[18:44:44.245] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[18:44:44.247] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateName]]
[18:44:44.267] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] for JDBC transaction]
[18:44:44.267] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] to manual commit]
[18:44:44.271] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[18:44:44.271] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set bookname = ? where bookid = ?]]
[18:44:44.277] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[18:44:44.278] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6]]
[18:44:44.282] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] after transaction]
[18:44:44.283] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[18:44:44.283] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[18:44:44.284] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd]]
[18:44:44.285] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] after transaction]
总结:
调用BookService层的两个方法都没有修改值,因此总事务回滚了。
注意:不能用try-catch处理异常,如果使用了try-catch,不管有没有捕获到异常,总事务总是提交。
4.3.7.2.2 测试REQUIRES_NEW模式
实现IBookService接口的类
@Transactional(readOnly = false,propagation = Propagation.REQUIRES_NEW)
@Override
public Integer updateName(Book book) {
int count = bookDao.updateName(book);
System.out.println(10/0);
return count;
}
@Transactional(readOnly = false,propagation = Propagation.REQUIRES_NEW)
@Override
public Integer updateMoney(Book book) {
Integer count = bookDao.updateMoney(book.getBookid(), book.getPrice());
return count;
}
编写TestService的类
@Service
public class TestService {
@Autowired
private IBookService bookService;
@Transactional
public void topTxMethod(){
Book book = new Book();
book.setBookid(4);
book.setPrice(99f);
bookService.updateMoney(book);
book.setBookname("张五");
bookService.updateName(book);
}
}
编写测试类
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
TestService bookService = context.getBean("testService", TestService.class);
bookService.topTxMethod();
}
事务的执行结果
[20:02:15.051] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Creating new transaction with name [com.zhang.service.impl.TopService.topTxMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
[20:02:17.687] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] for JDBC transaction]
[20:02:17.692] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] to manual commit]
[20:02:17.717] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateMoney]]
[20:02:17.740] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] for JDBC transaction]
[20:02:17.740] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] to manual commit]
[20:02:17.742] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[20:02:17.743] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set price = ? where bookid = ?]]
[20:02:17.811] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction commit]
[20:02:17.811] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234]]
[20:02:17.813] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] after transaction]
[20:02:17.815] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[20:02:17.816] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateName]]
[20:02:17.839] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] for JDBC transaction]
[20:02:17.839] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] to manual commit]
[20:02:17.844] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[20:02:17.844] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set bookname = ? where bookid = ?]]
[20:02:17.849] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[20:02:17.849] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6]]
[20:02:17.855] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] after transaction]
[20:02:17.856] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[20:02:17.857] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[20:02:17.857] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd]]
[20:02:17.858] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] after transaction]
总结:
如果在事务提交前未发生异常,则修改成功;如果在事务提交之前有异常发生,事务回滚。注意:这里是执行一条SQL语句,事务提交一次。
注意:不能用try-catch处理异常,如果使用了try-catch,不管有没有捕获到异常,总事务总是提交。
4.4 基于XML的声明式事务
4.4.1 在pom.xml文件中加入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
4.4.2 将之前的注解全部去掉,并且修改Spring-conf.xml的配置文件
<aop:config>
<!-- 配置切入点表达式,将事务功能定位到具体方法上 -->
<aop:pointcut id="txPoint" expression="execution(* com.zhang..*Service.*(..))"/>
<!-- 将事务通知和切入点表达式关联起来 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- tx:method标签:配置具体的事务方法 -->
<!-- name属性:指定方法名,可以使用星号代表多个字符 -->
<tx:method name="query*" read-only="true"/>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
4.4.3 测试
@Override
public Integer updateName(Book book) {
int update = bookDao.updateName(book);
System.out.println(10/0);
return update;
}
结果:它没有发生回滚。说明设置事务成功!