Spring AOP + 注解实现统一注解功能
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. 代码
以上的详细的代码见下面