1. AOP的基本概念
AOP(Aspect Oriented Programming),即面向切面编程。 (OOP:Object 面向对象编程)
比如去银行取款和查询余额
有了AOP,你写代码时不需要把这个验证用户步骤写进去,即完全不考虑验证用户。只写取款和显示余额的业务代码。而在另一个地方,写好验证用户的代码。这个验证用户的代码就是切面代码,以后在执行取款和显示余额的时候,利用代理模式。将验证用户的功能在执行取款和显示余额前调用。
代码在Spring容器中执行的时候,通过配置告诉Spring你要把这段代码加到哪几个地方,Spring就会在执行正常业务流程的时候帮你把验证代码和取款代码织入到一起。
AOP真正目的是:你写代码的时候,只需考虑主流程,而不用考虑那些不重要的,但又必须要写的其它相同的代码,这些其它的相同代码所在的类就是切面类。
面向切面编程的核心就是代理模式。
AOP术语
JoinPoint(连接点):在程序执行过程中的某个阶段点,连接点就是指主业务方法的调用,它是客观存在的。
Pointcut(切入点):切入点指的是类或者方法名,满足某一规则的类或方法都是切入点,通过切入点表达式来制定规则。
Advice(通知):切入点处所要执行的程序代码,即切面类中要执行的公共方法。通知的类型有: 前置通知,后置通知,异常通知,最终通知,环绕通知。
Target(目标对象):被代理的对象。比如动态代理案例中的明星,房东。
Weaving(织入):织入指的是把新增的功能用于目标对象,创建代理对象的过程。
Proxy(代理):一个类被AOP织入增强后产生的结果类,即代理类。比如动态代理案例中的经纪人或中介
Aspect(切面):切面指的是切入点(规则)和通知(织入方法)的类 = 切入点+通知
Spring中代理方式的说明
-
Spring在AOP编程中使用代理的方式
-
目标对象有接口:使用JDK代理
-
目标对象没有接口:使用CGLIB代理
-
-
在Spring的AOP编程中,代理对象则由Spring创建,不用自己写了。
-
我们要做的就是配置AOP
2. AOP编程:基于XML的配置
例子
当向数据库中保存账户的时候,使用日志记录下这次保存操作
面向切面编程的流程
-
开发业务类:添加账户
-
开发切面类:记录日志
-
使用AOP将业务类与切面类织入到一起,实现需求的功能
步骤
-
AccountService业务接口有void save() 添加账户的方法
-
AccountServiceImpl实现业务接口类,输出"保存账户"
-
创建LogAspect切面类,编写通知方法void printLog(),输出:"执行添加操作"
-
配置文件applicationContext.xml
-
<aop:config>
-
<aop:pointcut>
-
<aop:aspect>
-
<aop:before>
-
操作
pom.xml
-
导入spring-context
-
导入开源的面向切面编程的组件: aspectjweaver
-
导入JUnit5
<?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.it</groupId>
<artifactId>AOP</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- Spring IOC 依赖以及Cglib支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- AspectJ切面表达式支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.0.RELEASE</version>
<scope>test</scope>
</dependency>
<!--junit5-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
业务接口
package com.it.service;
/**
* 账户业务接口
*/
public interface AccountService {
/**
* 保存账户
*/
void save();
}
业务实现类
package com.it.service.impl;
import com.it.service.AccountService;
public class AccountServiceImpl implements AccountService {
/**
* 保存账户
*/
@Override
public void save() {
System.out.println("保存账户");
}
}
记录日志的工具类
package com.it.utils;
import java.sql.Timestamp;
/**
* 记录日志功能的类:切面类 = 切入点(规则)+通知(方法)
*/
public class LogAspect {
/**
* 记录日志
*/
public void printLog() {
System.out.println(new Timestamp(System.currentTimeMillis()) + " 记录日志");
}
}
执行流程分析
XML中关于AOP的配置
applicationContext.xml配置文件
-
配置日志记录类,这是切面类:LogAspect
-
配置正常的业务类:AccountServiceImpl
-
AOP配置 execution(public void com.it.service.impl.AccountServiceImpl.save())
注:在导入aop的命名空间,idea可以自动导入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--正常业务对象-->
<bean class="com.it.service.impl.AccountServiceImpl" id="accountService"/>
<!-- 切面类:日志记录对象 -->
<bean class="com.it.utils.LogAspect" id="logAspect"/>
<!-- 编写aop的配置,要导入aop的命名空间 -->
<aop:config>
<!--
配置切入点,通过切入点表达式配置
id:给表达式定义唯一标识
expression: 使用切入点函数定义表达式,语法:访问修饰符 返回类型 包名.类名.方法名(参数类型) 抛出异常类型
-->
<aop:pointcut id="pt" expression="execution(public void com.it.service.impl.AccountServiceImpl.save())"/>
<!-- 切面配置, ref引用切面对象id -->
<aop:aspect ref="logAspect">
<!--
使用什么类型的通知:前置通知,后置通知等
method:表示切面中方法名字
pointcut-ref:引用上面切入点表达式
-->
<aop:before method="printLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
</beans>
测试类
/**
* 测试类
*/
@ExtendWith(SpringExtension.class) //指定第三方运行类
@ContextConfiguration("classpath:applicationContext.xml")
public class TestAccount {
@Autowired
private AccountService accountService;
@Test
public void testSave() {
System.out.println("业务对象类型:" + accountService.getClass());
accountService.save();
}
}
执行效果
业务对象类型:class com.sun.proxy.$Proxy17
2021-01-11 09:22:10.164 记录日志
保存账户
3. AspectJ表达式
作用
切入点表达式的作用:一组规则,指定哪些类和方法要被切入
切点函数
切入点函数 | 作用 |
---|---|
execution | 细粒度函数,精确到方法 |
within | 粗粒度,只能精确到类 |
bean | 粗粒度,精确到类,从容器中通过id获取对象 |
execution表达式语法
?表示出现0次或1次
() 没有参数
(*) 1个或多个参数
(..) 0个或多个参数.. 表示当前包和子包
4. AOP编程:常用标签和通知类型
AOP中的标签
通知类型介绍
-
前置通知:在主业务方法前执行
-
后置通知:在主业务方法后执行
-
异常通知:在主业务方法抛出异常的时候执行
-
最终通知:无论主业务方法是否出现异常,都会执行。注:如果配置在后置通知的前面,会先执行最终通知
-
环绕通知:相当于上面所有通知的组合
5.AOP编程:环绕通知
环绕通知的功能
-
可以环绕目标方法来执行
-
可以获取目标方法的各种信息
-
可以修改目标方法的参数和返回值
ProceedingJoinPoint接口
Spring框架提供了ProceedingJoinPoint接口,作为环绕通知的参数。在环绕通知执行的时候,Spring框架会提供接口的对象,我们直接使用即可。
ProceedingJoinPoint接口中方法 | 功能 |
---|---|
Object[] getArgs() | 获取目标方法的参数 |
proceed(Object[] args) | 调用目标方法,如果没有执行这句话,目标方法不会执行 |
proceed() | 调用目标方法,使用它原来的参数 |
getSignature() | 获取目标方法其它的信息,如:类名,方法名等 |
在通知类中编写环绕通知方法:
package com.it.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import java.util.Arrays;
/**
* 记录日志功能的类:切面类 = 切入点(规则)+通知(方法)
*/
public class LogAspect {
/**
* 环绕通知方法
* @param joinPoint 接口,由Spring注入对象
* @return 方法的返回值
*/
public Object around(ProceedingJoinPoint joinPoint) {
//获取目标方法签名对象
Signature signature = joinPoint.getSignature();
System.out.println("目标方法名字:" + signature.getName());
//获取参数的数组
Object[] args = joinPoint.getArgs();
System.out.println("目标方法参数:" + Arrays.toString(args));
System.out.println("目标对象实现接口的全名:" + signature.getDeclaringTypeName());
Object result = null;
//修改目标方法参数
args[0] = "白骨精";
try {
System.out.println("前置通知");
//如果修改了参数,要使用带参数的方法。(如果不执行这句话,目标方法不会执行)
result = joinPoint.proceed(args); //直接调用目标方法
System.out.println("后置通知");
} catch (Throwable throwable) {
System.out.println("异常通知");
} finally {
System.out.println("最终通知");
}
return 99;
}
}
applicationContext.xml 配置环绕通知的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--正常业务对象-->
<bean class="com.it.service.impl.AccountServiceImpl" id="accountService"/>
<!-- 切面类:日志记录对象 -->
<bean class="com.it.utils.LogAspect" id="logAspect"/>
<!-- 编写aop的配置,要导入aop的命名空间 -->
<aop:config>
<!--
配置切入点,通过切入点表达式配置
id:给表达式定义唯一标识
expression: 使用切入点函数定义表达式,语法:访问修饰符 返回类型 包名.类名.方法名(参数类型) 抛出异常类型
-->
<aop:pointcut id="pt" expression="execution(* update(..))"/>
<!-- 切面配置, ref引用切面对象id -->
<aop:aspect ref="logAspect">
<!--
使用什么类型的通知:前置通知,后置通知等
method:表示切面中方法名字
pointcut-ref:引用上面切入点表达式
-->
<aop:around method="around" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
</beans>
6. AOP编程:注解方式实现
相关的注解
基于XML声明式的AOP需要在配置文件中配置不少信息。为了解决这个问题,AspectJ框架为AOP的实现提供了一套注解,用以取代applicationContext.xml文件中配置代码。
注解 | 说明 |
---|---|
@Aspect | 用在类上,表示这是一个切面类。 这个切面类要放到IoC容器中,所以类上还要加@Component注解 |
@Before | 用在方法上,表示这是一个前置通知 value:指定切入点表达式 |
@AfterReturning | 用在方法上,表示这是一个后置通知 |
@AfterThrowing | 用在方法上,表示这是一个异常通知 |
@After | 用在方法上,表示这是一个最终通知。注:最终通知在后置通知之前执行 |
@Around | 用在方法上,表示这是一个环绕通知 |
@Pointcut | 用在方法上,用来定义切入点表达式 方法名:随意起 返回值:void 方法体:为空 |
相关的配置标签
作用:开启Spring中自动代理,注解的方式来使用AOP
<aop:aspectj-autoproxy/>标签 | 作用 |
---|---|
proxy-target-class | true 使用CGLIB进行代理 |
false 默认使用JDK代理,如果有接口才起作用 |
AccountServiceImpl,使用@Service注解的方式
package com.it.service.impl;
import com.it.service.AccountService;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
/**
* 保存账户
*/
@Override
public void save() {
System.out.println("保存账户");
}
/**
* 更新账户
* @param name
* @return
*/
@Override
public int update(String name) {
System.out.println("更新了" + name + "的账户");
if ("newboy".equals(name)) {
throw new RuntimeException("这是个穷人");
}
return 1;
}
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描基包 -->
<context:component-scan base-package="com.it"/>
<!-- 配置使用注解来实现aop -->
<aop:aspectj-autoproxy/>
</beans>
修改切面类,使用注解
package com.it.utils;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 记录日志功能的类:切面类 = 切入点(规则)+通知(方法)
*/
@Component
@Aspect //这是一个切面类
public class LogAspect {
//定义切入点函数
@Pointcut("execution(* com.itheima.service..*(..))")
public void pt() {
}
@Before("pt()") //写方法的名字
public void before() {
System.out.println("---前置通知---");
}
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("---后置通知---");
}
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("---异常通知---");
}
@After("pt()")
public void after() {
System.out.println("---最终通知---");
}
}
7.AOP注解:使用环绕通知
步骤
-
方法带参数ProceedingJoinPoint
-
方法有返回值Object,并且抛出Throwable异常
-
使用注解@Around,指定切点表达式为com.itheima.service下面的类
-
分别输出:前置,后置,异常,最终通知。
/**
* 环绕通知方法
* @param joinPoint 接口,由Spring注入对象
* @return 方法的返回值
*/
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) {
//获取目标方法签名对象
Signature signature = joinPoint.getSignature();
System.out.println("目标方法名字:" + signature.getName());
//获取参数的数组
Object[] args = joinPoint.getArgs();
System.out.println("目标方法参数:" + Arrays.toString(args));
System.out.println("目标对象实现接口的全名:" + signature.getDeclaringTypeName());
Object result = null;
//修改目标方法参数
args[0] = "白骨精";
try {
System.out.println("前置通知");
//如果修改了参数,要使用带参数的方法。(如果不执行这句话,目标方法不会执行)
result = joinPoint.proceed(args); //直接调用目标方法
System.out.println("后置通知");
} catch (Throwable throwable) {
System.out.println("异常通知");
} finally {
System.out.println("最终通知");
}
return 99;
}
8.声明式事务
事务的概念
事务以后我们主要用在业务层
业务层中一个方法会多次调用DAO层中增删改,所有的方法必须全部执行成功,如果有一个方法执行失败,就要进行回滚。要么全部成功,要么全部失败。
事务的特性
事务特性 | 说明 |
---|---|
原子性(Automicity) | 每个事务是一个最小的执行单元,它做为一个整体运行,不可再拆分 |
一致性(Consistency) | 事务执行前和执行后对数据库的状态影响是一致的 如:转账前和转账后的总金额是一致的 |
隔离性(Isolation) | 事务与事务之间不能相互影响,它们之间是相互隔离的 |
持久性(Durability) | 事务提交后对数据库的影响是永久的,关机以后也是存在的 |
事务的隔离级别
事务管理方式介绍
-
编程式事务管理:
通过编写代码实现的事务管理,包括定义事务的开始,正常执行后事务提交和异常时的事务回滚。
-
声明式事务管理:
-
通过AOP技术实现事务管理,主要思想是将事务管理作为一个"切面"代码单独编写,然后通过AOP技术将事务管理的"切面"代码织入到业务目标类中。
-
优点在于开发者无须通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中。
-
Spring对事务的处理方式介绍
事务是切面,主业务类还是以前的方法
-
JavaEE体系进行分层开发,事务处理位于业务层。
-
Spring 框架为我们提供了一组事务控制的接口,这组接口是在spring-tx-版本.RELEASE.jar中。
-
Spring 的事务控制都是基于 AOP 的,它既可以使用编程的方式实现,也可以使用配置的方式实现。这种基于AOP方式实现的事务称为声明式事务。
9. XML实现声明式事务的配置
技术点
<tx:advice>
使用之前要需要导入tx命名空间,注:不要选错了,事务的空间,而不是缓存
功能:事务通知配置的父元素
属性 | 说明 |
---|---|
id | 事务通知配置的标识 |
transaction-manager | 从容器中获取事务管理器对象,前提:在IoC中要存在一个事务管理器对象 |
<tx:attributes>
功能:tx:method标签的父标签,指定不同方法事务的属性
<tx:method>
事务通知的配置
这里事务是切面类,要织入到主业务类中的对象
AOP的配置
步骤:Spring声明式事务控制配置
-
配置事务管理器DataSourceTransactionManager(spring提供的事务切面类)
-
注入dataSource的对象
-
-
使用tx命名空间配置通知规则,表示拦截到方法后匹配这里的规则。
-
配置<tx:advice> ,注:导入的是事务空间,而不是同名的tx缓存空间
-
id属性给下面的AOP使用,指定配置事务管理器
-
配置transfer方法,指定事务的传播行为REQUIRED,不是只读事务
-
配置查询方法find开头的方法,传播行为SUPPORTS,只读事务
-
-
AOP的配置<aop:config>
-
配置切入点表达式<aop:pointcut>
-
建立切入点表达式与通知规则的对应关系<aop:advisor>
-
-
执行测试类
代码
-
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--1. 开启注解扫描-->
<context:component-scan base-package="com.it"/>
<!--2. 加载外部的配置文件druid.properties-->
<context:property-placeholder location="classpath:druid.properties"/>
<!--3. 创建数据源,引用上面的属性-->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driverClassName}"/>
</bean>
<!--4. 创建JdbcTemplate,注入数据源-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 声明式事务切面的配置,指定事务管理器 -->
<tx:advice id="interceptor" transaction-manager="transactionManager">
<tx:attributes>
<!--
指定哪些方法需要使用事务,以及使用事务的规则
read-only: 是否只读事务
propagation:传播行为
isolation:隔离级别
-->
<tx:method name="transfer" read-only="false" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="find*" read-only="true" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<!-- AOP的配置 -->
<aop:config>
<!-- 切面表达式的配置 -->
<aop:pointcut id="pt" expression="execution(* com.itheima.service..*.*(..))"/>
<!-- 配置上面的事务 -->
<aop:advisor advice-ref="interceptor" pointcut-ref="pt"/>
</aop:config>
</beans>
2.业务层代码(模拟转账失败)
package com.it.service.impl;
import com.it.dao.AccountDao;
import com.it.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
/**
* 实现转账的功能
* @param fromUser 转出账户
* @param toUser 转入账户
* @param money 金额
*/
@Override
public void transfer(String fromUser, String toUser, double money) {
//扣钱
accountDao.updateAccount(fromUser, -money);
//模拟出现异常
//System.out.println(1 / 0);
//加钱
accountDao.updateAccount(toUser, money);
System.out.println("转账成功");
}
}
10.注解实现声明式事务的API
## @Transactional
#### 作用
放在类或方法上,让这个方法使用事务
#### 注解的应用范围
1. 类上面:表示这个类中所有的方法都使用事务
2. 方法上面:表示这个方法使用事务
3. 接口:表示只要实现这个接口的所有子类中所有方法都使用事务
4. 接口中方法:表示实现这个接口的子类中这个方法使用事务#### 注解的参数和作用
| 参数名称 | 描述 |
| ------------- | ------------------------------------------------ |
| propagation | 传播行为 |
| isolation | 隔离级别 |
| readOnly | 是否只读 |
| timeout | 超时时间,默认是-1,表示不超时 |
| rollbackFor | 哪些异常会进行回滚,默认只对非运行时异常进行回滚 |
| noRollbackFor | 哪些异常不进行回滚 |
注解式事务的步骤
-
在配置文件中:配置事务管理器,注入数据源,配置注解式事务驱动:\<tx:annotation-driven>,指定事务管理器。
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置注解式事务的驱动,指定事务管理器 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
2.在业务代码中类或方法上使用注解:@Transactional
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
/**
* 实现转账的功能
* @param fromUser 转出账户
* @param toUser 转入账户
* @param money 金额
*/
@Override
@Transactional(noRollbackFor = ArithmeticException.class, propagation = Propagation.REQUIRED)
public void transfer(String fromUser, String toUser, double money) {
//扣钱
accountDao.updateAccount(fromUser, -money);
//模拟出现异常
//System.out.println(1 / 0);
//加钱
accountDao.updateAccount(toUser, money);
System.out.println("转账成功");
}
}