利用Spring AOP切面对用户访问进行监控
开发系统时往往需要考虑记录用户访问系统查询了那些数据、进行了什么操作,尤其是访问重要的数据和执行重要的操作的时候将数记录下来尤显的有意义。有了这些用户行为数据,事后可以以用户为条件对用户在系统的访问和操作进行统计。
同时因为系统对登录用户在系统的行为有较为详细的记录,客观上也增加了系统的安全性。
记录那些数据
根据多年的经验,系统一般自动记录用户以下内容基本可以满足需要:
谁,在什么时候,在哪里,做了什么,结果如何。
使用什么方式
使用Spring AOP切面记录用户在系统中行为再合适不过了。使用Spring AOP切面横切需要需要记录的用户访问的方法,在切面中记录:谁,在什么时候,在哪里,做了什么,结果如何。 如下图所示
使用Spring AOP实现记录用户行为的切面
首先回顾下AOP的术语
Advice(通知):
定义了切面完成的工作以及何时执行这个工作。
Spring切面定义了5种类型的通知:
前置通知(Before):在目标方法调用之前调用。
后置通知(After):在目标方法完成之后调用。
返回通知(After-returning) :在目标方法成功执行之后调用。
异常通知(After-throwing):在目标方法抛出异常之后条用。
环绕通知(Around):包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。
Pointcut(切点): Advice(通知)定义了 何时、做什么
的问题,那么切点定义了哪里做
的问题。切点Pointcut的定义会匹配通知所要织入的一个或多个连接点JoinPoint。稍后我们用注解的方式描述切点Pointcut。
Join Point(连接点):应用通知的时机,在SpringAOP中指的的就是方法。
切面(Aspect) :就是通知和切点的结合。它们共同定义了在何时何处做点什么。
示意图:
编码实现
标注了AuditAction注解的方法都将被AuditAspectAuditAspect拦截,并在通知中将用户的行为记录下来。
用注解的方式定义切点
被此注解标注的方法都将被拦截织入通知。
package com.sdsxblp.common.log.aspect;import java.lang.annotation.Documented;import java.lang.annotation.Retention;import java.lang.annotation.Target;?import static java.lang.annotation.ElementType.METHOD;import static java.lang.annotation.RetentionPolicy.RUNTIME;?@Documented@Retention(RUNTIME)@Target(METHOD)/** * @Description: 标记需要记录操作日志的方法 * @author Bruce Zou() */public @interface AuditAction { /** * 对资源(比如用户管理功能等)进行操作 * * @return */ String resource() default "";? /** * 资源的描述 * * @return */ String resourceDes() default "";? /** * 操作的类型 * * @return */ String actionType() default "";}
定义切面
package com.sdsxblp.common.log.aspect;?import com.sdsxblp.common.log.entity.ActionLog;import com.sdsxblp.common.log.util.AuditLogHolder;import com.sdsxblp.common.log.service.ActionLogService;import com.sdsxblp.common.log.api.IAuditActionDataService;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;?import java.lang.reflect.Method;import java.util.Date;?/** * @author Bruce Zou() * @Description:记录操作日志的切面 */@Aspect@Component@Slf4jpublic class AuditAspect { @Autowired //将用户行为数据保存到db中 private ActionLogService actionLogService; //使用环绕通知。 //被com.sdsxblp.common.log.aspect.AuditAction标注的方法都将被拦截 @Around(value = "@annotation(com.sdsxblp.common.log.aspect.AuditAction)") public Object executeAudit(ProceedingJoinPoint joinPoint) throws Throwable { log.info("**开始记录操作日志:"); //执行目标方法 Object returnValue = joinPoint.proceed(); ActionLog actionLog = new ActionLog(); actionLog.setActionDate(new Date()); setValueFromAnnotation(joinPoint, actionLog); //为了提高性能使用异步方法保存行为数据 actionLogService.asynSaveLog(actionLog); log.info("**操作日志记录结束。"); // 返回目标方法的结果 return returnValue; }?? /** * 从代理的Target方法中获取资源、资源描述和操作类型。 * * @param joinPoint * @param actionLog * @throws NoSuchMethodException */ private void setValueFromAnnotation(ProceedingJoinPoint joinPoint, ActionLog actionLog) throws NoSuchMethodException { //获取接口方法签名 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String methodName = methodSignature.getName(); //获取实现类方法 Method method = joinPoint.getTarget().getClass().getMethod(methodName, methodSignature.getParameterTypes()); //获取实现类注解 AuditAction auditAction = method.getAnnotation(AuditAction.class); if (auditAction != null) { String resource = auditAction.resource(); if (StringUtils.isNotBlank(resource)) { actionLog.setResource(resource); } String resourceDes = auditAction.resourceDes(); if (StringUtils.isNotBlank(resourceDes)) { actionLog.setResourceDes(resourceDes); } String actionType = auditAction.actionType(); if (StringUtils.isNotBlank(actionType)) { actionLog.setActionType(actionType); } }? }}
备注 ActionLog主要代码如下
public class ActionLog { private String userName; private Date actionDate; private String fomIp; private String macAddress; private String resource; private String resourceDes;? private String actionType; private String beforeValue; private String afterValue; ... }