Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

java基础(十七)之注解

2017-06-10
nivelle

注解

注解(也称为为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时候非常方便地使用这些数据.

注解在一定程度上是在把元数据与源代码文件结合在一起,而不是保存在外部文档中这一大的趋势下所催生的.

注解是众多引入到Java SE5中的重要的语言变化之一.它们可以提供用来完整地描述程序所需的信息,而这些信息是无法用java来表达的.因此,注解使得我们能够以将由编译器来测试和验证的格式,存储有关程序的额外信息.

注解可以用来生成描述文件,甚至或是新的类定义,并且有助于减轻编写”样板”代码的负担.通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具,同时,注解的优点还包括:更加干净易读的代码以及编译期类型检查等.


每当你创建描述符性质的类或接口时,一旦其中包含了重复性的工作,那就可以考虑使用注解来简化与自动化该过程.

注解是真正语言级的概念,一旦构造出来,就享有编译期的类型检查保护.注解(annotation)是在实际的源代码级别保存所有的信息,而不是某种解释的文字,这使得代码更整洁.

基本语法

注解本身并不做任何事情,但是编译器要确保在其构造器上必须有注解的定义.

定义注解

定义注解时,程序或者工具需要一些元注解:

  • @Target:用来定义你的注解将应用在什么地方,CONSTRUCTOR:构造器,FIELD:域声明,LOCAL_VARIABLE:局部变量声明,METHOD:方法声明,PACKAGE:包声明,PARAMETER:参数声明,TYPE:类\接口(包括注解类型声明)或enum

  • @Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE):注解将被编译器丢弃,类文件中(CLASS):注解在Class文件中可用,但会被VM丢弃,或者运行时(RUNTIME):VM将在运行期间也保留注解,因此可以通过反射机制读取注解的信息

  • @Documented:将此注解包含在Javadoc中

  • @Inherited:允许子类继承父类中的注解

注解中,一般会包含一些元素以表示某些值.当分析处理注解时,程序或工具可以利用这些值.注解的元素看起来就像接口的方法,唯一的区别是你可以为其指定默认值.

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 UserCase {
  public int id();
  public String description() default "no description";

}


id和description类似方法定义,由于编译器会对id进行类型检查,因此将用例文档的追踪数据库与源代码相关联是可靠的.


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

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

  @UserCase(id = 49, description = "new passwords cant equal previously used ones")
  public boolean checkForNewPassword(List<String> prevPasswords, String password) {
    return !prevPasswords.contains(password);
  }
}

注解的元素在使用时表现为名-值对的形式,并需要置于@UseCase声明之后的括号内.

编写注解处理器

如果没有用来读取注解的工具,哪注解也不会比注释更有用.使用注解的过程中,很重要的一部分就是创建与使用注解处理器.JAVA SE5扩展了反射机制的API,以帮助程序员构造这类工具.同时,它还提供了一个外部工具apt帮助程序员解析带有注解的java源代码.

public class UserCaseTracker {
  public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
    for (Method m : cl.getDeclaredMethods()) {
      UserCase uc = m.getAnnotation(UserCase.class);
      if (uc != null) {
        System.out.println("found use case:" + uc.id() + "" + uc.description());
        useCases.remove(new Integer(uc.id()));
      }
    }
    for (int i : useCases) {
      System.out.println("waring:missing use case-" + i);
    }

  }

  public static void main(String args[]) {
    List<Integer> useCases = new ArrayList<>();
    Collections.addAll(useCases, 47, 48, 49, 50);
    trackUseCases(useCases, PasswordUtils.class);
  }

}


结果:

found use case:47password must contain at least one one numeric

found use case:48no description

found use case:49new passwords cant equal previously used ones

waring:missing use case-50

上面程序使用了两个反射方法:getDeclaredMethods()和getAnnotation(),它们都属于AnnotatedElement接口(Class,Method,与Field等类都实现了该接口). getAnnotation()返回指定类型的注解对象.此处就是UseCase

注解元素

标签@UseCase由UseCase.java定义,其中包含int元素id,以及一个String元素description.注解元素可用的类型如下所示:

  • 所有基本类型(int,float,boolean)

  • String

  • Class

  • enum

  • Annotation

  • 以上类型的数组

如果使用了其他类型,哪编译器就会报错.注意,也不允许使用任何包装类型,不过由于自动打包的存在,这算不上什么限制.注解也可以作为元素的类型,也就是说注解可以嵌套.

默认值限制

元素不能有不确定的值,也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值.其次,对于非基本类型的元素,无论是在源代码中声明时,或是在注解接口中定义默认值时,都不能以null作为其值.这个约束使得处理器很难表现一个元素的存在或缺失的状态.因为,在每个注解的声明中,所有的元素都存在,并且都具有相应的值.为了绕开这个约束,我们自己定义一些特殊的值,例如空字符串或负数,以此表示这个元素不存在.


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


生成外部文件

假设你希望提供一些基本的对象/关系映射功能,能够自动生成数据库表,用以存储javaBean对象.你可以选择使用XML描述文件,指明类的名字,每个成员以及数据库映射的相关信息.然而,若使用注解,你可以将所有信息都保存在JavaBean源文件中.为此,我们需要新注解,用以定义与Bean关联的数据库表名字,以及与Bean属性关联的列名字和SQL类型.

以下是一个注解定义,它告诉编译器,你需要为我生成一个数据库表:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {

  public String name() default "";
}


@DBTable有一个name()元素,该注解通过这个元素为处理器创建数据库表提供表的名字.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
  boolean primaryKey() default false;

  boolean allowNull() default true;

  boolean unique() default false;
}


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

  Constraints constraints() default @Constraints
  ;
}


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
  int value() default 0;

  String name() default "";

  Constraints constraints() default @Constraints
  ;
}


注解处理器通过@Constraints注解提取出数据库表的元数据.虽然对于数据库所能提供的所有约束而言,@Constraints注解只表示了它的一个很小的子集,不过它所表达的思想已经足够清楚了.

另外连个@interface定义的是SQL类型.这些SQL类型具有name()元素和constraints()元素.后者利用了嵌套注解的功能,将column类型的数据库约束信息嵌入其中.注意Constraints()元素的默认值是@Constraints.

如果要令嵌入的@Constraints注解中的unique()元素为true,并以此作为constraints()元素的默认值,则需要如下定义该元素:

public @interface Uniqueness{
  default @Constraints(unique=true)
}

使用以上注解的一个Bean例子:

@DBTable(name="MEMBER")
public class Member {
  @SQLString(30) String firstName;
  @SQLString(50) String lastName;
  @SQLInteger Integer age;
  @SQLString(value=30,constraints=@Constraints(primaryKey=true)) String handle;
  static int memberCount;
  
  public String getHandle(){return handle;}
  public String getFirstName(){return firstName;}
  public String getLastName(){return lastName;}
  public String toString(){return handle;}
  public Integer getAge() {return age;}
}



其中,如果成员注解中定义了名为value的元素,并且在应用该注解的时候,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名-值对的这种语法,只需要在括号内给出value元素所需要的值即可.

注解不支持继承

实现处理器

public class TableCreator {
  
  public static void main(String args[]) throws Exception{
   
    for(String className:args){
      Class<?>cl = Class.forName(className);
      DBTable dbTable = cl.getAnnotation(DBTable.class);
      if(dbTable==null){
        System.out.println("no dbTable annotations in class "+className);
        continue;
      }
      String tableName = dbTable.name();
      if(tableName.length()<1){
        tableName = cl.getName().toUpperCase();
      }
      List<String> columnDefs = new ArrayList<>();
      for(Field field:cl.getDeclaredFields()){
        String columnName = null;
        Annotation[] anns = field.getDeclaredAnnotations();
        if(anns.length<1) continue;
        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.constraints()));
        }
        
        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;
  }
}

使用apt处理注解

与javac一样,apt被设计为操作java源文件,而不是编译后的类,默认情况下,apt会在处理完源文件后编译它们.如果系统构建过程中会自动创建一些新的源文件,那么这个特性非常有用,事实上,apt会检查新生成的源文件中注解,然后将所有文件一同编译.

当注解处理器生成一个新的源文件时,该文件会在新的一轮的注解处理中接收检查.该工具会一轮一轮地处理,直到不再有新的源文件产生为止,然后它再编译所有的源文件.

程序员自定义的每个注解都需要自己的处理器,而apt工具能容易地将多个处理容器组合在一起,有了它,程序员就可以指定多个要处理的类,这比自己变了所有的类文件简单多了.还可添加监听器,并在一轮注解处理过程结束时收到通知信息.

通过使用AnnotationProcessorFactory,apt能够为每一个它发现的注解生成一个真确的注解处理器.当使用apt时,必须指明一个工厂类,或者指明能找到apt所需的工厂类的路径.

使用apt生成注解处理器时,我们无法利用java反射机制,因为我们操作的是源代码,而不是编译后的类.使用mirror API能够解决这个问题,它使我们能够在为编译的源代码中查看方法,域以及类型.


评论