AOP:
springAOP是什么:
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。
使用场景:
比如你想统计业务中每个方法的执行耗时,那我们最初的想法就是对每个方法一开始写一个获取方法运行开始时间,然后再来一个获取方法运行结束时间
然后相减,那这个做法思路简单,不过问题也很明显,就是如果都每个方法都这样做,我们需要写很多重复的代码,所以AOP就可以解决这样的问题
再比如你想知道谁在业务中调用了增,删,改方法,想将这个调用方法的信息存入到信息日志表中,我们也可以用AOP来解决这样的问题。
springAOP的代理设计思想:
代理模式:
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
生活中的代理:
-
广告商找大明星拍广告需要经过经纪人
-
合作伙伴找大老板谈合作要约见面时间需要经过秘书
-
房产中介是买卖双方的代理
-
太监是大臣和皇上之间的代理 相关术语:
静态代理:
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("方法内部 result = " + result);
return addResult;
}
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理
动态代理:
动态代理技术分类
-
JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)
-
cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹) JDK动态代理技术实现(了解)
代理工程:基于jdk代理技术,生成代理对象
AOP的核心概念和执行流程:
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象:Target,通知所应用的对象
连接点:
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型
JoinPoint的基本方法:
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
Object res = joinPoint.proceed(); //执行原始方法,获取返回值(环绕通知)(最后一种只有环绕通知能使用)
通知类型:
1:@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(如果目标方法有异常,那环绕通知的第三格部分结束方法就不会执行)
2:@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3:@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
4:@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(这个通知程序必须正常执行才会运行)
5:@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行(这个只有程序有异常了才会运行)
从上面几种通知就能看出来,第四个和第五个是两个对立的通知。
注意:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。(拿不到返回值的话,界面上就不会显示数据)
切入点:
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知 (Advice)
常见形式:
1:execution(……):根据方法的签名来匹配
2:@annotation(……) :根据注解匹配
切入点表达式的语法:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
例子:execution(public void com.springboottlias.service.impl.DeptServiceImpl.deletedept(java.lang.Integer))
访问修饰符:可省略(比如: public、protected)
切入点表达式中的通配符:* 和 ..
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
例子:execution(* com.*.service.*.update*(*))
第一个*表示:任意返回值
第二个*表示:com包下的任意包
第三个*也表示service的包名或类名
第四个*表示方法名中开头是update的方法名,如果是*deptservice这样的,说明以deptservice结尾的方法名
第五个*表示update*方法中任意的一个参数
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
例子:execution(* com.itheima..DeptService.*(..))
表示这个函数里面可以有任意个参数。
AOP执行流程:
一旦我们进行了AOP的方法开发,那我们运行得就不是原始对象得方法了,运行得就是一个代理对象
这个代理对象如何理解呢:可以理解为是原始对象方法得加强版(这个加强得意思就是在这个代理对象中多了其它的方法)
当我们想执行原来的原始对象方法,这个时候我们就不是执行原始对象,我们执行的就是这个代理对象。
AOP案例:
将案例中 增、删、改 相关接口的操作日志记录到数据库表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
1:准备:
这个准备包括引入AOP的依赖,然后建一个日志记录表还有一个对呀日志记录表的实体类
1:在案例工程中引入AOP的起步依赖
<!-- AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2:导入资料中准备好的数据库表结构,并引入对应的实体类
下面是实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
Sql脚本:
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
2:自定义注解 @Log:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
3:定义切面类,完成记录操作日志的逻辑
@Slf4j
@Component//将这个类交给IOC容器管理
@Aspect//声明这个类是一个AOP类
public class OperateLogAspect{
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Pointcut("@annotation(com.springboottlias.anno.Log)")
private void pt(){}
@Around("pt()")
public Object RecordtoDatebase(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//操作人ID - 当前登录员工ID
//获取请求头中的jwt令牌,解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUserId = (Integer) claims.get("id");
//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作类名
String classname = proceedingJoinPoint.getTarget().getClass().getName();
//操作方法名
String methodname = proceedingJoinPoint.getSignature().getName();
//操作方法参数
Object[] args = proceedingJoinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();//原始方法执行之前的时间
//调用原始目标方法运行
Object res = proceedingJoinPoint.proceed();
//方法返回值
String returnValue = JSONObject.toJSONString(res);
//操作耗时
long end = System.currentTimeMillis();//原始方法执行之后的时间
long costtime = end-begin;
//记录操作日志
OperateLog operateLog = new OperateLog(null,operateUserId,operateTime,classname,
methodname,methodParams,returnValue,costtime);
operateLogMapper.insert(operateLog);
log.info("AOP操作日志:{}",operateLog);
return res;
}
}
这一步有几个注意点:
1:你想要获得操作人的ID,就得用到JWT令牌功能:你得先从请求头中获得jwt令牌,并且解析token
现在的问题就转化为了,你怎么得到这个令牌,我们知道令牌是在请求头中的,请求头的对象是Request
所以,我们可以从IOC容器中获得Request对象,然后获取令牌。
@Autowired
private HttpServletRequest request;
//操作人ID - 当前登录员工ID
//获取请求头中的jwt令牌,解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUserId = (Integer) claims.get("id");
2:第二个处理的点就是这个方法返回值这边的处理:
用到了之前的一个工具类,将对象转化成json格式的数据。
4:在需要的方法上添加注解@Log
事务:
简单理解:事务就是不可拆分的最小事件
Spring事务管理(底层是AOP)
位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
@Transactional这个注解可以作用在方法,接口和类上,不过我们一般作用在业务层的增删改业务上,也就是需要访问多次数据访问的方法上。
@Transactional
@Override
public void deletedept(Integer id) {
deptmapper.deletedept(id);//根据id删除部门
int i = 1/0;
empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工
}
通过这个方法可以解决方法中出现RuntimeError,也就是运行时错误
但是,如果你去特意抛一个异常,那还是解决不了,比如下面
@Transactional
@Override
public void deletedept(Integer id) throws Exception {
deptmapper.deletedept(id);//根据id删除部门
if(true){
throw new Exception("错误");
}
empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工
}
这个时候就需要用到spring事务中的一个回滚事务注释@Transactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public void deletedept(Integer id) throws Exception {
deptmapper.deletedept(id);//根据id删除部门
if(true){
throw new Exception("错误");
}
empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工
}
这样处理之后,所有的异常都会回滚
事务属性-传播行为:
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
设想一个场景:在一个事务里,又开启了另一个事务,那这个时候,这两个事务的回滚或者叫传播行为是怎么样的呢
这个注解可以解释@Transactional(propagation = Propagation.REQUIRED)
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
当这个注解的值是这个的时候,就说明,两个事务时绑定在一起的,比如在delete这个方法中又新建了一个事务,然后delete方法因为
异常rollback了,那这个事务也得rollback。
但如果你设置的值是这个:@Transactional(propagation = Propagation.REQUIRES_NEW)
那这个时候,就会新开一个事务,就算上面的delete方法rollback了,你这个事务还是会接着执行。
propagation = Propagation.REQUIRES_NEW:比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
举个例子:
@Transactional
public void removeDepartmentAndEmployees(Integer deptId) {
deletedept(deptId); // 调用配置了 REQUIRES_NEW 的方法
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void deletedept(Integer id) {
deptMapper.deletedept(id); // 如果这条操作成功
empMapper.deleteByDeptid(id); // 假设这条操作失败
}
在上面的例子中,假设 deletedept
方法中的 deptMapper.deletedept(id)
调用成功,但 empMapper.deleteByDeptid(id)
调用失败:
如果 deletedept
使用了 REQUIRES_NEW
,则 deptMapper.deletedept(id)
将成功提交,即使后面的 empMapper.deleteByDeptid(id)
失败,部门的删除操作仍然会被提交成功。
这个功能还是挺强大的,不过也需要主要是否会发生数据不一致的问题