大聪明教你学Java | Spring Boot 使用自定义注解实现操作日志的记录

17,071 阅读7分钟

前言

我们无论开发什么应用,其中都会有一个功能需求——记录操作日志,有了操作日志的记录既保证应用的完成性,也可以在因为误操作而出现系统崩溃的情况下通过操作日志进行溯源,可以说记录操作日志的功能在任何一款应用软件中都是不可或缺的。那么各位小伙伴可以想一下,如果我们要实现记录操作日志的功能,我们该怎么实现呢?

最简单粗暴的办法就是在每一个方法里增加一行代码来记录本次操作(插入操作日志表,本质就是一条 insert 语句),这样虽然可以实现我们所需要的功能,但是实现过程就比较麻烦了,而且容易出错误。这时候如果使用自定义注解的话就会方便很多,很大程度上简化了我们的代码,而且让代码可读性更强。👍

那么今天就和大家分享一下如何利用自定义注解实现“记录操作日志”的功能🤞

利用自定义注解实现操作日志的记录

实现自定义注解

我们还是先引入 Maven 依赖👇

<!-- SpringBoot 拦截器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

接下来我们实现一个注解类(注解在 Java 中与类、接口的声明类似,只是所使用的关键字有所不同,声明注解使用 @interface 关键字。在底层实现上,所有定义的注解都会自动继承 java.lang.annotation.Annotation 接口。)

import java.lang.annotation.*;

/**
 * 自定义注解-实现操作日志记录
 * 此处代码仅作参考
 * 实际开发过程中根据自己的业务功能添加所需要的字段
 * 
 * @description: Log
 * @author: 庄霸.liziye
 * @create: 2021-12-21 12:47
 **/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
    /**
     * 模块 
     */
    public String title() default "";

    /**
     * 操作类型
     * 
     * 此处的 BusinessType 是我自己定义的枚举类
     * BusinessType.OTHER 代表其他操作
     */
    public BusinessType businessType() default BusinessType.OTHER;
    
}

关于注解的定义,咱们多说几句😁 ① 访问修饰符必须为 public,不写的话则默认为 public。 ② 自定义注解中元素的类型只能是基本数据类型、String、Class、枚举类型,也可以是注解类型(体现了注解的嵌套效果)以及上述类型的一位数组。 ③ 自定义注解中的 default 代表默认值。 ④ 自定义注解上的 @Target 注解的作用是限定自定义注解能够被应用在哪些Java 元素上。ElementType 是一个枚举类型,其源码如下👇

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

⑤ 自定义注解上的 @Retention 注解用来定义其声明周期。注解的生命周期有三个阶段,分别是:源文件阶段;编译阶段;运行阶段。RetentionPolicy 同样是一个枚举类型,其源码如下👇

ublic enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

⑥ 自定义注解上的 @Documented 是一个标记注解,表明该自定义注解应该被 JavaDoc 工具记录.。默认情况下 JavaDoc 的记录是不包括注解的, 但如果声明注解时指定了 @Documented ,该注解就会被 JavaDoc 之类的工具处理,所以注解类型信息也会被生成到 JavaDoc 文档中。

下面我们就需要定义一个切面类,来进一步的实现自定义注解的功能,代码如下👇(这里就只贴一下重要代码)

/**
 * 自定义注解-操作日志记录处理
 * 此处代码仅作参考,开发过程中根据自己的业务功能需求添加所需要业务逻辑
 *
 * @description: Log
 * @author: 庄霸.liziye
 * @create: 2021-12-22 13:07
 **/
@Aspect
@Component
public class LogAspect
{
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    // 配置织入点-xxx代表自定义注解的存放位置,如:com.test.annotation.Log
    @Pointcut("@annotation(xxxx)")
    public void logPointCut()
    {
    }

    /**
     * 处理完请求后执行此处代码
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
    {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 如果处理请求时出现异常,在抛出异常后执行此处代码
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
    {
        handleLog(joinPoint, controllerLog, e, null);
    }

	/**
     * 日志处理
     */
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
    {
        try
        {
            //此处为处理日志的具体业务逻辑
            //最终将操作信息插入至操作日志表
            //使用自定义注解的时候只需要将注解写在对应方法上即可(记得给自定义注解中的属性赋值哦),博客最后有代码截图~
            //获取自定义注解赋值的方式也很简单,以上述自定义注解类代码为例,
            //获取 title 时直接使用 controllerLog.title() 即可,
            //获取枚举类型的 businessType 时,使用controllerLog.businessType().ordinal(),需要注意的是 ordinal() 方法返回的是枚举对象的序号。
        }
        catch (Exception exp)
        {
            // 记录本地异常日志
            log.error("**** 出现异常 ****");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }
}

这里咱们主要解释一下切面类中的各个注解的作用:

① @Aspect:一个标识性的注解,表示当前类是一个切面,可以被容器读取。

② @Component:表示该类被 Spring 管理,可以理解为将该类注入进 Spring(就像向 Spring 注入 bean 一样)。

③ @Pointcut:Pointcut是植入切面的触发条件。每个 Pointcut 的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public 及 void。可以将Pointcut 中的方法看作是一个被切面引用的标记符号,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码。

④ @AfterReturning:后置增强,相当于AfterReturningAdvice,给方法增加该注解后表示当方法正常退出时执行此方法。

⑤ @AfterThrowing:异常抛出增强,相当于ThrowsAdvice,给方法增加该注解后表示当方法抛出异常时执行此方法。

⑥ pointcut/value 这两个属性的作用是一样的,它们都属于指定切入点对应的切入表达式。一样既可以是已有的切入点,也可直接定义切入点表达式。当指定了 pointcut 属性值后,value 属性值将会被覆盖。

⑦ returning:该属性指定一个形参名,用于表示方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。需要注意的是,在方法中定义该形参时指定了类型,则限制目标方法必须返回此类型的值或没有返回值。

至此,我们的自定义注解就配置完成了,最后我们再来看一下如何使用自定义注解~

使用自定义注解

使用自定义注解就很简单了,使用方式和其他注解是相同的👇 在这里插入图片描述

只要我们在需要记录操作日志的方法上增加配置好的自定义注解(别忘了给自定义注解中的字段赋值哦~)就可以使用啦,我们每次请求方法时,都会帮助我们记录本次操作。 在这里插入图片描述 P.S. 虽然定义自定义注解的过程麻烦了一些,但是磨刀不误砍柴工,合理的使用自定义注解不仅仅提升了编码效率,而且可以瞬间提升代码的逼格💪

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●'◡'●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

爱你所爱 行你所行 听从你心 无问东西