背景:使用AOP面向切面记录用户进入系统后的操作轨迹。
其他:SpringBoot + MybatisPlus + …

  • 相关依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 定义日志监控切面
@Aspect
@Component
public class LogAspect {

	@Autowired
	private SystemLogService service;

    /**
     * 环绕通知
     *      ("within(com.yc..*) && @annotation(log)")[清楚记录操作痕迹,需添加@writeLog()注解]
     *      ("within(com.yc.*.controller..*)")[只记录方法名称,无需修改原代码]
     *
     * @param jp 连接点
     * @param log 注解
     * @return 对象
     */
	@Around("within(com.yc..*) && @annotation(log)")
    public Object around (ProceedingJoinPoint jp,WriteLog log){
        long startTimeMillis = System.currentTimeMillis();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        try{
            Object result = jp.proceed();
            long costTimeMillis = System.currentTimeMillis() - startTimeMillis;
            service.write(request,log.opPosition(),log.optype(),log.logType(),
                    jp.getSignature().getDeclaringTypeName()+"."+jp.getSignature().getName(),
                    costTimeMillis,CommonConstant.OPSTATE_SUCCESS);
            return result;
        }catch (Throwable throwable) {
            long costTimeMillis = System.currentTimeMillis() - startTimeMillis;
            service.write(request,log.opPosition(),log.optype(),log.logType(),
                    jp.getSignature().getDeclaringTypeName()+"."+jp.getSignature().getName(),
                    costTimeMillis,CommonConstant.OPSTATE_FAILURE);
            throwable.printStackTrace();
            throw new RuntimeException(throwable.getMessage());
        }
    }
}

说明:

  • 根据具体业务,可定义不同的通知(前置,异常,后置…)
  • 切点的定义语法详见文章的扩展 2
  1. 定义日志注解接口
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface WriteLog {

    /**
     * 操作位置
     * @return 所处位置
     */
	String opPosition();

    /**
     * 操作类型(0:增 1:删  2:改 3:查)
     * @return 操作类型
     */
	int optype() default CommonConstant.OPTYPE_READ;

    /**
     * 日志类型
     *     0:操作日志;1:登录日志;2:定时任务;
     *
     * @return 日志类型
     */
    int logType() default CommonConstant.LOG_TYPE_0;

}
  • 架构日志服务类
@Service
@Slf4j
public class SystemLogService {

    private final SysLogMapper sysLogMapper;
    private final DaoApi daoApi;

    @Autowired
    public SystemLogService(SysLogMapper sysLogMapper, DaoApi daoApi) {
        this.sysLogMapper = sysLogMapper;
        this.daoApi = daoApi;
    }

    boolean write(HttpServletRequest request,String opPosition,int opType,int logType,String requestMethod,
                  long costTimeMillis,int opResult) {
        String message = new String[] { "创建", "删除", "更新", "读取" }[opType] + "位置【" + opPosition + "】" + (opResult != 1
                ? "成功" : "失败");
        String requestParams = JSONObject.toJSONString(request.getParameterMap());
        return write(daoApi.getCurrentUser(),opType,logType, requestMethod,request.getRequestURI(),
                request.getMethod(),requestParams,costTimeMillis,message);
    }

    private boolean write(SysUser sysUser,int opType,int logType,
                          String requestMethod,String requestUrl,String requestType,String requestParams,
                          long costTimeMillis,String... describe) {
        sysUser = sysUser !=null ? sysUser : new SysUser();
        SysLog log = new SysLog();
        log.setRequestMethod(requestMethod);
        log.setRequestUrl(requestUrl);
        log.setRequestType(requestType);
        log.setRequestParam(requestParams.trim());
        log.setCreateTime(LocalDateTime.now());
        log.setCreateUserId(sysUser.getSysUserId());
        log.setIpAddress(LocalHostUtil.getIpAddress());
        log.setOpType(opType);
        log.setCostTime(costTimeMillis);
        log.setLogType(logType);
        String tmpDesc = "";
        for(int i = 0; i<describe.length; i++){
            tmpDesc += describe[i] + (i<describe.length-1?"\r\n":"");
        }
        log.setLogContent(tmpDesc);
        int result = sysLogMapper.insert(log);
        return result >0 ? true : false;
    }
}

说明

  • @Target(ElementType.METHOD):用于限定注解使用范围,method:用于方法上
  • @Retention(RetentionPolicy.RUNTIME):指定注解不仅保存在class文件中,JVM加载class文件之后,仍然存在
  • @Documented:表明使用这个注解会被javadoc记录,注解类型信息会被记录在生成的文档中
  • @Inherited:该注解会被子类继承
  1. 日志实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SysLog implements Serializable {

    private static final long serialVersionUID = 1L;
    @TableId(value = "sys_log_id", type = IdType.UUID)
    private String sysLogId;
    /**
     * 操作类型(0:增 1:删  2:改 3:查)
     */
    private Integer opType;
    /**
     * 日志类型(0.操作日志 1.登录登出日志 2.定时任务)
     */
    private Integer logType;
    /**
     * 日志内容
     */
    private String logContent;
    /**
     * 请求方法
     */
    private String requestMethod;
    /**
     * 请求参数
     */
    private String requestParam;
    /**
     * 请求路径
     */
    private String requestUrl;
    /**
     * 请求类型
     */
    private String requestType;
    /**
     * IP地址
     */
    private String ipAddress;
    /**
     * 耗时
     */
    private Long costTime;
    /**
     * 备注
     */
    private String remark;
    /**
     * 创建人
     */
    private String createUserId;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;

}
  • Controller 中使用@WriteLog注解记录用户操作轨迹
@GetMapping(value = "/dictTree")
@WriteLog(opPosition = "加载字典树" ,optype = CommonConstant.OPTYPE_READ)
public List<TreeNode> dictTree(@RequestParam(value = "name",required = false)String name){
    try {
        return service.dictTree(name);
    }catch (Exception e){
        throw new RunningException("".equals(e.getMessage()) ?  "系统错误,请联系管理员!" : e.getMessage());
    }
}

扩展 1 Spring AOP

  • 面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如:事务,权限,过滤器,日志等功能
  • 相关概念

    图片引用
  • 基本概念
    1.Advice(通知):
     切面类要完成的工作就是通知,定义切面"何时做"和"做什么"
       (1)Before(前置通知):在目标方法调用之前调用通知,
       (2)After(后置通知):在目标方法调用之后调用通知;
       (3)AfterReturning(返回通知):在目标方法执行成功后执行
       (4)AfterThrowing(异常通知):在目标方法出现异常时候执行
       (5)Around(环绕通知):在目标方法调用之前和之后执行
    2.Join Point(连接点)
      在我们的应用程序中有可能有数以万计的时机可以应用通知,而这些时机就被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时 甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
    连接点是一个虚概念,可以把连接点看成是切点的集合。
    3.Aspect(切面):
      通知和切点的结合,两者共同定义了切面的全部内容。因为通知定义的是"在何时做"和"要做什么",而切点定义的是"在何处做"。两者结合,完美展现切面在何时,何地,做什么。
    4.PointCut(切点)
      切点定义"去何处做",让通知找到处理的方法。切点会定义匹配通知所要植入的一个或多个连接点;
      “何处做”:@within,@args,@target,@annotation;举例:
//匹配com.lh下的所有方法以及@Service类中的所有方法
within(com.lh..*)||within(org.springframework.stereotype.Service)
//匹配标注了@RequestMapping的方法
annotation(org.springframework.web.bind.annotation.RequestMapping)

5.Weaving (织入)
  可以理解为对方法的增强,将切面的代码应用到目标函数的过程。

扩展 2 切入点指示符
  为了使方法能够应用到对应的目标函数中,Spring AOP 提供了匹配表达式
通配符
  […]匹配任意数量的参数,以及类定义中的任意数量包

//任意返回值,任意名称,任意参数的公共方法
execution(public * *(..))
//匹配com.yc.common包及其子包中所有类中的所有方法
within(com.yc.common..*)

  [+] 匹配给定类的任意子类

//匹配实现了UserService接口的所有子类的方法
within(com.yc.common.UserService+)

  [*]匹配任意数量的字符

//匹配com.yc.common包及其子包中所有类的所有方法
within(com.yc.common..*)
//匹配以set开头,参数为int类型,任意返回值的方法
execution(* set*(int))

within

//匹配com.yc.common包及其子包中所有类中的所有方法
@Pointcut("within(com.yc.common..*)")
//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.yc.common.UserDaoImpl)")
//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.yc.common.UserDaoImpl+)")
//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.yc.common.UserDao+)")

execution

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.yc.common.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.yc.common.UserDaoImpl.*(int , ..))")

其他指示符

bean:Spring AOP扩展的,用于匹配特定名称的Bean对象的执行方法;
//匹配名称中带有后缀Service的Bean。
@Pointcut("bean(*Service)")
private void myPointcut(){}
this :用于匹配当前AOP代理对象类型的执行方法;
//匹配了任意实现了UserDao接口的代理对象的方法进行过滤
@Pointcut("this(com.yc.common.UserDao)")
private void myPointcut(){}
target :用于匹配当前目标对象类型的执行方法;
//匹配任意实现UserDao接口的目标对象的方法进行过滤
@Pointcut("target(com.yc.common.UserDao)")
private void myPointcut(){}
@within:用于匹配所以持有指定注解类型内的方法;与within是有区别的, within是用于匹配指定类型内的方法执行;
//匹配使用MarkerAnnotation注解的类(注意是类)
@Pointcut("@within(com.yc.common.MarkerAnnotation)")
private void myPointcut(){}
@annotation: 根据所应用的注解进行方法过滤
//匹配使用MarkerAnnotation注解的方法
@Pointcut("@annotation(com.cy.common.MarkerAnnotation)")
private void myPointcut(){}

附:切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下:

//匹配任意实现UserDao接口的目标对象的方法并且该接口不在com.yc.dao包及其子包下
@Pointcut("target(com.yc.common.UserDao) !within(com.yc.dao..*)")
private void myPointcut(){}
//匹配任意实现UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.yc.common.UserDao)&&execution(* com.yc.common.UserDao.addUser(..))")
private void myPointcut(){}