Spring AOP + 注解实现统一注解功能

 更新时间:2018年05月09日 14:01:13   作者:hry2015  
本文我们通过Spring AOP和Java的自定义注解来实现日志的插入功能,非常不错,具有一定的参考借鉴价值,需要的朋友一起看看吧

1. 概述

在一般系统中,当我们做了一些重要的操作时,如登陆系统,添加用户,删除用户等操作时,我们需要将这些行为持久化。本文我们通过Spring AOP和Java的自定义注解来实现日志的插入。此方案对原有业务入侵较低,实现较灵活

2. 日志的相关类定义

我们将日志抽象为以下两个类:功能模块和操作类型

使用枚举类定义功能模块类型ModuleType,如学生、用户模块

public enum ModuleType {
  DEFAULT("1"), // 默认值
  STUDENT("2"),// 学生模块
  TEACHER("3"); // 用户模块
  private ModuleType(String index){
    this.module = index;
  }
  private String module;
  public String getModule(){
    return this.module;
  }
}

使用枚举类定义操作的类型:EventType。如登陆、添加、删除、更新、删除等

public enum EventType {
  DEFAULT("1", "default"), ADD("2", "add"), UPDATE("3", "update"), DELETE_SINGLE("4", "delete-single"),
  LOGIN("10","login"),LOGIN_OUT("11","login_out");

  private EventType(String index, String name){
    this.name = name;
    this.event = index;
  }
  private String event;
  private String name;
  public String getEvent(){
    return this.event;
  }

  public String getName() {
    return name;
  }
}

3. 定义日志相关的注解

3.1. @LogEnable

这里我们定义日志的开关量,类上只有这个值为true,这个类中日志功能才开启

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
  /**
   * 如果为true,则类下面的LogEvent启作用,否则忽略
   * @return
   */
  boolean logEnable() default true;
}

3.2. @LogEvent

这里定义日志的详细内容。如果此注解注解在类上,则这个参数做为类全部方法的默认值。如果注解在方法上,则只对这个方法启作用

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD, ElementType.TYPE})
public @interface LogEvent {
  ModuleType module() default ModuleType.DEFAULT; // 日志所属的模块
  EventType event() default EventType.DEFAULT; // 日志事件类型
  String desc() default ""; // 描述信息
}

3.3. @LogKey

此注解如果注解在方法上,则整个方法的参数以json的格式保存到日志中。如果此注解同时注解在方法和类上,则方法上的注解会覆盖类上的值。

@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogKey {
   String keyName() default ""; // key的名称
   boolean isUserId() default false; // 此字段是否是本次操作的userId,这里略
   boolean isLog() default true; // 是否加入到日志中
}

4. 定义日志处理类

4.1. LogAdmModel

定义保存日志信息的类

public class LogAdmModel {
  private Long id;
  private String userId; // 操作用户
  private String userName;
  private String admModel; // 模块
  private String admEvent; // 操作
  private Date createDate; // 操作内容
  private String admOptContent; // 操作内容
  private String desc; // 备注
  set/get略
}

4.2. ILogManager

定义日志处理的接口类ILogManager

我们可以将日志存入数据库,也可以将日志发送到开中间件,如果redis, mq等等。每一种日志处理类都是此接口的实现类

public interface ILogManager {
  /**
   * 日志处理模块
   * @param paramLogAdmBean
   */
  void dealLog(LogAdmModel paramLogAdmBean);
}

4.3. DBLogManager

ILogManager实现类,将日志入库。这里只模拟入库

@Service
public class DBLogManager implements ILogManager {
  @Override
  public void dealLog(LogAdmModel paramLogAdmBean) {
    System.out.println("将日志存入数据库,日志内容如下: " + JSON.toJSONString(paramLogAdmBean));
  }
}

5. AOP的配置

5.1. LogAspect定义AOP类

使用@Aspect注解此类

使用@Pointcut定义要拦截的包及类方法

我们使用@Around定义方法

@Component
@Aspect
public class LogAspect {
  @Autowired
  private LogInfoGeneration logInfoGeneration;

  @Autowired
  private ILogManager logManager;

  @Pointcut("execution(* com.hry.spring.mvc.aop.log.service..*.*(..))")
  public void managerLogPoint() {
  }

  @Around("managerLogPoint()")
  public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
  ….
  } 
}

aroundManagerLogPoint:主方法的主要业务流程

1. 检查拦截方法的类是否被@LogEnable注解,如果是,则走日志逻辑,否则执行正常的逻辑

2. 检查拦截方法是否被@LogEvent,如果是,则走日志逻辑,否则执行正常的逻辑

3. 根据获取方法上获取@LogEvent 中值,生成日志的部分参数。其中定义在类上@LogEvent 的值做为默认值

4. 调用logInfoGeneration的processingManagerLogMessage填充日志中其它的参数,做个方法我们后面再讲

5. 执行正常的业务调用

6. 如果执行成功,则logManager执行日志的处理(我们这里只记录执行成功的日志,你也可以定义记录失败的日志)

@Around("managerLogPoint()")
    public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {

      Class target = jp.getTarget().getClass();
      // 获取LogEnable
      LogEnable logEnable = (LogEnable) target.getAnnotation(LogEnable.class);
      if(logEnable == null || !logEnable.logEnable()){
        return jp.proceed();
      }

      // 获取类上的LogEvent做为默认值
      LogEvent logEventClass = (LogEvent) target.getAnnotation(LogEvent.class);
      Method method = getInvokedMethod(jp);
      if(method == null){
        return jp.proceed();
      }

      // 获取方法上的LogEvent
      LogEvent logEventMethod = method.getAnnotation(LogEvent.class);
      if(logEventMethod == null){
        return jp.proceed();
      }

      String optEvent = logEventMethod.event().getEvent();
      String optModel = logEventMethod.module().getModule();
      String desc = logEventMethod.desc();

      if(logEventClass != null){
        // 如果方法上的值为默认值,则使用全局的值进行替换
        optEvent = optEvent.equals(EventType.DEFAULT) ? logEventClass.event().getEvent() : optEvent;
        optModel = optModel.equals(ModuleType.DEFAULT) ? logEventClass.module().getModule() : optModel;
      }

      LogAdmModel logBean = new LogAdmModel();
      logBean.setAdmModel(optModel);
      logBean.setAdmEvent(optEvent);
      logBean.setDesc(desc);
      logBean.setCreateDate(new Date());
      logInfoGeneration.processingManagerLogMessage(jp,
          logBean, method);
      Object returnObj = jp.proceed();

      if(optEvent.equals(EventType.LOGIN)){
        //TODO 如果是登录,还需要根据返回值进行判断是不是成功了,如果成功了,则执行添加日志。这里判断比较简单
        if(returnObj != null) {
          this.logManager.dealLog(logBean);
        }
      }else {
        this.logManager.dealLog(logBean);
      }
      return returnObj;
    }

    /**
     * 获取请求方法
     *
     * @param jp
     * @return
     */
    public Method getInvokedMethod(JoinPoint jp) {
      // 调用方法的参数
      List classList = new ArrayList();
      for (Object obj : jp.getArgs()) {
        classList.add(obj.getClass());
      }
      Class[] argsCls = (Class[]) classList.toArray(new Class[0]);

      // 被调用方法名称
      String methodName = jp.getSignature().getName();
      Method method = null;
      try {
        method = jp.getTarget().getClass().getMethod(methodName, argsCls);
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
      return method;
    }
  }

6. 将以上的方案在实际中应用的方案

这里我们模拟学生操作的业务,并使用上文注解应用到上面并拦截日志

6.1. IStudentService

业务接口类,执行一般的CRUD

public interface IStudentService {
  void deleteById(String id, String a);
  int save(StudentModel studentModel);
  void update(StudentModel studentModel);
  void queryById(String id);
}

6.2. StudentServiceImpl:

@LogEnable : 启动日志拦截
类上@LogEvent定义所有的模块
方法上@LogEven定义日志的其它的信息
@Service
@LogEnable // 启动日志拦截
@LogEvent(module = ModuleType.STUDENT)
public class StudentServiceImpl implements IStudentService {
  @Override
  @LogEvent(event = EventType.DELETE_SINGLE, desc = "删除记录") // 添加日志标识
  public void deleteById(@LogKey(keyName = "id") String id, String a) {
    System.out.printf(this.getClass() + "deleteById id = " + id);
  }
  @Override
  @LogEvent(event = EventType.ADD, desc = "保存记录") // 添加日志标识
  public int save(StudentModel studentModel) {
    System.out.printf(this.getClass() + "save save = " + JSON.toJSONString(studentModel));
    return 1;
  }
  @Override
  @LogEvent(event = EventType.UPDATE, desc = "更新记录") // 添加日志标识
  public void update(StudentModel studentModel) {
    System.out.printf(this.getClass() + "save update = " + JSON.toJSONString(studentModel));
  }
  // 没有日志标识
  @Override
  public void queryById(String id) {
    System.out.printf(this.getClass() + "queryById id = " + id);
  }
}

执行测试类,打印如下信息,说明我们日志注解配置启作用了:

将日志存入数据库,日志内容如下:

{"admEvent":"4","admModel":"1","admOptContent":"{\"id\":\"1\"}","createDate":1525779738111,"desc":"删除记录"}

7. 代码

以上的详细的代码见下面

github代码,请尽量使用tag v0.21,不要使用master,因为我不能保证master代码一直不变

相关文章

  • Mybatis plus多租户方案的实战踩坑记录

    Mybatis plus多租户方案的实战踩坑记录

    MybaitsPlus多租户处理器是一个对于多租户问题的解决方案,下面这篇文章主要给大家介绍了关于Mybatis plus多租户方案踩坑的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2021-12-12
  • Java对象转json的方法过程解析

    Java对象转json的方法过程解析

    这篇文章主要介绍了Java对象转json的方法过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Logback MDCAdapter日志跟踪及自定义效果源码解读

    Logback MDCAdapter日志跟踪及自定义效果源码解读

    这篇文章主要为大家介绍了Logback MDCAdapter日志跟踪及自定义效果源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • java如何用递归生成树形结构

    java如何用递归生成树形结构

    作者分享了自己在使用脚本之家资源进行编程时的经验,包括准备实体对象、测试数据、构造树形结构递归函数、测试以及输出结果等步骤,作者希望这些经验能对大家有所帮助,并鼓励大家支持脚本之家
    2025-03-03
  • 使用JPA中@Query 注解实现update 操作方法(必看)

    使用JPA中@Query 注解实现update 操作方法(必看)

    下面小编就为大家带来一篇使用JPA中@Query 注解实现update 操作方法(必看)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Java编写通用的导出任何对象列表数据到excel的工具类

    Java编写通用的导出任何对象列表数据到excel的工具类

    在工作中经常会遇到列表数据的导出,每次需要的时候都要去开发一次,且数据不断在变化,所以本文将利用Java编写一个工具类可以导出任何对象列表数据到excel,希望对大家有所帮助
    2024-12-12
  • Java线程池ThreadPoolExecutor的使用及其原理详细解读

    Java线程池ThreadPoolExecutor的使用及其原理详细解读

    这篇文章主要介绍了Java线程池ThreadPoolExecutor的使用及其原理详细解读,线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务,线程池线程都是后台线程,需要的朋友可以参考下
    2023-12-12
  • java设计模式之桥接模式(Bridge)

    java设计模式之桥接模式(Bridge)

    这篇文章主要为大家详细介绍了java设计模式之桥接模式Bridge,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • mybatis于xml方式和注解方式实现多表查询的操作方法

    mybatis于xml方式和注解方式实现多表查询的操作方法

    在数据库中,单表的操作是最简单的,但是在实际业务中最少也有十几张表,并且表与表之间常常相互间联系,本文给大家介绍mybatis于xml方式和注解方式实现多表查询的操作方法,感兴趣的朋友一起看看吧
    2023-12-12
  • springboot vue前后端接口测试树结点添加功能

    springboot vue前后端接口测试树结点添加功能

    这篇文章主要为大家介绍了springboot vue前后端接口测试树结点添加功能,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05

最新评论