Spring-AOP&事务管理
# AOP
# AOP简介
# AOP简介和作用
AOP(Aspect Oriented Programming),及面向切面编程,可以说是OOP(Object Oriented Programming),面向对象编程的补充和完善。 OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。但是OOP允许开发者定义纵向的关系, 但并不适合定义横向的关系,比如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心业务功能毫无关系, 对于其他类型的代码,如安全性、异常处理等也都是如此,这种散布在各处的与核心业务功能无关的代码被称为横切(cross cutting), 在OOP设计中,会导致大量代码的重复,不利于各模块的重用。
AOP技术恰恰相反,它利用一种称为横切的技术,剖开封装对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块, 并将其命名为Aspect,即切面。所谓切面,简单说就是那些业务无关,却为业务模块所共同调用的逻辑封装起来, 便于减少系统的重复代码,降低模块之间的耦合度,有助于系统的可操作性和可维护性。
使用横切技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点, 与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多次,且各处基本相似,比如权限认证、日志、事务。 AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
- **AOP(Aspect Oriented Programming)**面向切面编程,一种编程思想,指导开发者如何组织程序结构
- **OOP(Object Oriented Programming)**面向对象编程,也是一种编程思想
- AOP作用:在不改变原始设计的基础上为核心关注点进行功能增强。
- Spring理念:无侵入式
# AOP核心概念
- 连接点(JoinPoint):正在执行的方法,例如:
update()
、delete()
、select()
等都是连接点; - 切入点(Pointcut):进行功能增强了的方法,例如:
update()
、delete()
方法,select()
方法没有被增强所以不是切入点,但是是连接点。- 在Spring AOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 匹配一个具体方法:
com.itheima.dao
包下的BookDao
接口中的无形参无返回值的save
方法 - 匹配多个方法:所有的
save
方法,或者所有get
开头的方法,所有以Dao
结尾的接口中的任意方法,所有带有一个参数的方法,等等。
- 匹配一个具体方法:
- 在Spring AOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 通知(Advice):在切入点前后执行的操作,也就是增强的共性功能
- 在Spring AOP中,功能最终以方法的形式呈现
- 通知类:通知方法所在的类叫做通知类
- 切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。
# AOP入门案例
# AOP入门案例思路分析
- 案例设定:测定接口执行效率
- 简化设定:在接口执行前输出当前系统事件
- 开发模式:XML 还是 注解
- 思路分析:
- 初始化模块
- 导入坐标(pom.xml)
- 制作连接点方法(基础操作,
dao
接口与实现类) - 制作共性功能(通知类与通知)
- 定义切入点
- 绑定切入点与通知关系
- 运行测试
# AOP入门案例实现
- 创建spring-17-aop-quickstart模块,并初始化模块目录结构
- 引入AOP编程所需依赖坐标(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>spring-17-aop-quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring核心依赖,会将spring-aop传递进来 -->
<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>
</dependencies>
</project>
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
- 创建
com.itheima.dao
和com.itheima.dao.impl
包,并在响应包下创建BookDao
接口和BookDaoImpl
实现类
package com.itheima.dao;
public interface BookDao {
void save();
void update();
}
2
3
4
5
6
7
8
9
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
@Override
public void update() {
System.out.println("book dao update ...");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 创建
com.itheima.aop
包,在该包下定义通知类MyAdvice
,制作通知方法
package com.itheima.aop;
import org.springframework.stereotype.Component;
// 通知类必须配置成Spring管理的bean
@Component
public class MyAdvice {
public void method() {
System.out.println(System.currentTimeMillis());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 修改
MyAdvice
通知类,定义切入点表达式、配置切面(绑定切入点和通知关系)
package com.itheima.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
// 通知类必须配置成Spring管理的bean
@Component
// 设置当前类为切面类
@Aspect
public class MyAdvice {
// 设置切入点,@Pointcut注解要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt() {}
// 设置在切入点pt()的前面运行增强操作(前置通知)
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 创建
com.itheima.config
包,在该包下创建SpringConfig
配置类
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.itheima")
// 开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
2
3
4
5
6
7
8
9
10
11
12
- 在
com.itheima
包下创建测试类AppForAopQuickStart
package com.itheima;
import com.itheima.config.SpringConfig;
import com.itheima.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AppForAopQuickStart {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
bookDao.update();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# AOP工作流程
- Spring容器启动
- 读取所有切面配置的切入点
- 初始化
Bean
,判定Bean
对应的类中的方法是否匹配到任意切入点- 匹配失败,创建原始对象
- 匹配成功,创建原始对象(目标对象)的代理对象
- 获取
Bean
,并执行方法- 获取的
Bean
是原始对象时,调用方法并执行,完成操作 - 获取的
Bean
是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作。
- 获取的
AOP核心概念:
目标对象(Target):被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强 代理对象(Proxy):代理后生成的对象,由Spring帮我们创建代理对象。
在测试类中验证代理对象:
package com.itheima;
import com.itheima.config.SpringConfig;
import com.itheima.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AppForAopQuickStart {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
// bookDao.save();
// bookDao.update();
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# AOP切入点表达式
# 语法格式
切入点:要增强的方法
切入点表达式:要增强的方法的描述方式
- 描述方式一:执行
com.itheima.dao
包下的BookDao
接口中的无参数update
方法
execution(void com.itheima.dao.BookDao.update())
1- 描述方式二:执行
com.itheima.dao.impl
包下的BookDaoImpl
类中的无参数update
方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
1- 描述方式一:执行
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名(参数) 异常名
execution(public User com.itheima.service.UserService.findById(int))
- 动作关键字:描述切入点的行为动作,例如
execution
表示执行指定切入点 - 访问修饰符:
public
、private
等,可以省略 - 返回值:写返回值类型
- 包名:多级包使用点连接
- 类/接口名:
- 方法名:
- 参数:直接写参数类型,多个类型用逗号分割
- 异常名:方法定义中抛出指定异常,可以省略
# 通配符
目的:可以使用通配符描述切入点。
- ::单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
示例:匹配com.itheima
包下的任意包中的UserService
类或接口中所有find
开头的带有一个参数的方法
execution(public * com.itheima.*.UserService.find*(*))
- ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
示例:匹配com
包下的任意包中的UserService
类或接口中所有名称为findById
的方法
execution(public User com..UserService.findById(..))
- +: 专门用于匹配子类类型
execution(* *..*Service+.*(..))
package com.itheima.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
// 通知类必须配置成Spring管理的bean
@Component
// 设置当前类为切面类
@Aspect
public class MyAdvice {
// 设置切入点,@Pointcut注解要求配置在方法上方
// 切入点表达式
// @Pointcut("execution(void com.itheima.dao.BookDao.update())")
// @Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())")
// @Pointcut("execution(* com.itheima.dao.impl.BookDaoImpl.update(*))")
// @Pointcut("execution(void com.*.*.*.update())")
// @Pointcut("execution(* *..*(..))")
// @Pointcut("execution(* *..*e(..))")
// @Pointcut("execution(void com..*())")
// @Pointcut("execution(* com.itheima.*.*Service.find*(..))")
// 执行com.itheima包下的任意包下的名称以Dao结尾的类或接口中的save方法,参数任意,返回值任意
@Pointcut("execution(* com.itheima.*.*Dao.save(..))")
private void pt() {}
// 设置在切入点pt()的前面运行增强操作(前置通知)
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
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
# 切入点表达式书写技巧
- 所有代码都要按照规范开发,也就是遵循开发规范,否则一下技巧全部失效
- 通过表达式描述切入点通常描述接口,而不描述实现类
- 访问控制修饰符针对接口开发均采用
public
描述,可省略访问控制修饰符 - 针对增、删、改方法的返回值类型使用精准类型加速匹配,对于查询类使用
*
通配符快速描述 - 包名书写尽量不使用..匹配,主要是因为效率过低,常用
*
做单个包描述匹配,或精准匹配 - 接口名/类名与模块相关的部分采用*匹配,例如
UserService
可写为*Service
- 方法名以动词建议使用精准匹配,名词采用
*
通配符匹配,如getById
可写成getBy*
- 参数规则较为复杂,需根据业务方法灵活调整
- 通常不使用异常作为匹配规则
# AOP通知类型
# AOP通知分类
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
AOP通知共分为5种类型:
- 前置通知:在切入点方法执行之前执行
- 后置通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,后置通知都会执行
- 环绕通知(重点):手动调用切入点方法并对其进行增强的通知方式
- 返回后通知(了解):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行
- 抛出异常后通知(了解):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行
# AOP通知详解
前置通知
- 名称:
@Before
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
- 属性:切入点方法名
- 范例:
// @Before: 前置通知,在原始方法运行之前执行 @Before("pt()") public void before() { System.out.println("before advice ..."); }
1
2
3
4
5- 名称:
后置通知
- 名称:
@After
- 类型:方法注解
- 位置:通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 属性:切入点方法名
- 范例:
// @After: 后置通知,在原始方法运行之后执行 @After("pt()") public void after() { System.out.println("after advice ..."); }
1
2
3
4
5- 名称:
返回后通知
- 名称:
@AfterReturning
(了解即可) - 类型:方法注解
- 位置:通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
- 属性:切入点方法名
- 范例:
// @AfterReturning: 返回后通知,在原始方法执行完毕后运行,且原始方法执行 // 过程中未出现异常现象 @AfterReturning("pt2()") public void afterReturning() { System.out.println("afterReturning advice ..."); }
1
2
3
4
5
6- 名称:
抛出异常后通知
- 名称:
@AfterThrowing
(了解) - 类型:方法注解
- 位置:通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
- 属性:切入点方法名
- 范例:
// @AfterThrowing: 抛出异常后通知,在原始方法执行过程中出现异常后运行 @AfterThrowing("pt2()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); }
1
2
3
4
5- 名称:
环绕通知
- 名称:
@Around
(重点,常用) - 类型:方法注解
- 位置:通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
- 属性:切入点方法名
- 范例:
// @Around: 环绕通知,在原始方法运行的前后执行 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); // 表示对原始方法的调用 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; }
1
2
3
4
5
6
7
8
9- 名称:
通知类完整代码如下:
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt() {}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2() {}
// @Before: 前置通知,在原始方法运行之前执行
// @Before("pt()")
public void before() {
System.out.println("before advice ...");
}
// @After: 后置通知,在原始方法运行之后执行
// @After("pt()")
public void after() {
System.out.println("after advice ...");
}
// @Around: 环绕通知,在原始方法运行的前后执行
// @Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
// 表示对原始方法的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
// @Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
// 表示对原始操作的调用
Integer ret = (Integer) pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
// @AfterReturning: 返回后通知,在原始方法执行完毕后运行,且原始方法执行
// 过程中未出现异常现象
// @AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
// @AfterThrowing: 抛出异常后通知,在原始方法执行过程中出现异常后运行
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
注意事项
- 环绕通知方法形参必须是
ProceedingJoinPoint
,表示正在执行的连接点,使用该对象的proceed()
方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值; - 环绕通知方法的返回值建议使用
Object
类型,用于将原始对象方法的返回值进行返回,什么地方使用代理对象就返回到哪里。
# AOP案例
# 案例-测量业务接口万次执行效率
# 需求分析
需求:任意业务层接口执行均可显示其执行时长 分析:
- 业务功能:业务层接口执行前后分别记录当前系统事件,求差值得到执行效率
- 通知类型选择前后均可以增强的类型--环绕通知
# 代码实现
- 前置工作环境准备
- 创建spring-20-aop-case模块,并初始化pom.xml文件和目录结构
- 拷贝spring-15-mybatis模块代码,该模块代码已经集成了MyBatis和Junit
- 在spring-20-aop-case模块的pom.xml文件中引入aspectjweaver依赖坐标
- 创建
com.itheima.aop
包,并在该包下编写通知类ProjectAdvice
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
// 通知类必须配置成Spring管理的bean
@Component
// 设置当前类为切面类
@Aspect
public class ProjectAdvice {
// 设置切入点,匹配业务层所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt() {}
// 设置环绕通知,在原始对象方法运行前后记录执行时间
@Around("ProjectAdvice.servicePt()") // 本类类名ProjectAdvice可以省略不写
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
// 获取执行的签名对象
Signature signature = pjp.getSignature();
// 获取接口或类的全限定名
String className = signature.getDeclaringTypeName();
// 获取方法名
String methodName = signature.getName();
// 记录开始时间
long start = System.currentTimeMillis();
// 调用原始对象方法一万次
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
// 记录结束事件
long end = System.currentTimeMillis();
long time = end - start;
// 打印执行结果
System.out.println("万次执行:" + className + "." + methodName + "------->" + time + "ms");
}
}
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
- 修改
SpringConfig
配置类,添加@EnableAspectJAutoProxy
开启AOP注解功能
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan("com.itheima")
@Import({DruidConfig.class, MyBatisConfig.class})
@EnableAspectJAutoProxy
public class SpringConfig {
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 运行测试类,查看测试结果
package com.itheima.service;
import com.itheima.config.SpringConfig;
import com.itheima.domain.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
// 第2步:使用Spring整合Junit专用的类加载器
@RunWith(SpringJUnit4ClassRunner.class)
// 第3步:加载配置文件或者配置类
@ContextConfiguration(classes = {SpringConfig.class}) // 加载配置类
// @ContextConfiguration(locations = {"classpath:applicationContext.xml"}) // 加载配置文件
public class UserServiceTest {
// 支持自动装配注入
@Autowired
private UserService userService;
@Test
public void testFindAll() {
List<User> all = userService.findAll();
// System.out.println(all);
}
}
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
# AOP切入点数据获取
# 通知方法获取原始方法参数
JoinPoint
对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
// JoinPoint: 用于描述切入点的对象,必须配置成通知方法中的第一个参数,
// 可用于获取原始方法的参数列表
@Before("pt()")
public void before(JoinPoint jp) {
Signature signature = jp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println(className + "." + methodName + ": before advice ...");
}
2
3
4
5
6
7
8
9
10
11
ProceedingJoinPoint
是JoinPoint
的子类,因此JoinPoint
具有的方法ProceedingJoinPoint
一样有
// ProceedingJoinPoint: 专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
// 环绕通知可以用来修改原始方法的参数
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
Object[] args = pjp.getArgs();
args[0] = 666;
Object ret = null;
try {
ret = pjp.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 获取原始防范的返回值
说明
在返回后通知@AfterReturning
和环绕通知@Around
中都可以获取到连接点方法的返回值
- 原始方法正常运行后,使用形参可以接收对应的返回值
// 设置返回后通知获取原始方法的返回值,要求returnning属性值必须与方法形参名相同
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(JoinPoint jp, String ret) {
Signature signature = jp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
System.out.println(className + "." + methodName + ": afterReturning advice ..." + ret);
}
2
3
4
5
6
7
8
- 环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 手动调用连接点方法,返回值就是连接点方法的返回值
Object ret = pjp.proceed();
return ret;
}
2
3
4
5
6
# 获取原始防范的异常信息
说明
在抛出异常后通知@AfterThrowing
和环绕通知@Around
中都可以获取到连接点方法中出现的异常。
- 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
// 设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(JoinPoint jp, Throwable t) {
Signature signature = jp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println(className + "." + methodName + ": afterReturning advice ..." + t);
}
2
3
4
5
6
7
8
9
10
- 环绕通知可以捕获原始方法的异常信息
// ProceedingJoinPoint: 专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
// 环绕通知可以用来修改原始方法的参数
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
Object[] args = pjp.getArgs();
args[0] = 666;
Object ret = null;
try {
ret = pjp.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 案例-百度网盘密码数据兼容处理
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理
分析:
- 在业务方法执行之前对所有的输入参数进行格式化处理--
trim()
- 使用处理后的参数调用原始方法--环绕通知中存在对原始方法的调用
代码实现:
- 环境准备创建spring-22-aop-case-passwd模块,并初始化pom.xml文件和目录结构
- 修改pom.xml文件,引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>spring-21-aop-advice-data</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring核心依赖,会将spring-aop传递进来 -->
<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>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<!-- spring 整合junit -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
</project>
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
- 创建
com.itheima.dao
和com.itheima.dao.impl
包,并在包下创建ResourcesDao
接口和ResourcesDaoImpl
实现类
package com.itheima.dao;
public interface ResourcesDao {
boolean readResources(String url, String password);
}
2
3
4
5
package com.itheima.dao.impl;
import com.itheima.dao.ResourcesDao;
import org.springframework.stereotype.Repository;
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
@Override
public boolean readResources(String url, String password) {
System.out.println(password.length());
return "root".equals(password);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 创建
com.itheima.service
和com.itheima.service.impl
包,并在该包下创建ResourcesService
接口和ResourcesServiceImpl
实现类
package com.itheima.service;
public interface ResourcesService {
boolean openUrl(String url, String password);
}
2
3
4
5
package com.itheima.service.impl;
import com.itheima.dao.ResourcesDao;
import com.itheima.service.ResourcesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;
@Override
public boolean openUrl(String url, String password) {
return resourcesDao.readResources(url, password);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 创建
com.itheima.aop
包,并在该包下创建DataAdvice
通知类
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*, *))")
private void servicePt() {}
@Around("servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
// 判断参数是不是字符串
if (args[i] instanceof String) {
args[i] = args[i].toString().trim();
}
}
Object ret = pjp.proceed(args);
return ret;
}
}
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
- 创建
com.itheima.config
包,并在该包下创建SpringConfig
类
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2
3
4
5
6
7
8
9
10
11
- 在test/java目录下创建
com.itheima.service
包,在该包下创建ResourcesServiceTest
类
package com.itheima.service;
import com.itheima.config.SpringConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
// 使用Spring整合Junit专用类加载器
@RunWith(SpringJUnit4ClassRunner.class)
// 加载配置类,通过配置类初始化Spring容器
@ContextConfiguration(classes = {SpringConfig.class})
public class ResourcesServiceTest {
@Autowired
private ResourcesService resourcesService;
@Test
public void testOpenUrl() {
boolean flag = resourcesService.openUrl("http://pan.baidu.com/haha", "root ");
System.out.println(flag);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 运行测试类,查看效果
# AOP开发总结
# AOP的核心概念
- 概念:*AOP(Aspect Oriented Programming)*面向切面编程,一种编程范式
- 作用:在不改动原始设计的基础上为方法进行功能增强
- 核心概念:
- 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
- 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
- 切入点(Pointcut):匹配连接点的表达式,也是具有共性功能的方法描述
- 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
- 切面(Aspect):描述通知与切入点的对应关系
- 目标对象(Target):被代理的原始对象称为目标对象
# 切入点表达式语法
- 表达式标准语法格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(* com.itheima.service.Service(..))
- 切入点表达式描述通配符:
- 作用:用于快速描述,范围描述
*
:匹配任意符号(常用)..
:匹配多个连续的任意符号(常用)+
:匹配子类类型
- 切入点表达式书写技巧:
- 按标准规范写
- 查询操作的返回值建议使用
*
匹配 - 减少使用
..
的形式描述包 - 对接口进行描述,使用
*
表示模块名,例如UserService
的匹配描述为*Service
- 方法名书写保留动词,例如
get
,使用*
表示名词,例如getById
匹配描述为getBy*
- 参数根据实际情况灵活调整
# 物种通知类型
- 前置通知
@Before
- 后置通知
@After
- 环绕通知
@Around
(重点)- 环绕通知依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用 - 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回值设置为
Object
类型 - 环绕通知中可以对原始方法调用过程中出现的异常进行处理
- 环绕通知依赖形参
- 返回后通知
@AfterReturning
- 抛出异常后通知
@AfterThrowing
# Spring事务管理
# Spring事务简介
# Spring事务作用
- 事务作用:在数据层保障一系列的数据库操作同时成功或同时失败
- Spring事务作用:在数据层或业务层保障一系列的数据库操作同时成功或者同时失败
下面我们依旧通过转账的案例来演示Spring事务。
# 案例-需求分析
- 需求:实现任意两个账户间的转账操作,简单地说,就是A账户减钱,B账户加钱
- 分析:
- 数据层提供基础操作,指定账户减钱(
outMoney
),指定账户加钱(inMoney
) - 业务层提供转账操作(
transfer
),调用数据层减钱与加钱的操作 - 提供2个账号和金额操作方法执行转账操作
- 基于Spring整合MyBatis环境搭建上述操作
- 数据层提供基础操作,指定账户减钱(
# 案例-代码实现
环境准备
- 数据库准备,在
db1
库下创建账户表
-- 删除表 drop table if exists account; -- 创建账户表 create table account ( id int not null auto_increment, name varchar(10), money double(10,2), constraint pk_account primary key(id) ); -- 添加数据 insert into account(name, money) values('zhangsan', 1000), ('lisi', 1000);
1
2
3
4
5
6
7
8
9
10
11
12
13- 工程准备
- 创建spring-23-transfer模块,初始化pom.xml文件和目录结构
- 数据库准备,在
参考spring-15-mybatis模块,集成MyBatis和Junit
- 在pom.xml文件中引入Spring、MyBatis、Junit依赖坐标
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itheima</groupId> <artifactId>spring-23-transfer</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!-- mysql 驱动坐标 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- druid 依赖坐标 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!-- spring 依赖坐标 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!-- MyBatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <!-- Spring整合MyBatis依赖 --> <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> <!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> </dependency> <!-- spring 整合junit --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies> </project>
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67- 创建
com.itheima.config
包,并在该包下创建三个配置类DruidConfig
、MyBatisConfig
、SpringConfig
package com.itheima.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @PropertySource("druid.properties") public class DruidConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(this.driver); ds.setUrl(this.url); ds.setUsername(this.username); ds.setPassword(this.password); return ds; } // 配置事务管理器,MyBatis使用的就是Jdbc事务 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
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
45package com.itheima.config; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setTypeAliasesPackage("com.itheima.domain"); return sqlSessionFactoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); mapperScannerConfigurer.setBasePackage("com.itheima.mapper"); 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
25
26package com.itheima.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @ComponentScan("com.itheima") @Import({DruidConfig.class, MyBatisConfig.class}) @EnableTransactionManagement public class SpringConfig { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14创建
com.itheima.mapper
包,在该包下创建AccountMapper
Dao层接口package com.itheima.mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper { /** * 加钱 * @param name * @param money */ void inMoney(@Param("name") String name, @Param("money") Double money); /** * 减钱 * @param name * @param money */ void outMoney(@Param("name") String name, @Param("money") Double money); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23在main/resources目录下创建
com/itheima/mapper
目录结构,在mapper目录下创建AccountMapper.xml
映射文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.mapper.AccountMapper"> <update id="inMoney"> update account set money = money + #{money} where name = #{name} </update> <update id="outMoney"> update account set money = money - #{money} where name = #{name} </update> </mapper>
1
2
3
4
5
6
7
8
9
10在main/resources目录下,创建druid.properties属性配置文件
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/db1 jdbc.username=root jdbc.password=1234
1
2
3
4创建
com.itheima.service
和com.itheima.service.impl
包,并在相应包中创建AccountService
接口和AccountServiceImpl
实现类package com.itheima.service; public interface AccountService { /** * 转账操作 * @param out * @param in * @param money */ // @Transactional void transer(String out, String in, Double money); }
1
2
3
4
5
6
7
8
9
10
11
12
13package com.itheima.service.impl; import com.itheima.mapper.AccountMapper; import com.itheima.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void transer(String out, String in, Double money) { accountMapper.inMoney(in, money); int i = 1/0; accountMapper.outMoney(out, money); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20在test/java目录下创建
com.itheima.service.AccountServiceTest
测试类,根据实际情况进行测试package com.itheima.service; import com.itheima.config.SpringConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; // 使用Spring整合Junit专用类加载器 @RunWith(SpringJUnit4ClassRunner.class) // 加载配置类初始化容器 @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer() { accountService.transer("zhangsan", "lisi", 500d); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
测试结果分析
- 不添加Spring事务
- 程序正常执行时,账户金额zhangsan减,lisi加,没有问题
- 程序出现异常后,转账失败,但是异常之前的操作成功,异常之后的操作失败
- 添加Spring事务管理
- 程序正常执行时,账户金额zhangsan减,lisi加,没有问题
- 程序出现异常后,转账失败,可以保障异常前后的操作同时失败
注意
rollbackFor
可以指定触发事务回滚的异常类型,Spring抛出unchecked
异常(继承自RuntimeException
运行时异常)或者Error
才回滚事务;
其他异常不会触发回滚事务,如果在业务方法中抛出了其他类型的异常,还期望Spring能回滚事务,就必须指定rollbackFor
属性。
# Spring事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
# Spring事务相关配置
# 事务配置
# 事务传播
# 事务传播案例
需求分析:
- 需求:实现任意两个账户间转账操作,并对每一次转账操作在数据库进行留痕,即是A账户减钱,B账户加钱,数据库记录日志
- 分析:
- 基于转账操作案例添加日志模块,实现数据库记录日志
- 业务层操作(
transfer
),调用减钱、加钱与记录日志功能
- 实现效果预期:无论转账操作是否成功,均进行转账操作的日志记录
- 存在问题:日志的记录与转账操作同属于一个事务,要么同时成功要么同时失败
- 实现改进:无论转账操作是否成功,日志必须保留
- 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
# 事务传播案例-代码实现
- 数据库准备,创建
tb_log
表
CREATE TABLE tb_log(
id INT PRIMARY KEY AUTO_INCREMENT,
info VARCHAR(255),
createDate DATE
);
2
3
4
5
- 在
com.itheima.mapper
包下创建LogMapper
接口,并在main/resources目录下的com/itheima/mapper
目录下创建LogMapper.xml
映射文件
package com.itheima.mapper;
import org.springframework.stereotype.Repository;
@Repository
public interface LogMapper {
void log(String info);
}
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.LogMapper">
<insert id="log">
insert into tb_log(info, createDate) values(#{info}, now())
</insert>
</mapper>
2
3
4
5
6
7
- 在
com.itheima.service
和com.itheima.service.impl
包下分别创建LogService
接口和LogServiceImpl
实现类
package com.itheima.service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public interface LogService {
// propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
2
3
4
5
6
7
8
9
10
package com.itheima.service.impl;
import com.itheima.mapper.LogMapper;
import com.itheima.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Override
public void log(String out, String in, Double money) {
logMapper.log("转账操作由" + out + "到" + in + ", 金额:" + money);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 修改
AccountServiceImpl
中调用LogService
中添加日志方法
package com.itheima.service.impl;
import com.itheima.mapper.AccountMapper;
import com.itheima.service.AccountService;
import com.itheima.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private LogService logService;
@Override
public void transer(String out, String in, Double money) {
try {
accountMapper.inMoney(in, money);
int i = 1/0;
accountMapper.outMoney(out, money);
} finally {
logService.log(out, in, money);
}
}
}
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
- 运行测试类,查看效果