为什么要记录接口日志?
至于为什么,具体看到这里的小伙伴心里都有一个答案吧,我这里简略列一下罕用的场景吧🙈
- 用户登录记录统计
- 重要增删改操作留痕
- 须要统计用户的拜访次数
- 接口调用状况统计
- 线上问题排查
- 等等等 …
既然有这么多应用场景,那咱们该怎么解决,总不能一条一条的去记录吧🥶
面试是不是老是被问 Spring 的 Aop 的应用场景,那这个典型的场景就来了,咱们能够应用 Spring 的 Aop,完满的实现这个性能,接下来上代码😁
先定义一下日志存储的对象吧
本文波及到依赖:
- lombok
- swagger
- mybatisplus
简略如下,能够依据本人的需要进行批改
贴一下建表 sql 吧
CREATE TABLE `sys_operate_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) DEFAULT ''COMMENT' 模块题目 ',
`business_type` int(2) DEFAULT '4' COMMENT '业务类型(0 查问 1 新增 2 批改 3 删除 4 其余)',
`method` varchar(100) DEFAULT ''COMMENT' 办法名称 ',
`resp_time` bigint(20) DEFAULT NULL COMMENT '响应工夫',
`request_method` varchar(10) DEFAULT ''COMMENT' 申请形式 ',
`browser` varchar(255) DEFAULT NULL COMMENT '浏览器类型',
`operate_type` int(1) DEFAULT '3' COMMENT '操作类别(0 网站用户 1 后盾用户 2 小程序 3 其余)',
`operate_url` varchar(255) DEFAULT ''COMMENT' 申请 URL',
`operate_ip` varchar(128) DEFAULT ''COMMENT' 主机地址 ',
`operate_location` varchar(255) DEFAULT ''COMMENT' 操作地点 ',
`operate_param` text COMMENT '申请参数',
`json_result` text COMMENT '返回参数',
`status` int(1) DEFAULT '0' COMMENT '操作状态(0 失常 1 异样)',
`error_msg` text COMMENT '谬误音讯',
`create_id` bigint(20) DEFAULT NULL COMMENT '操作人 id',
`create_name` varchar(50) DEFAULT ''COMMENT' 操作人员 ',
`create_time` datetime DEFAULT NULL COMMENT '操作工夫',
`update_id` bigint(20) NULL DEFAULT NULL COMMENT '更新人 id',
`update_name` varchar(64) NULL DEFAULT ''COMMENT' 更新者 ',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新工夫',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理 - 操作日志记录';
应用的 mybatis plus 的主动生成代码性能生成的对象,详情参考[SpringBoot 集成 Mybatis Plus](),真香🤪
package com.maple.demo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.maple.demo.config.bean.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 系统管理 - 操作日志记录
* </p>
*
* @author 笑小枫
* @since 2022-07-21
*/
@Getter
@Setter
@TableName("sys_operate_log")
@ApiModel(value = "OperateLog 对象", description = "系统管理 - 操作日志记录")
public class OperateLog extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("模块题目")
private String title;
@ApiModelProperty("业务类型(0 查问 1 新增 2 批改 3 删除 4 其余)")
private Integer businessType;
@ApiModelProperty("办法名称")
private String method;
@ApiModelProperty("响应工夫")
private Long respTime;
@ApiModelProperty("申请形式")
private String requestMethod;
@ApiModelProperty("浏览器类型")
private String browser;
@ApiModelProperty("操作类别(0 网站用户 1 后盾用户 2 小程序 3 其余)")
private Integer operateType;
@ApiModelProperty("申请 URL")
private String operateUrl;
@ApiModelProperty("主机地址")
private String operateIp;
@ApiModelProperty("操作地点")
private String operateLocation;
@ApiModelProperty("申请参数")
private String operateParam;
@ApiModelProperty("返回参数")
private String jsonResult;
@ApiModelProperty("操作状态(0 失常 1 异样)")
private Integer status;
@ApiModelProperty("谬误音讯")
private String errorMsg;
}
mapper 代码就不贴了,都是生成的,只用到了 mybatis plus 的 insert 办法,上面别再问我为什么少个类了😂
定义切点、Aop 实现性能
定义波及到枚举类
在 config 包下创立一个专门寄存枚举的包 enums
吧(父包名称不应该叫 vo 的,是我格局小了,一误再误吧🙈)
业务类型
BusinessTypeEnum 枚举类:
package com.maple.demo.config.enums;
/**
* @author 笑小枫
* @date 2022/7/21
*/
public enum BusinessTypeEnum {
// 0 查问 1 新增 2 批改 3 删除 4 其余
SELECT,
INSERT,
UPDATE,
DELETE,
OTHER
}
操作类别 OperateTypeEnum
枚举类:
package com.maple.demo.config.enums;
/**
* @author 笑小枫
* @date 2022/6/27
*/
public enum OperateTypeEnum {
// 0 网站用户 1 后盾用户 2 小程序 3 其余
BLOG,
ADMIN,
APP,
OTHER
}
定义切点的注解
定义一个自定义注解MapleLog.java
,哪些接口须要记录日志就靠它了,命名依据本人的调整哈,我的 maple,谁叫我是笑小枫呢,不要好奇的点这个链接,不然你会发现惊喜😎
package com.maple.common.model;
import com.maple.common.enums.BusinessTypeEnum;
import com.maple.common.enums.OperateTypeEnum;
import java.lang.annotation.*;
/**
* @author 笑小枫
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MapleLog {
// 0 网站用户 1 后盾用户 2 小程序 3 其余
OperateTypeEnum operateType() default OperateTypeEnum.OTHER;
// 0 查问 1 新增 2 批改 3 删除 4 其余
BusinessTypeEnum businessType() default BusinessTypeEnum.SELECT;
// 返回保留后果是否落库,没用的大后果能够不记录,比方分页查问等等,设为 false 即可
boolean saveResult() default true;}
Aop 实现性能
应用了 Aop 的盘绕告诉,其中 JwtUtil
是零碎中存储登录用户用的,能够参考 [SpringBoot 集成 Redis]() 依据本人的零碎来,没有去掉就 OK
OperateLogMapper
是 mybatis plus 生成的保留到数据的,依据本人的业务来,不须要入库,能够间接打印 log,疏忽它🙈
参数和返回后果的值,数据库类型是 text,长度不能超过 65535,这里截取了 65000
形容取的 Swagger 的 @ApiOperation
注解的值,如果我的项目没有应用 Swagger,能够在自定义注解增加一个 desc 形容😅
package com.maple.demo.config.aop;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.maple.demo.config.annotation.MapleLog;
import com.maple.demo.config.bean.GlobalConfig;
import com.maple.demo.entity.OperateLog;
import com.maple.demo.mapper.OperateLogMapper;
import com.maple.demo.util.JwtUtil;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;
/**
* @author 笑小枫
* 配置切面类,@Component 注解把切面类放入 Ioc 容器中
*/
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class SystemLogAspect {
private final OperateLogMapper operateLogMapper;
@Pointcut(value = "@annotation(com.maple.demo.config.annotation.MapleLog)")
public void systemLog() {// nothing}
@Around(value = "systemLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
int maxTextLength = 65000;
Object obj;
// 定义执行开始工夫
long startTime;
// 定义执行完结工夫
long endTime;
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 取 swagger 的形容信息
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
MapleLog mapleLog = method.getAnnotation(MapleLog.class);
OperateLog operateLog = new OperateLog();
try {operateLog.setBrowser(request.getHeader("USER-AGENT"));
operateLog.setOperateUrl(request.getRequestURI());
operateLog.setRequestMethod(request.getMethod());
operateLog.setMethod(String.valueOf(joinPoint.getSignature()));
operateLog.setCreateTime(new Date());
operateLog.setOperateIp(getIpAddress(request));
// 取 JWT 的登录信息,无需登录能够疏忽
if (request.getHeader(GlobalConfig.TOKEN_NAME) != null) {operateLog.setCreateName(JwtUtil.getAccount());
operateLog.setCreateId(JwtUtil.getUserId());
}
String operateParam = JSON.toJSONStringWithDateFormat(joinPoint.getArgs(), "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteMapNullValue);
if (operateParam.length() > maxTextLength) {operateParam = operateParam.substring(0, maxTextLength);
}
operateLog.setOperateParam(operateParam);
if (apiOperation != null) {operateLog.setTitle(apiOperation.value() + "");
}
if (mapleLog != null) {operateLog.setBusinessType(mapleLog.businessType().ordinal());
operateLog.setOperateType(mapleLog.operateType().ordinal());
}
} catch (Exception e) {e.printStackTrace();
}
startTime = System.currentTimeMillis();
try {obj = joinPoint.proceed();
endTime = System.currentTimeMillis();
operateLog.setRespTime(endTime - startTime);
operateLog.setStatus(0);
// 判断是否保留返回后果,列表页能够设为 false
if (Objects.nonNull(mapleLog) && mapleLog.saveResult()) {String result = JSON.toJSONString(obj);
if (result.length() > maxTextLength) {result = result.substring(0, maxTextLength);
}
operateLog.setJsonResult(result);
}
} catch (Exception e) {
// 记录异样信息
operateLog.setStatus(1);
operateLog.setErrorMsg(e.toString());
throw e;
} finally {endTime = System.currentTimeMillis();
operateLog.setRespTime(endTime - startTime);
operateLogMapper.insert(operateLog);
}
return obj;
}
/**
* 获取 Ip 地址
*/
private static String getIpAddress(HttpServletRequest request) {String xip = request.getHeader("X-Real-IP");
String xFor = request.getHeader("X-Forwarded-For");
String unknown = "unknown";
if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor)) {
// 屡次反向代理后会有多个 ip 值,第一个 ip 才是实在 ip
int index = xFor.indexOf(",");
if (index != -1) {return xFor.substring(0, index);
} else {return xFor;}
}
xFor = xip;
if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor)) {return xFor;}
if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {xFor = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {xFor = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {xFor = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {xFor = request.getRemoteAddr();
}
return xFor;
}
}
就这样,简略吧,拿去用吧
写个测试类吧
package com.maple.demo.controller;
import com.maple.demo.config.annotation.MapleLog;
import com.maple.demo.config.bean.ErrorCode;
import com.maple.demo.config.enums.BusinessTypeEnum;
import com.maple.demo.config.enums.OperateTypeEnum;
import com.maple.demo.config.exception.MapleCommonException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
/**
* @author 笑小枫
* @date 2022/7/21
*/
@RestController
@RequestMapping("/example")
@Api(tags = "实例演示 - 日志记录演示接口")
public class TestSystemLogController {@ApiOperation(value = "测试带参数、有返回后果的 get 申请")
@GetMapping("/testGetLog/{id}")
@MapleLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER)
public Test testGetLog(@PathVariable Integer id) {Test test = new Test();
test.setName("笑小枫");
test.setAge(18);
test.setRemark("大家好,我是笑小枫,喜爱我的小伙伴点个赞呗");
return test;
}
@ApiOperation(value = "测试 json 参数、抛出异样的 post 申请")
@PostMapping("/testPostLog")
@MapleLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER, saveResult = false)
public Test testPostLog(@RequestBody Test param) {Test test = new Test();
test.setName("笑小枫");
if (test.getAge() == null) {
// 这里应用了自定义异样,测试能够间接抛出 RuntimeException
throw new MapleCommonException(ErrorCode.COMMON_ERROR);
}
test.setRemark("大家好,我是笑小枫,喜爱我的小伙伴点个赞呗");
return test;
}
@Data
static class Test {
private String name;
private Integer age;
private String remark;
}
}
浏览器申请 http://localhost:6666/example/testGetLog/1
再模仿一下 post 异样申请吧:POST http://localhost:6666/example/testPostLog
看一下数据落库的后果吧,emmm… operate_location
没采集,疏忽吧🤣
对于笑小枫💕
本章到这里完结了,喜爱的敌人关注一下我呦😘😘,大伙的反对,就是我保持写下去的能源。
老规矩,懂了就点赞珍藏;不懂就问,日常在线,我会就会回复哈~🤪
微信公众号:笑小枫
笑小枫集体博客:https://www.xiaoxiaofeng.com
CSDN:https://zhangfz.blog.csdn.net
本文源码:https://github.com/hack-feng/maple-demo