为什么要记录接口日志?
至于为什么,具体看到这里的小伙伴心里都有一个答案吧,我这里简略列一下罕用的场景吧
- 用户登录记录统计
- 重要增删改操作留痕
- 须要统计用户的拜访次数
- 接口调用状况统计
- 线上问题排查
- 等等等...
既然有这么多应用场景,那咱们该怎么解决,总不能一条一条的去记录吧
面试是不是老是被问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)@Documentedpublic @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@AllArgsConstructorpublic 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