springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏

Posted bug菌¹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏相关的知识,希望对你有一定的参考价值。

👨‍🎓作者:bug菌

🎉简介:在CSDN、掘金等社区优质创作者,全网合计6w粉+,对一切技术都感兴趣,重心偏java方向,目前运营公众号[猿圈奇妙屋],欢迎小伙伴们的加入,一起秃头。

🚫特别声明:原创不易,转载请附上原文出处链接和本文声明,谢谢配合。

🙏版权声明:文章里可能部分文字或者图片来源于互联网或者百度百科,如有侵权请联系bug菌处理。

【开发云】年年都是折扣价,不用四处薅羊毛

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

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

一、前言🔥

       在Spring体系中,实现自定义注解的方式有很多,比如呢可以写一个公共方法,然后在每个Controller中手动传参调用。

       而我今天则要在这里隆重介绍一下,如何通过AOP切面思想方式来实现用户自定义的注解?大家想不想听听看,感不感兴趣呢?

       其实啊,感不感兴趣都没关系啦,我写了,自然会有愿意看的人看,毕竟我对自己写的东西,还是比较有自信滴。我会做到句句是重点,关键我是要教那些没用过spring aop实现自定义注解的人群,帮助他们,就等于在帮助我自己,我为人人,人人为我!至于大佬级别的,也随时欢迎你们对文章进行批评指正啦。

       好啦,咱们就开始今天的内容吧。

二、正文🔥

1️⃣概念

       这个大家肯定都知道。AOP(Aspect Oriented Programming)是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。可以说是OOPObject-Oriented Programing,面向对象编程)的补充和完善。

2️⃣应用场景

  • 日志打印及记录
  • 权限认证
  • 全局异常处理拦截
  • 返回值统一处理
  • 多数据源切换
  • 事务处理
  • ... ...

       总之,就是要明白Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。只要符合,那么aop就适用。

3️⃣如何使用AOP?

       原则上来说,我们只要引入 Spring 框架中 AOP 的相应依赖就可以直接使用 Spring 的 AOP 支持了,不过,为了进一步为大家使用 SpringAOP 提供便利,SpringBoot 还是“不厌其烦”地为我们提供了一个 spring-boot-starter-aop 自动配置模块。 

 spring-boot-starter-aop 自动配置行为由两部分内容组成:

  • spring-boot-autoconfigure下的org.springframework.boot.autoconfigure.aop.AopAutoConfiguration 提供 @Configuration 配置类和相应的配置项。 
  • spring-boot-starter-aop 模块自身提供了针对 spring-aopaspectjrt 和 aspectjweaver 的依赖。 

       一般情况下,只要项目依赖中加入了 spring-boot-starter-aop,其实就会自动触发 AOP 的关联行为,包括构建相应的 AutoProxyCreator,将横切关注点织入(Weave)相应的目标对象等。

       不过 AopAutoConfiguration 依然为我们提供了可怜的两个配置项,用来有限地干预 AOP 相关配置:  spring.aop.auto=true ;spring.aop.proxy-target-class=false ,对我们来说,这两个配置项的最大意义在于:允许我们投反对票,比如可以选择关闭自动的 aop 配置spring.aop.auto=false,或者启用针对 class 而不是 interface 级别的 aop 代理(aop proxy)。

4️⃣如何实现AOP?

       实现AOP的技术,主要分为两大类:

  • 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

然而殊途同归,实现AOP的技术特性却是相同的,分别为:

  • 1、Joinpoint(连接点):是程序执行中的一个精确执行点,例如类中的一个方法。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。
  • 2、Pointcut(切入点):本质上是一个捕获连接点的结构。在AOP中,可以定义一个point cut,来捕获相关方法的调用。
  • 3、Advice(通知):是point cut的执行代码,是执行“方面”的具体逻辑。

拓展一下:advice(通知)可分为以下5种通知类型:

  1. 前置通知(Before):在目标方法被调用之前调用通知功能。
  2. 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
  3. 返回通知(AfterReturning):在目标方法成功执行之后调用通知。
  4. 异常通知(AfterThrowing):在目标方法抛出异常后调用通知。
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  • 4、Aspect(方面):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类,但它代表的更多是对象间横向的关系。
  • 5、Introduce(引入):为对象引入附加的方法或属性,从而达到修改对象结构的目的。有的AOP工具又将其称为mixin。

       上述的技术特性组成了基本的AOP技术,大多数AOP工具均实现了这些技术。它们也可以是研究AOP技术的基本术语。

... ...

       那么,我么接下来就来演示一下,如何具体在项目中进行配置。

三、项目配置🔥

        我们只需要在项目依赖中加入 spring-boot-starter-aop 就可以使用AOP了。

1️⃣pom依赖

        添加aop相关依赖。

<!--AOP-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

       如上在pom中加入aop starter、等依赖从maven源仓库下载好后,我们就可以正常使用AOP了。

2️⃣application-dev.yaml

       除了引用aop依赖,我们可能还会涉及到一个配置。就像我上述所介绍的那样,允许我们手动关闭自动的aop配置,就可以直接在我们的项目配置文件中加上如下auto 与 proxy-target-class 配置项就行,一般我们是不需要配,但是我们得知道有这么个东西就行。

spring:
  aop:
    auto: true #自动代理开启;false:关闭
    proxy-target-class: false #属性值决定是基于接口的还是基于类的代理被创建.true表示基于类的代理将使用,false表示默认使用Jdk基于接口的代理

四、实战教学🔥

        接下来,我就带着大家代码实现自定义注解记录接口调用日志,并且记录调用接口时的接口返回码、接口耗时、请求时间等参数,具有很好的参考价值。

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());
  • 核心一步,编写环绕类型通知

如下是环绕Controller层面进行切入,是针对整个Controller层。若要根据自定义注解精准切入,请看我如下一步。

切点为整个Controller层写法:

/**
 * 环绕通知 用于拦截指定内容,记录用户的操作
 * 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;

切点为存在自定义注解写法: 


    //定义切点 @Pointcut
    //在注解的位置切入代码
    @Pointcut("@annotation( com.example.review.annotation.SysLog)")
    public void logPointCut() 

    


    /**
     * 环绕通知 用于拦截指定内容,记录用户的操作
     * pjp:ProceedingJoinPoint 是切入点对象
     *
     * @param pjp 切入点
     */
    @Around("logPointCut()")
    public Object postLogAspect(ProceedingJoinPoint pjp) throws Throwable 

        //初始化log
        SysLogsEntity 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;
    

       以上演示了两种范围的环切法。针对以上有任何不清楚的地方,欢迎评论区留言,不懂就问,我也如此。仅供参考哈。

9️⃣进行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是获取到的localhost地址,所以这串地址也是127.0.0.1的ipv6地址,需要对获取客户端ip那个方法最后进行一下判断即可。重点是咱们添加自定义注解的业务接口能成功被记录日志并且保存入库,这就很棒哦🤓

五、往期推荐🔥

六、文末🔥

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

【开发云】年年都是折扣价,不用四处薅羊毛

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

        最后送大家两句我很喜欢的话,与诸君共勉!


☘️做你想做的人,没有时间限制,只要愿意,什么时候都可以start。

🍀你能从现在开始改变,也可以一成不变,这件事,没有规矩可言,你可以活出最精彩的自己。


​​​

💌如果文章对您有所帮助,就请留下您的吧!(#^.^#);

💝如果喜欢bug菌分享的文章,就请给bug菌点个关注吧!(๑′ᴗ‵๑)づ╭❤~;

💗如果对文章有任何疑问,还请文末留言或者加群吧;

💞鉴于个人经验有限,所有观点及技术研点,如有异议,请直接回复参与讨论(请勿发表攻击言论,谢谢);

💕版权声明:原创不易,转载请附上原文出处链接和本文声明,版权所有,盗版必究!!!谢谢。

以上是关于springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏

Spring Boot2 系列教程(二十一) | 自动配置原理

二十一SpringBoot2核心技术——整合activiti7

Spring入门第二十一课

SpringBoot和Vue集成视频播放组件——基于SpringBoot和Vue的后台管理系统项目系列博客(二十二)

Android开发系列(二十一):Spinner的功能和使用方法以及实现列表选择框