Spring基础知识

Spring基础知识
Silence核心容器
前面已经完成bean与依赖注入的相关知识学习,接下来我们主要学习的是IOC容器中的核心容器。
这里所说的核心容器,我们可以把它简单的理解为ApplicationContext,前面虽然已经用到过,但是并没有系统的学习,接下来我们从以下几个问题入手来学习下容器的相关知识:
- 如何创建容器?
- 创建好容器后,如何从容器中获取bean对象?
- 容器类的层次结构是什么?
- BeanFactory是什么?
环境准备
- 创建一个Maven项目
- pom.xml添加Spring的依赖
1
2
3
4
5
6
7
8
9
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies> - resources下添加applicationContext.xml
1
2
3
4
5
6
7
8
9
10
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
</beans> - 添加BookDao和BookDaoImpl类
1 | public interface BookDao { |
1 | public class BookDaoImpl implements BookDao { |
- 创建运行APP类
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
}
}
容器
容器的创建方式
- 案例中创建ApplicationContext的方式如下:
- 这种方式翻译为:类路径下的XML配置文件
1
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
- 除了上面这种方式,Spring还提供了另外一种创建方式
- 这种方式翻译为:文件系统下的XML配置文件,路径需要写绝对路径
- 这种方式虽能实现,但是当项目的位置发生变化后,代码也需要跟着改,耦合度高,不推荐使用。
1
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\xxx/xxx\applicationContext.xml");
获取bean的三种方式
- 方式一,就是我们之前用的方式
- 这种方式存在的问题是每次获取的时候都需要进行类型转换,有没有更简单的方式呢?
1
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
- 方式二
这种方式可以解决类型强转问题,但是参数又多加了一个,相对来说没有简化多少。1
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
- 方式三
- 这种方式就类似我们之前所学习依赖注入中的按类型注入。必须要确保IOC容器中该类型对应的bean对象只能有一个。
1
BookDao bookDao = ctx.getBean(BookDao.class);
BeanFactory的使用
容器的最上级的父接口为BeanFactory
使用BeanFactory也可以创建IOC容器
1 | public class AppForBeanFactory { |
为了更好的看出BeanFactory和ApplicationContext之间的区别,在BookDaoImpl添加如下构造函数
1 | public class BookDaoImpl implements BookDao { |
如果不去获取bean对象,打印会发现:
- BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建
- ApplicationContext是立即加载,容器加载的时候就会创建bean对象
- ApplicationContext要想成为延迟加载,只需要将lazy-init设为true
1
2
3
4
5
6
7<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl" lazy-init="true"/>
</beans>
核心容器总结
容器相关
- BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
- ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载
- ApplicationContext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
- ApplicationContext接口常用初始化类
- ClassPathXmlApplicationContext(常用)
- FileSystemXmlApplicationContext
bean相关
IOC/DI注解开发
Spring的IOC/DI对应的配置开发就已经讲解完成,但是使用起来相对来说还是比较复杂的,复杂的地方在配置文件。
Spring到底是如何简化代码开发的呢?
要想真正简化开发,就需要用到Spring的注解开发,Spring对注解支持的版本历程:
- 2.0版开始支持注解
- 2.5版注解功能趋于完善
- 3.0版支持纯注解开发
关于注解开发,这里会讲解两块内容注解开发定义bean和纯注解开发。
注解开发定义bean用的是2.5版提供的注解,纯注解开发用的是3.0版提供的注解
环境准备
创建一个Maven项目
pom.xml添加Spring的依赖
1
2
3
4
5
6
7<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>resources下添加applicationContext.xml
1
2
3
4
5
6
7
8
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
</beans>添加BookDao、BookDaoImpl、BookService、BookServiceImpl类
1
2
3public interface BookDao {
public void save();
}1
2
3
4
5public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
}1
2
3public interface BookService {
public void save();
}1
2
3
4
5public class BookServiceImpl implements BookService {
public void save() {
System.out.println("book service save ...");
}
}创建运行类App
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
}
}
注解开发定义bean
- 步骤一:删除原有的XML配置
将配置文件中的bean标签删除掉
1
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
- 步骤二:在Dao上添加注解
在BookDaoImpl类上添加@Component注解
1
2
3
4
5
6
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}注意:@Component注解不可以添加在接口上,因为接口是无法创建对象的。
- 步骤三:配置Spring的注解包扫描
为了让Spring框架能够扫描到写在类上的注解,需要在配置文件上进行包扫描
1
2
3
4
5
6
7
8
9
10
11
12
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<context:component-scan base-package="com.blog"/>
</beans> - 说明:component-scan
- component:组件,Spring将管理的bean视作自己的一个组件
- scan:扫描
base-package指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。 - 包路径越多如 :com.blog.dao.impl,扫描的范围越小速度越快
- 包路径越少如:com.blog,扫描的范围越大速度越慢
- 一般扫描到项目的组织名称即Maven的groupId下如 :com.blog即可。
- 步骤四:运行程序 book dao save …
- 步骤五:Service上添加注解
在BookServiceImpl类上也添加@Component交给Spring框架管理
1
2
3
4
5
6
public class BookServiceImpl implements BookService {
public void save() {
System.out.println("book service save ...");
}
} - 步骤六:运行程序
在App类中,从IOC容器中获取BookServiceImpl对应的bean对象结果
1
2
3
4
5
6
7
8
9
10
11public class App {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//按照名称获取bean
BookDao bookDao = (BookDao) context.getBean("bookDao");
//按照类型获取bean
BookService bookService = context.getBean(BookService.class);
bookDao.save();
bookService.save();
}
}
book dao save …
book service save …
说明:
- BookServiceImpl类没有起名称,所以在App中是按照类型来获取bean对象
- @Component注解如果不起名称,会有一个默认值就是当前类名首字母小写,所以也可以按照名称获取,如
对于@Component注解,还衍生出了其他三个注解 @Controller、@Service、@Repository
1
BookService bookService = (BookService) context.getBean("bookServiceImpl");
通过查看源码会发现:这三个注解和@Component注解的作用是一样的,为什么要衍生出这三个呢?
这是方便我们后期在编写类的时候能很好的区分出这个类是属于表现层、业务层还是 数据层的类。
纯注解开发模式
思路分析
实现思路为:
- 将配置文件applicationContext.xml删掉,用类来替换
实现步骤
- 步骤一:创建配置类
创建一个配置类SpringConfig
1
2public class SpringConfig {
} - 步骤二:标识该类为配置类
在配置类上面加一个@Configuration注解,将其标识为一个配置类,用于替换掉applicationContext.xml
1
2
3
public class SpringConfig {
} - 步骤三:用注解替换包扫描配置
在配置类上添加包扫描注解@ComponentScan替换<context:component-scan base-package=””/>
注意:添加的扫描包不要直接放在java包,要在java包下面 例如Test 亲测java包会找不到Bean
1
2
3
4
public class SpringConfig {
} - 步骤四:创建运行类
1
2
3
4
5
6
7
8
9public class AppForAnnotation {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) context.getBean("bookDao");
bookDao.save();
BookService bookService = context.getBean(BookService.class);
bookService.save();
}
}
运行AppForAnnotation,可以看到两个对象依然被获取成功
book dao save …
book service save …
至此,纯注解开发的方式就已经完成了,主要内容包括:
- Java类替换Spring核心配置文件
- @Configuration注解用于设定当前类为配置类
- @ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
1
- 读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象 | 名称 | @Configuration |
1
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
| —— | —————————– |
| 类型 | 类注解 |
| 位置 | 类定义上方 |
| 作用 | 设置该类为Spring配置类 |
| 属性 | value(默认):定义bean的id |
- 知识点:@ComponentScan
名称 @ComponentScan
类型 类注解
位置 类定义上方
作用 设置spring配置类扫描路径,用于加载使用注解格式定义的bean
属性 value(默认):扫描路径,此路径可以逐层向下扫描
小结
这部分要重点掌握的是使用注解完成Spring的bean管理,需要掌握的内容为:
- 记住@Component,@Controller、@Service、@Repository这四个注解
- applicationContext.xml中context:component-san/的作用是指定扫描包路径,注解为@ComponentScan
- @Configuration标识该类为配置类,使用类替换applicationContext.xml文件
- ClassPathXmlApplicationContext是加载XML配置文件
- AnnotationConfigApplicationContext是加载配置类
注解开发bean的作用范围和生命周期
使用注解已经完成了bean的管理,接下来按照前面所学习的内容,将通过配置实现的内容都换成对应的注解实现,包含两部分内容:bean作用范围(scope)和bean生命周期(init和destroy)。
bean的作用范围
修改AppForAnnotation类,并运行查看结果
1 | public class AppForAnnotation { |
结果:
com.blog.dao.impl.BookDaoImpl@77e4c80f
com.blog.dao.impl.BookDaoImpl@77e4c80f
- 要想将BookDaoImpl变成非单例,只需要在其类上添加@scope注解
1
2
3
4
5
6
7
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
} - 运行结果:
com.blog.dao.impl.BookDaoImpl@176d53b2
com.blog.dao.impl.BookDaoImpl@971d0d8
知识点:@scope
名称 @Scope
类型 类注解
位置 类定义上方
作用 设置该类创建对象的作用范围,可用于设置创建出的bean是否为单例对象
属性 value(默认):定义bean作用范围,默认值singleton(单例),可选值prototype(非单例)
bean的生命周期
在BookDaoImpl中添加两个方法,init和destroy,方法名可以任意,再添加一个构造方法
复制1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("construct ... ");
}
public void save() {
System.out.println("book dao save ...");
}
public void init() {
System.out.println("init ... ");
}
public void destroy() {
System.out.println("destroy ... ");
}
}如何对方法进行标识,哪个是初始化方法,哪个是销毁方法?
只需要在对应的方法上添加@PostConstruct和@PreDestroy注解即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("construct ... ");
}
public void save() {
System.out.println("book dao save ...");
}
// 在构造方法之后执行,替换 init-method
public void init() {
System.out.println("init ... ");
}
// 在销毁方法之前执行,替换 destroy-method
public void destroy() {
System.out.println("destroy ... ");
}
}要想看到两个方法执行,需要注意的是destroy只有在容器关闭的时候,才会执行,所以需要修改App的类
1
2
3
4
5
6
7
8public class AppForAnnotation {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) context.getBean("bookDao");
System.out.println(bookDao);
context.registerShutdownHook();//关闭容器
}
}运行AppForAnnotation类查看打印结果,证明init和destroy方法都被执行了,且init确实是在构造方法后执行的。
construct …
init …
com.blog.dao.impl.BookDaoImpl@971d0d8
destroy …
注意:JDK8版本以上,如果找不到@PostConstruct和@PreDestroy注解,需要导入下面的jar包
1 | <dependency> |
找不到的原因是,从JDK9以后jdk中的javax.annotation包被移除了,这两个注解刚好就在这个包中。
知识点@PostConstruct
名称 @PostConstruct
类型 方法注解
位置 方法上
作用 设置该方法为初始化方法
属性 无
知识点PreDestroy
名称 @PreDestroy
类型 方法注解
位置 方法上
作用 设置该方法为销毁方法
属性 无
小结
配置文件中的bean标签中的
id对应@Component(“”),@Controller(“”),@Service(“”),@Repository(“”)
scope对应@scope()
init-method对应@PostConstruct
destroy-method对应@PreDestroy
注解开发依赖注入
环境准备
创建一个Maven项目
pom.xml添加Spring的依赖
1
2
3
4
5
6
7
8
9
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>添加一个配置类SpringConfig
1
2
3
4
public class SpringConfig {
}添加BookDao、BookDaoImpl、BookService、BookServiceImpl类
1
2
3public interface BookDao {
public void save();
}1
2
3
4
5
6
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
}1
2
3public interface BookService {
public void save();
}1
2
3
4
5
6
7
8
9
10
11
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}创建运行类AppForAnnotation
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookService bookService = ctx.getBean(BookService.class);
bookService.save();
}
}环境准备好后,直接运行App类会有问题,因为还没有提供配置注入BookDao的,所以bookDao对象为Null,调用其save方法就会报空指针异常。
注解实现按照类型注入
对于这个问题使用注解该如何解决?
- 在BookServiceImpl类的bookDao属性上添加@Autowired注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BookServiceImpl implements BookService {
private BookDao bookDao;
// public void setBookDao(BookDao bookDao) {
// this.bookDao = bookDao;
// }
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}注意:
- @Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是写在属性上并将setter方法删除掉
- 为什么setter方法可以删除呢?
- 自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
- 普通反射只能获取public修饰的内容
- 暴力反射除了获取public修饰的内容还可以获取private修改的内容
- 所以此处无需提供setter方法
- @Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是写在属性上并将setter方法删除掉
- @Autowired是按照类型注入,那么对应BookDao接口如果有多个实现类,比如添加BookDaoImpl2
1
2
3
4
5
6
public class BookDaoImpl2 implements BookDao {
public void save() {
System.out.println("book dao save ...2");
}
}
这个时候再次运行App,就会报错NoUniqueBeanDefinitionException
此时,按照类型注入就无法区分到底注入哪个对象,解决方案:按照名称注入
- 先给两个Dao类分别起个名称
1
2
3
4
5
6
7
8
9
10
11
12
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
}
public class BookDaoImpl2 implements BookDao {
public void save() {
System.out.println("book dao save ...2" );
}
}
此时就可以注入成功,但是得思考个问题:
- @Autowired是按照类型注入的,给BookDao的两个实现起了名称,它还是有两个bean对象,为什么不报错?
- @Autowired默认按照类型自动装配,如果IOC容器中同类的Bean找到多个,就按照变量名和Bean的名称匹配。因为变量名叫bookDao而容器中也有一个booDao,所以可以成功注入。
- 那下面这种情况可以成功注入吗还是不行的,因为按照类型会找到多个bean对象,此时会按照bookDao名称去找,因为IOC容器只有名称叫bookDao1和bookDao2,所以找不到,会报NoUniqueBeanDefinitionException
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
}
public class BookDaoImpl2 implements BookDao {
public void save() {
System.out.println("book dao save ...2" );
}
}
注解实现按照名称注入
当根据类型在容器中找到多个bean,注入参数的属性名又和容器中bean的名称不一致,这个时候该如何解决,就需要使用到@Qualifier来指定注入哪个名称的bean对象。@Qualifier注解后的值就是需要注入的bean的名称。
1
2
3
4
5
6
7
8
9
10
11
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
注意:
注意:@Qualifier不能独立使用,必须和@Autowired一起使用
简单数据类型注入
- 引用类型看完,简单类型注入就比较容易懂了。简单类型注入的是基本数据类型或者字符串类型,下面在BookDaoImpl类中添加一个name属性,用其进行简单类型注入
1
2
3
4
5
6
7
public class BookDaoImpl implements BookDao {
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
} - 数据类型换了,对应的注解也要跟着换,这次使用@Value注解,将值写入注解的参数中就行了
1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
注意数据格式要匹配,如将”abc”注入给int值,这样程序就会报错。
介绍完后,会有一种感觉就是这个注解好像没什么用,跟直接赋值是一个效果,还没有直接赋值简单,所以这个注解存在的意义是什么?继续往下看
注解读取properties配置文件
@Value一般会被用在从properties配置文件中读取内容进行使用,具体如何实现?
- 步骤一:在resource下准备一个properties文件
1
name=Stephen
- 步骤二:步骤二:使用注解加载properties配置文件
在配置类上添加@PropertySource注解1
2
3
4
5
public class SpringConfig {
} - 步骤三:使用@Value读取配置文件中的内容
1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
} - 步骤四:运行程序
运行App类,查看运行结果,说明配置文件中的内容已经被加载
book service save …
book dao save …Stephen
注意:
- 如果读取的properties配置文件有多个,可以使用@PropertySource的属性来指定多个
1
- @PropertySource注解属性中不支持使用通配符*,运行会报错
1
- @PropertySource注解属性中可以把classpath:加上,代表从当前项目的根路径找文件
1
知识点1:@Autowired
名称 @Autowired
类型 属性注解 或 方法注解(了解) 或 方法形参注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方 或 方法形参前面
作用 为引用类型属性设置值
属性 required:true/false,定义该属性是否允许为null
知识点2:@Qualifier
名称 @Qualifier
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为引用类型属性指定注入的beanId
属性 value(默认):设置注入的beanId
知识点3:@Value
名称 @Value
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准set方法上方 或 类set方法上方
作用 为 基本数据类型 或 字符串类型 属性设置值
属性 value(默认):要注入的属性值
知识点4:@PropertySource
名称 @PropertySource
类型 类注解
位置 类定义上方
作用 加载properties文件中的属性值
属性 value(默认):设置加载的properties文件对应的文件名或文件名组成的数组
IOC/DI注解开发管理第三方bean
前面定义bean的时候都是在自己开发的类上面写个注解就完成了,但如果是第三方的类,这些类都是在jar包中,我们没有办法在类上面添加注解,这个时候该怎么办?
遇到上述问题,我们就需要有一种更加灵活的方式来定义bean,这种方式不能在原始代码上面书写注解,一样能定义bean,这就用到了一个全新的注解@Bean。
环境准备
学习@Bean注解之前,我们先来准备一个环境
- 创建一个Maven项目
- 在pom.xml中添加Spring依赖
1
2
3
4
5
6
7<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies> - 添加一个配置类SpringConfig
1
2
3
public class SpringConfig {
} - 添加BookDao、BookDaoImpl类
1
2
3
4
5
6
7
8
9public interface BookDao {
public void save();
}
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ..." );
}
} - 创建运行类App
1
2
3
4
5public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
}
}
注解开发管理第三方bean
在上述环境中完成对Druid数据源的管理,具体的实现步骤为(Druid数据源中包含了jdbc-connection 不需要我们单独配置)
步骤一:导入对应的jar包
1
2
3
4
5<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>步骤二:在配置类中添加一个方法
注意该方法的返回值就是要创建的Bean对象类型1
2
3
4
5
6
7
8
9
10
11
public class SpringConfig {
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("PASSWORD");
return dataSource;
}
}步骤三:在方法上添加@Bean注解
@Bean注解的作用是将方法的返回值作为一个Spring管理的bean对象1
2
3
4
5
6
7
8
9
10
11
12
public class SpringConfig {
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("PASSWORD");
return dataSource;
}
}步骤四:从IOC容器中获取对象并打印
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
DataSource dataSource = ctx.getBean(DataSource.class);
System.out.println(dataSource);
}
}输出结果如下:
{
CreateTime:“2022-09-02 10:36:33”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}至此使用@Bean来管理第三方bean的案例就已经完成。
如果有多个bean要被Spring管理,直接在配置类中多写几个方法,方法上添加@Bean注解即可。
引入外部配置类
如果把所有的第三方bean都配置到Spring的配置类SpringConfig中,虽然可以,但是不利于代码阅读和分类管理,所有我们就想能不能按照类别将这些bean配置到不同的配置类中?
那么对于数据源的bean,我们可以把它的配置单独放倒一个JdbcConfig类中
1
2
3
4
5
6
7
8
9
10
11 public class JdbcConfig {
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("PASSWORD");
return dataSource;
}
}
那现在又有了一个新问题,这个配置类如何能被Spring配置类加载到,并创建DataSource对象在IOC容器中?
针对这个问题,有两个解决方案:
使用包扫描引入
- 步骤一:在Spring的配置类上添加包扫描
注意要将JdbcConfig类放在包扫描的地址下1
2
3
4
public class SpringConfig {
} - 步骤二:在JdbcConfig上添加@Configuration注解
JdbcConfig类要放入到com.blog.config包下,需要被Spring的配置类扫描到即可1
2
3
4
5
6
7
8
9
10
11
12
public class JdbcConfig {
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
} - 步骤三:运行程序
仍然可以获取到bean对象并输出到控制台
{
CreateTime:“2022-09-02 10:52:50”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
这种方式虽然能够扫描到,但是不能很快的知晓都引入了哪些配置类(因为把包下的所有配置类都扫描了),所有这种方式不推荐使用。
使用@Import引入
方案一实现起来有点小复杂,Spring早就想到了这一点,于是又给我们提供了第二种方案。
这种方案可以不用加@Configuration注解,但是必须在Spring配置类上使用@Import注解手动引入需要加载的配置类
步骤一:去除JdbcConfig类上的注解
1
2
3
4
5
6
7
8
9
10
11public class JdbcConfig {
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("PASSWORD");
return dataSource;
}
}步骤二:在Spring配置类中引入
1
2
3
4
public class SpringConfig {
}注意:
- 扫描注解可以移除
- @Import参数需要的是一个数组,可以引入多个配置类。
- @Import注解在配置类中只能写一次
步骤三:运行程序
依然能获取到bean对象并打印控制台
{
CreateTime:“2022-09-02 11:02:12”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
知识点1:@Bean
名称 @Bean
类型 方法注解
位置 方法定义上方
作用 设置该方法的返回值作为spring管理的bean
属性 value(默认):定义bean的id
知识点2:@Import
名称 @Import
类型 类注解
位置 类定义上方
作用 导入配置类
属性 value(默认):定义导入的配置类类名,
当配置类有多个时使用数组格式一次性导入多个配置类
注解开发实现为第三方bean注入资源
在使用@Bean创建bean对象的时候,如果方法在创建的过程中需要其他资源该怎么办?
这些资源会有两大类,分别是简单数据类型 和引用数据类型。
简单数据类型
对于下面代码关于数据库的四要素不应该写死在代码中,应该是从properties配置文件中读取。如何来优化下面的代码?
1 | public class JdbcConfig { |
步骤一:提供对应的四个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}步骤二:使用@Value注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}扩展
现在的数据库连接四要素还是写在代码中,需要做的是将这些内容提取到jdbc.properties配置文件,在上面我们已经实现过了,这里再来复习一遍
resources目录下添加jdbc.properties
配置文件中提供四个键值对分别是数据库的四要素
1
2
3
4jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:13306/spring_db
jdbc.username=root
jdbc.password=PASSWORD.这个properties 放到resources目录下,这样就可以在代码中通过@Value注解来读取配置文件中的内容了。
使用@PropertySource加载jdbc.properties配置文件
修改@Value注解属性的值,将其修改为${key},key就是键值对中的键的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
{
CreateTime:“2022-09-02 11:13:45”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
引用数据类型
假设在构建DataSource对象的时候,需要用到BookDao对象,该如何把BookDao对象注入进方法内让其使用呢?
- 步骤一:在SpringConfig中扫描BookDao
扫描的目的是让Spring能管理到BookDao,也就是要让IOC容器中有一个BookDao对象
1 |
|
- 步骤二:在JdbcConfig类的方法上添加参数
引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象。1
2
3
4
5
6
7
8
9
10
public DataSource dataSource(BookDao bookDao) {
bookDao.save();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
} - 步骤三:运行程序
结果如下,说明bookDao已经成功注入
book dao save …
{
CreateTime:“2022-09-02 11:29:54”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
注解开发总结
占位
Spring整合
Spring整合MyBatis
环境准备
步骤一:准备数据库表
MyBatis是用来操作数据库表的,所以我们先来创建库和表1
2
3
4
5
6
7
8
9
10
11
12create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
INSERT INTO tbl_account(`name`,money) VALUES
('Tom',2800),
('Jerry',3000),
('Jhon',3100);步骤二:创建项目导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>步骤三:根据表创建模型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47public class Account {
private Integer id;
private String name;
private Double money;
public Account() {
}
public Account(Integer id, String name, double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}步骤四:创建Dao接口(在之前是Mapper接口,且要配置一个对应的xml文件,不过这里没涉及到复杂的sql语句,所以没配置xml文件,采用注解开发)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface AccountDao {
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}步骤五:创建Service接口和实现类
1
2
3
4
5
6
7
8
9
10
11public interface AccountService {
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public void save(Account account) {
accountDao.save(account);
}
public void delete(Integer id) {
accountDao.delete(id);
}
public void update(Account account) {
accountDao.update(account);
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findById(Integer id) {
return accountDao.findById(id);
}
}步骤六:添加jdbc.properties文件
1
2
3
4jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db
jdbc.username=root
jdbc.password=PASSWORD.步骤七:添加Mybatis核心配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<configuration>
<!--读取外部properties配置文件-->
<properties resource="jdbc.properties"></properties>
<!--别名扫描的包路径-->
<typeAliases>
<package name="com.blog.domain"/>
</typeAliases>
<!--数据源-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!--映射文件扫描包路径-->
<mappers>
<package name="com.blog.dao"></package>
</mappers>
</configuration>步骤八:编写应用程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class App {
public static void main(String[] args) throws IOException {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 加载mybatis-config.xml配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 3. 创建SqlSessionFactory对象
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(inputStream);
// 4. 获取SqlSession
SqlSession sqlSession = factory.openSession();
// 5. 获取mapper
AccountDao mapper = sqlSession.getMapper(AccountDao.class);
//6. 执行方法进行查询
Account account = mapper.findById(2);
System.out.println(account);
//7. 释放资源
sqlSession.close();
}
}步骤九:运行程序,结果如下
Account{id=2, name=‘Jerry’, money=3000.0}
思路分析
Mybatis的基础环境我们已经准备好了,接下来就得分析下在上述的内容中,哪些对象可以交给Spring来管理?
*
Mybatis程序核心对象分析
从图中可以获取到,真正需要交给Spring管理的是SqlSessionFactory
- 整合Mybatis,就是将Mybatis用到的内容交给Spring管理,分析下配置文件
说明:
第一部分读取外部properties配置文件,Spring有提供具体的解决方案@PropertySource,需要交给Spring
第二部分起别名包扫描,为SqlSessionFactory服务的,需要交给Spring
第三部分主要用于做连接池,Spring之前我们已经整合了Druid连接池,这块也需要交给Spring
前面三部分一起都是为了创建SqlSession对象用的,那么用Spring管理SqlSession对象吗?回忆下SqlSession是由SqlSessionFactory创建出来的,所以只需要将SqlSessionFactory交给Spring管理即可。
第四部分是Mapper接口和映射文件[如果使用注解就没有该映射文件],这个是在获取到SqlSession以后执行具体操作的时候用,所以它和SqlSessionFactory创建的时机都不在同一个时间,可能需要单独管理。
整合步骤
前面我们已经分析了Spring与Mybatis的整合,大体需要做两件事,
第一件事是:Spring要管理MyBatis中的SqlSessionFactory第二件事是:Spring要管理Mapper接口的扫描
步骤一:项目中导入整合需要的jar包
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>步骤二:创建Spring的主配置类
1
2
3
4
5
6//配置类注解
//包扫描,主要扫描的是项目中的AccountServiceImpl类
public class SpringConfig {
}步骤三:创建数据源的配置类
在配置类中完成数据源的创建1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}步骤四:主配置类中读properties并引入数据源配置类
1
2
3
4
5
6
public class SpringConfig {
}步骤五:创建Mybatis配置类并配置SqlSessionFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class MyBatisConfig {
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
//设置模型类的别名扫描
sqlSessionFactory.setTypeAliasesPackage("com.blog.domain");
//设置数据源
sqlSessionFactory.setDataSource(dataSource);
return sqlSessionFactory;
}
//定义bean,返回MapperScannerConfigurer对象
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.blog.dao");
return msc;
}
}
说明:
- 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息
- SqlSessionFactoryBean是前面我们讲解FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,我们只需要将其需要的内容设置即可。
- 方法中有一个参数为dataSource,当前Spring容器中已经创建了Druid数据源,类型刚好是DataSource类型,此时在初始化SqlSessionFactoryBean这个对象的时候,发现需要使用DataSource对象,而容器中刚好有这么一个对象,就自动加载了DruidDataSource对象。
- sqlSessionFactory.setTypeAliasesPackage(“com.blog.domain”);,替换掉配置文件中的
1
2
3<typeAliases>
<package name="com.blog.domain"/>
</typeAliases> - sqlSessionFactory.setDataSource(dataSource);,替换掉配置文件中的 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中
1
2
3
4
5
6
7
8
9
10
11
12<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
这个MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类
MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径
步骤六:主配置类中引入Mybatis配置类
1
2
3
4
5
6
public class SpringConfig {
}步骤七:编写运行类
在运行类中,从IOC容器中获取Service对象,调用方法获取结果1
2
3
4
5
6
7
8public class App {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = context.getBean(AccountService.class);
Account account = accountService.findById(1);
System.out.println(account);
}
}步骤八:运行程序
Account{id=1, name=‘Tom’, money=2800.0}
至此,Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:
SqlSessionFactoryBean
MapperScannerConfigurer
Spring整合JUnit
- 步骤一:引入依赖
1
2
3
4
5
6
7
8
9
10
11
12<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency> - 步骤二:编写测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//设置类运行器,这个是4.0版本默认的,固定写法
//设置Spring环境对应的配置类
//加载配置类
public class AccountServiceTest {
//支持自动装配注入bean
private AccountService accountService;
public void test(){
Account account = accountService.findById(1);
System.out.println(account);
}
public void selectAll(){
List<Account> accounts = accountService.findAll();
System.out.println(accounts);
}
}注意:
* 单元测试,如果测试的是注解配置类,则使用<span class='p red'>@ContextConfiguration(classes = 配置类.class)</span> * 单元测试,如果测试的是配置文件,则使用<span class='p red'>@ContextConfiguration(locations={配置文件名</span> * Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西<span class='p red'>SpringJUnit4ClassRunner</span> * 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象,下面的工作就和以前做Junit单元测试完全一样了
知识点1:@RunWith
名称 @RunWith
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit运行器
属性 value(默认):运行所使用的运行期
知识点2:@ContextConfiguration
名称 @ContextConfiguration
类型 测试类注解
位置 测试类定义上方
作用 设置JUnit加载的Spring核心配置
属性 classes:核心配置类,可以使用数组的格式设定加载多个配置类
locations:配置文件,可以使用数组的格式设定加载多个配置文件名称
AOP简介
什么是AOP?
- AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式,指导开发者如何组织程序结构
- OOP(Object Oriented Programming)面向对象编程
我们都知道OOP是一种编程思想,那么AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式。
AOP作用
- 作用:在不惊动原始设计的基础上为其进行功能增强
AOP核心概念
为了能更好的理解AOP的相关概念,我们准备了一个环境,整个环境的内容我们暂时可以不用关注,最主要的类为:BookDaoImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
代码的内容很简单,就是测试一下万次执行的耗时
当在App类中从容器中获取bookDao对象后,分别执行其save和select方法后会有如下的打印结果
book dao save …
book dao save …
book dao save …
book dao save …
book dao save …
book dao save …
执行万次消耗时间:79ms
book dao delete …
book dao delete …
book dao delete …
book dao delete …
book dao delete …
book dao delete …
执行万次消耗时间:81ms
book dao update …
book dao update …
book dao update …
book dao update …
book dao update …
book dao update …
执行万次消耗时间:63ms
book dao select …
对于计算万次执行消耗的时间只有save方法有,为什么delete和update方法也会有呢?
delete和update方法有,那什么select方法为什么又没有呢?
这个案例中其实就使用了Spring的AOP,在不惊动(改动)原有设计(代码)的前提下,想给谁添加额外功能就给谁添加。这个也就是Spring的理念:
- 无入侵式/无侵入式
说了这么多,Spring到底是如何实现的呢?
- 前面一直在强调,Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。对于上面的案例中BookServiceImpl中有save,这些方法我们给起了一个名字叫连接点
- 在BookServiceImpl的四个方法中,update和delete只有打印没有计算万次执行消耗时间,但是在运行的时候已经有该功能,那也就是说update和delete方法都已经被增强,所以对于需要增强的方法我们给起了一个名字叫切入点
- 执行BookServiceImpl的update和delete方法的时候都被添加了一个计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,我们给起了个名字叫通知
- 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们给起了个名字叫切面
- 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类
至此AOP中的核心概念就已经介绍完了,总结下:
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在SpringAOP中,理解为方法的执行
- 切入点(Pointcut):匹配连接点的式子
- 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 一个具体的方法:如com.blog.dao包下的BookDao接口中的无形参无返回值的save方法
- 匹配多个方法:所有的save方法/所有的get开头的方法/所有以Dao结尾的接口中的任意方法/所有带有一个参数的方法
- 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
- 通知(Advice):在切入点处执行的操作,也就是共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知类:定义通知的类
- 切面(Aspect):描述通知与切入点的对应关系。
小结
这部分需要掌握的内容是
什么是AOP?
AOP的作用是什么?
AOP中核心概念分别指的是什么?
连接点
切入点
通知
通知类
切面
AOP入门案例
需求分析
案例设定:测算接口执行效率,但是这个案例稍微复杂了点,我们对其进行简化。
简化设定:在方法执行前输出当前系统时间。
那现在我们使用SpringAOP的注解方式完成在方法执行的前打印出当前系统时间。
思路分析
- 导入坐标
- 制作连接点(原始操作,Dao接口及实现类)
- 制作共性功能
- 定义切入点
- 绑定切入点和通知的关系
环境准备
创建一个Maven项目
添加依赖
1
2
3
4
5<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>添加BookDao和BookDaoImpl类
1
2
3
4public interface BookDao {
public void save();
public void update();
}1
2
3
4
5
6
7
8
9
10
11
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update() {
System.out.println("book dao update ...");
}
}创建Spring配置类
1
2
3
4
public class SpringConfig {
}编写APP运行类
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
bookDao.update();
}
}说明:
目前打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
对于update方法来说,就没有该功能
我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。
AOP实现步骤
- 步骤一:添加依赖 因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
1
2
3
4
5<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。 - 步骤二:定义接口和实现类
准备环境的时候已经完成 - 步骤三:定义通知类和通知
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。
类名和方法名没有要求,可以任意。1
2
3
4
5public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
} - 步骤四:定义切入点
BookDaoImpl中有两个方法,分别是update()和save(),我们要增强的是update方法,那么该如何定义呢?说明:1
2
3
4
5
6
7
8
9public class MyAdvice {
private void pt() {
}
public void method() {
System.out.println(System.currentTimeMillis());
}
}
切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
execution及后面编写的内容,之后我们会专门去学习。
- 步骤五:制作切面
切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置1
2
3
4
5
6
7
8
9
10public class MyAdvice {
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型,后面会讲。
那这里就会在执行update()之前,来执行我们的method(),输出当前毫秒值 - 步骤六:将通知类配给容器并标识其为切面类
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyAdvice {
private void pt() {
}
public void method() {
System.out.println(System.currentTimeMillis());
}
} - 步骤七:开启注解格式AOP功能
使用@EnableAspectJAutoProxy注解1
2
3
4
5
public class SpringConfig {
} - 步骤八:运行程序
这次我们再来调用update()控制台成功输出了当前毫秒值1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
bookDao.update();
}
}
1662367945787
book dao update …
知识点1:@EnableAspectJAutoProxy
名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能
知识点2:@Aspect
名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类
知识点3:@Pointcut
名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式
知识点4:@Before
名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
AOP工作流程
AOP的入门案例已经完成,对于刚才案例的执行过程,我们就得来分析分析,这一节我们主要讲解两个知识点:AOP工作流程和AOP核心概念。其中核心概念是对前面核心概念的补充。
AOP工作流程
由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起
流程一:Spring容器启动
容器启动就需要去加载bean,哪些类需要被加载呢?
需要被增强的类,如:BookServiceImpl
通知类,如:MyAdvice
注意此时bean对象还没有创建成功
流程二:读取所有切面配置中的切入点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyAdvice {
private void ptx() {
}
private void pt() {
}
public void method() {
System.out.println(System.currentTimeMillis());
}
}上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用,所以不会被读取。
流程三:初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
注意第一步在容器启动的时候,bean对象还没有被创建成功。
要被实例化bean对象的类中的方法和切入点进行匹配
匹配失败,创建原始对象,如UserDao
匹配失败说明不需要增强,直接调用原始对象的方法即可。
匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
匹配成功说明需要对其进行增强
对哪个类做增强,这个类对应的对象就叫做目标对象
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
流程四:获取bean执行方法
获取的bean是原始对象时,调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
下面我们来验证一下容器中是否为代理对象
如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
步骤一:修改App运行类,获取类的类型并输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
```
* 步骤二:修改MyAdvice类,改为不增强
将定义的切入点改为updatexxx,而BookDaoImpl类中不存在该方法,所以BookDao中的update方法在执行的时候,就不会被增强
所以此时容器中的对象应该是目标对象本身。
``` java
public class MyAdvice {
private void pt() {
}
public void method() {
System.out.println(System.currentTimeMillis());
}
}步骤三:运行程序
输出结果如下,确实是目标对象本身,符合我们的预期
com.blog.dao.impl.BookDaoImpl@bcec361
class com.blog.dao.impl.BookDaoImpl步骤四:修改MyAdvice类,改为增强
将定义的切入点改为update,那么BookDao中的update方法在执行的时候,就会被增强
所以容器中的对象应该是目标对象的代理对象1
2
3
4
5
6
7
8
9
10
11
12
public class MyAdvice {
private void pt() {
}
public void method() {
System.out.println(System.currentTimeMillis());
}
}步骤五:运行程序
结果如下
com.blog.dao.impl.BookDaoImpl@3d34d211
class com.sun.proxy.$Proxy19
至此对于刚才的结论,我们就得到了验证,这块我们需要注意的是:
不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管是不是代理对象,打印的结果都是一样的,原因是内部对toString方法进行了重写。
AOP核心概念
在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
上面这两个概念比较抽象,简单来说
目标对象就是要增强的类如:BookServiceImpl类对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。
SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知如:MyAdvice中的method方法内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。
小结
这部分我们需要掌握的内容有:
能说出AOP的工作流程
AOP的核心概念
目标对象、连接点、切入点
通知类、通知
切面
代理
SpringAOP的本质或者可以说底层实现是通过代理模式。
AOP配置管理
AOP切入点表达式
前面我们已经接触过了切入点表达式,下面我们来具体学习一下
1 |
对于AOP中切入点表达式,我们总共会学习三个内容,分别是语法格式、通配符和书写技巧。
语法格式
首先我们先要明确两个概念:
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
对于切入点的描述,我们其实是有两中方式的,先来看下前面的例子
由于BookDaoImpl类实现了BookDao接口,那么有如下两种方式来描述 - 描述方式一:执行com.blog.dao包下的BookDao接口中的无参数update方法
1
execution(void com.blog.dao.BookDao.update())
- 描述方式二:执行com.blog.dao.impl包下的BookDaoImpl类中的无参数update方法 因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。
1
execution(void com.blog.dao.impl.BookDaoImpl.update())
对于切入点表达式的语法为:
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
对于这个格式,我们不需要硬记,通过一个例子,理解它:1
execution(public User com.blog.service.UserService.findById(int))
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是public,private等,可以省略
User:返回值,写返回值类型
com.blog.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开
异常名:方法定义中抛出指定异常,可以省略
切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想这块就会觉得将来编写起来会比较麻烦,有没有更简单的方式呢?使用通配符
通配符
我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?
- *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
匹配com.blog包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
1
execution(public * com.blog.*.UserService.find*(*))
- ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
1
execution(public User com..UserService.findById(..))
- +:专用于匹配子类类型
这个使用率较低,描述子类的,*Service+,表示所有以Service结尾的接口的子类
1
execution(* *..*Service+.*(..))
下面我们来具体分析一下各种用法
匹配接口,能匹配到
1
execution(void com.blog.dao.BookDao.update())
匹配实现类,能匹配到
1
execution(void com.blog.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
1
execution(* com.blog.dao.impl.BookDaoImpl.update())
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
1
execution(* com.blog.dao.impl.BookDaoImpl.update(*))
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
1
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包一层包下的任意类的update方法,匹配到的是接口,能匹配
1
execution(void com.*.*.*.update())
返回值为void,方法名是update的任意包下的任意类,能匹配
1
execution(void *..update())
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
1
execution(* *..*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
1
execution(* *..u*(..))
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
1
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
1
execution(void com..*())
将项目中所有业务层方法的以find开头的方法匹配
1
execution(* com.blog.*.*Service.find*(..))
将项目中所有业务层方法的以save开头的方法匹配
1
execution(* com.blog.*.*Service.save*(..))
书写技巧
对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知类型
前面的案例中,有涉及到如下内容
1 |
它所代表的含义是将通知添加到切入点方法执行的前面。
除了这个注解外,还有没有其他的注解,换个问题就是除了可以在前面加,能不能在其他的地方加?
类型介绍
我们先来回顾下AOP通知:
- AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
那么具体可以将通知添加到哪里呢?一共提供了5种通知类型 - 前置通知
- 后置通知
- 环绕通知(重点)
- 返回后通知(了解)
- 抛出异常后通知(了解)
为了更好理解这几种通知类型,用图说明:
- 前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
- 后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
- 返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
- 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
- 环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,需要我们往下学习。
环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
- 添加BookDao和BookDaoImpl类
1 | public interface BookDao { |
1 |
|
- 创建Spring的配置类
1
2
3
4
5
public class SpringConfig {
} - 创建通知类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyAdvice {
private void pt() {
}
public void before() {
System.out.println("before advice ...");
}
public void after(){
System.out.println("after advice ...");
}
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
public void afterReturning(){
System.out.println("afterReturning advice ...");
}
public void afterThrowing(){
System.out.println("afterThrowing advice ...");
}
} - 编写App运行类
1
2
3
4
5
6
7public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
bookDao.update();
}
}
通知类型的使用
- 前置通知
修改MyAdvice,在before方法上添加@Before注解运行程序,输出如下1
2
3
4
5
6
7
8
9
10
11
12
public class MyAdvice {
private void pt() {
}
public void before() {
System.out.println("before advice ...");
}
}
before advice …
book dao update …
- 后置通知运行程序,输出如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyAdvice {
private void pt() {
}
public void before() {
System.out.println("before advice ...");
}
public void after(){
System.out.println("after advice ...");
}
}
before advice …
book dao update …
after advice …
- 环绕通知
- 基本使用around before advice …
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyAdvice {
private void pt() {
}
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
}
around after advice …
运行结果中,通知的内容打印出来,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体如何实现?
- 在方法参数中添加ProceedingJoinPoint,同时在需要的位置使用proceed()调用原始操作运行程序,输出如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyAdvice {
private void pt() {
}
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
}
around before advice …
book dao update …
around after advice …
注意事项
当原始方法中有返回值时
修改MyAdvice,对BookDao中的select方法添加环绕通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyAdvice {
private void pt() {
}
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
pjp.proceed();
System.out.println("around after advice ...");
}
}修改App类,调用select方法
1
2
3
4
5
6
7
8public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
int select = bookDao.select();
System.out.println(select);
}
}运行程序,报错
org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: …
错误大概的意思是:空的返回不匹配原始方法的int返回void就是返回Null
- 原始方法的返回值是BookDao下的select方法
所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:
1 |
|
- 运行程序,结果如下
around before advice …
book dao select …
around after advice …
100
说明:
- 为什么返回的是Object而不是int的主要原因是Object类型更通用。
- 在环绕通知中是可以对原始方法返回值就行修改的。例如上面的例子,可以改为return res+666;,最终的输出结果也会变为766
- 返回后通知book dao select …
1
2
3
4
5
6
7
8
9
10
11
12
public class MyAdvice {
private void pt() {
}
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
afterReturning advice …
100
注意:
返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。
现在我们在select()方法中加一个异常
1 | public int select() { |
运行程序,输出如下,没有输出afterReturning advice …
book dao select …
Exception in thread “main” java.lang.ArithmeticException: / by zero
…
我们再换成后置输出,运行程序,结果如下,输出了after advice …
输出如下
book dao select …
after advice …
Exception in thread “main” java.lang.ArithmeticException: / by zero
…
- 异常后通知book dao select …
1
2
3
4
5
6
7
8
9
10
11
12
public class MyAdvice {
private void pt() {
}
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
afterThrowing advice …
Exception in thread “main” java.lang.ArithmeticException: / by zero
…
通知类型总结
知识点1:@After
名称 @After
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
知识点2:@AfterReturning
名称 @AfterReturning
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行
知识点3:@AfterThrowing
名称 @AfterThrowing
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
知识点4:@Around
名称 @Around
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
知识点5:@Before
名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
环绕通知注意事项:
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
业务层接口执行效率
需求分析
这个需求也比较简单,前面我们在介绍AOP的时候已经演示过:
- 需求:任意业务层接口执行均可显示其执行效率(执行时长)
这个案例的目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来方便优化。
具体实现的思路:
- 开始执行方法之前记录一个时间
- 执行方法
- 执行完方法之后记录一个时间
- 用后一个时间减去前一个时间的差值,就是我们需要的结果。
- 所以要在方法执行的前后添加业务,经过分析我们将采用环绕通知。
**说明:**原始方法如果只执行一次,时间太快,两个时间差可能为0,所以我们要执行万次来计算时间差。
环境准备
创建一个Maven项目
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>创建一个数据库和表
1
2
3
4
5
6
7
8
9
10
11
12create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
INSERT INTO tbl_account(`name`,money) VALUES
('Tom',2800),
('Jerry',3000),
('Jhon',3100);添加AccountService、AccountServiceImpl、AccountDao与Account类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47public class Account {
private Integer id;
private String name;
private Double money;
public Account() {
}
public Account(Integer id, String name, double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface AccountDao {
void selectAll();
void save(Account account);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}1
2
3
4
5
6
7public interface AccountService {
void save(Account account);
void update(Account account);
void delete(Integer id);
List<Account> findAll();
Account findById(Integer id);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public void save(Account account) {
accountDao.save(account);
}
public void update(Account account) {
accountDao.update(account);
}
public void delete(Integer id) {
accountDao.delete(id);
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findById(Integer id) {
return accountDao.findById(id);
}
}resources下提供一个jdbc.properties
1
2
3
4jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:13306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=poassword.创建相关配置类
1
2
3
4
5
6
public class SpringConfig {
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class MyBatisConfig {
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.blog.domain");
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.blog.dao");
return mapperScannerConfigurer;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public void save(Account account) {
accountDao.save(account);
}
public void update(Account account) {
accountDao.update(account);
}
public void delete(Integer id) {
accountDao.delete(id);
}
public List<Account> findAll() {
return accountDao.findAll();
}
public Account findById(Integer id) {
return accountDao.findById(id);
}
}编写Spring整合Junit的测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountServiceTestCase {
private AccountService accountService;
public void testFindById(){
Account account = accountService.findById(2);
}
public void testFindAll(){
List<Account> accountList = accountService.findAll();
}
}
功能开发
步骤一:开启SpringAOP的注解功能
在Spring的主配置文件SpringConfig类中添加注解1
步骤二:编写AOP切面类
该类要被Spring管理,需要添加@Component
要标识该类是一个AOP的切面类,需要添加@Aspect
配置切入点表达式,需要添加一个方法,并添加@Pointcut
1
2
3
4
5
6
7
8
9
10
11
public class ProjectAdvice {
public void servicePt() {
}
public void runSpeed() {
}
}步骤三:添加环绕通知
在runSpeed()方法上添加@Around1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProjectAdvice {
public void servicePt() {
}
public Object runSpeed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object res = proceedingJoinPoint.proceed();
return res;
}
}步骤四:完成核心业务,记录万次执行的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ProjectAdvice {
public void servicePt() {
}
public void runSpeed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
proceedingJoinPoint.proceed();
}
long end = System.currentTimeMillis();
System.out.println("业务层接口万次执行时间: " + (end - start) + "ms");
}
}步骤五:运行单元测试类
运行结果如下
业务层接口万次执行时间: 2312ms
业务层接口万次执行时间: 1578ms步骤六: 程序优化
目前还存在一个问题,当我们一次执行多个方法时,控制台输出的都是业务层接口万次执行时间: XXXms
我们无法得知具体哪个方法的耗时,那么该如何优化呢?
ProceedingJoinPoint中有一个getSignature()方法来获取签名,然后调用getDeclaringTypeName可以获取类名,getName()可以获取方法名1
2
3
4
5
6
7
8
9
10
11
12
public void runSpeed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Signature signature = proceedingJoinPoint.getSignature();
String typeName = signature.getDeclaringTypeName();
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
proceedingJoinPoint.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行 " + typeName + "." + methodName + " 耗时" + (end - start) + "ms");
}
再次运行程序,结果如下
万次执行 com.blog.service.AccountService.findAll 耗时2086ms
万次执行 com.blog.service.AccountService.findById 耗时1365ms
说明:
当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程。
这块只是通过该案例把AOP的使用进行了学习,具体的实际值是有很多因素共同决定的。
AOP通知获取数据
目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容,我们将从获取参数、获取返回值和获取异常三个方面来研究切入点的相关信息。
前面我们介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗?
我们先来逐一分析下:
- 获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
- 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知
- 环绕通知
- 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
- 抛出异常后通知
- 环绕通知
环境准备
创建一个maven项目
添加Spring依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>添加BookDao和BookDaoImpl类
1
2
3public interface BookDao {
String findName(int id);
}1
2
3
4
5
6
7
public class BookDaoImpl implements BookDao {
public String findName(int id) {
System.out.println("id:" + id);
return "TestName";
}
}创建Spring配置类
1
2
3
4
5
public class SpringConfig {
}编写通知类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyAdvice {
public void pt(){}
public void before(){
System.out.println("before advice ...");
}
public void after(){
System.out.println("after advice ...");
}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object res = pjp.proceed();
return res;
}
public void afterReturning(){
System.out.println("afterReturning advice ...");
}
public void afterThrowing(){
System.out.println("afterThrowing advice ...");
}
}编写App运行类
1
2
3
4
5
6
7
8public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
String name = bookDao.findName(9527);
System.out.println(name);
}
}
获取参数
- 非环绕通知获取方式
在方法上添加JoinPoint,通过JoinPoint来获取参数运行App类,可以获取如下内容,说明参数9527已经被获取1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyAdvice {
public void pt(){}
public void before(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ...");
}
}
[9527]
before advice …
id:9527
TestName
思考:方法的参数只有一个,为什么获取的是一个数组?
- 因为参数的个数是不固定的,所以使用数组更通配些。
- 如果将参数改成两个会是什么效果呢?
修改BookDao和BookDaoImpl类
1 | public interface BookDao{ |
1 |
|
- 修改App类,调用方法传入多个参数 输出结果如下,两个参数都已经被获取到
1
2
3
4
5
6
7
8public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
String name = bookDao.findName(9527,"Tony");
System.out.println(name);
}
}
[9527, Tony]
before advice …
id:9527
TestName
- 环绕通知获取方式
环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法,我们去验证下
1 |
|
运行App后查看运行结果,说明ProceedingJoinPoint也是可以通过getArgs()获取参数
[9527, Tony]
id:9527
TestName
注意:
pjp.proceed()方法是有两个构造方法,分别是:
proceed()
proceed(Object[] object)
调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
所以调用这两个方法的任意一个都可以完成功能
但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyAdvice {
public void pt(){}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 9421;
Object res = pjp.proceed(args);
return res;
}
}
运行程序,输出结果如下
[9527, Tony]
id:9421
TestName
有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,还可以根据参数来给予不同的权限,提高代码的健壮性。
获取返回值
对于返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取,具体如何获取?
- 环绕通知获取返回值上述代码中,res就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyAdvice {
public void pt(){}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 9421;
Object res = pjp.proceed(args);
return res;
}
} - 返回后通知获取返回值运行程序,输出如下,成功获取了返回值
1
2
3
4
5
6
7
8
9
10
11
12
public class MyAdvice {
public void pt(){}
public void afterReturning(Object res){
System.out.println("afterReturning advice ..." + res);
}
}
id:9527
afterReturning advice …TestName
TestName
几点注意:
- 参数名的问题
- 赋给returning的值,必须与Object类型参数名一致,上面的代码中均为res
- afterReturning方法参数类型的问题
- 参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型
- afterReturning方法参数的顺序问题
- 如果存在JoinPoint参数,则必须将其放在第一位,否则运行将报错
1
public void afterReturning(JoinPoint jp,Object res)
获取异常
对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取,具体如何获取?
- 环绕通知获取异常
这块比较简单,以前我们是抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了在catch方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务需求有关了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyAdvice {
public void pt(){}
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 9421;
Object res = null;
try {
res = pjp.proceed(args);
} catch (Throwable e) {
throw new RuntimeException(e);
}
return res;
}
} - 抛出异常后通知获取异常那现在我们只需要让原始方法抛一个异常来看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyAdvice {
public void pt() {
}
public void afterThrowing(Throwable throwable) {
System.out.println("afterThrowing advice ..." + throwable);
}
}运行程序,输出如下,成功输出了异常1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
public String findName(int id, String name) {
System.out.println("id:" + id);
int a = 1 / 0;
return "TestName";
}
}
[9527, Tony]
id:9421
afterThrowing advice …java.lang.ArithmeticException: / by zero
Exception in thread “main” java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
…
至此,AOP通知如何获取数据就已经讲解完了,数据中包含参数、返回值、异常(了解)。
百度网盘密码数据兼容处理
需求分析
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。
问题描述:
- 当我们从别人发给我们的内容中复制提取码的时候,有时候会多复制到一些空格,直接粘贴到百度的提取码输入框
- 但是百度那边记录的提取码是没有空格的
- 这个时候如果不做处理,直接对比的话,就会引发提取码不一致,导致无法访问百度盘上的内容
- 所以多输入一个空格可能会导致项目的功能无法正常使用。
- 此时我们就想能不能将输入的参数先帮用户去掉空格再操作呢?
- 答案是可以的,我们只需要在业务方法执行之前对所有的输入参数进行格式处理——trim()
- 那要对所有的参数都需要去除空格么?
- 也没有必要,一般只需要针对字符串处理即可。
- 以后涉及到需要去除前后空格的业务可能会有很多,这个去空格的代码是每个业务都写么?
- 可以考虑使用AOP来统一处理。
- AOP有五种通知类型,该使用哪种呢?
- 我们的需求是将原始方法的参数处理后在参与原始方法的调用,能做这件事的就只有环绕通知。
环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency> - 添加ResourcesDao和ResourcesDaoImpl,ResourcesService,ResourcesServiceImpl类
1
2
3public interface ResourceDao {
boolean readResource(String url,String password);
}1
2
3
4
5
6public class ResourceDaoImpl implements ResourceDao {
public boolean readResource(String url, String password) {
//模拟校验
return password.equals("root");
}
}1
2
3public interface ResourceService {
public boolean openURL(String url,String password);
}1
2
3
4
5
6
7
8
public class ResourceServiceImpl implements ResourceService {
private ResourceDao resourceDao;
public boolean openURL(String url, String password) {
return resourceDao.readResource(url,password);
}
} - 创建Spring配置类
1
2
3
4
public class SpringConfig {
} - 编写App运行类现在项目的效果是,当输入密码为”root”控制台打印为true,如果密码改为”root “控制台打印的是false
1
2
3
4
5
6
7
8public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourceService service = context.getBean(ResourceService.class);
boolean flag = service.openURL("https://pan.baidu.com/xx", "root");
System.out.println(flag);
}
}
需求是使用AOP将参数进行统一处理,不管输入的密码root前后包含多少个空格,最终控制台打印的都是true。
具体实现
- 步骤一:开启SpringAOP的注解功能
1
2
3
4
5
6
public class SpringConfig {
} - 步骤二:编写通知类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyAdvice {
public void pt(){}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
if ((String.class).equals(args[i].getClass())){
args[i] = args[i].toString().trim();
}
}
Object res = pjp.proceed(args);
return res;
}
} - 步骤三:运行程序
不管密码root前后是否加空格,最终控制台打印的都是true
AOP总结
AOP核心概念
- 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
- 作用:在不惊动原始设计的基础上为方法进行功能增强
- 核心概念
- 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
- 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
- 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
- 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
- 切面(Aspect):描述通知与切入点的对应关系
- 目标对象(Target):被代理的原始对象成为目标对象
切入点表达式
- 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)
1
execution(* com.itheima.service.*Service.*(..))
- 切入点表达式描述通配符:
- 作用:用于快速描述,范围描述
- *:匹配任意符号(常用)
- .. :匹配多个连续的任意符号(常用)
- +:匹配子类类型
- 切入点表达式书写技巧 :
- 按标准规范开发
- 查询操作的返回值建议使用*匹配
- 减少使用..的形式描述包,效率低
- 对接口进行描述,使用表示模块名,例如UserService的匹配描述为Service
- 方法名书写保留动词,例如get,使用表示名词,例如getById匹配描述为getBy
- 参数根据实际情况灵活调整
五种通知类型
- 前置通知
- 后置通知
- 环绕通知(重点)
- 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
- 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回值设置为Object类型
- 环绕通知中可以对原始方法调用过程中出现的异常进行处理
- 返回后通知
- 抛出异常后通知
通知中获取参数
- 获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
- 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知
- 环绕通知
- 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
- 抛出异常后通知
- 环绕通知
AOP事务管理
Spring事务简介
相关概念
相关概念
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
数据层有事务我们可以理解,为什么业务层也需要处理事务呢?举个简单的例子转账业务会有两次数据层的调用,一次是加钱一次是减钱
把事务放在数据层,加钱和减钱就有两个事务
没办法保证加钱和减钱同时成功或者同时失败
这个时候就需要将事务放在业务层进行处理。
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager1
2
3
4
5
6
7public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction( TransactionDefinition var1)throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
}commit是用来提交事务,rollback是用来回滚事务。
PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:
1 | public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { |
从名称上可以看出,我们只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。其内部采用的是JDBC的事务。所以说如果你持久层采用的是JDBC相关的技术,就可以采用这个事务管理器来管理你的事务。而Mybatis内部采用的就是JDBC的事务,所以后期我们Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。
转账案例–需求分析
接下来通过一个案例来学习下Spring是如何来管理事务的。
先来分析下需求:
- 需求: 实现任意两个账户间转账操作
- 需求微缩: A账户减钱,B账户加钱
为了实现上述的业务需求,我们可以按照下面步骤来实现下:
- 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
- 业务层提供转账操作(transfer),调用减钱与加钱的操作
- 提供2个账号和操作金额执行转账操作
- 基于Spring整合MyBatis环境搭建上述操作
转账案例–环境搭建
步骤一:准备数据表
Tom和Jerry初始金额都是10001
2
3
4
5
6
7
8CREATE DATABASE spring_db CHARACTER SET utf8;
USE spring_db;
CREATE TABLE tbl_account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(35),
money DOUBLE
);
INSERT INTO tbl_account(`name`,money) VALUES('Tom',1000),('Jerry',1000);步骤二:创建项目导入jar包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>步骤三:根据表创建模型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47public class Account {
private Integer id;
private String name;
private Double money;
public Account() {
}
public Account(Integer id, String name, Double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}步骤四:创建Dao接口
1
2
3
4
5
6
7
8public interface AccountDao {
void inMoney(; String name, Double money)
void outMoney(; String name, Double money)
}步骤五:创建Service接口和实现类
1
2
3
4
5
6
7
8
9
10public interface AccountService {
/**
* 转账操作
* @param out 转出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in,Double money);
}1
2
3
4
5
6
7
8
9
10
public class AccountServiceImpl implements AccountService {
protected AccountDao accountDao;
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
}
}步骤六:添加jdbc.properties文件
1
2
3
4jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:13306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=poassword.步骤七:创建JdbcConfig配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}步骤八:创建MybatisConfig配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MyBatisConfig {
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setTypeAliasesPackage("com.blog.domain");
factory.setDataSource(dataSource);
return factory;
}
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.blog.dao");
return msc;
}
}步骤九:创建SpringConfig配置类
1
2
3
4
5
6
public class SpringConfig {
}步骤十:编写测试类
1
2
3
4
5
6
7
8
9
10
11
public class AccountServiceTest {
private AccountService accountService;
public void testTransfer() {
accountService.transfer("Tom", "Jerry", 100D);
}
}
事务管理
上述环境,运行单元测试类,会执行转账操作,Tom的账户会减少100,Jerry的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如
1 |
|
这个时候就模拟了转账过程中出现异常的情况,此时进行转账,Tom的账户会减少100,而Jerry的账户却不会增加100
那我们来分析一下刚才的结果
- 程序正常执行时,账户金额A减B加,没有问题
- 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而Spring的事务管理就是用来解决这类问题的。
Spring事务管理具体的实现步骤如下
步骤一:在需要被事务管理的方法上添加@Transactional注解
1
2
3
4
5
6
7
8
9
10
11
12
public class AccountServiceImpl implements AccountService {
protected AccountDao accountDao;
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
int a = 1 / 0;
accountDao.inMoney(in, money);
}
}注意:注意:@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上
- 写在接口类上,该接口的所有实现类的所有方法都会有事务
- 写在接口方法上,该接口的所有实现类的该方法都会有事务
- 写在实现类上,该类中的所有方法都会有事务
- 写在实现类方法上,该方法上有事务
- 建议写在实现类或实现类的方法上
步骤二:在JdbcConfig类中配置事务管理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
//配置事务管理器,mybatis使用的是jdbc事务
public PlatformTransactionManager platformTransactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager
- 步骤三:在SpringConfig类中配置事务管理器
1
2
3
4
5
6
7
public class SpringConfig {
} - 步骤四:修改单元测试类
运行程序之后,我们去数据库查看Tom和Jerry的金额,发现没有变化
那么说明在转换的业务出现错误后,事务就可以控制回滚,保证数据的正确性。
知识点1:@EnableTransactionManagement
名称 @EnableTransactionManagement
类型 配置类注解
位置 配置类定义上方
作用 设置当前Spring环境中开启注解式事务支持
知识点2:@Transactional
名称 @Transactional
类型 接口注解 类注解 方法注解
位置 业务层接口上方 业务层实现类上方 业务方法上方
作用 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)
Spring事务角色
这部分我们重点要理解两个概念,分别是事务管理员和事务协调员。
当未开启Spring事务之前
- AccountDao的outMoney因为是修改操作,会开启一个事务T1
- AccountDao的inMoney因为是修改操作,会开启一个事务T2
- AccountService的transfer没有事务,
- 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
- 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
- 就会导致数据出现错误
当开启Spring的事务管理后
transfer上添加了@Transactional注解,在该方法上就会有一个事务T
AccountDao的outMoney方法的事务T1加入到transfer的事务T中
AccountDao的inMoney方法的事务T2加入到transfer的事务T中
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
通过上面例子的分析,我们就可以得到如下概念:
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
注意:目前的事务管理是基于DataSourceTransactionManager和SqlSessionFactoryBean使用的是同一个数据源。
Spring事务属性
这部分我们主要学习三部分内容事务配置、转账业务追加日志、事务传播行为。
事务配置
属性 作用 示例
readOnly 设置是否为只读事务 readOnly = true 只读事务
timeout 设置事务超时时间 timeout = -1(永不超时)
rollbackFor 设置事务回滚异常(class) rollbackFor{NullPointException.class}
rollbackForClassName 设置事务回滚异常(String) 同上格式为字符串
noRollbackFor 设置事务不回滚异常(class) noRollbackFor{NullPointExceptior.class}
noRollbackForClassName 设置事务不回滚异常(String) 同上格式为字符串
isolation 设置事务隔离级别 isolation = Isolation. DEFAULT
propagation 设置事务传播行为 …
上面这些属性都可以在 ** @Transactional ** 注解的参数上进行设置。
- eadOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
- imeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
- ollbackFor:当出现指定异常进行事务回滚
- oRollbackFor:当出现指定异常不进行事务回滚
- 思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
- oRollbackFor是设定对于指定的异常不回滚,这个好理解
- ollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
- 事实上Spring的事务只会对Error异常和RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的,如下面的代码就不会回滚
1 |
|
所以当我们运行程序之后,Tom会少100块钱,而Jerry不会多100块钱,这100块钱就凭空消失了
- 此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
1
2
3
4
5
6
7
8
9
10
11
12
public class AccountServiceImpl implements AccountService {
protected AccountDao accountDao;
public void transfer(String out, String in, Double money) throws IOException {
accountDao.outMoney(out, money);
if (true) throw new IOException();
accountDao.inMoney(in, money);
}
} - rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
- isolation设置事务的隔离级别
- DEFAULT :默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED : 读未提交
- READ_COMMITTED : 读已提交
- REPEATABLE_READ : 重复读取
- SERIALIZABLE: 串行化
介绍完上述属性后,还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例。
转账业务追加日志
需求分析
- 在前面的转账案例的基础上添加新的需求,完成转账后记录日志。
- 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
- 需求微缩:A账户减钱,B账户加钱,数据库记录日志
- 基于上述的业务需求,我们来分析下该如何实现:
- 基于转账操作案例添加日志模块,实现数据库中记录日志
- 业务层转账操作(transfer),调用减钱、加钱与记录日志功能
- 需要注意一点就是,我们这个案例的预期效果为:
- 无论转账操作是否成功,均进行转账操作的日志留痕
环境准备
步骤一:创建日志表
1
2
3
4
5create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)步骤二:添加LogDao接口
1
2
3
4
5public interface LogDao {
void log(String info);
}步骤三:添加LogService接口和实现类
1 | public interface LogService { |
1 |
|
- 步骤四:修改转账业务实现类,添加日志记录功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AccountServiceImpl implements AccountService {
protected AccountDao accountDao;
protected LogService logService;
public void transfer(String out, String in, Double money) throws IOException {
try {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
} finally {
logService.log(out, in, money);
}
}
} - 步骤五:运行程序
- 当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
- 当转账业务之间出现异常(int i =1 / 0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据,说明也回滚了
- 这个结果和我们想要的不一样,什么原因?该如何解决?
- 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败(同回滚)
- 解决方案:继续往下看
- 预期效果:无论转账操作是否成功,日志必须保留
事务传播行为
对于上述案例的分析:
- log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
- transfer因为加了@Transactional注解,也开启了事务T
- 前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
- 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
- 这和我们的需求不符,这个时候我们就想能不能让log方法单独是一个事务呢?
- 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
- 事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
- 具体如何解决,就需要用到之前我们没有说的propagation属性。
- 修改logService改变事务的传播行为运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。
1
2
3
4
5
6
7
8
9
10
public class LogServiceImpl implements LogService {
private LogDao logDao;
public void log(String out, String in, Double money) {
logDao.log(out + "向" + in + "转账" + money + "元");
}
}
事务传播行为的可选值
传播属性 事务管理员 事务协调员
REQUIRED(默认) 开启T 加入T
无 新建T2
REQUIRES_NEW 开启T 新建T2
无 新建T2
SUPPORTS 开启T 加入T
无 无
NOT_SUPPORTED 开启T 无
无 无
MANDTORY 开启T 加入T
无 ERROR
NEVER 开启T ERROR
无 无
NESTED 设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚