AOP基础
- AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
AOP的优势:
- 减少重复代码
- 提高开发效率
- 维护方便
使用场景
- 记录系统的操作日志
- 权限控制
- 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务
核心概念
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
特例:切面类:@Aspect切面所在的类,我们一般称为切面类**(被@Aspect注解标识的类)
5. 目标对象:Target,通知所应用的对象
通知Advice
通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
-
@Around:环绕通知,此注解标注的通知方法在目标方法前,后都被执行
-
@Around环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行。
-
@Around环绕通知方法的返回值,必须是Object,来返回原始方法的返回值
-
实例:
@Around("execution(* com.alibaba.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{ //略}
-
-
@PointCut
- 实例:
@Pointcut("execution(* com.alibaba.service.EmpService.login(com.itcast.pojo.Emp))") public void pt(){}; @Around("com.alibaba.aop.TimeAspect.pt()") @Around("pt()") public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) { /* 略 */ return proceed; }
注意事项:
- public:修饰外部切面类也可以引用表达式
- private:修饰只能在当前切面类使用
- @Pointcut注解:可以将切入点表达式抽去到一个方法中,如上方pt()
- 通知类型:在通知类型中@Around需要使用 ProceedingJoinPoint.proceed() 来调用原始方法,其他通知不需要考虑目标方法执行。
通知顺序:(当使用多个切面时,可以通过@Order注解类来控制切面类的执行优先级)
-
不同切面类同时使用,默认按照切面类的类名称字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
-
@Order(number) 加在切面类上来控制顺序()
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
切入点表达式
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
@Before("execution(void com.alibaba.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
-
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 -
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
- 方法的访问修饰符可以省略
- 返回值可以使用
*
号代替(任意返回值类型) - 包名可以使用
*
号代替,代表任意包(一层包使用一个*
) - 使用
..
配置包名,标识此包以及此包下的所有子包 - 类名可以使用
*
号代替,标识任意类 - 方法名可以使用
*
号代替,表示任意方法 - 可以使用
*
配置参数,一个任意类型的参数 - 可以使用
..
配置参数,任意个任意类型的参数
注意事项:
-
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.alibaba.service.DeptService.list(..)) || execution(* com.alibaba.service.DeptService.delete(..))
annotation
-
@annotation 切入点表达式,用于匹配标识有特定注解的方法。
-
@annotation(com.itheima.anno.Log)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.alibaba.anno.MyLog)")
public void before(){
log.info("MyAspect6 -> before ...");
}
}
- execution切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
- annotation 切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
-
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
-
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
ProceedingJoinPoint常用方法:
-
getTarget() //获取目标
-
getTarget().getClass().getName() //获取类名
-
getSignature() //获取目标方法签名
-
getSignature().getName() //获取方法名
-
getArgs(); //获取目标方法运行的参数
-
proceed() //执行原始方法,获取返回值(环绕通知使用)
JoinPoint常用方法:
- getTarget() //获取目标
- getTarget().getClass().getName() //获取目标类名
- getSignature() //获取方法签名
- getSignature().getName() //获取目标方法名
- getArgs() //获取目标方法运行参数
AOP案例
需求:将案例中增、删、改相关接口的操作日志记录到数据库表中
- 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。
操作日志信息包含:
- 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
- 导入依赖:在pom.xml中导入AOP的依赖
- 编写AOP程序:针对于特定方法根据业务需要进行编程
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
实现:
@Component // 声明该类为Spring组件
@Aspect // 声明该类为切面
@Slf4j // 使用Lombok注解,简化日志记录
public class StoreLogs {
@Autowired // 自动注入CreateLogMapper对象
CreateLogMapper createLogMapper;
@Autowired // 自动注入HttpServletRequest对象,用于获取HTTP请求的信息
HttpServletRequest httpServletRequest;
// 定义切点,匹配所有以Insert、Update、Delete结尾的方法
@Pointcut("execution(* com.alibaba.controller.*.*Insert*(..)) || execution(* com.alibaba.controller.*.*Update*(..)) || execution(* com.alibaba.controller.*.*Delete*(..)) ")
public void logs(){}
// 定义环绕通知,记录日志并执行被切入的方法
// @Around("logs()")
public Object aopLogs(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取HTTP请求头中的token
String token = httpServletRequest.getHeader("token");
log.info("token:{}",token);
// 解析token,获取用户信息
Claims claims = JwtUtils.parsJwt(token);
// 获取方法参数
Object[] args = proceedingJoinPoint.getArgs();
// 记录方法执行开始时间
long start = System.currentTimeMillis();
// 执行被切入的方法
Object proceed = proceedingJoinPoint.proceed();
// 记录方法执行结束时间
long end = System.currentTimeMillis();
// 获取方法参数
Object[] args1 = proceedingJoinPoint.getArgs();
// 计算方法执行时间
String sumTime = end-start+"ms";
// 创建日志对象
CreateLog createLog = new CreateLog(claims.get("id").toString()+":"+claims.get("username").toString(),proceedingJoinPoint.getTarget().getClass().getName(),proceedingJoinPoint.getSignature().getName(),Arrays.toString(args),Arrays.toString(args1),sumTime);
// 将日志对象插入到数据库中
createLogMapper.insertLog(createLog);
// 返回方法执行结果
return proceed;
}
}
@Mapper
public interface CreateLogMapper {
@Insert("insert into createlog (name, class_name, method_name, request, response, run_time) value " +
"(#{name},#{className},#{methodName},#{request},#{response},#{runTime})")
void insertLog(CreateLog createLog);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateLog {
private String name;
private String className;
private String methodName;
private String request;
private String response;
private String runTime;
}
create table createlog
(
id int auto_increment comment '主键'
primary key,
name varchar(50) not null comment '操作人',
class_name varchar(100) not null comment '执行方法的全类名',
method_name varchar(100) not null comment '执行方法名',
request varchar(500) not null comment '方法运行时参数',
response varchar(500) not null comment '返回值',
run_time varchar(50) not null comment '方法执行时长',
create_time datetime default CURRENT_TIMESTAMP not null comment '操作时间'
)
collate = utf8mb4_general_ci;
事务
事务: 是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。
事务的操作主要有三步:
- 开启事务(一组操作开始前,开启事务):start transaction / begin ;
- 提交事务(这组操作全部成功后,提交事务):commit ;
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
Transactional注解
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
- 方法
- 当前方法交给spring进行事务管理
- 类
- 当前类中所有的方法都交由spring进行事务管理
- 接口
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
@Transactional注解当中的两个常见的属性:
-
异常回滚的属性:rollbackFor
- 结论:
- 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
- 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
// 所有异常全部回滚 @Transactional(rollbackFor = Exception.class)
-
事务传播行为:propagation
什么是事务的传播行为呢?
- 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
案例
接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。
**需求:**解散部门时需要记录操作日志
由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。
步骤:
- 执行解散部门的业务:先删除部门,再删除部门下的员工(前面已实现)
- 记录解散部门的日志,到日志表(未实现)
准备工作:
- 创建数据库表 dept_log 日志表:
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
- 引入资料中提供的实体类:DeptLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
- 引入资料中提供的Mapper接口:DeptLogMapper
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog log);
}
- 引入资料中提供的业务接口:DeptLogService
public interface DeptLogService {
void insert(DeptLog deptLog);
}
- 引入资料中提供的业务实现类:DeptLogServiceImpl
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional //事务传播行为:有事务就加入、没有事务就新建事务
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
代码实现:
业务实现类:DeptServiceImpl
@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
//根据部门id,删除部门信息及部门下的所有员工
@Override
@Log
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
try {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常
if(true){
throw new Exception("出现异常了~~~");
}
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}finally {
//不论是否有异常,最终都要执行的代码:记录日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
//调用其他业务类中的方法
deptLogService.insert(deptLog);
}
}
//省略其他代码...
}