SpringBoot AOP完美记录用户操作日志,附源码
记录内容
- 接口名称
- 浏览器名称
- 操作系统
- 请求ip
- 接口入参、出参
- 接口耗时
- 。。。。
表结构
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_log -- ---------------------------- DROP TABLE IF EXISTS `sys_log`; CREATE TABLE `sys_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `module_name` varchar(256) DEFAULT NULL COMMENT '模块名称', `browser_name` varchar(1024) DEFAULT NULL COMMENT '浏览器名称', `os_name` varchar(256) DEFAULT NULL COMMENT '操作系统名称', `ip_addr` varchar(256) DEFAULT NULL COMMENT '请求ip', `app_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '服务名称', `class_name` varchar(1024) DEFAULT NULL COMMENT '类名', `method_name` varchar(512) DEFAULT NULL COMMENT '方法', `request_url` varchar(1024) DEFAULT NULL COMMENT '请求url', `request_method` varchar(255) DEFAULT NULL COMMENT '请求方式,POST、GET', `request_param` text COMMENT '请求参数', `result_text` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '响应参数', `status` tinyint(1) DEFAULT NULL COMMENT '接口状态(0成功 1失败)', `error_text` text COMMENT '错误信息', `take_up_time` varchar(64) DEFAULT NULL COMMENT '耗时', `edit_table_id` bigint(20) DEFAULT NULL COMMENT '编辑的表主键,只有修改时才有值', `edit_table_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '编辑的表名称,只有修改时才有值', `create_time` datetime DEFAULT NULL COMMENT '操作时间', `create_user_id` bigint(20) DEFAULT NULL COMMENT '创建人id', `create_phone_number` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '创建人手机号', `create_user_name` varchar(64) DEFAULT NULL COMMENT '创建人姓名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统操作日志'; SET FOREIGN_KEY_CHECKS = 1;
添加依赖
<!-- AOP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- 获取浏览器信息 --> <dependency> <groupId>eu.bitwalker</groupId> <artifactId>UserAgentUtils</artifactId> <version>1.21</version> </dependency>
自定义注解(一)
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { }
备注:被该注解修饰的方法,会被记录到日志中
自定义注解(二)
import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogPlus { /** * 编辑的表主键 * @return */ String editTableId() default "id"; /** * 编辑的表名称 * @return */ String editTableName() default "未知"; }
备注:被该注解修饰的类,会记录从表的id值和从表的表名(用于记录某张表的一行记录,历史修改信息,不需要可忽略)
日志表实体类
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.Date; /** * <p> * 系统操作日志 * </p> * * @author chenyanbin * @since 2021-10-13 */ @Data @TableName("sys_log") @ApiModel(value = "SysLogDO对象", description = "系统操作日志") public class LogDO implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "模块名称") private String moduleName; @ApiModelProperty(value = "浏览器名称") private String browserName; @ApiModelProperty(value = "操作系统名称") private String osName; @ApiModelProperty(value = "请求ip") private String ipAddr; @ApiModelProperty(value = "服务名称") private String appName; @ApiModelProperty(value = "类名") private String className; @ApiModelProperty(value = "方法") private String methodName; @ApiModelProperty(value = "请求url") private String requestUrl; @ApiModelProperty(value = "请求方式,POST、GET") private String requestMethod; @ApiModelProperty(value = "请求参数") private String requestParam; @ApiModelProperty(value = "响应参数") private String resultText; @ApiModelProperty(value = "接口状态(0成功 1失败)") private Byte status; @ApiModelProperty(value = "错误信息") private String errorText; @ApiModelProperty(value = "耗时") private String takeUpTime; @ApiModelProperty(value = "编辑的表主键,只有修改时才有值") private Long editTableId; @ApiModelProperty(value = "编辑的表名称,只有修改时才有值") private String editTableName; @ApiModelProperty(value = "操作时间") private Date createTime; @ApiModelProperty(value = "创建人id") private Long createUserId; @ApiModelProperty(value = "创建人手机号") private String createPhoneNumber; @ApiModelProperty(value = "创建人姓名") private String createUserName; }
Mapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface LogMapper extends BaseMapper<LogDO> { }
Aspect (AOP)
import com.alibaba.fastjson.JSON; import eu.bitwalker.useragentutils.Browser; import eu.bitwalker.useragentutils.OperatingSystem; import eu.bitwalker.useragentutils.UserAgent; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import javax.servlet.http.HttpServletRequest; /** * 操作日志处理 * * @Author:chenyanbin */ @Slf4j @Aspect
@Component public class LogAspect { @Autowired LogMapper logMapper; @Value("${spring.application.name}") private String appNname; @Pointcut("@annotation(com.yida.annotation.Log)") public void logPoint() { } @Around("logPoint()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object result = null; LogDO logDO = new LogDO(); try { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); HttpServletRequest request = (HttpServletRequest) requestAttributes .resolveReference(RequestAttributes.REFERENCE_REQUEST); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); //浏览器对象 Browser browser = userAgent.getBrowser(); //操作系统对象 OperatingSystem operatingSystem = userAgent.getOperatingSystem(); logDO.setBrowserName(browser.getName()); ApiOperation aon = methodSignature.getMethod().getAnnotation(ApiOperation.class); if (aon != null) { logDO.setModuleName(aon.value()); } logDO.setOsName(operatingSystem.getName()); logDO.setIpAddr(CommonUtil.getIpAddr(request)); logDO.setAppName(appNname); logDO.setClassName(joinPoint.getTarget().getClass().getName()); logDO.setMethodName(methodSignature.getMethod().getName()); logDO.setRequestUrl(request.getRequestURI()); logDO.setRequestMethod(request.getMethod()); //获取请求参数 CommonUtil.getRequestParam(joinPoint, methodSignature, logDO); logDO.setResultText(JSON.toJSONString(result)); logDO.setStatus((byte) 0); logDO.setCreateTime(CommonUtil.getCurrentDate()); logDO.setCreateUserId(CommonUtil.getCurrentUserId()); logDO.setCreatePhoneNumber(CommonUtil.getCurrentPhoneNumber()); logDO.setCreateUserName(CommonUtil.getCurrentUserName()); long startTime = System.currentTimeMillis(); result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); logDO.setTakeUpTime(String.format("耗时:%s 毫秒", endTime - startTime)); logDO.setResultText(result.toString()); } catch (Exception e) { logDO.setStatus((byte) 1); if (e instanceof BizException) { BizException bizException = (BizException) e; result = JsonData.buildCodeAndMsg(bizException.getCode(), bizException.getMessage()); logDO.setErrorText(result.toString()); } else if (e instanceof RpvException) { RpvException ve = (RpvException) e; result = JsonData.buildCodeAndMsg(ve.getCode(), ve.getMessage()); logDO.setErrorText(result.toString()); } else { logDO.setErrorText(e.getMessage()); result = e.getMessage(); } } finally { logMapper.insert(logDO); } return result; } }
String agent=request.getHeader("User-Agent"); //解析agent字符串 UserAgent userAgent = UserAgent.parseUserAgentString(agent); //获取浏览器对象 Browser browser = userAgent.getBrowser(); //获取操作系统对象 OperatingSystem operatingSystem = userAgent.getOperatingSystem(); System.out.println("浏览器名:"+browser.getName()); System.out.println("浏览器类型:"+browser.getBrowserType()); System.out.println("浏览器家族:"+browser.getGroup()); System.out.println("浏览器生产厂商:"+browser.getManufacturer()); System.out.println("浏览器使用的渲染引擎:"+browser.getRenderingEngine()); System.out.println("浏览器版本:"+userAgent.getBrowserVersion()); System.out.println("操作系统名:"+operatingSystem.getName()); System.out.println("访问设备类型:"+operatingSystem.getDeviceType()); System.out.println("操作系统家族:"+operatingSystem.getGroup()); System.out.println("操作系统生产厂商:"+operatingSystem.getManufacturer());
其他类
import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.MessageDigest; import java.util.*; /** * 公共工具类 * * @Author:chenyanbin */ @Slf4j public class CommonUtil { private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); /** * 获取ip * * @param request * @return */ public static String getIpAddr(HttpServletRequest request) { String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() // = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress = ""; } return ipAddress; } /** * 获取当前时间戳 * * @return */ public static long getCurrentTimestamp() { return System.currentTimeMillis(); } /** * 获取当前日期 * * @return */ public static Date getCurrentDate() { return new Date(); } /** * 获取当前操作用户的主键id * * @return */ public static Long getCurrentUserId() { LoginUser loginUser = getLoginUser(); if (loginUser == null) { return null; } return loginUser.getId(); } private static LoginUser getLoginUser() { return JwtFilter.threadLocal.get(); } /** * 获取当前操作用户的手机号 * * @return */ public static String getCurrentPhoneNumber() { LoginUser loginUser = getLoginUser(); if (loginUser == null) { return null; } return loginUser.getPhoneNumber(); } /** * 获取当前操作用户的名称 * * @return */ public static String getCurrentUserName() { LoginUser loginUser = getLoginUser(); if (loginUser == null) { return null; } return loginUser.getUserName(); } /** * 判断当前用户是否管理员 */ public static boolean isAdmin() { return getCurrentUserId() == 1; } /** * 获取请求参数 * * @param joinPoint 切入点 * @param signature 方法签名 * @param logDO 日志对象 */ public static void getRequestParam(ProceedingJoinPoint joinPoint, MethodSignature signature, LogDO logDO) { // 参数值 Object[] args = joinPoint.getArgs(); ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); Method method = signature.getMethod(); String[] parameterNames = pnd.getParameterNames(method); Map<String, Object> paramMap = new HashMap<>(32); for (int i = 0; i < parameterNames.length; i++) { paramMap.put(parameterNames[i], args[i]); if (args[i] != null) { //反射获取具体的值 LogPlus logPlus = args[i].getClass().getAnnotation(LogPlus.class); if (logPlus != null) { Field f = null; try { f = args[i].getClass().getDeclaredField(logPlus.editTableId()); f.setAccessible(true); Object obj = f.get(args[i]); logDO.setEditTableId(Long.valueOf(obj + "")); logDO.setEditTableName(logPlus.editTableName()); } catch (Exception e) { log.error("反射获取编辑的表主键异常:{}", e.getMessage()); } finally { if (f != null) { f.setAccessible(false); } } } } } logDO.setRequestParam(paramMap.toString()); } }
import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** * 登录用户 * @Author:chenyanbin */ @Data public class LoginUser { /** * 主键 */ private Long id; /** * 手机号 */ @JsonProperty("phone_number") private String phoneNumber; /** * 用户昵称 */ @JsonProperty("user_name") private String userName; /** * 是否货主(0是 1否) */ @JsonProperty("cargo_master") private byte cargoMaster; /** * 管理员 (0是 1否) */ private byte admin; }
import lombok.Data; /** * 业务异常类 * @Author:chenyanbin */ @Data public class BizException extends RuntimeException { private int code; private String message; public BizException(int code, String message) { super(message); this.code = code; this.message = message; } public BizException(BizCodeEnum bizCodeEnum) { super(bizCodeEnum.getMessage()); this.code = bizCodeEnum.getCode(); this.message = bizCodeEnum.getMessage(); } }
import lombok.Data; /** * Request Param Value:请求参数异常 * @Author:chenyanbin */ @Data public class RpvException extends RuntimeException { private int code; private String message; public RpvException(int code, String message) { this.code = code; this.message = message; } }
package com.yida.utils; import com.yida.enums.BizCodeEnum; import java.io.Serializable; /** * @Description:统一协议JsonData工具类 * @Author:chenyanbin * @Date:2021/5/9 下午8:09 * @Versiion:1.0 */ public class JsonData<T> implements Serializable { /** * 状态码 0 表示成功,1表示处理中,-1表示失败 */ private Integer code; /** * 数据 */ private T data; /** * 描述 */ private String msg; private JsonData() { } private static <T> JsonData<T> build(Integer code, T data, String msg) { JsonData json = new JsonData(); json.setCode(code); json.setData(data); json.setMsg(msg); return json; } /** * 成功,传⼊数据 * * @return */ public static <T> JsonData<T> buildSuccess() { return build(0, (T) "", ""); } /** * 默认添加成功 * * @return */ public static <T> JsonData<T> buildAddSuccess() { return build(0, (T) "添加成功", ""); } /** * 默认修改成功 * * @return */ public static <T> JsonData<T> buildEditSuccess() { return build(0, (T) "修改成功", ""); } /** * 默认删除成功 * * @return */ public static <T> JsonData<T> buildRemoveSuccess() { return build(0, (T) "删除成功", ""); } /** * 成功,传⼊数据 * * @param data * @return */ public static <T> JsonData<T> buildSuccess(T data) { return build(0, data, ""); } /** * 失败,传⼊描述信息 * * @param msg * @return */ public static <T> JsonData<T> buildError(String msg) { return build(-1, (T) "", msg); } /** * ⾃定义状态码和错误信息 * * @param code * @param msg * @return */ public static <T> JsonData<T> buildCodeAndMsg(int code, String msg) { return build(code, null, msg); } /** * 传⼊枚举,返回信息 * * @param codeEnum * @return */ public static <T> JsonData<T> buildResult(BizCodeEnum codeEnum) { return buildCodeAndMsg(codeEnum.getCode(), codeEnum.getMessage()); } /** * 判断接口响应是否成功,只是判断状态码是否等于:0 * * @param data * @return */ public static boolean isSuccess(JsonData data) { return data.getCode() == 0; } /** * 判断接口响应是否失败,状态码除了0以外的,默认调用失败 * * @param data * @return */ public static boolean isFailure(JsonData data) { return !isSuccess(data); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public T getData() { return data; } public void setData(T data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "JsonData{" + "code=" + code + ", data=" + data + ", msg='" + msg + '\'' + '}'; } }
控制器添加@Log注解
@ApiOperation("用户登录") @PostMapping("login") @Log public JsonData login( @ApiParam("用户登录对象") @RequestBody UserLoginRequest userLoginRequest ) { return userService.login(userLoginRequest); }
修改接口,json对象添加@LogPlus
@Data @ApiModel(value = "角色编辑对象", description = "用户编辑请求对象") @LogPlus(editTableId = "id", editTableName = "sys_role") public class RoleEditRequest { @ApiModelProperty(value = "主键id", example = "-1") @DecimalMin(value = "1", message = "角色id最小为1") private Long id; @ApiModelProperty(value = "角色名称", example = "货主") @JsonProperty("role_name") @NotBlank(message = "角色名称不能为空") private String roleName; @ApiModelProperty(value = "角色状态(0正常 1停用)", example = "0") @Min(value = 0, message = "角色状态(0正常 1停用)") @Max(value = 1, message = "角色状态(0正常 1停用)") private byte status; @ApiModelProperty(value = "备注", example = "货主角色") private String remark; @ApiModelProperty(value = "菜单id列表", example = "[1,2,3,4]") private List<Long> menuIds; }
效果演示