SpringBoot自定义注解

相信很多小伙伴和我一开始一样,对SpringBoot的注解了解只是一知半解,只知道注解仅仅是作为一个标识,并继承自Annotation这一接口,使用@interface定义。但是对于注解具体的实现逻辑却找不到代码去看,因此本篇博客就详细来讲清楚SpringBoot自定义一个功能复杂的注解。

先给出一个具体的自定义注解在来解释:

package com.example.log;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    /**
     * 定义变量:方法描述 des
     * 默认变量:value
     */
    String des() default "";
    String value() default "666";
}
元注解

自定义注解必须使用@interface来标识,并可以定义变量,这里定义的变量与传统Java类的变量不太一样,看起来更像一个方法;default 用于定义默认值;注意:参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组。那么我们需要关注的是MyLog类上的注解,我们也称为JDK的元注解;

  • @Retention:定义注解的保留策略
    • @Retention(RetentionPolicy.SOURCE) ,注解仅存在于源码中,在class字节码文件中不包含
    • @Retention(RetentionPolicy.CLASS) ,默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
    • @Retention(RetentionPolicy.RUNTIME) ,注解会在class字节码文件中存在,在运行时可以通过反射获取到
  • @Target:指定被修饰的Annotation可以放置的位置(被修饰的目标)
    • @Target(ElementType.TYPE) ,接口、类
    • @Target(ElementType.FIELD) ,属性
    • @Target(ElementType.METHOD) ,方法
    • @Target(ElementType.PARAMETER),方法参数
    • @Target(ElementType.CONSTRUCTOR),构造函数
    • @Target(ElementType.LOCAL_VARIABLE) ,局部变量
    • @Target(ElementType.ANNOTATION_TYPE) ,注解
    • @Target(ElementType.PACKAGE) ,包

一般@Retention设置成RetentionPolicy.RUNTIME即可,@Target一般设置在类或者方法或者变量上。那么搞清楚了如何写一个自定义注解类后,此时是不是有疑惑,那么注解的逻辑在哪里实现呢?这里简单剧透一下,注解仅仅作为一个标识,当某个方法执行时我们需要在自定义注解逻辑中去判断是否有被这个注解所标识,如果有则具体处理,否则放行;以上你很容易想到使用AOP切片拦截器实现。这两个方法的共同点就是在加了注解的方法执行之前可以先进行注解标识判断。接下来就很容易了:

编写自定义注解的AOP切面类
package com.example.log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

@Aspect //说明当前对象时一个切面
@Component
public class MyLogAspect {
    //@Around环绕通知,说明切面的作用范围——任何写了MyLog注解的目标方法都在执行之前执行切面方法
    @Around("@annotation(com.example.log.MyLog)")
    public String MyLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取目标方法上的注解
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); 
        Method method = methodSignature.getMethod();
        MyLog logAnn = method.getAnnotation(MyLog.class);

        if(logAnn==null){
           //判断是否有MyLog注解,如果没有加MyLog注解:不进行逻辑处理,直接放行;
           return (String) joinPoint.proceed();
        }

        // 获取注解中定义的字段值
        String value = logAnn.value();
        String des = logAnn.des();

        // 处理注解的字段值,并根据字段值 do something!
        System.out.println("logAnn value: " + value);
        System.out.println("logAnn des: " + des);

        //执行目标方法
        return (String) joinPoint.proceed();
    }
}
使用自定义注解

如下代码所示,当加了MyLog注解后,控制台会输出value和des的信息,如果没有加MyLog注解则不会输出;

package com.example.service;
import com.example.log.MyLog;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogService {
    @MyLog(des = "描述", value = "888")
    @GetMapping("testMylog")
    public String testMyLog() {
        System.out.println("do something!");
        return "执行testMyLog!";
    }
}

拦截器中自定义注解

接下来我们自定义一个注解:需要实现的功能是加了@MyLog注解后,打印方法的信息和执行时间,例如:

package com.example.controller;
import com.example.log.MyLog;
import com.example.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {
    //HelloService在我们自定义的starter中已经完成了自动配置,所以此处可以直接注入
    @Autowired
    private HelloService helloService;

    @MyLog(desc = "sayHello方法") //日志记录注解
    @GetMapping("/say")
    public String sayHello(){
        return helloService.sayHello();
    }
}

访问地址:http://localhost:8080/hello/say,查看控制台输出:

请求uri:/hello/say
请求方法名:cn.itcast.controller.HelloController.sayHello
方法描述:sayHello方法
方法执行时间:36ms
自定义MyLog注解
package com.example.log;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    /**
     * 方法描述
     */
    String desc() default "";
}
自定义日志拦截器MyLogInterceptor
package com.example.log;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 日志拦截器
 */
public class MyLogInterceptor extends HandlerInterceptorAdapter {
    private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();//获得被拦截的方法对象
        MyLog myLog = method.getAnnotation(MyLog.class);//获得方法上的注解
        if(myLog != null){
            //方法上加了MyLog注解,需要进行日志记录
            long startTime = System.currentTimeMillis();
            startTimeThreadLocal.set(startTime);
        }
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
                           Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();//获得被拦截的方法对象
        MyLog myLog = method.getAnnotation(MyLog.class);//获得方法上的注解
        if(myLog != null){
            //方法上加了MyLog注解,需要进行日志记录
            long endTime = System.currentTimeMillis();
            Long startTime = startTimeThreadLocal.get();
            long optTime = endTime - startTime;

            String requestUri = request.getRequestURI();
            String methodName = method.getDeclaringClass().getName() + "." + 
                				method.getName();
            String methodDesc = myLog.desc();

            System.out.println("请求uri:" + requestUri);
            System.out.println("请求方法名:" + methodName);
            System.out.println("方法描述:" + methodDesc);
            System.out.println("方法执行时间:" + optTime + "ms");
        }
    }
}
创建自动配置类MyLogAutoConfiguration,用于自动配置拦截器、参数解析器等web组件
package com.example.config;

import cn.itcast.log.MyLogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置类,用于自动配置拦截器、参数解析器等web组件
 */

@Configuration
public class MyLogAutoConfiguration implements WebMvcConfigurer{
    //注册自定义日志拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyLogInterceptor());
    }
}
在spring.factories中追加MyLogAutoConfiguration配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration = cn.itcast.config.MyLogAutoConfiguration
总结

有两种方法实现自定义注解,能用拦截器实现的也可以使用AOP切片的方式实现,强烈建议使用AOP切片的方式实现,因为不用追加配置;两种方法其实都是使用到了代理模式,主打一个共性:在执行加了注解的方法之前先判断是否有特定的注解标识再做逻辑处理。以上!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐