继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

java编程思想之注解详解

晁东洋
关注TA
已关注
手记 22
粉丝 7048
获赞 756

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

注解在一定程度上是在把元数据与源代码文件结合在一起,而不是保存在外部文档中。注解是众多引入 javaSE5 中的重要语言变化之一。他们可以提供用来完整地描述程序所需的信息,而这些信息是无法用 Java 来表达的。注解可以用来生成描述文件,甚至或是新的类定义,并且有助于编写减轻样板代码的负担。通过使用注解,我们可以将这些元数据保存在 Java 源代码中,并利用 annotation API 为自己的注解构造处理工具,同时,注解的优点还包括:更加干净易读的代码以及编译期类型检查等。虽然 Java SE5 预先定义了一些元数据,但一般来说,主要还是需要程序员自己添加新的注解,并且按照自己的方式使用它们。

注解的语法比较简单,除了 @ 符号的使用外,它基本与 Java 固有的语法一样。Java SE5 内置了三种,定义在 java.lang 中的注解:

  • @Override,表示当前的方法定义将覆盖超类中的方法。
  • @Deprecated,如果程序员使用了注解为它的元素,那么编译器会发出警告信息。
  • @SuppressWarnings,关闭不当的编译器警告信息。Java SE5 之前的版本也可以使用这个注解,不过会被忽略不起作用。

每当你创建了描述性质的类或接口时,一旦其中包含了重复性的工作,那就可以考虑使用注解来简化与自动化该过程。注解是在实际的源代码级别保存所有的信息,而不是某种注释性的文字,这使得代码更整洁,且便于维护。通过使用扩展的 annotation API,或外部的字节码工具类库,程序员拥有对源代码以及字节码强大的检查与操作能力。

基本语法

下面示例中,使用 @Test 对 TestExecute() 方法进行注解。这个注解本身并不做任何事情,但是编译器要确保在其构造路径上必须有 @Test注解的定义。

public @interface Test {

}

public class Testble {
    public void execute() {
        System.out.println("Executing..");
    }

    @Test
    void testExecute(){
        execute();
    }
}

被注解的方法与其他的方法没有区别。@Test 可以与任何修饰符共同作用域方法。

定义注解

上面的例子注解的定义我们已经看到了。注解的定义看起来很像接口的定义。事实上与任何 Java 文件一样,注解也会被编译为 class 文件。除了 @ 符号以外,@Test 的定义很像一个空的接口。定义注解时会需要一些元注解,如 @Target@Retention@Target 用来定义你的注解将应用于什么地方。@Deprecated 用来定义应该用于哪一个级别可用,在源代码中、类文件中或者运行时。

在注解中一般都会包含某些元素用以表示某些值。当分析出来注解时,程序和工具可以利用这些值。注解的元素看起来就像接口的方法,唯一的区别是你可以为他指定默认值。没有元素的注解被称为标记注解。

下面是一个简单的注解,它可以跟踪一个项目中的用例。程序员可以在该方法上添加注解,我们就可以计算有多少已经实现了该用例。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    public int id();
    public String description() default "没有描述";
}

注意:id 和 description 类似方法的定义。description 元素有一个 default 值,如果在注解某个方法时没有给出 description 的值,则就会使用这个默认值。

下面的三个方法被注解:

public class PasswordUtils {
    @UseCase(id =47,description = "password 哈哈哈防止破解")
    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 = "是否包含在这个密码库中")
    public boolean checkForNewPassword(List<String> prevPassword,String password) {
        return !prevPassword.contains(password);
    }
}

注解的元素在使用时是名值对的形式放入注解的括号内。

元注解

Java 目前只内置了三种标准注解,以及四种元注解。元注解就是注解的注解:

<table width="999" border="0" cellpadding="0" cellspacing="0" style='width:499.50pt;border-collapse:collapse;table-layout:fixed;'>
<col width="139" style='mso-width-source:userset;mso-width-alt:3388;'/>
<col width="96" span="5" style='width:48.00pt;'/>
<col width="284" style='mso-width-source:userset;mso-width-alt:6923;'/>
<col width="96" style='width:48.00pt;'/>
<tr height="28" style='height:14.00pt;'>
<td class="xl65" height="250" width="139" rowspan="8" style='height:125.00pt;width:69.50pt;border-right:none;border-bottom:none;' x:str>@Target</td>
<td class="xl66" width="764" colspan="6" rowspan="8" style='width:382.00pt;border-right:none;border-bottom:none;' x:str>表示注解可以用在什么地方。ElementType 的参数包括:<br/>CONSTRUCTOR:构造器的声明<br/>FIELD:域声明<br/>LOCAL_VARIABLE:局部变量声明<br/>METHOD:方法声明<br/>PACKAGE:包声明<br/>PARAMETER:参数声明<br/>TYPE:类、接口或enum声明</td>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="54" style='height:27.00pt;mso-height-source:userset;mso-height-alt:540;'>
</tr>
<tr height="28" style='height:14.00pt;'>
<td class="xl65" height="130" rowspan="4" style='height:65.00pt;border-right:none;border-bottom:none;' x:str>@Retention</td>
<td class="xl66" colspan="6" rowspan="4" style='border-right:none;border-bottom:none;' x:str>表示需要在什么级别保存注解信息。可选的RententionPolicy参数:<br/>SOURCE:注解将被编译器丢失<br/>CLASS:注解在class文件中可用,但会被 vm 丢失<br/>RUNTIME:vm 在运行期也保留注解,因此可以通过反射机制读取注解的信息。</td>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="28" style='height:14.00pt;'>
</tr>
<tr height="46" style='height:23.00pt;mso-height-source:userset;mso-height-alt:460;'>
</tr>
<tr height="52" style='height:26.00pt;mso-height-source:userset;mso-height-alt:520;'>
<td height="52" style='height:26.00pt;' x:str>@Documented</td>
<td class="xl68" colspan="6" style='border-right:none;border-bottom:none;' x:str>将此注解包含在 javaDoc 中</td>
</tr>
<tr height="56" style='height:28.00pt;mso-height-source:userset;mso-height-alt:560;'>
<td height="56" style='height:28.00pt;' x:str>@Inherited</td>
<td class="xl68" colspan="6" style='border-right:none;border-bottom:none;' x:str>允许子类继承父类中的注解</td>
</tr>

</table>

大多数时候我么都是编写字节的注解,并编写自己的处理器处理他们。

编写注解处理器

如果没有用来读取注解的工具,那么注解就不会这有用。使用注解很重要的就是创建和使用注解处理器。Java SE5 扩展了反射机制的 API,方便我们构造这种工具。同时还提供了一个外部工具 apt 帮助我们解析带有注解的 Java 源代码。

下面我们就用反射来做一个简单的注解处理器。我们用它来读取上面的 PasswordUtils 类。

 public class UseCaseTracker {

    public static void trackUseCase(List<Integer> useCase,Class<?> cl) {
        for (Method method : cl.getDeclaredMethods()) {
            UseCase uCase = method.getAnnotation(UseCase.class);
            if (uCase != null) {
                System.out.println("方法上的注解信息:"+uCase.id()+"  "+uCase.description());
            }
        }

        for (Integer integer : useCase) {
            System.out.println("参数:"+integer);
        }
    }

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

}

测试结果:

方法上的注解信息:49  是否包含在这个密码库中
方法上的注解信息:48  没有描述
方法上的注解信息:47  password 哈哈哈防止破解
参数:47
参数:48
参数:49
参数:50

上面用到了两个反射的方法:getDeclaredMethods() 和 getAnnotation(),getAnnotation() 方法返回指定类型的注解对象,在这里使用 UseCse。如果被注解的方法上没有改类型的注解,则返回 null 值。然后我们从返回的 UseCase 对象中提取元素的值。

注解元素

标签 @UseCase 由 UseCase,java 定义,包含 int 类型的元素 id,以及一个 String 类型的元素 description。注解元素可以使用的类型包括:

  • 所有的基本类型
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

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

默认值限制

编译器对元素的默认值具有严格的限制。首先,元素不能有不确定的值。也就是说元素必须要具有默认的值,要嘛在使用注解时提供元素的值。其次,对于非基本类型的元素,无论在源代码中声明时,或是在注解接口中定义时,都不能以 null 作为值。为了绕开这个限制,我们只能定义一些特殊的值,例如;空字符串或者是负数表示某个元素不存在:

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

生成外部文件

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

下面是一个注解的示例,告诉注解处理器,我们需要生成一个数据库表:

@Retention(RUNTIME)
@Target(TYPE)
public @interface DBtable {
    public String name() default "";
}

注意:@Target 标签内可以有多个值用逗号分开,也可以没有值表示应用所有类型。其中的 name() 元素我们用来为处理器创建数据库表提供名字。

接下来是修饰 javaBean 对象准备的注解:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface SqlString {
    int value() default 0;
    String name() default "";
    Constraints constraints() default @Constraints;
}

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

注解处理器通过 @Constraints 注解提取出数据库表的元数据。虽然对于数据库所能提供的所有约束而言只是一小部分,但足以表达我们的思想。并且我们也为三个元素提供了默认值。另外两个注解定义的是 SQL 类型。这些 sql 类型具有 name() 元素和 constraints() 元素。后者利用注解嵌套的功能将列的约束信息嵌入其中。我们看到 @Constraints 注解类型之后没有指明元素的值而是用一个注解作为默认值。如果要让嵌入的 @Constraints 注解中的 unique() 元素为 true,并以此作为 constraints() 元素的默认值,则需要如下定义:

@Retention(RUNTIME)
@Target(FIELD)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints(unique = true);
}

下面使我们的 Bean 的示例:

@DBtable(name = "Member")
public class Member {
    @SqlString(30)
    String firstname;

    @SqlString(50)
    String lasttname;

    @SQLInteger
    Integer age;

    @SqlString(value = 30,constraints = @Constraints(primaryKey=true))
    String handle;

    static int menberCount;

    public String getFirstname() {
        return firstname;
    }

    public String getLasttname() {
        return lasttname;
    }

    public Integer getAge() {
        return age;
    }

    public String getHandle() {
        return handle;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return handle;
    }
}

类的注解 @DBTable 给定了值 MEMBER,他将会用来作为表的名字。Bean 的属性 firstname 和 lasttname ,都被注解为 @SqlString 类型,并且为其元素赋值为 30。我们看到这其中使用了快捷方式。如果你的注解中定义了名为 value() 的元素,并且该元素在应用的时候是唯一需要赋值的元素。那么此时无需使用名值对的语法,只需要在括号内给出 value 的值即可。

注解不支持继承

不能使用关键字 extends 来继承某个 @interface。很遗憾如果注解支持继承的话可以大大减少我们打字的工作量,并且使得语法更加整洁。

实现处理器

下面是一个注解处理器的示例,它将读取一个类文件,检查其上的数据库注解,并生成用来创建数据库的 SQL 命令:

public class TableCreator {

    public static void main(String[] args) throws Exception{
         if(args.length < 1) {
              System.out.println("arguments: annotated classes");
              System.exit(0);
          }
        for (String className : args) {
            Class<?> cl = Class.forName(className);
            DBtable dBtable = cl.getAnnotation(DBtable.class);
            if (dBtable != null) {
                System.out.println("这个类没哟创建数据库:"+className);
                continue;
            }

            //数据库的名字
            String tableName = dBtable.name();
            if (tableName.length()<1) {
                //如果名字没有赋值就用类名并且大写
                tableName = cl.getName().toUpperCase();
            }
            //查询出所有的列
            List<String> columnName = new ArrayList<>();
            for (Field field : cl.getDeclaredFields()) {
                String colum = null;
                //获取对象上的注解
                Annotation[] anns = field.getDeclaredAnnotations();
                if (anns.length <1) {
                    continue;
                }

                if (anns[0] instanceof SQLInteger) {
                    SQLInteger sInteger = (SQLInteger) anns[0];
                    if (sInteger.name().length()<1) {
                        colum = field.getName().toUpperCase();
                    }else {
                        colum = sInteger.name();
                    }
                    columnName.add(columnName + " INT" +getConstraints(sInteger.constraints()));
                }

                if(anns[0] instanceof SqlString) {
                    SqlString sString = (SqlString) anns[0];
                      // Use field name if name not specified.
                      if(sString.name().length() < 1)
                          colum = field.getName().toUpperCase();
                      else
                          colum = sString.name();
                      columnName.add(columnName + " VARCHAR(" +
                        sString.value() + ")" +
                        getConstraints(sString.constraints()));
                  }

                 StringBuilder createCommand = new StringBuilder(
                          "CREATE TABLE " + tableName + "(");
                        for(String columnDef : columnName)
                          createCommand.append("\n    " + columnDef + ",");
                        // Remove trailing comma
                        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;
      }

}

我们使用注解来解析构造 sql 语句。上面的示例是非常简洁的一个例子。对于真正的对象影射数据库是非常复杂的。现在有很多这样的框架,可以将对象影射到关系数据库。比如:大名鼎鼎的 greenDAO。

使用 apt 处理注解

注解处理工具 apt,这是 sun 为了帮助注解处理的过程提供的工具。与 Javac 一样,apt 被设计为操作 Java 的源文件,而不是编译后的类。默认情况下 apt 会在处理完源文件后编译他们。当注解处理器生成一个新的源文件时,改文件会在新一轮的注解处理中接受检查。该工具会一轮一轮的处理,直到不再有新的源文件产生。

我们定义的每一个注解都需要自己的处理器,而 apt 工具可以很容易的将多个处理器组合在一起。这样我们就可以指定多个要处理的类。通过使用 AnnotationProcessorFactory,apt 能够为每一个它发现的注解生成一个正确的注解处理器。使用 apt 生成注解处理器时,我们无法利用 Java 的反射机制,因为我们操作的是源代码,而不是编译后的类。使用 mirror API 能够解决这个问题,他使得我们能够在未经编译的源代码中查看方法、对象以及类型。

下面是一个自定义的注解,使用它可以把一个类的 public 方法提取出来,构造成一个新的接口:

@Retention(SOURCE)
@Target(TYPE)
public @interface ExtractInterface {
    public String value();
}

我们看到 @Retention(SOURCE) 是 SOURCE。因为我们从使用了该注解的类抽取接口之后没必要在保留这些注解信息。下面的类有一个公共的方法,我们将把他抽取到一个接口中:

@ExtractInterface("Multiplier")
public class Multiplier {

    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  add(int x,int y) {
        return x+y;
    }

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

    }

}

测试结果:

11*16=176

在 Multiplier 类中有一个 multiply() 方法,该方法经过循环调用私有的 add() 方法实现乘法操作。add() 方法不是公共的,因此不将其作为接口的一部分。注解给了类名作为值,这就是将要生成的接口的名字:

import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.Modifier;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;

import genericity.New;

public class InterfaceExtractorProcessor implements AnnotationProcessor{

    private final AnnotationProcessorEnvironment aenv;

    private ArrayList<MethodDeclaration> interfaceMethods = new ArrayList<>();

    protected InterfaceExtractorProcessor(AnnotationProcessorEnvironment aenv) {
        super();
        this.aenv = aenv;
    }

    @Override
    public void process() {
        for (TypeDeclaration typeeclaration : aenv.getSpecifiedTypeDeclarations()) {
            ExtractInterface annot = typeeclaration.getAnnotation(ExtractInterface.class);
            if (annot == null) {
                break;
            }

            for (MethodDeclaration methodDeclaration : typeeclaration.getMethods()) {
                if (methodDeclaration.getModifiers().contains(Modifier.PUBLIC) && !(methodDeclaration.getModifiers().contains(Modifier.STATIC))) {
                    interfaceMethods.add(methodDeclaration);
                }
            }

            if (interfaceMethods.size() >0) {
                try {
                    PrintWriter writer = aenv.getFiler().createSourceFile(annot.value());
                     writer.println("package " +
                     typeeclaration.getPackage().getQualifiedName() +";");
                     writer.println("public interface " +
                     annot.value() + " {");
                     for(MethodDeclaration m : interfaceMethods) {
                            writer.print("  public ");
                            writer.print(m.getReturnType() + " ");
                            writer.print(m.getSimpleName() + " (");
                            int i = 0;
                            for(ParameterDeclaration parm :
                              m.getParameters()) {
                              writer.print(parm.getType() + " " +
                                parm.getSimpleName());
                              if(++i < m.getParameters().size())
                                writer.print(", ");
                            }
                            writer.println(");");
                          }
                          writer.println("}");
                          writer.close();

                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }
        }
    }

}

程序中用到的 mirror 的 jar 包可以到下面的地址下载:

http://www.java2s.com/Code/Jar/a/Downloadaptmirrorapi01jar.htm

所有的工作都在 process() 中完成。在分析一个类的时候,我们用 MethodDeclaration 类以及其上的 getModifiers() 方法找到 public 方法。一旦找到就将其保存到一个 ArrayList 中。然后在一个 .java 文件中创建新的接口中的方法定义。

注意在构造器中以 AnnotationProcessorEnvironment 对象为参数。通过该对象我们知道 apt 正在处理的所有类型,并且可以通过他获取 Messager 对象和 Filer 对象。Filer 对象是一种 PrintWriter,我们可以通过他创建新的文件。不使用普通的 PrintWriter 而是使用 Filer 对象主要原因是:只有这样 apt 才知道我们创建的新文件,从而对新文件进行注解处理,并且在需要的时候编译他们。

createSourceFile() 方法以将要新建的类或接口名字,打开了一个普通的输出流。

apt 工具需要一个工厂类来为其指明正确的处理器,然后它才能调用处理器上的 process() 方法:

public class InterfaceExtractorProcessorFactor implements AnnotationProcessorFactory{

    @Override
    public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> arg0,
            AnnotationProcessorEnvironment arg1) {

        return new InterfaceExtractorProcessor(arg1);
    }

    @Override
    public Collection<String> supportedAnnotationTypes() {
        // TODO Auto-generated method stub
        return Collections.singleton("annotations.ExtractInterface");
    }

    @Override
    public Collection<String> supportedOptions() {
        // TODO Auto-generated method stub
        return Collections.emptySet();
    }

}

AnnotationProcessorFactory 接口只有三个方法。其中 getProcessorFor() 方法注解处理器,该方法包含类型声明的 Set 以及 AnnotationProcessorEnvironment 对象作为参数。另外两个方法是 supportedAnnotationTypes() 和 supportedOptions(),可以通过他们检查一下是否 apt 工具发现的所有的注解都有相应的处理器,是否所有控制台输入的参数都是你提供的可选项。如有疑问关注我联系我

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP