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

时间:2022-10-01 17:09:15

1. 概述

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

2. 日志的相关类定义

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
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。如登陆、添加、删除、更新、删除等

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,这个类中日志功能才开启

?
1
2
3
4
5
6
7
8
9
10
@documented
@retention(retentionpolicy.runtime)
@target({elementtype.type})
public @interface logenable {
  /**
   * 如果为true,则类下面的logevent启作用,否则忽略
   * @return
   */
  boolean logenable() default true;
}

3.2. @logevent

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

?
1
2
3
4
5
6
7
8
@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的格式保存到日志中。如果此注解同时注解在方法和类上,则方法上的注解会覆盖类上的值。

?
1
2
3
4
5
6
7
8
@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

定义保存日志信息的类

?
1
2
3
4
5
6
7
8
9
10
11
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等等。每一种日志处理类都是此接口的实现类

?
1
2
3
4
5
6
7
public interface ilogmanager {
  /**
   * 日志处理模块
   * @param paramlogadmbean
   */
  void deallog(logadmmodel paramlogadmbean);
}

4.3. dblogmanager

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

?
1
2
3
4
5
6
7
@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定义方法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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执行日志的处理(我们这里只记录执行成功的日志,你也可以定义记录失败的日志)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@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

?
1
2
3
4
5
6
public interface istudentservice {
  void deletebyid(string id, string a);
  int save(studentmodel studentmodel);
  void update(studentmodel studentmodel);
  void querybyid(string id);
}

6.2. studentserviceimpl:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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);
  }
}

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

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

?
1
{"admevent":"4","admmodel":"1","admoptcontent":"{\"id\":\"1\"}","createdate":1525779738111,"desc":"删除记录"}

7. 代码

以上的详细的代码见下面

原文链接:https://blog.csdn.net/hry2015/article/details/80244765