深入理解JAVA中的注解

本文来说下JAVA中的注解,这个技术虽然我们每天都在使用,但是不一定知道其实现原理。本文来详细介绍下JAVA中注解相关的知识。


概述

现在主流的项目架构都是ssm、springcloud等,而这些框架都离不开spring,而spring中使用了大量的注解(包括spring自定义的注解)。因此想要会用注解,我们就得知道Java注解的原理和基本用法,这样有助于我们在项目中如鱼得水。

在这里插入图片描述


什么是注解

在JDK5.0中,新增了很多对现在影响很大的特性,如:枚举、自动装箱和拆箱、注解、泛型等等。其中注解的引入,是为了增加对元数据的支持

注解:是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注解里的元数据,注解能用来为程序元素(包、类、方法、成员变量等)设置元数据,它不影响程序代码的执行。如果希望让程序中的Annotation在运行时起一定作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,这个工具统称为APT(Annotation program Tool)

JDK5.0除了增加注解特性之外,还提供了5个基本的Annotation:

@Overrride:限定重写父类方法(旨在强制性提醒)
 
@Deprecated:表示某个程序元素(类、方法)已过时
 
@SuppressWarnings:抑制编译警告
 
@SafeVarargs:堆污染警告
 
@FunctionalInterface:指定某个接口必须为函数式接口
注:函数式接口是指一个接口中只包含一个抽象方法(可以包含多个默认方法或多个static方法)

以上这几个注解,在我们的项目经常可见,但是因为他们对程序只是一个强制提醒或者警告作用,并不影响程序的执行,因为我们都没在意这些注解的作用,而当spring出来之后,大量的注解眼花缭乱,作用各异,但他们都有一个共同作用:让我们在编码过程中简化了不少重复性的代码。


注解的本质是什么

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

实际上Java注解与普通修饰符(public、static、void等)的使用方式并没有多大区别,下面的例子是常见的注解:

public class AnnotationDemo {
       
     @Test
     public static void A(){
          System.out.println("Test.....");
     }
     
     @Deprecated
     @SuppressWarnings("uncheck")
     public static void B(){
     
      }
 }

@Controller注解反编译

相信大家肯定都使用过@Controller注解,我们从这个注解一起来看下注解的本质到底是什么。

@Controller注解的源代码如下

/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.stereotype;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

/**
 * Indicates that an annotated class is a "Controller" (e.g. a web controller).
 *
 * <p>This annotation serves as a specialization of {@link Component @Component},
 * allowing for implementation classes to be autodetected through classpath scanning.
 * It is typically used in combination with annotated handler methods based on the
 * {@link org.springframework.web.bind.annotation.RequestMapping} annotation.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 2.5
 * @see Component
 * @see org.springframework.web.bind.annotation.RequestMapping
 * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}

对@Controller注解生成的class文件进行反编译之后,我们来看下jvm到底为我们生成的是什么

在这里插入图片描述
我们看到jvm为我们生成的内容很简单,就是一个继承了Annotation接口的接口,里面有一个抽象的value()方法。那么我们知道了注解的本质其实就是一个特殊的接口


注解体系图

元注解

java.lang.annotation中提供了元注解,可以使用这些注解来定义自己的注解。主要使用的是Target和Retention注解。

在这里插入图片描述

Annotation接口

前面经过反编译后,我们知道Java所有注解都继承了Annotation接口,也就是说Java使用Annotation接口代表注解元素,该接口是所有Annotation类型的父接口。同时为了运行时能准确获取到注解的相关信息,Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,它主要用于表示目前正在 VM 中运行的程序中已使用注解的元素,通过该接口提供的方法可以利用反射技术地读取注解的信息,如反射包的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口。

在这里插入图片描述

AnnotationElement接口

既然上面定义了注解,那得有办法拿到我们定义的注解啊。java.lang.reflect.AnnotationElement接口则提供了该功能。注解的处理是通过java反射来处理的。如下,反射相关的类Class, Method, Field都实现了AnnotationElement接口。

在这里插入图片描述

反射处理

在这里插入图片描述

AnnotationElement接口方法

在这里插入图片描述

因此,只要我们通过反射拿到Class, Method, Field类,就能够通过getAnnotation(Class)拿到我们想要的注解并取值。


常用元注解

因此我们在了解了注解的原理之后,必须要能自定义一些注解并且使用它到项目中去,才能让我们更好的了解和使用它。而要想自定义注解,就必须得了解Java提供的几个元注解。那什么是元注解呢?

元注解:就是负责注解其它注解的注解

在Java5之后定义了4个标准的元注解,分别是:

@Target
@Retention
@Documented
@Inherited


@Target元注解

target注解用来标识注解所修饰的对象范围。

它的可用范围(ElementType的取值范围)有

  1. CONSTRUCTOR:用于描述构造器(构造方法)
  2. FIELD:用于描述域(成员变量)
  3. LOCAL_VARIABLE:用于描述局部变量(局部变量)
  4. METHOD:用于描述方法(普通方法)
  5. PACKAGE:用于描述包(包定义)
  6. PARAMETER:用于描述参数(如catch等参数)
  7. TYPE:用于描述类、接口(包括注解类型) 或enum声明
  8. ANNOTATION_TYPE:用于注解

使用实例:

 @Target(ElementType.FIELD)
 public @interface TargetTest6{
 }
 @Target({ElementType.TYPE_PARAMETER,ElementType.METHOD})
 public @interface TargetTest7{
 }

@Retention元注解

@Retention定义了该Annotation被保留的时间长短,取值在java.lang.annotation.RetentionPolicy中:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。

它的取值(RetentionPoicy)有

  1. SOURCE:在源文件中有效(即源文件保留),注解只保留在源代码中,编译器直接丢弃这种注解。
  2. CLASS:在class文件中有效(即class保留),编译器把注解记录在class文件中,当Java程序运行时,JVM不能获取该注解的信息。
  3. RUNTIME:在运行时有效(即运行时保留),编译器将把注解记录在class文件中,当Java运行时,JVM可以获取注解的信息,程序可以通过反射获取该注解的信息。

只有定义为RetentionPolicy.RUNTIME时,我们才能通过注解反射获取到注解。所以,假设我们要自定义一个注解,它用在字段上,并且可以通过反射获取到,功能是用来描述字段的长度和作用,可以定义如下。

使用实例:

@Target(ElementType.FIELD)  //  注解用于字段上
@Retention(RetentionPolicy.RUNTIME)  // 保留到运行时,可通过注解获取
public @interface MyField {

    String description();
    int length();
}

@Inherited元注解

@Inherited注解指定被它修饰的注解将具备继承性:如果莫个类使用了@XXX注解,则其子类自动被@XXX修饰

使用实例:

 @Target(ElementType.FIELD)
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 @interface TargetTest6{
 }
 //所有使用了@TargetTest6注解的类讲具备继承性,
 //也就是它的子类自动带上@TargetTest6注解

@Documented元注解

@Documented用于指定被该注解修饰的注解类将被javadoc工具提取成文档.

使用实例:

 @Target(ElementType.FIELD)
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 @Documented
 @interface TargetTest6{
 }
 //javadoc工具生成的API文档将提取@Documented的使用信息

自定义注解

以上就是所有的元注解以及他们的作用分析,有了这些元注解有什么用呢?当然是为了方便我们在项目中自定义注解。那自定义注解怎么自定义呢?下面我们来看看介绍如何自定义注解并利用注解完成一些实际的功能。


自定义注解语法


  类修饰符 @interface 注解名称{
  //成员变量,在注解中以无形参的形式存在
  //其方法名和返回值定义了该成员变的名字和类型
  String name();
  int age(); 
  
 }

可以看到:注解的语法和接口的定义非常类似,他也一样具备有作用域,但是它的成员变量的定义是以无形参的方法形式存在,名字定义了成员变量的名字,返回值定义了变量类型。

下面我们来自定义一个注解

 @Target(ElementType.FIELD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface AnonTest {
 
     int age();
     String name();
 }

注解@AnonTest在运行时有效,作用域在成员变量上,它有两个成员变量分别是int型的age和String型的name。

接下来我们就可以使用这个注解了

public class test {  
  
      @AnonTest(age = 0, name = "1")
      public Integer tes;
}

这样就是一个注解的定义和使用了,有人会疑惑说,spring中很多注解都是@xxx,为什么这个@AnonTest一定要要带上两个成员变量呢?

原因很简单

注解中的成员变量如果没有默认值,则在使用注解时必须要给成员变量赋值

但如果成员变量有默认值,那可以直接在定义注解时,赋值上去,这样在使用时就可以省略不写

 @Target(ElementType.FIELD)
 @Inherited
 @Retention(RetentionPolicy.RUNTIME)
 public @interface AnonTest {
 
     int age() default 1;
     String name() default "注解测试";
 }

这样在调用注解时就可以不赋值

public class test {
 
     @AnonTest
     public Integer tes;
}

上面看到,我们已经使用了注解,但是我们并没发现@AnonTest对我们的tes成员变量有任何作用,这是因为注解本身在程序中是不会生效的,而是需要程序来提取数据并且处理注解本应该做的工作。

有了AnnotationElement接口中的这些方法,我们就可以在类、方法、成员变量等程序元素中获取到注解信息:

 package cn.wideth.util.other;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.lang.annotation.Annotation;

@Component
@Service
public class Test {

    @AnonTest
    public Integer tes;

    @Deprecated
    public void m1(){

    }

    public static void main(String[] args) {
        try {
            //获取所有test类上的注解
            Annotation[] ar=Class.forName("cn.wideth.util.other.Test").getAnnotations();
            //获取所有test类上m1方法的注解
            Annotation[] ar1=Class.forName("cn.wideth.util.other.Test").getMethod("m1").getAnnotations();
            //遍历所有的注解,检测是否有我们想要的某个注解 如@Component注解
            for(Annotation a:ar){
                System.out.println(a.getClass());
                if(a instanceof  Component)
                    System.out.println("用了注解:"+a);
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

程序结果

在这里插入图片描述

利用AnnotatedElement提供的方法能获取到我们想要的注解信息之后,就可以针对注解做一些特定的行为。

例如,对于所有的注册用户信息,系统需要把名称和年龄上报到另一个年龄分布表中,就可以利用注解就可以完成这样的动作

package cn.wideth.util.other;

import java.lang.reflect.Field;

public class Test {

    @AnonTest(name="张小小",age=20)
    public Integer tes;

    public static void getInfo(Class<?> clazz) {
        //获取目标类的所有成员变量
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //查找变量中是否有存在@AnonTest注解
            if (field.isAnnotationPresent(AnonTest.class)) {
                //如果存在,则做相应处理
                AnonTest anon= field.getAnnotation(AnonTest.class);
                System.out.println("名称是:"+anon.name());
                System.out.println("年龄是:"+anon.age());
                System.out.println("上报信息到用户年龄分布表中");
                //上报信息到用户年龄分布表中
                //uploadInfoToLog(anon.name()),anon.age())
            }
        }
    }

    public static void main(String[] args) {
        Test.getInfo(Test.class);
    }

}

程序结果

在这里插入图片描述

最终程序按照我们的需求运行并且输出正确的信息,由此可见,注解对于Java来说是一种补充,他的存在与否对程序来说应该是无影响的,灵活使用注解能使开发的效率更高,而且程序规范性也得到增强


本文小结

本文详细介绍了注解相关的知识。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值