注解(也被成为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。

注解使得我们可以以编译器验证的格式存储程序的额外信息。

注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。

通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下有下优势:简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。

  • @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
  • @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
  • @SuppressWarnings:关闭不当的编译器警告信息。
  • @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
    @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口

1 基本语法

注解 @Test 可以和任何修饰符共同用于方法,诸如 public、static 或 void。

从语法的角度上看,注解的使用方式和修饰符的使用方式一致

package annotations;

import org.testng.annotations.Test;

public class Testable {
    public void excute(){
        System.out.println("Excuting...");
    }
    @Test
    void testExcute(){
        excute();
    }
}

1.1 定义注解

不包含任何元素的注解称为标记注解(marker annotation)

package annotations;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

简单注解,跟踪项目中的用例

package annotations;

import java.lang.annotation.*;


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    public int id();

    public String description() default "no description";
}

被注解为用例的方法

package annotations;

import java.util.List;

public class passwordUtils {
    @UseCase(id = 47,description =  "passwords must contain at least one numeric")
    public boolean validatePassword(String password){
        return (password.matches("\\w*\\d\\w*"));
    }

    @UseCase(id = 48)
    public String encryptPassword(String password){
        return new StringBuilder(password).reverse().toString();
    }

    @UseCase(id = 49,description = "new passwords can't equal previously used ones")
    public boolean checkForNewPassword(List<String> prePasswords, String password){
        return !prePasswords.contains(password);
    }
}

1.2 元注解

Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解

注解 解释
@Target 表示注解可以用于哪些地方。可能的 ElementType 参数包括:
CONSTRUCTOR:构造器的声明
FIELD:字段声明(包括 enum 实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口(包括注解类型)或者 enum 声明
@Retention 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括:
SOURCE:注解将被编译器丢弃
CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。
RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documente 将此注解保存在 Javadoc 中
@Interited 允许子类继承父类的注解
@Repeatable 允许一个注解可以被使用一次或者多次(Java 8)。
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。

2 编写注解处理器

package annotations;

import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class UseCaseTracker {
   public static void trackUseCases(List<Integer> useCases, Class<?> cl){
       
       for (Method m: cl.getDeclaredMethods()){
           UseCase uc = m.getAnnotation(UseCase.class);
           if (uc != null){
               System.out.println("Found Use Case " + uc.id() + "\n" + uc.description());
               useCases.remove(Integer.valueOf(uc.id()));
           }
       }
       useCases.forEach(i -> System.out.println("Missing use case " + i));
   }

   public static void main(String[] args) {
       List<Integer> usecases = IntStream.range(47,51).boxed().collect(Collectors.toList());
       trackUseCases(usecases,passwordUtils.class);
   }
}
/*
Found Use Case 47
passwords must contain at least one numeric
Found Use Case 48
no description
Found Use Case 49
new passwords can't equal previously used ones
Missing use case 50
*/

2.1 注解元素

注解元素可用的类型如下

  • 所有基本类型(int、float、boolean等)
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

2.2 默认值限制

元素要么有默认值,要么就在使用注解时提供元素的值。

任何非基本类型的元素,无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。

为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。习惯用法:

package annotations;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
    public int id() default -1;
    public String description() default "";
}

2.3 生成外部文件

package annotations.database;

import java.lang.annotation.*;

@Target(ElementType.TYPE) // Applies to classes only  可以指定 enum ElementType 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的 ElementType,那么可以省去 @Target 注解,但是这并不常见。
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable { // 数据库表名
    String name() default "";
}

package annotations.database;

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    int value() default 0;
    String name() default "";
    Constraints constrints() default @Constraints; // 注解作为元素的类型,嵌套注解

}
package annotations.database;

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}

package annotations.database;

@DBTable(name = "MEMBER")
public class Member {
    @SQLString(30) String firstName; // SQL列大小为30
    @SQLString(50) String lastName;
    @SQLInteger Integer age;
    @SQLString(value = 30,constrints = @Constraints(primaryKey = true)) // 主键
    String reference;
    static int menberCount;
    public String getReference(){return reference;}
    public String getFirstName(){return firstName;}
    public String getLastName(){return lastName;}

    public String toString(){return reference;}
    public Integer getAge(){return age;}
}

替代方案

第二种:你可以使用一个单一的注解类 @TableColumn,它拥有一个 enum 元素,元素值定义了 STRING,INTEGER,FLOAT 等类型。这消除了每个 SQL 类型都需要定义一个 @interface 的负担,不过也使得用额外信息修饰 SQL 类型变的不可能

第三种:对同一个目标使用多个注解

2.4 注解不支持继承

不能使用 extends 关键字来继承 @interfaces。

2.5 实现处理器

package annotations.database;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class TableCreator {
    public static void main(String[] args) throws ClassNotFoundException {
        if (args.length < 1){
            System.out.println("arguments: annotated classes");
            System.exit(0);
        }
        for (String className: args){ // 多个 类名
            Class<?> c1 = Class.forName(className);
            DBTable dbTable = c1.getAnnotation(DBTable.class); // 若有 @DBTable注解,则表名为存储
            if (dbTable == null){
                System.out.println("No DBTable annotations in class" + className);
                continue; // 查询下一个参数中的 类 -table
            }
            String tableName = dbTable.name();
            if (tableName.length() < 1)
                tableName = c1.getName().toUpperCase(); // 若表名为空,则设置为 大写的类名
            List<String> columnDefs = new ArrayList<>(); // 字段列表
            for (Field field: c1.getDeclaredFields()){//该类的所有字段
                String columnName = null;
                Annotation[] anns = field.getDeclaredAnnotations(); // 字段的所有注解
                if (anns.length < 1)
                    continue; // 不是 db table column
                if (anns[0] instanceof SQLInteger){//整型
                  SQLInteger sInt = (SQLInteger)anns[0];
                  if (sInt.name().length() < 1)
                      columnName = field.getName().toUpperCase();
                  else
                      columnName = sInt.name();
                  columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));

                }
                if (anns[0] instanceof SQLString){//字符串
                    SQLString sString = (SQLString)anns[0];
                    if (sString.name().length() < 1)
                        columnName = field.getName().toUpperCase();
                    else
                        columnName = sString.name();
                    columnDefs.add(columnName + " VARCHAR(" + sString.value() +")"+ getConstraints(sString.constrints()));
                }

                StringBuilder createCommand = new StringBuilder("Create TABLE"  + tableName +"(");
                for (String columnDef :columnDefs)
                    createCommand.append(
                            "\n " + columnDef + ",");
                // 去掉最后一个逗号
                String tableCreate = createCommand.substring(0,createCommand.length()-1) + ");";
                System.out.println("Table Creation SQL for " + className + " is: \n" + tableCreate);
            }
        }
    }
    private static String getConstraints(Constraints con){ // 处理嵌套注解
        String constraints = "";
        if (!con.allowNull())
            constraints += " NOT NULL";
        if (con.primaryKey())
            constraints += " PRIMARY KEY";
        if (con.unique())
            constraints += " UNIQUE";
        return constraints;
    }
}
/*
Table Creation SQL for annotations.database.Member is:
Create TABLEMEMBER(
 FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
Create TABLEMEMBER(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
Create TABLEMEMBER(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50),
 AGE INT);
Table Creation SQL for annotations.database.Member is:
Create TABLEMEMBER(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50),
 AGE INT,
 REFERENCE VARCHAR(30) PRIMARY KEY);
 */

3 使用javac处理注解

通过 javac,你可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。

如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。

每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。

3.1 最简单的处理器

注解的定义

package annotations.simplest;

// A bare-bones annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD, // 用逗号隔开的形式指定多个值,若想应用与所有的ElementType,则可以省去@Target元注解
ElementType.CONSTRUCTOR,
        ElementType.ANNOTATION_TYPE,
        ElementType.PACKAGE,
        ElementType.FIELD,
        ElementType.LOCAL_VARIABLE,})
@Retention(RetentionPolicy.SOURCE) // 元注解的保留策略 是 SOURCE, 注解 将会被编译器 丢弃,javac是唯一有机会处理注解的代理
public @interface Simple {
    String value() default "-default-";
}


测试示例

package annotations.simplest;

@Simple
public class SimpleTest {
    @Simple
    int i;
    @Simple
    public SimpleTest(){}
    @Simple
    public void foo(){
        System.out.println("SimpleTest.foo()");
    }
    @Simple
    public void bar(String s,int i, float f){
        System.out.println("SimpleTest.bar()");
    }
    @Simple
    public static void main(String[] args){
        @Simple
        SimpleTest st = new SimpleTest();
        st.foo();
    }
}
/*
SimpleTest.foo()
 */

如下为:十分简单的处理器,其所作的事情就是把注解相关的信息打印出来

package annotations.simplest;

// A bare-bones annotation processor

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import java.util.Set;

//(旧的,失效的)apt 版本的处理器需要额外的方法来确定支持哪些注解以及支持的 Java 版本。
// 不过,你现在可以简单的使用 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解
@SupportedAnnotationTypes("annotations.simplest.Simple")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SimpleProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 第一个参数告诉你哪个注解是存在的which annotations are present,第二个参数保留了剩余信息 contains all the rest of the information。
        for (TypeElement t: annotations)
            System.out.println(t);
        for (Element el:roundEnv.getElementsAnnotatedWith(Simple.class))//遍历 所有被@Simple注解的元素
            display(el);
        return false;
    }
    private void display(Element el){
        System.out.println("==== " + el + " ====");
        System.out.println(el.getKind() +
                " : " + el.getModifiers() + //是否为 public 和 static
                " : " + el.getSimpleName() +
                " : " + el.asType()
                );
        if (el.getKind().equals(ElementKind.CLASS)){
            TypeElement te = (TypeElement)el;
            System.out.println(te.getQualifiedName());
            System.out.println(te.getSuperclass());
            System.out.println(te.getEnclosedElements());
        }
        if (el.getKind().equals(ElementKind.METHOD)){
            ExecutableElement ex = (ExecutableElement)el;
            System.out.println(ex.getReturnType() + " ");
            System.out.println(ex.getSimpleName() + "(");
            System.out.println(ex.getParameters() + ")");
        }
    }
}


javac -processor annotations.simplest.SimpleProcessor SimpleTest.java

没跑通……
erin$ javac -processor annotations.SimpleProcessor SimpleTest.java
错误: 找不到注释处理程序 'annotations.SimpleProcessor'

书上结果
annotations.simplest.Simple
==== annotations.simplest.SimpleTest ====
CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest
annotations.simplest.SimpleTest
java.lang.Object
i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])
==== i ====
FIELD : [] : i : int
==== SimpleTest() ====
CONSTRUCTOR : [public] : <init> : ()void
==== foo() ====
METHOD : [public] : foo : ()void
void foo()
==== bar(java.lang.String,int,float) ====
METHOD : [public] : bar : (java.lang.String,int,float)void
void bar(s,i,f)
==== main(java.lang.String[]) ====
METHOD : [public, static] : main : (java.lang.String[])void
void main(args)


3.2 更复杂的处理器

因为处理的是源代码,而不是编译后的class文件,所有,创建用于javac注解处理器时,不能使用java的反射特性。

解决各种mirror问题,通过允许你在未编译的源码中查看方法、字段、和类型.

下面是从一个class中提取public methods的注解,可以抽取成为一个接口。

package annotations.ifx;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)//提取类中的接口后,不再将注解信息保留到 class文件中
public @interface ExtractInterface {
    String interfaceName() default "-!!-";
}

测试类提供了一些公用方法,这些方法可以成为接口到一部分

package annotations.ifx;

@ExtractInterface(interfaceName = "IMultiplier") //接口名称
public class Multiplier {
    public boolean flag = false;
    private int n = 0;
    public int multiply(int x,int y){
        int total = 0;
        for (int i =0;i<x;i++)
            total = add(total,y);
        return total;
    }

    public int fortySeven(){return 47;}
    
    private int add(int x, int y){// 私有方法,不能成为接口的一部分
        return x + y;
    }
    public double timesTen(double arg){
        return arg * 10;
    }

    public static void main(String[] args) {
        Multiplier m = new Multiplier();
        System.out.println("11 * 16 = " + m.multiply(11,16));
    }
}

这里有一个编译时处理器用于提取有趣的方法,并创建一个新的 interface 源代码文件(这个源文件将会在下一轮中被自动编译

4 基于注解的单元测试

要生产一个非嵌入式的测试,最简单的方式就是继承:

使用组合来创建非嵌入式的测试

4.1 在 @Unit 中使用泛型

4.2 不需要任何套件

4.3 实现 @Unit

@Test 标签

package annotations.atunit;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

其他需要的注解

package annotations.atunit;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObjectCreate {
}


package annotations.atunit;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObjectCleanup {
}


package annotations.atunit;

import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestProperty {
}

通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。

基于注解的单元测试框架


4.4 移除测试代码

 posted on 2020-02-05 11:54  erinchen  阅读(171)  评论(0编辑  收藏  举报