自定义操作日志注解
在开发中,会有保存用户操作信息的这种需求,我们可以使用自定义注解的方式实现。
准备工作
我们是要将操作信息保存到数据库,会有一个操作信息表 sys_oper_log
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `sys_oper_log`;
CREATE TABLE `sys_oper_log` (
`oper_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '模块标题',
`business_type` int(2) NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)',
`method` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '方法名称',
`request_method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求方式',
`operator_type` int(1) NULL DEFAULT 0 COMMENT '操作类别(0其它 1后台用户 2手机端用户)',
`oper_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作人员',
`dept_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '部门名称',
`oper_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求URL',
`oper_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '主机地址',
`oper_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作地点',
`oper_param` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求参数',
`json_result` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '返回参数',
`status` int(1) NULL DEFAULT 0 COMMENT '操作状态(0正常 1异常)',
`error_msg` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '错误消息',
`oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`oper_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
然后创建一个 SpringBoot 工程,引入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
配置文件
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql:///test08?serverTimezone=Asia/Shanghai&useSSL=false
mybatis.mapper-locations= classpath*:mapper/**/*Mapper.xml
创建 SysOperLog 实体类
public class SysOperLog {
private static final long serialVersionUID = 1L;
/**
* 日志主键
*/
private Long operId;
/**
* 操作模块
*/
private String title;
/**
* 业务类型(0其它 1新增 2修改 3删除)
*/
private Integer businessType;
/**
* 业务类型数组
*/
private Integer[] businessTypes;
/**
* 请求方法
*/
private String method;
/**
* 请求方式
*/
private String requestMethod;
/**
* 操作类别(0其它 1后台用户 2手机端用户)
*/
private Integer operatorType;
/**
* 操作人员
*/
private String operName;
/**
* 部门名称
*/
private String deptName;
/**
* 请求url
*/
private String operUrl;
/**
* 操作地址
*/
private String operIp;
/**
* 操作地点
*/
private String operLocation;
/**
* 请求参数
*/
private String operParam;
/**
* 返回参数
*/
private String jsonResult;
/**
* 操作状态(0正常 1异常)
*/
private Integer status;
/**
* 错误消息
*/
private String errorMsg;
/**
* 操作时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date operTime;
// ...省略set/get方法
}
自定义注解
定义一个 Log 注解:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}
功能 和 操作人员类别 是两个枚举类:
public enum BusinessType {
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 授权
*/
GRANT,
/**
* 导出
*/
EXPORT,
/**
* 导入
*/
IMPORT,
/**
* 强退
*/
FORCE,
/**
* 生成代码
*/
GENCODE,
/**
* 清空数据
*/
CLEAN,
}
/**
* 操作人类别
*/
public enum OperatorType {
/**
* 其它
*/
OTHER,
/**
* 后台用户
*/
MANAGE,
/**
* 手机端用户
*/
MOBILE
}
保存操作记录
定义好 @Log 注解后,在创建一个切面在添加注解时进行操作日志处理:
/**
* 操作日志记录处理
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
ObjectMapper mapper = new ObjectMapper();
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(200);
operLog.setOperName("张三");
// 请求的地址
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip = IpUtils.getIpAddr(requestAttributes.getRequest());
operLog.setOperIp(ip);
operLog.setOperUrl(requestAttributes.getRequest().getRequestURI());
if (e != null) {
operLog.setStatus(500);
operLog.setErrorMsg(e.getMessage().substring(0,2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(requestAttributes.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && jsonResult != null) {
// 将返回值转成 json 格式
operLog.setJsonResult(mapper.writeValueAsString(jsonResult));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
// 参数放在请求体里面的情况
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(params);
} else {
// 参数放在地址栏里面的情况
// 提取地址栏中的参数
Map<?, ?> paramsMap = (Map<?, ?>) requestAttributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operLog.setOperParam(paramsMap.toString());
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (o != null && !isFilterObject(o)) {
try {
String jsonString = mapper.writeValueAsString(o);
params += jsonString + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
/**
* 判断是否需要过滤的对象。
*上传文件对象,集合中保存的上传对象,map中保存了文件上传对象、HttpServletRequest、HttpServletResponse、BindingResult 都过滤掉
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
@AfterReturning 返回通知,有返回值会触发此方法,@AfterThrowing 异常通知,当有异常时会触发此方法。这两个方法都会调用 handleLog 方法对 SysOperLog 进行赋值。
在 handleLog() 方法中,对 status、operName 赋值,operName 我这里写死了这里是登录用户的名称。然后给 ip 和 operUrl 赋值,如果有异常更新状态和异常信息。
getControllerMethodDescription() 中获取注解上的信息给 operLog 赋值,如果需要保存参数信息,需要在 setRequestValue() 方法中对请求方式进行判断,如果是 POST 或 PUT 方式,获取请求体中的全部参数调用 argsArrayToString() 方法遍历参数,同时在isFilterObject() 中判断是否需要将参数过滤,因为文件上传对象等参数只是一个二进制代码,没有意义所以要过滤掉,最后将参数转成 JSON 进行拼接。如是 GET 等其他方式,直接获取地址栏的参数进行赋值。最后将返回值也进行赋值。
最后执行 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); ,关于异步的操作可以看这篇文章 异步管理器
/**
* 异步工厂(产生任务用)
*
* @author tienchin
*/
public class AsyncFactory {
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
/**
* 操作日志记录
*
* @param operLog 操作日志信息
* @return 任务task
*/
public static TimerTask recordOper(final SysOperLog operLog) {
return new TimerTask() {
@Override
public void run() {
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
}
};
}
}
SpringUtils.getBean 获取被 Spring 管理的 Bean。如果不了解怎样在非 Spring 容器中获取 Spring 中的 Bean,可以参考:Aware
/**
* 操作日志 服务层
*
* @author tienchin
*/
public interface ISysOperLogService {
/**
* 新增操作日志
*
* @param operLog 操作日志对象
*/
public void insertOperlog(SysOperLog operLog);
}
@Service
public class SysOperLogServiceImpl implements ISysOperLogService {
@Autowired
private SysOperLogMapper operLogMapper;
/**
* 新增操作日志
*
* @param operLog 操作日志对象
*/
@Override
public void insertOperlog(SysOperLog operLog) {
operLogMapper.insertOperlog(operLog);
}
}
@Mapper
public interface SysOperLogMapper {
/**
* 新增操作日志
*
* @param operLog 操作日志对象
*/
public void insertOperlog(SysOperLog operLog);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.zxqs.log_annotation.mapper.SysOperLogMapper">
<resultMap type="top.zxqs.log_annotation.domain.SysOperLog" id="SysOperLogResult">
<id property="operId" column="oper_id" />
<result property="title" column="title" />
<result property="businessType" column="business_type" />
<result property="method" column="method" />
<result property="requestMethod" column="request_method" />
<result property="operatorType" column="operator_type" />
<result property="operName" column="oper_name" />
<result property="deptName" column="dept_name" />
<result property="operUrl" column="oper_url" />
<result property="operIp" column="oper_ip" />
<result property="operLocation" column="oper_location" />
<result property="operParam" column="oper_param" />
<result property="jsonResult" column="json_result" />
<result property="status" column="status" />
<result property="errorMsg" column="error_msg" />
<result property="operTime" column="oper_time" />
</resultMap>
<sql id="selectOperLogVo">
select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time
from sys_oper_log
</sql>
<insert id="insertOperlog" parameterType="top.zxqs.log_annotation.domain.SysOperLog">
insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time)
values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, sysdate())
</insert>
</mapper>