Spring Boot 自定义注解

419 阅读2分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

自定义注解在日志记录、鉴权验签等AOP常见应用场景和参数解析方面有广泛应用,下面学习其基本用法。

前置知识

面向切面编程AOP

面向切面编程AOP和控制反转IOC是Spring框架两大特点。其中AOP是指通过预编译方式和运行期动态代理的方式实现不修改源代码的情况下给程序动态统一添加功能的技术。

AOP技术利用一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为Aspect切面。所谓的切面,简单来说就是与业务无关,却为业务模块所共同调用的逻辑,将其封装起来便于减少系统的重复代码,降低模块的耦合度,有利用未来的可操作性和可维护性。

参数解析器

参数解析器,顾名思义,即为对参数进行自动解析的一段代码,用于对前端传来的参数做自动解析并封装成Bean对象,我们熟知的@RequestParam@RequestBody都是参数解析器。

元注解

元注解意为作用于注解的注解,Java 5 定义了 4 个注解,分别是 @Documented@Target@Retention@Inherited。Java 8 又增加了 @Repeatable@Native两个注解。这些注解都可以在 java.lang.annotation 包中找到。

@Documented

@Documented 是一个标记注解,没有成员变量。用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。

@Target

声明注解的作用域,参数为枚举类ElementType。

package java.lang.annotation;
​
public enum ElementType {
    TYPE,               /* 类、接口(包括注释类型)或枚举声明  */
    FIELD,              /* 字段声明(包括枚举常量)  */
    METHOD,             /* 方法声明  */
    PARAMETER,          /* 形式参数声明  */
    CONSTRUCTOR,        /* 构造方法声明  */
    LOCAL_VARIABLE,     /* 局部变量声明  */
    ANNOTATION_TYPE,    /* 注释类型声明  */
    PACKAGE,            /* 包声明  */
    TYPE_PARAMETER,     /* 类型参数声明 @since 1.8*/
    TYPE_USE            /* 任何类型声明 @since 1.8*/
}

@Retention

声明注解的生命周期,参数为枚举类RetentionPolicy。

public enum RetentionPolicy {
    // 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
    SOURCE,
    // 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
    CLASS,
    // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
    RUNTIME
}

这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。

需要明确的是生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

@Inherited

@Inherited 是一个标记注解,阐述了某个被标注的类型是被继承的。 如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

@Repeatable

@Repeatable注解表明标记的注解可以多次应用于相同的声明或类型,当我们需要重复使用某个注解时,希望利用相同的注解来表现所有的形式时,我们可以借助@Repeatable注解。

@Native

指定字段是一个常量,其值引用native code。

AOP

首先建立包annotation,并声明一个注解,注解的声明借助关键字@interface

package com.example.demo.annotation;
​
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 printLog {
}

然后建立包AOP,通过AOP进行注解解析。

package com.example.demo.aop;
​
​
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;
​
import java.text.SimpleDateFormat;
import java.util.Date;
​
@Component
@Aspect
public class PrintLogAspect {
​
    // 声明切入点
    @Pointcut("@annotation(com.example.demo.annotation.printLog)")
    private void pointcut(){}
​
    // 作用于切点之前
    @Before("pointcut()")
    public void printBeginTimeStamp(){
        System.out.println("--------------");
        Date date = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        System.out.println("begin timestamp:"+simpleDateFormat.format(date));
    }
​
    // 作用切点之后
    @After("pointcut()")
    public void printEndTimeStamp(){
        Date date = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        System.out.println("end timestamp:"+simpleDateFormat.format(date));
        System.out.println("--------------");
    }
}
​

对该注解进行测试

HelloController.java

package com.example.demo.controller;
​
import com.example.demo.annotation.printLog;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class HelloController {
​
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @printLog
    public Object hello(){
        System.out.println("Hello world!");
        return "hello world!";
    }
​
}

AnnotationTest.Java

package com.example.demo;
​
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
​
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AnnotationTest {
​
    @Autowired
    private TestRestTemplate testRestTemplate;
​
    @Test
    void annotationTest(){
        String forObject = testRestTemplate.getForObject("/hello", String.class);
    }
}

执行结果如下:

image-20210803103958671

参数解析器

我们都知道在有注解的接口方法中加上@RequestBody等注解,springMVC会自动的将消息体等地方的里面参数解析映射到请求的方法参数中。

但如果我们想要的信息不完全是来自消息体等地方,比如说一部分是消息体,一部分是消息头,甚至一部分从配置中获取。这个时候我们又希望在方法入参进来就将这些信息组装好的时候,自定义参数解析器就很有必要。

自定义参数解析器主要由自定义注解HandlerMethodArgumentResolver的实现类配置类组成。

示例代码如下:

TestController.java

/**
 * @description:
 * @time: 2021/8/5 15:11
 */
@RestController
public class TestController {
    
​
    @RequestMapping(value = "/test",method = RequestMethod.POST)
    public Object test(@TestRequestBody User user){
        return user.toString();
    }
​
}

TestRequestBody.java

/**
 * @description:自定义注解
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestRequestBody {
}

TestArgumentResolver.java

package com.example.demo.Resolver;
​
import com.alibaba.fastjson.JSONObject;
import com.example.demo.Entity.User;
import com.example.demo.annotation.TestRequestBody;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
​
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
​
/**
 * @description:参数解析器
 * @time: 2021/8/5 15:44
 */
public class TestArgumentResolver implements HandlerMethodArgumentResolver {
​
    /**
     * @description:当此方法返回true时,启用该参数解析器
     * @param parameter
            * @return: [org.springframework.core.MethodParameter]
            * @time: 2021/8/5 16:02
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(TestRequestBody.class);
    }
​
    /**
     * @description:用于解析参数的方法
     * @param parameter
     * @param mavContainer
     * @param webRequest
     * @param binderFactory
            * @return: [org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory]
            * @time: 2021/8/5 16:14
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 获取HttpServletRequest
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        // 读取Body内容
        BufferedReader reader = request.getReader();
        StringBuilder sb = new StringBuilder();
        char[] buf = new char[1024];
        int rd;
        while((rd = reader.read(buf)) != -1){
            sb.append(buf, 0, rd);
        }
        System.out.println(sb.toString());
        // Json转User
        User user = JSONObject.parseObject(sb.toString(), User.class);
        // 补充User信息
        user.setName("Lei Li");
        return user;
    }
}
​

WebConfig.java

package com.example.demo;
​
import com.example.demo.Resolver.TestArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
​
import java.util.List;
​
/**
 * @description:绑定参数解析器
 * @time: 2021/8/5 16:25
 */
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
​
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new TestArgumentResolver());
    }
}

测试结果如下:

image-20210805164908254