spring自定义注解及使用
作者:mmseoamin日期:2023-12-13

文章目录

      • 简介
      • 一、定义注解
        • 1.注解示例
        • 2.元注解含义
          • (1)@Target
          • (2)@Retention
          • (3)@Documented
          • (4)@Inherited
          • (5)@Native
          • (6)@Repeatable
          • 二、使用注解
            • 1.aop切点使用注解
            • 2.拦截器获取注解
            • 3.class获取注解
            • 4.spring容器获取注解

              简介

                    在spring项目中使用注解,简化了代码量,减轻对业务代码的侵入性;对框架统一处理鉴权、日志等起到极大的作用,可以结合着拦截器、aop在请求调用前后添加额外处理。spring有内置的@Controller、@Service等注解,出于业务考虑,我们可以自定义想要的注解。

              一、定义注解

                    自定义注解类似于定义接口,但是需要指明注解的作用范围、生命周期等属性

              1.注解示例

                    下面是一个简单的自定义注解示例,使用@interface修饰,定义了三个属性值,使用注解的时候可以给这些属性赋值。

              @Target({ElementType.METHOD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              @Inherited
              public @interface LogAnnotation {
                  String moduleName() default "";
                  String operaName() default "";
                  String operaType() default "";
              }
              
              2.元注解含义

                    从jdk1.5开始,在包java.lang.annotation下提供了四种元注解:@Target、@Retention、@Documented、@Inherited,java1.8后,annotation包下新提供了两种元注解:@Native、@Repeatable。自定义注解的时候需要使用元注解修饰,来看下各个元注解的使用说明。

              (1)@Target

                    标识注解可以使用的范围,例如使用在方法、字段、构造方法上。看下源码:

              //Target源码
              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target(ElementType.ANNOTATION_TYPE)
              public @interface Target {
                  ElementType[] value();
              }
              //Target可配置的类型
              public enum ElementType {
                  TYPE,
                  FIELD,
                  METHOD,
                  PARAMETER,
                  CONSTRUCTOR,
                  LOCAL_VARIABLE,
                  ANNOTATION_TYPE,
                  PACKAGE,
                  TYPE_PARAMETER,
                  TYPE_USE
              }
              

              从源码中可以看出@Target只有一个属性value,属性类型为ElementType类型的数组,ElementType各个枚举值的作用范围如下:

              ①ElementType.TYPE:允许被修饰的注解作用在:类、接口、枚举上;

              ②ElementType.FIELD:允许被修饰的注解作用在:属性字段上;

              ③ElementType.METHOD:允许被修饰的注解作用在:方法上;

              ④ElementType.PARAMETER:允许被修饰的注解作用在:方法参数上;

              ⑤ElementType.CONSTRUCTOR:允许被修饰的注解作用在:构造器上;

              ⑥ElementType.LOCAL_VARIABLE:允许被修饰的注解作用在:本地局部变量上;

              ⑦ElementType.ANNOTATION_TYPE:允许被修饰的注解作用在:注解上;

              ⑧ElementType.PACKAGE:允许被修饰的注解作用在:包名上;

              ⑨ElementType.TYPE_PARAMETER:允许被修饰的注解作用在:类型参数上,jdk1.8提供;

              //ElementType.TYPE_PARAMETER示例
              @Target(ElementType.TYPE_PARAMETER)
              @Retention(RetentionPolicy.RUNTIME)
              public @interface TypeParameterAnnotation {
              }
              //泛型声明
              public class TypeParameterClass<@TypeParameterAnnotation T> {
                  public <@TypeParameterAnnotation P> T too(T t){
                      return t;
                  }
              }
              

              ⑩ElementType.TYPE_USE:允许被修饰的注解作用在:任何语句中(声明语句、泛型、强制转化),jdk1.8提供。

              (2)@Retention

                    标识注解的生命周期,来看下源码:

              //Retention源码
              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target(ElementType.ANNOTATION_TYPE)
              public @interface Retention {
                  RetentionPolicy value();
              }
              //RetentionPolicy源码
              public enum RetentionPolicy {
                  SOURCE,
                  CLASS,
                  RUNTIME
              }
              

              从源码可以看出@Retention只有一个属性value,属性类型为RetentionPolicy,看下RetentionPolicy枚举值的生命周期:

              ①RetentionPolicy.SOURCE:编译阶段丢弃,编译之后注解没有任何作用,不会写入字节码文件中。例如@Override、@SuppressWarnings、@Deprecated都属于这类注解;

              ②RetentionPolicy.CLASS:类加载阶段丢弃,类加载进jvm后没有任何作用,在字节码文件处理中有用。注解默认使用这种方式;

              ③RetentionPolicy.RUNTIME:始终不会丢弃,程序运行期也保留此注解,自定义注解通常使用这种方式,因此可以通过反射获取到注解配置的属性值。

              (3)@Documented

                    标识注解是否在javadoc文档中显示,看下源码:

              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target(ElementType.ANNOTATION_TYPE)
              public @interface Documented {
              }
              

              当定义的注解中加入了@Documented元注解,则生成的javadoc文档中包含注解,来看一个例子:

              @Documented
              public @interface DocumentAnnotation {
                  String name() default "张三";
                  int age() default 18;
              }
              public class DocumentTest {
                  @DocumentAnnotation(name="lisi",age = 30)
                  public void test(){
                  }
              }
              

              此时生成javadoc文件,生成的方式为:

              spring自定义注解及使用,在这里插入图片描述,第1张

              spring自定义注解及使用,在这里插入图片描述,第2张

              ​ 文档中包含注解信息:

              spring自定义注解及使用,在这里插入图片描述,第3张

              自定义注解DocumentAnnotation去掉@Documented,javadoc文档中不包含注解:

              spring自定义注解及使用,在这里插入图片描述,第4张

              (4)@Inherited

                    标识注解是否能继承到子类,看下源码:

              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target(ElementType.ANNOTATION_TYPE)
              public @interface Inherited {
              }
              

              使用@Inherited修饰的注解,在class使用它时,class的子类能够继承此注解,类似于InheritableThreadLocal,父子类能够共享资源。

              (5)@Native

                    标识字段是否可以被本地代码引用,看下源码:

              @Documented
              @Target(ElementType.FIELD)
              @Retention(RetentionPolicy.SOURCE)
              public @interface Native {
              }
              

              此注解作用在字段上,生命周期为编译阶段丢弃。

              (6)@Repeatable

                    标识可以重复使用注解,看下源码:

              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target(ElementType.ANNOTATION_TYPE)
              public @interface Repeatable {
                  Class value();
              }
              

              作用在注解上,只有一个属性value,属性的类型继承了Annotation,之所以继承Annotation是因为Annotation是所有注解的父接口,看下关系图:

              spring自定义注解及使用,在这里插入图片描述,第5张

              来看一个demo:

              //定义注解
              @Target(value={ElementType.FIELD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              public @interface RepeatableAnnotations {
                  RepeatableAnnotation[] value();
              }
              //定义注解,Repeatable声明RepeatableAnnotations
              @Target(value={ElementType.FIELD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              @Repeatable(RepeatableAnnotations.class)
              public @interface RepeatableAnnotation {
                  String name();
                  int age();
              }
              //测试类
              public class RepeatableDemo {
                  @RepeatableAnnotation(name="张三",age=18)
                  @RepeatableAnnotation(name="李四",age=30)
                  private String userMessage;
                  public static void main(String[] args) throws NoSuchFieldException {
                      Field declaredField = RepeatableDemo.class.getDeclaredField("userMessage");
                      Annotation[] annotations = declaredField.getDeclaredAnnotations();
                      System.out.println("注解的数量:"+annotations.length);
                      System.out.println("注解内容:"+Arrays.toString(annotations));
                  }
              }
              

              测试类输出结果:

              注解的数量:1
              注解内容:[@com.RepeatableAnnotations(value=[@com.RepeatableAnnotation(name=张三, age=18), @com.RepeatableAnnotation(name=李四, age=30)])]
              

              定义一个可重复的注解,需要使用@Repeatable来声明,@Repeatable的值为此原注解数组形式的新注解。从测试类可以看出最终注解的数量还是1个,是使用@Repeatable值的数组形式接收,每个值为原注解类型。

                    在spring中ComponentScan定义bean的扫描范围,就是这样使用的,看下它的源码:

              @Retention(RetentionPolicy.RUNTIME)
              @Target({ElementType.TYPE})
              @Documented
              @Repeatable(ComponentScans.class)
              public @interface ComponentScan {
              }
              @Retention(RetentionPolicy.RUNTIME)
              @Target({ElementType.TYPE})
              @Documented
              public @interface ComponentScans {
                  ComponentScan[] value();
              }
              //使用
              @ComponentScan(basePackages = {"com.xxx1","com.xxx2"})
              

              使用@Repeatable注意事项:

              ①原注解的@Target作用范围要比@Repeatable值的范围大或者相同,否则编译错误,例如:

              //比RepeatableAnnotation多了ElementType.METHOD
              @Target(value={ElementType.FIELD,ElementType.METHOD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              public @interface RepeatableAnnotations {
                  RepeatableAnnotation[] value();
              }
              @Target(value={ElementType.FIELD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              @Repeatable(RepeatableAnnotations.class)
              public @interface RepeatableAnnotation {
                  String name();
                  int age();
              }
              

              spring自定义注解及使用,在这里插入图片描述,第6张

              ②原注解的@Retention生命周期要比@Repeatable值的小或者相同,否则编译错误,生命周期大小:SOURCE <

              CLASS < RUNTIME。例如:

              //定义的CLASS比RUNTIME要小
              @Target(value={ElementType.FIELD})
              @Retention(RetentionPolicy.CLASS)
              @Documented
              public @interface RepeatableAnnotations {
                  RepeatableAnnotation[] value();
              }
              @Target(value={ElementType.FIELD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              @Repeatable(RepeatableAnnotations.class)
              public @interface RepeatableAnnotation {
                  String name();
                  int age();
              }
              

              spring自定义注解及使用,在这里插入图片描述,第7张

              二、使用注解

                    定义注解就是为了方便系统开发,现在来看一些使用场景。

              1.aop切点使用注解

                    自定义注解结合着aop来使用的场景很多,例如日志的收集就可以使用。

              ①定义注解:

              @Target({ElementType.METHOD})
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              public @interface LogAnnotation {
                  //模块名称(枚举类)
                  ModuleNameEnum moduleName() default ModuleNameEnum.UNKNOWN;
                  //操作对象
                  String operaName() default "";
                  //操作类型(枚举类)
                  OperaTypeEnum operaType() default OperaTypeEnum.UNKNOWN;
              }
              

              ②定义aop切面类:

              @Aspect
              @Component
              @Slf4j
              public class LogAspect {
                  @Autowired
                  XxxLogService xxxLogService;
                  //切点:使用LogAnnotation注解标识的方法都进行切入,也可以使用通配符配置具体要切入的方法名
                  @Pointcut("@annotation(com.xxx.aop.LogAnnotation)")
                  public void pointCut(){
                  }
                  //环绕通知
                  @Around("pointCut()")
                  public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
                      Object jsonResult = joinPoint.proceed(); //执行方法
                      try {
                          //获取请求签名
                          MethodSignature signature = (MethodSignature)joinPoint.getSignature();
                          //获取切入点所在的方法
                          Method method = signature.getMethod();
                          //获取注解值
                          LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
                          //获取属性
                          String moduleName = annotation.moduleName().getValue();
                          String operaName = annotation.operaName();
                          String operaType = annotation.operaType().getValue();
                          XxxLog xxxLog = new XxxLog();
                          xxxLog.setModuleName(moduleName);
                          xxxLog.setOperaName(operaName);
                          xxxLog.setOperaType(operaType);
                          //添加日志
                          xxxLogService.insertOne(xxxLog);
                      } catch (Exception e){
                          e.printStackTrace();
                      } catch (Throwable e) {
                          e.printStackTrace();
                      }
                      return jsonResult;
                  }
              }
              

              ③方法中添加注解

                     当注解属性名为value时,赋值的时候可以省略属性名,其他名称的属性名需要使用xx=yy的方式指定。

                  @LogAnnotation(moduleName= ModuleNameEnum.FeedBack,operaName="添加消息",operaType=OperaTypeEnum.Insert)
                  public void insertOne(Integer id) {
                      
                  }
              

              过程为:定义注解,定义属性值;创建切面类,使用@annotation来指定切点为自定义注解,环绕方法获取注解及属性值,把属性值保存到业务数据库中;业务代码中需要保存日志的方法加上注解,并设置属性值。

              2.拦截器获取注解

                    可以在拦截器中获取注解,在controller层响应前后做一些额外的处理或判断,例如判断权限、判断是否需要分页等。来看一个分页的demo:

              ①定义注解

              @Target(ElementType.METHOD)
              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              public @interface EnablePaging {
                  int value() default 50;
              }
              

              ②定义拦截器

              public class PagingInterceptor implements HandlerInterceptor {
                  //controller响应之前执行
                  @Override
                  public boolean preHandle(@NotNull HttpServletRequest request,
                                           @NotNull HttpServletResponse response,
                                           @NotNull Object handler) {
                      if (!(handler instanceof HandlerMethod)) {
                          return true;
                      }
                      HandlerMethod handlerMethod = (HandlerMethod) handler;
                      //获取方法中的注解
                      EnablePaging enablePaging = handlerMethod.getMethodAnnotation(EnablePaging.class);
                      //不包含注解,直接通过
                      if (enablePaging == null) {
                          return true;
                      }
                      //包含注解,则获取注解中的值,值保存到TreadLocal线程变量中(此处使用RequestContextHolder.currentRequestAttributes().setAttribute保存),在执行sql查询时取出使用
                      PagingContextData data = PagingContextData.getInstance(RequestAttributes.SCOPE_REQUEST, true);
                      data.setEnabled(true);
                      //把注解中配置的值设置进去
                      if (enablePaging.value() > 0) {
                          data.setDefaultPageSize(enablePaging.value());
                      }
                      return true;
                  }
              }
              

              ③注册拦截器

              @Configuration
              public class PagingHttpConfig implements WebMvcConfigurer {
                  @Override
                  public void addInterceptors(InterceptorRegistry registry) {
                      registry.addInterceptor(new PagingInterceptor()).order(Ordered.HIGHEST_PRECEDENCE);
                  }
              }
              

              ④方法中添加注解

                  @PostMapping("/datasource/xxxPage")
                  @EnablePaging(20)
                  public Object xxxPage(@RequestBody String json) {
                      return xxxService.xxxPage(json);
                  }
              

              过程为:定义注解,定义属性值;创建拦截器,在拦截器的方法中获取注解及属性值,把属性值保存到线程变量ThreadLocal中;把拦截器注册到InterceptorRegistry中;业务代码中需要分页的接口方法加上注解,并设置属性值。

              3.class获取注解

                    通过class可以获取到注解,提供了从method方法、field字段等获取注解。获取class的方式有:

              ①对象.getClass()方法

              Student stu = new Student();
              Class clazz = stu.getClass();
              

              ②对象.class

              Class clazz = Student.class;
              

              ③Class.forName(“xxx”),例如数据库驱动的获取

              Class clazz = Class.forName("com.xxx.Student")
              

              从method中获取注解示例:

              //获取所有方法
              Method[] methods = SampleClass.class.getMethods();
              for(int i = 0;i < methods.length;i++) {
                  //获取方法中的注解
                  CustomAnnotaion annotation = methods[i].getAnnotation(CustomAnnotaion.class);
                  if(null != annotation) {
                      //输出属性值
                      System.out.println(annotation.name());
                  }
              }
              //获取指定方法
              Method oneMethod = SampleClass.class.getDeclaredMethod("getSampleField");
              //获取方法中的注解值
              CustomAnnotaion annotation = oneMethod.getAnnotation(CustomAnnotaion.class);
              System.out.println("annotation="+annotation.name());
              

              从字段中获取注解示例:

              //获取指定字段
              Field declaredField = RepeatableDemo.class.getDeclaredField("userMessage");
              //获取字段中的注解
              Annotation[] annotations = declaredField.getDeclaredAnnotations();
              
              4.spring容器获取注解

                    在bean对象中加入注解,当spring容器加载完bean之后,可以从bean中获取到哪些方法加了指定的注解,从而拿到方法,对这些方法进行特殊处理。在xxl-job开源项目中就有使用,看下使用方式:

              private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
                      if (applicationContext == null) {
                          return;
                      }
                      // init job handler from method
                      //从程序上下文中获取到所有的bean名称集合
                      String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
                      //遍历bean集合
                      for (String beanDefinitionName : beanDefinitionNames) {
                          //根据bean名称从程序上下文获取到此bean对象
                          Object bean = applicationContext.getBean(beanDefinitionName);
                          Map annotatedMethods = null; 
                          try {
                              //对Bean对象进行方法过滤,查询到方法被XxlJob注解修饰,是则放到annotatedMethods集合中
                              annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                                      new MethodIntrospector.MetadataLookup() {
                                          @Override
                                          public XxlJob inspect(Method method) {
                                              //判断方法被XxlJob注解修饰才返回
                                              return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                                          }
                                      });
                          } catch (Throwable ex) {
                              logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
                          }
                          //当前遍历的bean没有被XxlJob注解修饰,则跳过处理
                          if (annotatedMethods==null || annotatedMethods.isEmpty()) {
                              continue;
                          }
                          //循环处理当前Bean下被XxlJob修饰的方法
                          for (Map.Entry methodXxlJobEntry : annotatedMethods.entrySet()) {
                              //执行的方法
                              Method executeMethod = methodXxlJobEntry.getKey();
                              //XxlJob注解类
                              XxlJob xxlJob = methodXxlJobEntry.getValue();
                              //注册此任务处理器
                              registJobHandler(xxlJob, bean, executeMethod);
                          }
                      }
                  }
              

              从spring上下文applicationContext中获取到所有的bean名称集合,遍历bean名称集合,根据bean名称从程序上下文获取到此bean对象,对Bean对象进行方法过滤,查询到被XxlJob注解修饰的方法,放到map集合中,循环处理map中的记录,key为Method方法,value为XxlJob注解,这也是使用注解的场景。