目录
AOP简介
什么是AOP?
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。
OOP(Object Oriented Programming)面向对象编程
AOP是在不改原有代码的前提下对其进行增强。
AOP作用
作用:在不惊动原始设计的基础上为其进行功能增强
spring理念:无入侵式/无侵入式
AOP核心概念
(1)前面一直在强调,Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。
对于上面的案例中BookServiceImpl中有save , update , delete 和select 方法,
这些方法起了一个名字叫连接点
(2)在BookServiceImpl的四个方法中,update 和delete 只有打印没有计算万次执行消耗时间,
但是在运行的时候已经有该功能,那也就是说update 和delete 方法都已经被增强,
所以对于需要增强的方法起一个名字叫切入点
(3)执行BookServiceImpl的update和delete方法的时候都被添加了一个计算万次执行消耗时间的功能,
将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,起了个名字叫通知
(4)通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,
那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,
那么对于通知和切入点之间的关系描述,我们给起了个名字叫切面
(5)通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类
连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
在SpringAOP中,理解为方法的执行
切入点(Pointcut):匹配连接点的式子
在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
一个具体的方法:如com.green.dao包下的BookDao接口中的无形参无返回值的save方法
匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,
所有带有一个参数的方法
通知(Advice):在切入点处执行的操作,也就是共性功能
在SpringAOP中,功能最终以方法的形式呈现
通知类:定义通知的类
切面(Aspect):描述通知与切入点的对应关系。
AOP入门案例
需求分析
案例设定:测算接口执行效率
简化设定:在方法执行前输出当前系统时间。
思路分析
1.导入坐标(pom.xml)
2.制作连接点(原始操作,Dao接口与实现类)
3.制作共性功能(通知类与通知)
4.定义切入点
5.绑定切入点与通知关系(切面)
环境准备
pom.xml添加Spring依赖
<?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>org.example</groupId>
<artifactId>spring_17_aop_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>13</maven.compiler.source>
<maven.compiler.target>13</maven.compiler.target>
</properties>
<dependencies>
<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>
//添加BookDao和BookDaoImpl类
public interface BookDao {
public void save();
public void update();
public void delete();
public void select();
}
@Repository
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 ...");
}
}
//创建Spring的配置类
@Configuration
@ComponentScan("com.green")
public class SpringConfig {
}
//编写App运行类
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}
AOP实现步骤
定义通知类和通知
定义切入点
要增强的是update方法,该如何定义呢?
//通知类
public class MyAdvice {
//切入点
@Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤5:制作切面
切面是用来描述通知和切入点之间的关系
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行
public class MyAdvice {
//切入点
@Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
private void pt(){}
@Before("pt()") //在切入点前面执行
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤6:将通知类配给容器并标识其为切面类
@Component
@Aspect
public class MyAdvice {
//定义切入点
@Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
private void pt(){}
@Before("pt()") //在切入点前面执行
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤7:开启注解格式AOP功能
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy//注解开发AOP
public class SpringConfig {
}
步骤8:运行程序
看到在执行update方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP编程成功。
名称 | @EnableAspectJAutoProxy | @Aspect | @Pointcut |
类型 | 配置类注解 | 类注解 | 方法注解 |
位置 | 配置类定义上方 | 切面类定义上方 | 切入点方法定义上方 |
作用 | 开启注解格式AOP功能 | 设置当前类为AOP切面类 | 设置切入点方法 |
属性 | value(默认):切入点表达式 |
名称 | @Before |
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |
AOP工作流程
由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起:
流程1:Spring容器启动
容器启动就需要去加载bean,哪些类需要被加载呢?
需要被增强的类,如:BookServiceImpl
通知类,如:MyAdvice
注意此时bean对象还没有创建成功
流程2:读取所有切面配置中的切入点
流程3:初始化bean
判定bean对应的类中的方法是否匹配到任意切入点
注意第1步在容器启动的时候,bean对象还没有被创建成功。
要被实例化bean对象的类中的方法和切入点进行匹配
匹配失败,创建原始对象,如UserDao
匹配失败说明不需要增强,直接调用原始对象的方法即可。
匹配成功,创建原始对象(目标对象)的代理对象,如: BookDao
匹配成功说明需要对其进行增强
对哪个类做增强,这个类对应的对象就叫做目标对象
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
流程4:获取bean执行方法
获取的bean是原始对象时,调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
验证容器中是否为代理对象
如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
验证思路
1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的getClass()方法
2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的getClass()方法
3.观察两次打印的结果
步骤1:修改App类,获取类的类型
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
步骤2:修改MyAdvice类,不增强
@Component
@Aspect
public class MyAdvice {
//定义切入点
@Pointcut("execution(void com.green.dao.BookDao.update1())") //连接点方法
private void pt(){}
@Before("pt()") //在切入点前面执行
public void method(){
System.out.println(System.currentTimeMillis());
}
}
因为定义的切入点中,被修改成update1 ,所以BookDao中的update方法在执行的时候,就不会被增强
所以容器中的对象应该是目标对象本身。
步骤3:运行程序
步骤4:修改MyAdvice类,增强
因为定义的切入点中,被修改成update ,所以BookDao中的update方法在执行的时候,就会被增强
所以容器中的对象应该是目标对象的代理对象
@Component
@Aspect
public class MyAdvice {
//定义切入点
@Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
private void pt(){}
@Before("pt()") //在切入点前面执行
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤5:运行程序
AOP核心概念
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
AOP配置管理
AOP切入点表达式
对于AOP中切入点表达式,总共会学习三个内容,分别是 语法格式 、 通配符和书写技巧
语法格式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
描述方式一:执行com.green.dao包下的BookDao接口中的无参数update方法
描述方式二:执行com.green.dao.impl包下的BookDaoImpl类中的无参数update方法
因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。
对于切入点表达式的语法为:
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.green.service.UserService.findById(int))
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是public,private等,可以省略
User:返回值,写返回值类型
com.green.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开异常名:方法定义中抛出指定异常,可以省略
通配符
* :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.green.*.UserService.find*(*))
匹配com.green包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
.. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
+ :专用于匹配子类类型
execution(* *..*Service+.*(..))
*Service+,表示所有以Service结尾的接口的子类。
书写技巧
所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常作为匹配规则
AOP通知类型
类型介绍
前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)
(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,
如果方法执行抛出异常,返回后通知将不会被添加
(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,
只有方法抛出异常后才会被添加
(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后
环境准备
pom.xml添加Spring依赖
<dependencies>
<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>
添加BookDao和BookDaoImpl类
创建Spring的配置类
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy//注解开发AOP
public class SpringConfig {
}
创建通知类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.green.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.green.dao.BookDao.select())")
private void pt2(){}
//切入点之前
// @Before("pt()")
public void before(){
System.out.println("before advice...");
}
//切入点之后
// @After("pt()")
public void after(){
System.out.println("after advice...");
}
//环绕
// @Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice...");
}
// @Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice...");
//表示对原始操作的调用
Object result = pjp.proceed();
System.out.println("around after advice...");
//需要将返回值扔回去
return result;
}
// @AfterReturning("pt2()")
public void afterReturning(){
System.out.println("afterReturning advice...");
}
@AfterThrowing("pt2()")
public void afterThrowing(){
System.out.println("afterThrowing advice...");
}
}
编写App运行类
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
int num = bookDao.select();
//int i = 1 / 0;
System.out.println(num);
}
}
名称 | @After |
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 |
名称 | @AfterReturning |
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行 |
名称 | @AfterThrowing |
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 |
名称 | @Around |
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行 |
环绕通知注意事项
1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,
进而实现原始方法调用前后同时添加通知
2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
业务层接口执行效率
需求分析
需求:任意业务层接口执行均可显示其执行效率(执行时长)
具体实现的思路:
(1) 开始执行方法之前记录一个时间
(2) 执行方法
(3) 执行完方法之后记录一个时间
(4) 用后一个时间减去前一个时间的差值,就是我们需要的结果。
pom.xml添加Spring依赖
<dependencies>
<!--spring依赖-->
<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.47</version>
</dependency>
<!--spring-jdbc整合-->
<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>
<!--解析切入点表达式依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
添加AccountService、AccountServiceImpl、AccountDao与Account类
public interface AccountService {
void save(Account account);//添加
void delete(Integer id);//删除
void update(Account account);//修改
List<Account> findAll();//查询所有
Account findById(Integer id);//根据id查询数据
}
@Service
public class AccountServiceImpl implements AccountService {
//自动装配
@Autowired
private AccountDao accountDao;
@Override
public void save(Account account) {
accountDao.save(account);
}
@Override
public void delete(Integer id) {
accountDao.delete(id);
}
@Override
public void update(Account account) {
accountDao.update(account);
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
@Override
public Account findById(Integer id) {
return accountDao.findById(id);
}
}
public interface AccountDao {
//添加数据
@Insert("insert into tbl_account(name,money)values(#{name},#{money})")
void save(Account account);
//删除数据
@Delete("delete from tbl_account where id = #{id} ")
void delete(Integer id);
//根据id修改数据
@Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
void update(Account account);
//查询所有数据
@Select("select * from tbl_account")
List<Account> findAll();
//根据id查询数据
@Select("select * from tbl_account where id = #{id} ")
Account findById(Integer id);
}
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
//setter..getter..toString方法省略
}
resources下提供一个jdbc.properties
创建相关配置类
Spring配置类:SpringConfig
@Configuration
@ComponentScan("com.green") //包扫描
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class}) //导入配置类
@EnableAspectJAutoProxy
public class SpringConfig {
}
//JdbcConfig配置类
public class JdbcConfig {
@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(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
//mybatis配置类
public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
//设置模型类的别名扫描
ssfb.setTypeAliasesPackage("com.green.domain");
//设置数据源
ssfb.setDataSource(dataSource);
return ssfb;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.green.dao");
return msc;
}
}
编写Spring整合Junit的测试类
功能开发
创建AOP的通知类
该类要被Spring管理,需要添加@Component
要标识该类是一个AOP的切面类,需要添加@Aspect
配置切入点表达式,需要添加一个方法,并添加@Pointcut
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有接口
@Pointcut("execution(* com.green.service.*Service.*(..))")
private void servicePt() {}
@Around("ProjectAdvice.servicePt()")
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();
System.out.println("万次执行: " + className + "." + methodName + "===>" + (end - start) + "ms");
}
}
AOP通知获取数据
获取参数
获取返回值
获取异常
获取切入点方法的参数,所有的通知类型都可以获取参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知
获取切入点方法返回值
返回后通知
环绕通知
获取切入点方法运行异常信息
抛出异常后通知
环绕通知
环境准备
pom.xml添加Spring依赖
添加BookDao和BookDaoImpl类
public interface BookDao {
String findName(int id,String password);
}
@Repository
public class BookDaoImpl implements BookDao {
@Override
public String findName(int id,String password) {
System.out.println("id:" + id);
return "ok";
}
}
创建Spring的配置类
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy
public class SpringConfig {
}
编写App运行类
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
String name = bookDao.findName(100,"root");
// if (true)throw new NullPointerException();//异常测试
System.out.println(name);
}
}
编写通知类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.green.dao.BookDao.findName(..))")
private void pt() {
}
// @Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ...");
}
// @After("pt()")
public void after(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("after advice ...");
}
// @Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args);
return ret;
}
// @AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning advice ..." + ret);
}
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..." + t);
}
}
使用JoinPoint的方式获取参数适用于前置 、 后置 、 返回后 、 抛出异常后 通知
获取返回值
获取异常
百度网盘密码数据兼容处理
需求分析
需求: 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。
①:在业务方法执行之前对所有的输入参数进行格式处理—— trim()
②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
环境准备
pom.xml添加Spring依赖
<dependencies>
<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>
添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl类
public interface ResourcesService {
public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;
public boolean openURL(String url, String password) {
return resourcesDao.readResources(url,password);
}
}
public interface ResourcesDao {
boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
public boolean readResources(String url, String password) {
System.out.println(password.length());
//模拟校验
return password.equals("root");
}
}
创建Spring的配置类
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy
public class SpringConfig {
}
运行类
public class AppCase {
public static void main(String[] args) {
ApplicationContext cxt = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourcesService resourcesService = cxt.getBean(ResourcesService.class);
boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
System.out.println(flag);
}
}
通知类
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.green.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
Object ret = pjp.proceed(args);
return ret;
}
}
AOP事务管理
Spring事务简介
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
举个简单的例子,
转账业务会有两次数据层的调用,一次是加钱一次是减钱
把事务放在数据层,加钱和减钱就有两个事务
没办法保证加钱和减钱同时成功或者同时失败
这个时候就需要将事务放在业务层进行处理。
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:
转账案例-需求分析
需求: 实现任意两个账户间转账操作
需求微缩: A账户减钱,B账户加钱
实现步骤:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
转账案例-环境搭建
步骤1:准备数据库表
步骤2:创建项目导入jar包
<dependencies>
<!--spring依赖-->
<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.47</version>
</dependency>
<!--spring-jdbc整合-->
<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>
</dependencies>
步骤3:根据表创建模型类
步骤4:创建Dao接口
步骤5:创建Service接口和实现类
步骤6:添加jdbc.properties文件
步骤7:创建JdbcConfig配置类
步骤8:创建MybatisConfig配置类
步骤9:创建SpringConfig配置类
步骤10:编写测试类
事务管理
上述环境,运行单元测试类,会执行转账操作, Tom 的账户会减少100,Jerry 的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:
@Service
public class AccountServiceImpl implements AccountService {
//自动装配
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String out, String in, Double money) {
accountDao.inMoney(in, money);
// int i = 1 / 0;
accountDao.outMoney(out, money);
}
}
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
步骤1:在需要被事务管理的方法上添加注解
@Service
public class AccountServiceImpl implements AccountService {
//自动装配
@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out, String in, Double money) {
accountDao.inMoney(in, money);
// int i = 1 / 0;
accountDao.outMoney(out, money);
}
}
注意:
写在接口类上,该接口的所有实现类的所有方法都会有事务
写在接口方法上,该接口的所有实现类的该方法都会有事务
写在实现类上,该类中的所有方法都会有事务
写在实现类方法上,该方法上有事务
建议写在实现类或实现类的方法上
步骤2:在JdbcConfig类中配置事务管理器
public class JdbcConfig {
@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(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//事务管路
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
步骤3:开启事务注解
@Configuration
@ComponentScan("com.green") //包扫描
//@PropertySource:加载类路径jdbc.properties文件
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class}) //导入配置类
@EnableTransactionManagement //事务管理
public class SpringConfig {
}
名称 | @EnableTransactionManagement |
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 设置当前Spring环境中开启注解式事务支持 |
名称 | @Transactional |
类型 | 接口注解 类注解 方法注解 |
位置 | 业务层接口上方 业务层实现类上方 业务方法上方 |
作用 | 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务) |
Spring事务角色
事务管理员 和 事务协调员
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
1. 未开启Spring事务之前:
AccountDao的outMoney因为是修改操作,会开启一个事务T1
AccountDao的inMoney因为是修改操作,会开启一个事务T2
AccountService的transfer没有事务
运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行就会导致数据出现错误
2. 开启Spring的事务管理后
transfer上添加了@Transactional注解,在该方法上就会有一个事务T
AccountDao的outMoney方法的事务T1加入到transfer的事务T中
AccountDao的inMoney方法的事务T2加入到transfer的事务T中
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
注意:
目前的事务管理是基于DataSourceTransactionManager和SqlSessionFactoryBean使用的是同一个数据源
Spring事务属性
事务配置
上面这些属性都可以在@Transactional 注解的参数上进行设置。
出现这个问题的原因是,Spring的事务只会对Error异常 和RuntimeException异常及其子类进行事务回滚
其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out, String in, Double money) throws IOException {
accountDao.outMoney(out, money);
//int i = 1/0;
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:添加LogDao接口
步骤3:添加LogService接口与实现类
步骤4:在转账的业务中添加记录日志
public interface AccountService {
/**
* 转账操作
*
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out, String in, Double money) throws IOException;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Transactional
public void transfer(String out, String in, Double money) {
try {
accountDao.outMoney(out, money);
accountDao.inMoney(in, money);
} finally {
logService.log(out, in, money); }
}
}
步骤5:运行程序
当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
这个结果和我们想要的不一样,什么原因?该如何解决?
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败最终效果:无论转账操作是否成功,日志必须保留
事务传播行为
1.修改logService改变事务的传播行为
2.事务传播行为的可选值