今天做了一个需求,对系统中重要操作进行操作日志的收集和持久化。
当然,这个需求最简单就是在控制层植入一段业务操作日志保存的逻辑,这样只适合体量非常小的项目。如果接口数量多,将重复写很多逻辑一样的代码,已开发上线的接口,还要重新再修改植入业务操作日志保存的逻辑并测试,就非常重复和麻烦。
一、Spring AOP
AOP(Aspect-Oriented Programming),⾯向切⾯编程,说起AOP,几乎学过Spring框架的人都知道,它是Spring的三大核心思想之一(IOC:控制反转,DI:依赖注入,AOP:面向切面编程)。能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
SpringAOP,则是AOP的一种具体实现,Spring内部对SpringAOP的应用最经典的场景就是Spring的事务,通过事务注解的配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略;与过滤器、拦截器相比,更加重要的是其适用范围不再局限于SpringMVC项目,可以在任意一层定义一个切点,织入相应的操作,并且还可以改变返回值。
二、AOP核心要素
1、什么时候切入:业务代码执行前还是后;
2、在哪里切入:即切入点在哪里;
3、干什么事情:即切入后的处理逻辑,比如权限校验,日志记录等。
三、具体实现
1、Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、自定义注解
/**
* 自定义操作日志注解
*/
@Target(ElementType.METHOD) // 注解放置的目标位置,METHOD表示可应用在方法上
@Retention(RetentionPolicy.RUNTIME) //指定注解的保留策略,RUNTIME表示在运行时任然存在
@Documented // 表示该注解可以出现在生成的Api文档中
public @interface OperateLog {
/**
* 业务名称
*/
String operModul() default "";
/**
* 操作说明
*/
String operDesc() default "";
/**
* 操作类型
*/
EnumOperateType operType() default EnumOperateType.GET;
}
3、操作类型枚举
/**
* 日志操作类型
*/
public enum EnumOperateType {
POST("新增"),
PUT("修改"),
DELETE("删除"),
GET("查询"),
UP("启用"),
DOWN("停用"),
ADDORUPDATE("新增或修改");
private final String typeName;
EnumOperateType(String typeName) {
this.typeName = typeName;
}
public String getTypeName() {
return typeName;
}
}
4、数据模型对象
/**
* 操作日志表;
*/
@Data
@TableName("t_operate_log")
public class TOperateLog extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 创建人
*/
@TableField("create_name")
private String createName;
/**
* 更新人
*/
@TableField("update_name")
private String updateName;
/**
* 业务名称
*/
@TableField("model_name")
private String modelName;
/**
* 操作记录
*/
@TableField("operate_content")
private String operateContent;
/**
* 操作账号
*/
@TableField("operate_account")
private String operateAccount;
/**
* 操作用户
*/
@TableField("operate_user")
private String operateUser;
/**
* 操作时间
*/
@TableField("operate_time")
private Date operateTime;
/**
* 操作单位
*/
@TableField("unit_id")
private String unitId;
/**
* 记录相关ID
*/
@TableField("row_id")
private String rowId;
/**
* 操作类型( select update...)
*/
@TableField("row_type")
private String rowType;
}
5、切面处理类
/**
* 自定义操作日志注解切面处理类
*/
@Aspect // 标记该类为切面,结合其他注解(@Before、@After、@Around 等)来具体定义切面的行为
@Component
public class OperLogAspect {
@Resource
private PlatformFeignClient platformFeignClient;
/**
* 操作日志切入点:表示在注解位置切入代码
* 括号内写自定义注解完全限定名
*/
@Pointcut("@annotation(com.csin.common.annotation.OperateLog)")
public void operLogPoinCut() {
}
/**
* 目标方法执行成功并返回后执行
*
* @param joinPoint 目标方法信息对象,可获取目标方法方法名、参数等
* @param keys 目标方法返回值,可以将目标方法返回值传递给切面方法这个参数
*/
@AfterReturning(value = "operLogPoinCut()", returning = "keys")// 切入点表达式;表示将目标方法返回值绑定到切面方法参数keys
public void saveOperLog(JoinPoint joinPoint, Object keys) {
// 获取 RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从 RequestAttributes 中获取 HttpServletRequest 的信息(可获取请求方法,URL,IP等)
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 创建一个 TOperateLog 对象用于保存操作日志
TOperateLog operateLog = new TOperateLog();
try {
// 通过反射机制获取切面织入点处的方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法对象
Method method = signature.getMethod();
// 获取方法上的 OperateLog 注解
OperateLog opLog = method.getAnnotation(OperateLog.class);
if (opLog != null) {
// 获取注解中的操作模块、操作类型和操作描述信息,并设置到操作日志对象中
String operModul = opLog.operModul(); // 获取操作模块
EnumOperateType operType = opLog.operType(); // 获取操作类型
String operDesc = opLog.operDesc(); // 获取操作描述
operateLog.setModelName(operModul); // 设置业务名称
operateLog.setRowType(operType.getTypeName()); // 设置操作类型
operateLog.setOperateContent(operDesc); // 设置操作记录
}
operateLog.setOperateTime(new Date());
operateLog.setUnitId(UserUtil.getUnitId());
operateLog.setOperateUser(UserUtil.getUsername());
operateLog.setOperateAccount(UserUtil.getAccount());
// 调用 platformFeignClient 的 add 方法,向日志服务中添加操作日志(根据场景实际进行持久化)
ApiResponse<Boolean> add = platformFeignClient.add(operateLog);
// 判断日志服务添加操作是否成功
if (!add.isSuccess()) {
throw new BusinessException("feign调用日志服务失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
6、测试
/**
* 测试控制器
**/
@RefreshScope
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/logTest")
@OperateLog(operModul = "测试接口模块", operDesc = "测试注解效果", operType = EnumOperateType.POST)
public ApiResponse<Boolean> logAnnotationTest () {
System.out.println("测试自定义操作日志注解.........");
return ApiResponse.success(true);
}
}
7、数据库表
CREATE TABLE `t_operate_log` (
`id` varchar(32) NOT NULL COMMENT '主键ID',
`create_name` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_name` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`model_name` varchar(32) NOT NULL COMMENT '业务名称',
`operate_content` varchar(255) NOT NULL COMMENT '操作记录',
`operate_account` varchar(255) DEFAULT NULL COMMENT '操作账号',
`operate_user` varchar(255) DEFAULT NULL COMMENT '操作用户',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
`unit_id` varchar(32) DEFAULT NULL COMMENT '操作单位',
`row_id` varchar(32) DEFAULT NULL COMMENT '记录相关ID',
`row_type` varchar(16) DEFAULT NULL COMMENT '操作类型( select update...)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='操作日志表;';
8、注意事项
如果你自定义的操作日志注解的切面逻辑未生效,可以按照以下步骤进行排查:
1、确保注解和切面类的定义正确:检查自定义的操作日志注解和切面类的定义是否正确,包括注解的声明和切面类的命名、注解的元注解等。
2、确保切面类被正确扫描:切面类需要被 Spring AOP 所扫描到才能生效。确保切面类所在的包是被扫描的,可以使用 @ComponentScan 或 <context:component-scan> 等注解或配置来指定扫描的包。
3、检查切面类的顺序:如果有多个切面类作用于同一个目标方法,切面的执行顺序可能会影响到最终的结果。可以为切面类添加 @Order 注解来指定切面的执行顺序。
4、确认目标类和方法的代理情况:如果目标类和方法是在同一个类中,并且是通过直接调用而非通过代理调用的方式,切面逻辑可能不会生效。确保目标类和方法是通过代理调用,例如使用基于接口的代理或者使用 Spring AOP 提供的代理机制。