@[toc]
概述:
该SpringBoot项目使用AOP的环绕@Around注解及自定义注解保存操作日志到数据库,自定义注解中会配置日志模板类型logModelType字段,通过该字段去匹配是创建、删除、修改...等等功能,本案例就是为了模拟现实项目中通过AOP及自定义注解如何保存操作详情日志功能。
特色
form表单除了input输入框,也会有一些按钮【Disable/Enable】,这些按钮就需要转换数字值然后动态拼接详情日志参数,日志操作类LogAopAction中拼接参数不只是简单的一堆get、set、if else去拼接,而是根据类型logModelType字段 =》 去找枚举LogDetailEnums =》通过枚举值找常量类LogDetailConstants,常量类中定义了各种类型操作的占位符,动态拼接参数使用MessageFormat.format(),这样使用更加简单、看起来更加优雅、实现可扩展性不用写一堆代码。
使用方式
- 第一步:pom引入AOP
- 第二步:创建自定义注解、Bean实体、枚举、常量类
- 第三步:Controller层方法使用自定义注解标识
- 第四步:新建一个日志操作类LogAopAction,专门用来处理操作保存日志
- 第五步:postman模拟调用接口,输出AOP中ProceedingJoinPoint获取目标方法,参数,注解
注意点
- 注意点1:日志操作类LogAopAction必须加两个注解@Aspect和@Component,其中@Aspect注解代表该类为切面,而@Component为了使该类能让spring容器扫描到
- 注意点2:@Around注解中配置@annotation注解用来指定生效的自定义注解名字
- 注意点3:该案例描述AOP中ProceedingJoinPoint获取目标方法,参数,注解
- 注意点4:接收实体Bean要重新toString方法,不然无法转成json,因为未重写toString方法中用的是等号 "=" 而不是冒号 ":"
- 注意点5:格式化常量类定义好的占位符请使用MessageFormat.format(),拼接起来更加方便
代码
第一步:pom引入AOP
<!--aop相关的依赖引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
第二步:创建自定义注解、Bean实体、枚举、常量类
自定义注解LogAnnotation
package com.example.demo.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LogAnnotation {
//日志模板类型
String logModelType();
}
实体类
package com.example.demo.bean;
import lombok.Data;
/**
* @Author 211145187
* @Date 2022/2/23 09:32
**/
@Data
public class OperateTeacherReq {
//id
private Integer id;
//姓名
private String name;
//开关【0:Disable、1:Enable】
private Integer pstnFlag;
@Override
public String toString() {
return "{" +
"id:" + id +
", name:'" + name + '\'' +
", pstnFlag:" + pstnFlag +
'}';
}
}
枚举类LogDetailEnums
package com.example.demo.enums;
public enum LogDetailEnums {
/**
* 开户
*/
CREATE_ACCOUNT("CREATE_ACCOUNT", LogDetailConstants.CREATE_ACCOUNT),
/**
* PSTN Flag值转换
*/
PSTN_ENABLE_FLAG("pstnFlag_1","Enable"),
PSTN_DISABLE_FLAG("pstnFlag_0","Disable");
private String code;
private String message;
LogDetailEnums(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public static String getDetailByCode(String messageCode){
try {
for (LogDetailEnums value : LogDetailEnums.values()) {
if (value.code.equals(messageCode)){
return value.message;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static LogDetailEnums getLogDetailEnum(String code){
if (code==null){
return null;
}
for (LogDetailEnums value : LogDetailEnums.values()) {
if (code.equals(value.code)){
return value;
}
}
return null;
}
}
常量类LogDetailConstants
package com.example.demo.enums;
public class LogDetailConstants {
public static final String CREATE_ACCOUNT = "【Add Account】id:[{0}], User Name:[{1}],pstnFlag:[{2}]";
}
第三步:Controller层方法使用自定义注解标识
package com.example.demo.controller;
import com.example.demo.bean.TeacherReq;
import com.example.demo.config.LogAnnotation;
import com.example.demo.mapper.TeacherMapper;
import com.example.demo.response.Response;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* @Author 211145187
* @Date 2022/5/7 11:34
**/
@RestController
public class Controller {
@RequestMapping(value = "/operateLog", method = RequestMethod.POST)
@LogAnnotation(logModelType = "CREATE_ACCOUNT")
public Response operateLog(@RequestBody OperateTeacherReq req) {
return Response.success(req);
}
}
第四步:新建一个日志操作类LogAopAction,专门用来处理操作保存日志
package com.example.demo.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.bean.OperateTeacherReq;
import com.example.demo.enums.LogDetailEnums;
import com.example.demo.response.Response;
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.stereotype.Component;
import java.lang.reflect.Method;
import java.text.MessageFormat;
@Aspect
@Component
public class LogAopAction {
/**
* AOP切面保存操作日志
* @Author 211145187
* @Around 注解描述的方法为一个环绕通知方法,在此方法中可以添加扩展业务逻辑,可以调用下一个切面对象或目标方法
* @param point 连接点(此连接点只应用@Around描述的方法)
* @Date 2022/5/16 14:38
* @param point
* @Return Response
**/
@Around("@annotation(com.example.demo.config.LogAnnotation) && execution(* com.example.demo.controller.*.*(..))")
public Response logOperate(ProceedingJoinPoint point) throws NoSuchMethodException {
//获取类的字节码对象,通过字节码对象获取方法信息
Class<?> targetCls=point.getTarget().getClass();
//获取方法签名(通过此签名获取目标方法信息)
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//获取目标方法上的注解指定的操作名称
Method targetMethod=
targetCls.getDeclaredMethod(
signature.getName(),
signature.getParameterTypes());
System.out.println("获取目标方法上的注解指定的操作名称:"+targetMethod);
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
String logModelType=logAnnotation.logModelType();
System.out.println("获取自定义注解参数值logModelType:" + logModelType);
//获取目标方法名(目标类型+方法名)
String targetClsName=targetCls.getName();
String targetObjectMethodName=targetClsName+"."+method.getName();
System.out.println("获取目标方法名:" + targetObjectMethodName);
//获取请求参数
Object[] args = point.getArgs();
//TODO 操作日志保存到数据库中
String logDetailInfo = LogDetailEnums.getDetailByCode(logModelType);
switch (LogDetailEnums.getLogDetailEnum(logModelType)) {
case CREATE_ACCOUNT:
OperateTeacherReq req = JSONObject.parseObject(JSON.toJSONString(args[0]), OperateTeacherReq.class);
System.out.println("获取请求参数:" + req);
JSONObject jsonObject = JSONObject.parseObject(req.toString());
logDetailInfo = MessageFormat.format(logDetailInfo, req.getId(), req.getName(), LogDetailEnums.getDetailByCode("pstnFlag_" + jsonObject.getString("pstnFlag")));
break;
default:
System.out.println("无该类型!");
break;
}
return Response.success(logDetailInfo);
}
}
第五步:postman模拟调用接口,输出AOP中ProceedingJoinPoint获取目标方法,参数,注解
常量类详情日志占位符:
public static final String CREATE_ACCOUNT = "【Add Account】id:[{0}], User Name:[{1}],pstnFlag:[{2}]";postman调用接口
postman结果打印
控制台打印:
获取目标方法上的注解指定的操作名称:public com.example.demo.response.Response com.example.demo.controller.Controller.operateLog(com.example.demo.bean.OperateTeacherReq)
获取自定义注解参数值logModelType:CREATE_ACCOUNT
获取目标方法名:com.example.demo.controller.Controller.operateLog
获取请求参数:{
id:1, name:'教师1', pstnFlag:1}
项目代码路径图片