很久没有用过Java的AOP,最近接触到了一个需求,恰好可以用AOP的思想来实现,就此总结一下。
AOP,即Aspect Oriented Programming,直接翻译过来的意思是“面向侧面的程序设计”,更常见的说法是“面向切面编程”,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。
关于AOP的概念这里不做过多介绍,我们只需要知道其中有三个非常重要的概念:
aspect(切面)、pointcut(切入点)、advice(通知)。
我们分别来看看这三者是什么意思。
① pointcut(切入点)
也就是具体拦截的某个业务点。
分为 execution(路径表达式)和 annotation 方式,被这两种方式修饰的代码将会被切面拦截处理。
切点和连接点有什么区别?
Joint point
:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
② advice(通知)
切面当中的处理方式,声明通知方法在业务层的执行位置,类型如下:
@Before:前置通知,在方法执行之前执行
@After:后置通知,在方法执行之后执行
@AfterRunning:返回通知,在方法返回结果后执行
@AfterThrowing:异常通知,在方法执行异常后执行
@Around:环绕通知,可以替代上述通知方法,并且可以控制方法是否执行以及何时执行
Spring AOP中的通知方法,用到了动态代理,动态代理主要有两种方式:JDK动态代理和CGLIB动态代理,关于动态代理的详细说明,可以参考这里,以及实现原理。
③ aspect(切面)
即 pointcut+ advice ,和拦截器(HandlerInterceptorAdapter)的作用并没有太大的区别,只是两者的作用域不同,相对而言,切面的作用域更灵活一些。两者之间的区别可以参考这里。
如果说MVC的三层架构是Java的纵向编程思想,那AOP可以说是横向编程思想,能够让我们在不影响软件原有功能上,横向拓展新功能。
常见的用法有:
说了那么多概念,下面让我们来看一个真实的应用场景:
为了更好地追踪项目中每个接口在执行时的状态,现在要求在原有接口基础上进行操作日志的记录,日志内容包括包括执行时间,执行人等。
这里有两种实现方式;
① 写一个公共方法,每个接口里都调用该方法,优点是易于实现,缺点是高耦合;
② 使用AOP框架,在不影响原有接口的基础上,进行日志记录,低耦合;
我们使用第二种方法,来实现该功能:
一、使用自定义注解,定义pointCut
我们在项目中自定义一个Log注解,作为切点:
package com.czq.aop.log;
public @interface Log {
}
并且在项目中定义一个操作日志类:
package com.czq.aop.bean;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
* 操作日志记录表 oper_log
*
* @author czq
*/
@Data
@ToString(onlyExplicitlyIncluded = true)
public class SysOperLog
{
private static final long serialVersionUID = 1L;
/** 操作人员 */
@JsonProperty("操作人员")
@ToString.Include
private String operator;
/** 操作地点 */
@JsonProperty("操作地点")
@ToString.Include
private String operLocation;
/** 请求参数 */
@JsonProperty("请求参数")
@ToString.Include
private String operParam;
/** 返回参数 */
@JsonProperty("返回参数")
@ToString.Include
private String jsonResult;
/** 错误消息 */
@JsonProperty("错误消息")
@ToString.Include
private String errorMsg;
/** 操作时间 */
@JsonProperty("操作时间")
@ToString.Include
private Date operTime;
}
二、实现advice(通知),用切面类来拦截处理被注解的方法
package com.czq.aop.aspect.log;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.czq.aop.bean.SysOperLog;
import lombok.extern.slf4j.Slf4j;
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;
import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @Author: czq
* @CreateTime: 2022-10-12 16:25
* @Description: 写得不好,瞎写
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/*
* 切点,也可以不写,直接注释在通知的注解里
* */
@Pointcut("@annotation(com.czq.aop.log.Log)")
public void pointCut() {
}
@Around("pointCut()")
public Object execute(ProceedingJoinPoint joinPoint) throws JsonProcessingException {
SysOperLog sysOperLog = getOperLog(joinPoint);
try {
// 执行完成后再打印日志
Object proceed = joinPoint.proceed();
sysOperLog.setJsonResult(proceed.toString());
log.info(new ObjectMapper().writeValueAsString(sysOperLog));
return proceed;
} catch (Throwable e) {
sysOperLog.setErrorMsg(e.getMessage());
if (e instanceof NullPointerException) {
sysOperLog.setErrorMsg("java.lang.NullPointerException");
}
// 执行异常时打印日志
log.error(new ObjectMapper().writeValueAsString(sysOperLog));
throw new RuntimeException(e);
}
}
private SysOperLog getOperLog(ProceedingJoinPoint joinPoint) {
SysOperLog sysOperLog = new SysOperLog();
sysOperLog.setOperTime(new Date());
// just demo to get current user and os
Map map = System.getenv();
// 获取// 用户名
String userName = map.get("USERNAME");
sysOperLog.setOperator(userName);
// 获取操作系统名
String os = map.get("OS");
sysOperLog.setOperLocation(os);
// 获取请求参数
Object[] args = joinPoint.getArgs();
List
三、在原有controller方法上加上注解:
package com.czq.aop.controller;
import com.czq.aop.log.Log;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: czq
* @CreateTime: 2022-10-12 11:21
* @Description: 测试用
*/
@RestController
public class AopController {
@RequestMapping("/test")
@Log
public String test( Long id) {
return id.toString();
}
}
四、请求该方法,查看操作日志:
① 请求正常时:
GET http://localhost:8080/test?id=110
查看请求结果:
② 请求出错时:
GET http://localhost:8080/test?110
查看请求结果:
参考文章
Java:由浅入深揭开 AOP 实现原理 - 知乎
CGLIB(Code Generation Library)详解_danchu的博客-CSDN博客
Java AOP的底层实现原理 - 健人雄 - 博客园