简介
AOP(面向切面编程)用于将横切关注点从核心的业务逻辑中抽离出来。横切关注点是与核心功能无关但是又散布在多个部分的关注点,如日志记录、事务管理、错误处理等。AOP的目的是提高代码的复用性、模块性、可维护性。
AOP的基本术语
· 切面(Aspect)
切面是通知和切点的结合,他们定义了切面的全部内容。通知定义的是切面的“要做什么”以及“什么时候做”而切点定义的就是“在何地做”。在实际应用中通常就是一个存放共有功能实现的普通java类。比如说在save方法上添加了一个记录日志
@Aspect注解用于定义一个切面类,它包含了一组通知和切点。在这个类中可以编写通知方法,以及指定何时及如何在目标方法周围执行额外的逻辑
· 增强处理(通知advice):
定义了切面是做什么以及什么时候使用,是切面的具体实现。包括前置增强、后置增强、环绕增强、异常抛出增强、引介增强等类型。就是增强的内容
(1)前置增强:org.springframework.aop.BeforeAdvice代表前置增强,spring只支持方法级的增强,目前可用MethodBeforeAdvice。
@ Before 用于定义前置通知,在目标方法执行前执行额外逻辑,比如:参数校验、日志记录
(2)后置增强:org.springframework.aop.AfterReturningAdvice代表后置增强,在目标方法执行后实施增强。
@AfterReturning 用于定义后置通知,在目标方法成功执行后执行额外的逻辑,常用于处理方法的返回值或执行清理工作
(3)环绕增强:org.aopalliance.intercept.MethodInterceptor代表环绕增强,在目标方法执行前后实施增强。
@Around注解用于定义环绕通知,可以在方法执行前、执行中和执行后执行额外逻辑,它需要显式地调用 ProceedingJoinPoint.proceed() 方法来控制目标方法的执行。
(4)异常抛出增强:org.springframework.aop.ThrowsAdvice,在目标方法执行抛出异常后实施增强。方法名必须为afterThrowing,
@AfterThrowing 注解用于定义异常通知,在目标方法抛出异常时执行额外的逻辑。异常通知通常用于记录异常信息或处理异常情况。
(5)引介增强:org.springframework.aop.IntroductionInterceptor,表示目标类添加一些新的方法和属性,连接点是类级别,而不是方法级别。
· 切入点(Pointcut)
切面方法执行的地点,通过一个表达式告诉SpringAOP到什么地方去增强。也就是实际增强的那个方法叫做切入点
· 连接点(JoinPoint)
程序在运行过程中能够插入切面的地点。即被通知的类中的方法都可能是切点,所以这些都是连接点,定义成切点后,这个连接点就变成了切点。
· 目标对象(target)
即将切入切面的对象,也就是那些被通知的对象
AOP记录日志的整体思想
1、基于自定义注解来确定切入点
2、基于环绕通知来完成日志记录
业务流程(参考hm):
1)在记录日志模块下导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2)自定义Log注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log{//自定义操作日志记录注解
public String title() ; // 模块名称
public OperatorType operatorType() default OperatorType.MANAGE; // 操作人类别
public int businessType() ; // 业务类型(0其它 1新增 2修改 3删除)
public boolean isSaveRequestData() default true; // 是否保存请求的参数
public boolean isSaveResponseData() default true; // 是否保存响应的参数
}
注:OperatorType是自定义的一个操作人枚举类,包括OTHER,MANAGE,MOBILE
3)定义一个log工具
public class LogUtil {
//操作执行之后调用
public static void afterHandlLog(Log sysLog, Object proceed,
SysOperLog sysOperLog, int status ,
String errorMsg) {
if(sysLog.isSaveResponseData()) {
sysOperLog.setJsonResult(JSON.toJSONString(proceed));
}
sysOperLog.setStatus(status);
sysOperLog.setErrorMsg(errorMsg);
}
//操作执行之前调用
public static void beforeHandleLog(Log sysLog,
ProceedingJoinPoint joinPoint,
SysOperLog sysOperLog) {
// 设置操作模块名称
sysOperLog.setTitle(sysLog.title());
sysOperLog.setOperatorType(sysLog.operatorType().name());
// 获取目标方法信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature() ;
Method method = methodSignature.getMethod();
sysOperLog.setMethod(method.getDeclaringClass().getName());
// 获取请求相关参数
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
sysOperLog.setRequestMethod(request.getMethod());
sysOperLog.setOperUrl(request.getRequestURI());
sysOperLog.setOperIp(request.getRemoteAddr());
// 设置请求参数
if(sysLog.isSaveRequestData()) {
String requestMethod = sysOperLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = Arrays.toString(joinPoint.getArgs());
sysOperLog.setOperParam(params);
}
}
sysOperLog.setOperName(AuthContextUtil.get().getUserName());
}
}
4)LogAspect
定义一个切面类,在切面类中提供环绕通知的方法
@Aspect
@Component
@Slf4j
public class LogAspect{
@Autowired
private AsyncOperLogService asyncOperLogService;
//环绕通知切面类定义
@Around(value="@annotation(sysLog)")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint,Log sysLog){
//构建前置参数
SysOperLog sysOperLog = new SysOperLog();
LogUtil.beforeHandleLog(sysLog , joinPoint , sysOperLog) ;
Object proceed = null;
try {
proceed = joinPoint.proceed();
// 执行业务方法
LogUtil.afterHandlLog(sysLog , proceed , sysOperLog , 0 , null) ;
// 构建响应结果参数
} catch (Throwable e) { // 代码执行进入到catch中,
// 业务方法执行产生异常
e.printStackTrace(); // 打印异常信息
LogUtil.afterHandlLog(sysLog , proceed , sysOperLog , 1 , e.getMessage()) ;
throw new RuntimeException();
}
//通过AysncManaer来执行一个异步任务保存系统操作日志,避免阻塞当前线程的执行,提高系统性能和响应速度
new AsyncManager().execute(new TimerTask(){
@Override
public void run(){
asyncOperLogService.saveSysOperLog(sysOperLog);
}
// 返回执行结果
return proceed ;
}
}
将操作日志实体类入库的操作定义为一个TimeTask任务,将该任务交由jdk提供的一个ScheduledExecutorService(定时任务线程池)来执行进行插入,这样就避免了系统在插入操作日志时如果出现异常了从而导致业务功能无法正常返回
/**
* 异步管理器:
* 内部维护一个线程池
*/
public class AsyncManager {
private ScheduledExecutorService scheduledThreadPool
= new ScheduledThreadPoolExecutor(50);
/**
* 使用内部的一个线程,去执行具体的任务
* @param task
*/
public void execute(TimerTask task){
scheduledThreadPool.execute(task); //延迟执行,延迟10毫秒
5)EnableLogAspect
想让LogAspect这个切面类在其他的业务服务中使用,需要将此切面类纳入到Spring容器中。由于SpringBoot默认扫描的是启动类所在包下的bean以及子包中的bean。因此LogAspect不满足扫描条件,所以要通过自定义注解来实现。别忘了需要在启动类上添加此注解@EnableLogAspect
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(value = LogAspect.class) // 通过Import注解导入日志切面类到Spring容器中
public @interface EnableLogAspect {
}
6)在要添加日志功能的业务接口上添加@Log注解
7)定义一个与日志数据表相对应的实体类以及写好对应的servicemapper等接口