前言
关于 Spring AOP 介绍及注解使用,请查看我的另一篇文章 Spring AOP介绍及注解使用。
本文示例代码已上传 GitHub:spring-boot-aop-audit
使用
添加 Spring AOP 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
添加自定义注解:
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AuditLog { /** * 日志描述 * @return */ String description() default ""; /** * 审计日志模块(登录/用户模块/角色模块等) * @return */ AuditLogModuleEnum module() default AuditLogModuleEnum.USER; /** * 审计日志类型(登录/新增/修改/删除等) * @return */ AuditLogOperationTypeEnum type() default AuditLogOperationTypeEnum.QUERY; }
有三个参数组成,description
指操作描述;module
是个枚举,用于说明属于哪个模块;type
也是个枚举,用于说明操作类型,比如「登录」、「新增」、「删除」等。
- 添加功能模块枚举
public enum AuditLogModuleEnum {
/**
* 定义审计日志模块
*/
LOGIN(0, "登录"),
USER(1, "用户模块"),
ROLE(2, "角色模块"),
PERMISSION(3, "权限模块");
AuditLogModuleEnum(int moduleCode, String moduleName) {
this.moduleCode = moduleCode;
this.moduleName = moduleName;
}
@Getter
private final int moduleCode;
@Getter
private final String moduleName;
}
- 添加操作类型枚举
public enum AuditLogOperationTypeEnum {
/**
* 定义审计日志操作类型枚举
*/
LOGIN(1, "登录"),
LOGOUT(2, "注销登录"),
INSERT(3, "新增"),
UPDATE(4, "修改"),
DELETE(5, "删除"),
QUERY(6, "查询");
AuditLogOperationTypeEnum(int typeCode, String typeName) {
this.typeCode = typeCode;
this.typeName = typeName;
}
@Getter
private final int typeCode;
@Getter
private final String typeName;
}
- 定义注解切面
@Aspect
@Component
@Slf4j
public class AuditLogAspect {
private final IOperationLogService operationLogService;
@Autowired
public AuditLogAspect(IOperationLogService operationLogService) {
this.operationLogService = operationLogService;
}
/**
* 定义注解切点,注解拦截
*/
@Pointcut("@annotation(com.lanweihong.aop.annotation.AuditLog)")
public void auditLog() {
}
/**
* 前置通知,方法调用前被调用
* 除非抛出一个异常,否则这个通知不能阻止连接点之前的执行流程
*/
@Before("auditLog()")
public void doBefore() {
log.info("进入 @Before ...");
}
/**
* 环绕增强
* 先执行 pjp.proceed() 然后进入 @Before,然后执行主方法,回到 @Around 的 pjp.proceed() 后
* @param pjp ProceedingJoinPoint
* @return
*/
@Around("auditLog()")
public Object doAround(ProceedingJoinPoint pjp) {
log.info("进入 @Around ......");
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("退出 @Around ......");
return result;
}
/**
* 后置通知,如果切入点抛出异常,则不会执行
*/
@AfterReturning(pointcut = "auditLog()", returning = "keys")
public void doAfterReturning(JoinPoint joinPoint, Object keys) {
// 获取 RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Assert.notNull(requestAttributes, "获取 RequestAttributes 为空");
// 获取 HttpServletRequest
HttpServletRequest request = (HttpServletRequest)requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 获取注解参数信息
AuditLogDO operationLogDO = new AuditLogDO();
// 从切入点通过反射获取切入点方法
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
// 获取执行方法,e.g. public void com.lanweihong.aop.controller.UserController.test()
Method method = signature.getMethod();
// 获取方法参数
Object[] args = joinPoint.getArgs();
// 获取注解
AuditLog operationLog = method.getAnnotation(AuditLog.class);
if (null != operationLog) {
operationLogDO.setModule(operationLog.module().getModuleCode());
operationLogDO.setOperationType(operationLog.type().getTypeCode());
// 解析表达式
String desc = parseDescriptionExpression(operationLog.description(), args);
operationLogDO.setDescription(desc);
}
// TODO 请自己获取用户信息
operationLogDO.setUserId(null)
.setUserName("");
Assert.notNull(request, "获取 HttpServletRequest 为空");
// 获取 IP
String ip = RequestUtils.getIpAddr(request);
String userAgent = request.getHeader("user-agent");
operationLogDO.setIp(ip)
.setUserAgent(userAgent);
operationLogService.addOperationLog(operationLogDO);
}
/**
* 异常返回通知,切入点抛出异常后执行
* @param joinPoint 切入点
* @param e 异常
*/
@AfterThrowing(pointcut = "auditLog()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("进入 @AfterThrowing...错误:" + e.getMessage());
}
/**
* 解析 SpEL 表达式,从参数中获取数据并生成描述
* @param descriptionExpression 描述表达式(使用 SpEL 表达式)
* @param args 参数
* @return
*/
private String parseDescriptionExpression(String descriptionExpression, Object[] args) {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(descriptionExpression, new TemplateParserContext());
return expression.getValue(new StandardEvaluationContext(args), String.class);
}
}
其中获取 IP 用到的工具类 RequestUtils
:
public class RequestUtils {
/**
* 定义获取 ip 的 header
*/
private static final String[] IP_HEADER_CANDIDATES = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR" };
/**
* 获取 IP 地址
* @param request HttpServletRequest
* @return IP
*/
public static String getIpAddr(HttpServletRequest request) {
for (String header : IP_HEADER_CANDIDATES) {
String ip = request.getHeader(header);
if (null != ip && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
}
String ip = request.getRemoteAddr();
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
- 新增审计实体类
@Getter
@Setter
@Accessors(chain = true)
@ToString
@Table(name = "audit_log")
public class AuditLogDO extends BaseEntity implements Serializable {
private static final long serialVersionUID = 8364702890093931325L;
@Column(name = "user_id")
private Long userId;
@Column(name = "user_name")
private String userName;
@Column(name = "description")
private String description;
@Column(name = "operation_type")
private Integer operationType;
@Column(name = "module")
private Integer module;
@Transient
private String moduleName;
@Column(name = "ip")
private String ip;
@Column(name = "user_agent")
private String userAgent;
}
新增相关的 Dao/Mapper/Service;为省略篇幅,这里就略过了,完整代码在 GitHub 中可看到,重点不是这个。
在 Controller 方法上添加注解
@AuditLog
使用:
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
@GetMapping("/{userid}")
public JsonResult<String> getUserById(@PathVariable(value = "userid") Long userId) {
// TODO...
return JsonResult.ok();
}
@AuditLog(description = "新增用户,用户名:#{[0].userName}", type = AuditLogOperationTypeEnum.INSERT)
@PostMapping
public JsonResult<String> addUser(@Valid @RequestBody UserParam param) {
// TODO...
return JsonResult.ok();
}
@AuditLog(description = "修改用户,用户Id:#{[0]},用户名:#{[1].userName}")
@PutMapping("/{userid}")
public JsonResult<String> updateUser(@PathVariable(value = "userid", required = false) Long userId, @Valid @RequestBody UserParam param) {
// TODO...
return JsonResult.ok();
}
@AuditLog(description = "删除用户,用户 id:#{[0]}", type = AuditLogOperationTypeEnum.DELETE)
@DeleteMapping("/{userid}")
public JsonResult<String> deleteUser(@PathVariable(value = "userid") Long userId) {
// TODO...
return JsonResult.ok();
}
}
其中,description
参数值用到 SpEL ,可以使用参数变量填充。简单介绍用法:#{}
表示引用参数变量,[]
表示方法的参数组使用的第 N 个参数,如 addUser
方法的就可以使用 #{[0].userName}
来获取方法参数中的第一个参数的 userName
属性。关于 SpEL 的用法,可去查看官方文档 Spring Expression Language (SpEL) 。
- 启动项目,测试请求。
启动项目后,使用 Postman 发送请求测试,查看数据库记录,发现已添加有审计日志。
总结
使用 Spring AOP 实现的审计日志功能很简单,在不改变原来的业务逻辑代码情况下,使用 AOP 可以很轻松优雅的实现审计功能。
本文仅介绍最简单的用法,实际开发中,有些需求是要记录数据的变化值(旧值与新值)的,这些都需要自己去拓展了。
还有,本例中未针对事务回滚做相应的配置。