Springboot系列(十五):基于AOP实现自定义注解,记录接口日志(实战篇二)

1,376 阅读9分钟

👨‍🎓作者:bug菌
✏️博客:CSDN掘金infoQ51CTO
🎉简介:CSDN博客专家,C站历届博客之星Top50,掘金/InfoQ/51CTO等社区优质创作者,全网合计8w粉+,对一切技术感兴趣,重心偏Java方向;硬核公众号「 猿圈奇妙屋」,欢迎小伙伴们的加入,一起秃头,一起变强。
..
✍️温馨提醒:本文字数:1999字, 阅读完需:约 5 分钟

       嗨,家人们,我是bug菌呀,我又来啦。今天我们来聊点什么咧,OK,接着为大家更《springboot零基础入门教学》系列文章吧。希望能帮助更多的初学者们快速入门!

       小伙伴们在批阅文章的过程中如果觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐+关注👨‍🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️

一、前言🔥

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

    上一期我们是简单造了一个自定义注解并且玩过了它的哪几种通知类型,对吧,ok,那这一期呢,我就具体带着大家玩一个实际项目中遇到比较多见的业务场景,就如标题所言,**记录业务日志然后保存入库,**而我们的实现方式则正是通过aop的思想来实现。

       想必很多小伙伴可能就会问了,”我为啥要以自定义注解的方式进行日志记录呢?我难道不可以通过封装一个日志记录的工具类然后进行统一调用么?“。”也没问题,这种就是以前还没玩aop的时候玩的思路,”

       我为什么要舍弃这种思路而使用aop切面的思想呢?其实啊😱,相比你在每个Controller接口方法上都加上你的记录日志方法的方式,维护成本过高吧,代码也非常冗余,基本每个业务接口都得加一遍,人工成本也过大。

       但是你使用aop就不一样了🧐,它耦合性低,能使业务处理和切面处理分开开发,扩展和修改方面,当引入了注解方式时,使用起来更加方便,你需要使用的时候,只需要在该方法或者整个类上,加上你自定义的注解即可。就能轻松实现你所谓的记录接口日志功能。

       是不是有点感受到它的魅力了🤓,那接下来我就带着大家具体来实现一个通过aop实现记录接口日志的业务场景吧。

👦二、具体实现

1、数据库新增一张log_info表

如下是我从本地数据库导出来的,给大家作为参考哈。

CREATE TABLE `log_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `type` varchar(255) DEFAULT NULL COMMENT '日志类型',
  `response_code` varchar(255) DEFAULT NULL COMMENT '接口返回状态码',
  `operate_time` datetime DEFAULT NULL COMMENT '请求时间',
  `spend_time` bigint(255) DEFAULT NULL COMMENT '消耗时间',
  `url` varchar(255) DEFAULT NULL COMMENT 'url',
  `body` varchar(255) DEFAULT NULL COMMENT '请求体',
  `ip` varchar(255) DEFAULT NULL COMMENT 'ip',
  `query` varchar(255) DEFAULT NULL COMMENT '查询参数',
  `exception` varchar(255) DEFAULT NULL COMMENT '异常信息',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2、定义LogInfo实体

/**
 * 系统日志实体
 *
 * @Author luoYong
 * @Date 2021-08-05 15:38
 */
@TableName("log_info")
@Data
@ApiModel(value = "系统日志实体", description = "系统日志实体")
public class LogInfo extends BaseEntity {

   private static final long serialVersionUID = 1L;

   @ApiModelProperty(value = "主键id 自增列")
   @TableId(value = "id", type = IdType.AUTO)
   private Integer id;

   @ApiModelProperty(value = "操作类型")
   @TableId(value = "log_type")
   private String logType;

   @ApiModelProperty(value = "日志内容")
   @TableId(value = "content")
   private String content;

   @ApiModelProperty(value = "日志类型")
   @TableId(value = "log_type")
   private LogTypeEnum logType;

   @ApiModelProperty(value = "操作")
   @TableId(value = "operation")
   private String operation;

   @ApiModelProperty(value = "ip地址")
   @TableId(value = "ip")
   private String ip;
}

3、定义logInfo持久层

/**
 * 系统日志持久层
 *
 * @Author luoYong
 * @Date 2021-08-05 15:27
 */
@Component
public interface LogInfoMapper extends BaseMapper<LogInfo> {

    /**
     * 清除指定日期之前的日志
     *
     * @param date 时间
     */
    void clear(Date date);
}

4、定义logInfo业务层接口

public interface ILogInfoService extends IService<LogInfo> {

}

5、定义logInfo接口实现类

@Slf4j
@Service
public class LogInfoServiceImpl extends ServiceImpl<LogInfoMapper, LogInfo> implements ILogInfoService {

}

6、定义Controller分发器

/**
 * 用户管理分发器
 */
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理模块", description = "用户管理模块")
public class UserController {

   
}

7、实现自定义注解类

package com.example.demo.annotation;

import com.example.demo.enums.LogTypeEnum;
import java.lang.annotation.*;


/**
 * 自定义注解类  @SysLog
 *
 * @Author luoYong
 * @version 1.0
 * @Date 2022-01-20 17:29
 */
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface SysLog {

    // 声明注解成员
    String operation() default "";

    LogTypeEnum logType() default LogTypeEnum.LOG_TYPE_QUERY;   // 日志类型默认是查询类型日志
}

8、实现切面处理类(核心)

       整文的核心就在这儿了,大家在看的过程中,如果有遇到不清楚或者不会的,还请及时提问,下方评论区留言,bug菌会第一时间给予你最有效的解答。

       先定义号一个切面类。

/**
 * 系统日志:切面处理类
 *
 * @author luoYong
 * @version 1.0
 * @date 2022/1/24 12:48
 */
@Slf4j
@Aspect
@Component
public class SysLogAspect {

    @Autowired
    private ILogInfoService iLogInfoService;

    @Around("execution(public * com.example.demo.controller.*.*(..))")
    public Object postLogAspect(ProceedingJoinPoint pjp) throws Throwable {
    // 核心逻辑,咱们一步一步拆解

   }
}

首先咱们先来看下,我们需要获取那些数据。

  1. 查询参数、ip、url、请求体
  2. 返回值(返回code、是否异常)
  3. 请求时间、目标接口消耗时间
  4. 操作类型

一步一步来,我们先来获取把需要处理的数据封装出来。

  • 获取接口消耗时间
private Object proceedController(ProceedingJoinPoint pjp, LogInfo log) throws Throwable {

    //记录开始接口时间
    long spendTime = System.currentTimeMillis();

    //调用目标接口及获取返回结果
    Object result = pjp.proceed(pjp.getArgs());

    //调用目标接口结束
    //计算接口耗时
    spendTime = (System.currentTimeMillis() - spendTime) / 1000;

    log.setSpendTime(spendTime);
    return result;
}
  • 获取接口返回值
private BaseResponse setResponseCode(LogInfo log, Object result) {

    //判断返回体类型是否为BaseResponse
    if (result != null && result instanceof BaseResponse) {
        BaseResponse restResult = (BaseResponse) result;
        return restResult;
    }
    return new BaseResponse();
}
  • 获取请求体
private void setBody(HttpServletRequest request, LogInfo log) {

    if (request instanceof ContentCachingRequestWrapper) {
        ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
        String body = StringUtils.toEncodedString(wrapper.getContentAsByteArray(),
                Charset.forName(wrapper.getCharacterEncoding()));
        log.setBody(body);
    }
}
  • 获取ip地址
private String getIpAddress() {

    // 通过RequestContextHolder获取request对象
    HttpServletRequest request = SpringServletContextUtils.getRequest();
    if (request != null) {
        try {
            return IpUtils.getIpAddr(request);
        } catch (Exception e) {
            log.error("unable to get ip address");
        }
    }
    return null;
}
  • 构建日志对象
private LogInfo createOpLog() {

    HttpServletRequest request = this.getRequest();
    LogInfo log = new LogInfo();

    log.setQuery(request.getQueryString());
    this.setBody(request, log);
    log.setOperateTime(new Date());
    log.setUrl(request.getServletPath());
    log.setIp(getIpAddress());

    return log;
}
  • 获取request对象
private HttpServletRequest getRequest() {

    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = servletRequestAttributes.getRequest();
    return request;
}
  • 获取操作类型
private void getLogType(ProceedingJoinPoint pjp, LogInfo log) {

    //从切面织入点处通过反射机制获取织入点处的方法
    MethodSignature signature = (MethodSignature) pjp.getSignature();

    //获取切入点所在的方法
    Method method = signature.getMethod();

    //获取操作
    SysLog sysLog = method.getAnnotation(SysLog.class);

    //获取
    log.setLogType(sysLog.logType().getValue());
}
  • 核心一步,编写环绕类型通知
/**
 * 环绕通知 用于拦截指定内容,记录用户的操作
 * pjp:ProceedingJoinPoint 是切入点对象
 * com.example.demo.controller.*.*(..)) 解析
 * 1、第一个*表示是返回任意类型
 * 2、com.nl.demo.controllers是包路径,针对所有的控制器
 * 3、第二个*是任意类
 * 4、第三个*是任意方法
 * 5、(..)的任意参数
 *
 * @param pjp 切入点
 */
@Around("execution(public * com.example.demo.controller.*.*(..))")
public Object postLogAspect(ProceedingJoinPoint pjp) throws Throwable {

    //初始化log
    LogInfo log = this.createOpLog();
    Object result = this.proceedController(pjp, log);

    //获取操作类型
    this.getLogType(pjp, log);

    //获取返回值编码code
    BaseResponse resData = this.setResponseCode(log, result);

    //赋值返回编码
    log.setResponseCode(resData.getCode());

    //记录非成功异常
    if (log.getResponseCode() != ResultEnum.SUCCESS.getKey()) {
        //记录异常
        log.setException(resData.getMsg());
    } 

   //调用service保存SysLog实体类到数据库
    iLogInfoService.save(log);

    return result;
}

        针对以上有任何不清楚的地方,欢迎评论区留言,不懂就问,我也如此。

8、进行swagger测试

/**
 * 用户管理分发器
 */
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理模块", description = "用户管理模块")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 根据用户id查询用户信息
     */
    @SysLog(logType = LogTypeEnum.LOG_TYPE_QUERY)
    @GetMapping("/getUser-by-id")
    @ApiOperation(value = "根据用户id查询用户信息", notes = "根据用户id查询用户信息")
    public ResultResponse<UserEntity> getUserById(@RequestParam(name = "userId") @ApiParam("请输入用户id") String userId) {
        return new ResultResponse<>(userService.getById(userId));
    }
}

       写好接口测试,咱们重启下项目,进行swagger调用。

       接口是调用成功了也无报错,可aop切入日志是否插入成功呢?咱们可以打开数据库查看log_info表,日志业务接口是否成功被记录?若成功,肯定会新生成一条记录的。

       如上图,唯独就是不清楚这个ip怎么没有被获取到,怎么是这样的格式,这个我再后边研究研究,可能是获取ip的方式不对🙄,不过这都不是重点啦,重点是咱们添加自定义注解的业务接口能成功被记录日志并且保存入库,这就很棒哦🤓

... ...

       OK,以上就是这期所有的内容啦,如果有任何问题欢迎评论区批评指正,咱们下期见👋🏻👋🏻👋🏻

👧三、往期热门推荐

文末🔥

       如果还想要学习更多,小伙伴们可关注bug菌专门为大家创建的专栏《springboot零基础入门教学》,从无到有,从零到一!希望能帮助到更多小伙伴们。

我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

感谢认真读完我博客的铁子萌,在这里呢送给大家一句话,不管你是在职还是在读,绝对终身受用。
时刻警醒自己:
抱怨没有用,一切靠自己;
想要过更好的生活,那就要逼着自己变的更强,生活加油!!!