Java注解浅析

注解(Annotation)是JDK1.5引入的机制,我们平时经常会遇到一些注解,比如@Override@Test等,也知道这些注解是在哪些情况下使用的,但是却很少去关注注解这一机制本身,比如注解是如何定义的,注解的实现机制是什么样的等等。本文将介绍注解的一些相关概念,并在文章最后实现一个自定义注解来强化我们对注解这一机制的认识。

常见注解举例

  • JDK中自带的@Override,@Deprecated
  • JUnit中常用的@Test
  • Spring中的@Component,@RequestMapping
  • Hibernate中的@Entity,@Id

本文的重点不是去介绍这些注解的用法,而是介绍注解本身这种机制。

如何定义一个注解

Override为例,Override注解在java.lang包下面,该注解的定义方式如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

注解的定义就是这么简单的,可以看到除了上面那些@开头的,注解的定义与接口的定义很接近,如果让你写一个名字为Override的空接口,你可能会这样写:public interface Override{},注解的定义只不过是将接口定义的interface关键字替换成了@interface,我们可以把@interface当做java中的一个关键字来理解,该关键字用在定义注解的地方。

元注解

我们应该对Override注解定义中头部的@Target@Retention这种形式不陌生,跟我们平时用的注解形式是一样一样的,它们也是注解,但是不是普通的注解,它们是用来注解注解的注解,叫做元注解,说白了元注解也是一种注解,只不过是用在注解这种类型上面,我们可以理解为Override也是一种类型——注解类型

能使用在注解类型上面的元注解总共有如下五种,前面四种是JDK1.5中引入的,最后一种是jdk1.8中引入的。

名称含义
@Target指定被定义的注解的使用目标,如使用在方法上,字段上等
@Retention指定注解的生存期,如源码级别,字节码级别,运行时级别
@Documented表示生成javadoc时是否会保留注解
@Inherited表示子类是否会继承父类的注解
@Repeatable表示被定义的这个注解是否能在同一地方重复使用

Target注解的取值

Target注解是用来限制注解的使用范围的,在Override注解的定义中,@Target后面括号中的ElementType.METHOD代表Override这个注解只能用在方法上面,而不能用在类上或者字段上。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注解的取值

Retention注解有三种取值如下所示:

取值含义
RetentionPolicy.SOURCE源码级别,编译成字节码之后不可见
RetentionPolicy.CLASS字节码级别,字节码中可见,但在JVM中运行时注解不可见
RetentionPolicy.RUNTIME运行时级别,在JVM中运行时也可见,因此可以在运行时利用反射获取注解的相关信息

Override注解就是源码级别可见,因为该注解主要是用来给编译器使用的,编译器在编译源文件的时候发现有这个注解就去执行这个注解的语义,不满足该语义编译器就报个编译错误。编译成字节码之后这个注解就没有存在的必要了,因此Override注解设置成源码级别可见就行。

还有一些其他的注解需要设置成运行时级别,因为需要在运行时获取该注解的相关信息,因此注解必须保留到运行时。总而言之这三种级别正好对应着java文件整个生命周期的一部分,从java源代码经过编译得到java字节码文件,然后字节码文件加载到JVM中然后开始执行,定义的注解需要设置哪种级别完全看你这个注解是在哪一步使用。

不包含任何元素的注解叫做标记注解,Override就是一个标记注解,不包含任何元素。

注解的元素

元素的定义很像接口中的方法定义,但有两点不同,一个是元素的类型是受限制的,只能是如下几种类型:
- byte,short,int,long,float,double,boolean,char八种基本类型
- String
- Class
- enum
- Annotation
- 以上几种类型的数组

另一个是元素的定义可以指定默认值,如:@Entity注解定义

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
    String name() default "";
}

Entity这个注解就包含一个元素name,类型是String类型,并且有默认值空字符串"",可以通过default来指定一个默认值。

在使用@Entity时,如果不指定name的值,则取默认值。如果是@Entity(name="aaa")就表示name的值为aaa。在使用注解时要给元素赋值就是使用这种key=value的形式,对于定义注解时有默认值的元素,在使用该注解时可以不指定值而取默认值。

如果定义注解时元素的名字为value,并且除了该元素外其余元素均有默认值,即只有value这一个元素必须指定值的话可以采用简写的方式如@MyAnnotation(value)这种形式,而不需要用key=value这种形式。比如定义一个注解如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    int value();
    String name() default "aaa";
}

那么使用这个注解的时候就可以用@MyAnnotation(10)这种方式给value元素赋值为10,注意,必须是元素名为value时才能使用这种简写方式,而且必须其他元素均不需赋值(即都采用默认值)。

再回过头看一下我们Override定义中用到的元注解@Target(ElementType.METHOD),是否能猜到,@Target使用了这种简写方式,因此Target的定义中肯定有一个元素是value,我们不妨看一下Target的定义:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

果然是有一个value元素,不过元素的类型是ElementType[]数组类型,由于Override定义中@Target只有一个取值,因此省略了数组的大括号,对于多个取值的情况,假设某个注解既可以用在字段上又可以用在方法上,则可以用如下方式:
@Target({ElementType.FIELD, ElementType.METHOD})

我们可以看到定义Target注解的时候还用到了它自身@Target(ElementType.ANNOTATION_TYPE),指定Target这个注解使用的范围是ANNOTATION_TYPE,即@Target只能使用在注解类型上。是不是能够理解了为什么那几个是元注解,就是因为那几个注解定义时的@Target指定了它们的适用范围就是ElementType.ANNOTATION_TYPE

以上就是定义一个注解时所涉及到的所有内容。今后当我们再看到某一个注解的时候,不妨点进去看一下该注解的定义方式,一旦我们看到了这个注解的定义,我们就能够知道这个注解用在哪里,需要赋值哪些元素等等,这样我们就能更好地使用这个注解了。

注解处理器

虽然定义一个注解很简单,但要这个定义的注解起作用却要花更多的功夫,真正要实现注解想实现的功能需要用到另一个叫做注解处理器的东西。不同用途的注解它的注解处理器实现可能会千差万别,但是大多数情况下我们可以利用反射机制来获取注解的信息,从而满足我们的需求。当然,具体问题需要具体分析,不同的注解应用场景可能需要使用不同的技术来达到目的。比如有一种注解@Getter,可以在字段上使用,这样我们可以不在源代码中实现get方法,而在别的地方却能调用get方法,并不影响程序运行的正确性。这里就可以通过反射机制拿到该字段上的注解,发现是一个@Getter注解,那么就要用字节码技术动态在类文件中插入该字段的get方法字节码。

一个自定义注解的实现案例

我将实现一个自己的@MyTest注解来仿照JUnit对我的业务方法进行测试,并报告最终测试结果。这里只是一个简化的模型,用来说明注解处理器的一般实现方式:
首先我们先写业务类MyService:

package cn.codecrazy.study.annotation;

public class MyService {

    public char getFirstChar(String str) {
        return str.charAt(0);
    }

    // 该方法特意实现错误的语义
    public char getSecondChar(String str) {
        // 返回第二个字符应该是charAt(1)
        return str.charAt(0);
    }
}

之后为该类写一个单元测试:

package cn.codecrazy.study.annotation.test;

import cn.codecrazy.study.annotation.MyService;
import cn.codecrazy.study.annotation.myunit.MyAssert;
import cn.codecrazy.study.annotation.myunit.MyTest;

public class MyServiceTest {

    @MyTest
    public void testGetFirstChar() throws Exception {
        char firstChar = new MyService().getFirstChar("abc");
        MyAssert.assertEqual('a', firstChar);
    }

    @MyTest
    public void testGetSecondChar() throws Exception {
        char secondChar = new MyService().getSecondChar("abc");
        MyAssert.assertEqual('b', secondChar);
    }
}

看起来是不是很熟悉?平时的开发过程当中开发完一个业务方法之后也会生成一个单元测试类,对业务方法进行测试,这里也是一样,只不过用的是我们自己的@MyTest注解,而非JUnit@Test

下面定义我们的注解@MyTest,相信有上面的基础应该能够很容易理解:

package cn.codecrazy.study.annotation.myunit;

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 MyTest {
}

表示我们的注解@MyTest只能用在方法上,并且是运行时级别,当然我们也可以给我们的MyTest注解加上一些元素,这样在使用MyTest时就可以传入一些值供我们使用。这里先不考虑添加其他元素。

接下来是在单元测试时要用到的MyAssert.assertEqual方法的定义,他们在MyAssert类中:

package cn.codecrazy.study.annotation.myunit;

public class MyAssert {
    public static void assertEqual(char expected, char actual) throws MyAssertException {
        if(expected != actual) {
            throw new MyAssertException(expected, actual);
        }
    }
}

就是一个工具类方法,用来判断两个值是否一致,不一致说明被测试的方法获取的结果跟我们期望的不一样,那么就抛出一个异常,表明该单元测试的这个用例失败了,自定义的异常类如下:

package cn.codecrazy.study.annotation.myunit;

public class MyAssertException extends Exception {
    public MyAssertException(char expected, char actual) {
        super("assert error, " + "expected: " + expected + ", but actual: " + actual);
    }
}

下面再来介绍一下我们的最重要的注解处理器MyTestProcessor:

package cn.codecrazy.study.annotation.myunit;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MyTestProcessor {
    public int caseCount = 0;
    public int failCount = 0;
    //最重要的一个方法process,接收一个类名,即单元测试所在类的类名
    public void process(String className) throws Exception {
        Class<?> clazz = Class.forName(className);
        //获取该单元测试所在类的所有方法
        Method[] methods = clazz.getDeclaredMethods();
        //遍历这些方法
        for(Method method: methods) {
            // 利用getAnnotation方法获取MyTest类型的注解
            MyTest myTest = method.getAnnotation(MyTest.class);
            //这里的MyTest是注解类型
            // 如果myTest不为空,说明该方法上面加了这个注解
            //那么我们就需要执行这个测试方法
            if(myTest != null) {
                //用来统计测试用例的数量,有@MyTest注解就当做是一个测试用例
                caseCount++;
                try {
                    // 执行该测试方法
                    method.invoke(clazz.newInstance(), null);
                } catch (InvocationTargetException e) {
                    // 如果测试方法抛出InvocationTargetException异常
                    // 则可以通过getTargetException
                    // 拿到我们的自定义MyAssertException异常
                    // 说明业务方法返回值与我们期望的值不一致
                    // 打印出异常信息,并将失败用例数加一
                    e.getTargetException().printStackTrace();
                    failCount++;
                }
            }
        }
        //打印简单统计结果
        System.out.println("Case count is: " + caseCount + " and Fail count is: " + failCount);
    }
}

上述代码重点关注这一行:
MyTest myTest = method.getAnnotation(MyTest.class);
假设MyTest中还定义了一个元素比如int id() default "0",那么在这里我们就可以通过myTest.id()拿到这个值0,而且如果我们在使用MyTest时,用的是@MyTest(id=10),那么在这里通过myTest.id()拿到的值就是10。这里只是说明一旦注解中定义了元素该如何使用该元素,我们这里的注解MyTest是没有定义任何元素的。

最后是我们的启动类:

package cn.codecrazy.study.annotation.myunit;

public class MyJUnit {
    public static void main(String[] args) throws Exception {
        MyTestProcessor myTestProcessor = new MyTestProcessor();
        myTestProcessor.process("cn.codecrazy.study.annotation.test.MyServiceTest");
    }
}

为了简单,这里指定了我们要进行单元测试的类,运行该启动类执行单元测试,控制台输出结果如下:

Case count is: 2 and Fail count is: 1
cn.codecrazy.study.annotation.myunit.MyAssertException: assert error, expected: b, but actual: a
    at cn.codecrazy.study.annotation.myunit.MyAssert.assertEqual(MyAssert.java:6)
    at cn.codecrazy.study.annotation.test.MyServiceTest.testGetSecondChar(MyServiceTest.java:18)
    at cn.codecrazy.study.annotation.myunit.MyTestProcessor.process(MyTestProcessor.java:17)
    at cn.codecrazy.study.annotation.myunit.MyJUnit.main(MyJUnit.java:6)

这里报告说总共有两个测试用例,失败了一个,失败的原因是expected: b, but actual: a,期望返回的字符是b,但实际上返回的是a。我们可以回过头去检查一下我们的业务方法getSecondChar,会发现我们那里特意写错方法的实现,用来模拟这种业务方法实现错误的场景。

注解的应用场景有很多,本文的介绍只是冰山一角,就算不需要去自定义注解的话,也希望能通过本文使大家能够更好地理解其他框架中注解的实现原理。

您的关注是我不断创作的动力源泉!期待认识更多的朋友,一起交流Java相关技术栈,共同进步!阅读更多技术文章,可关注我的公众号:codecrazy4j
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值