Java注解

Java注解

定义

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

产生背景

JavaSE5之前是没有注解的,注解是为了在一定程度上把元数据与源代码结合在一起,而不是保存在外部文档中这一趋势下所产生的。

定义

定义一个注解很简单:

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

}

可以看出,主要由两部分组成:

  • 元注解
  • 类似于定义接口的结构

先给出一个定义标记注解(什么都不做)的示例,在对示例进行说明以阐述注解定义过程,下面代码定义了一个Test注解。

package com.rainmonth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/27.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {

}

再给出一个一般注解的定义示例:

package com.rainmonth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/27.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();

    String descriptions() default "no description";
}

可以看到,但从代码表现形式上,其定义很像一个空接口的定义(多了一个@),注意,注解也将编译成class文件。定义注解时,会用到一些元注解(meta-annotation),即上面的@Target和@Retention。以下是对元注解的部分说明:

元注解

  • @Target,表示该注解将应用于什么地方,可选参数(参考java.lang.annotation.ElementType中的定义)包括:
    • TYPE,类、接口(包括注解类型)或enum等的声明;
    • FIELD,域声明(包括enum实例);
    • METHOD,方法声明;
    • PARAMETER,参数声明;
    • CONSTRUCTOR,构造函数声明;
    • LOCAL_VARIABLE,局部变量声明;
    • ANNOTATION_TYPE,注解类型声明;
    • PACKAGE,包声明;
    • TYPE_PARAMETER,类型参数声明(Java 1.8新增);
    • TYPE_USE,类型使用(Java 1.8新增);
  • @Retention,表示需要在什么级别保存该注解信息,可选参数(参考java.lang.annotation.RetentionPolicy中的定义)包括:
    • SOURCE,注解将被编译器丢弃;
    • CLASS,注解将在编译的class文件中保存,但会被VM丢弃;
    • RUNTIME,注解将在编译的class文件中保存,在VM运行期也会被保留,因而可以通过反射来读取此类注解信息;
  • @Documented,将此注解包含在Javadoc中;
  • @Inherited,表示允许子类继承父类的注解;
  • @Repeatable;
  • @Native;

标准注解

  • @Deprecated,将被他标记的元素标记为过时的,当程序使用该元素的时候,编译器会发出警告;
  • @FunctionalInterface
  • @Override,表示当前方法定义将覆盖父类中方法的定义;
  • @SafeVarargs
  • @SuppressWarnings,关闭不当的编译器警告信息;

注解元素

注解元素可用的类型如下

  • 所有的基本类型(int,float, boolean等)
  • String
  • Class
  • enum
  • Annotation(说明注解可以嵌套)
  • 以上类型的数组

默认值限制

  • 元素不能有不确定的值,要么具有默认值,要么在使用注解时提供元素值;
  • 对于非基本类型元素,无论是在源代码中声明或是在注解接口中定义默认值,其值都不能为null(妥协的方法就是利用一些特殊值来表示某些元素的却是状态);

使用

下面的代码演示如何使用上面定义的Test注解,@Test注解并不作什么操作(标记注解),但编译器必须能在对应路径找到它的定义。

下面演示标记注解的使用:

package com.rainmonth.annotation;

/**
 * Created by RandyZhang on 2017/9/27.
 */
public class UseTestAnnotation {

    public void execute() {

    }

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

接下来演示一般注解的使用:

package com.rainmonth.annotation;

import java.util.List;

/**
 * Created by RandyZhang on 2017/9/27.
 */
public class PasswordUtils {
    @UseCase(id = 47, descriptions = "Password must contain at least on numeric")
    public boolean validatePassword(String password) {
        return (password.matches("\\w*\\d\\w*"));
    }

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

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

}

编写注解处理器

注解处理器,就是用来读取注解的工具,可以通过JAVA SE5中扩展的反射机制API和其提供的外部工具APT来编写注解处理器。

使用反射

下面利用Java的反射来编写一个简单的注解处理器,我们会读取PasswordUtils,找到其中的@UseCase标记,然后判断提供的一组id值中,哪些址已经找到了对应的用例,哪些值对应的用例缺失:

package com.rainmonth.annotation;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by RandyZhang on 2017/9/28.
 */
public class UserCaseTracker {

    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() + " " +
                        uc.descriptions());
                useCases.remove(new Integer(uc.id()));
            }
        }
        for(int i : useCases) {
            System.out.println("Warning: Missing use case, case id=" + i);
        }
    }

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

该示例先使用getDeclaredMethods()来遍历PasswordUtils中的方法,然后利用getAnnotation()方法来获取使用了UseCase注解的方法,再做相应输出处理,很简单。

利用注解生成外部文件

这里以通过为JavaBean对象添加注解来自动生成创建存储该JavaBean对象的数据表脚本(sql语句)为例,来描述如何利用注解生成外部文件的。

首先是注解的定义:

DBTable.java

package com.rainmonth.annotation.database;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/28.
 */

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

JavaBean域相关注解

SQInteger.java

package com.rainmonth.annotation.database;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/28.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {

    String name() default "";
    // 注解嵌套
    Constraints constraints() default @Constraints;
}

SQLString.java

package com.rainmonth.annotation.database;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/28.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    // String的长度
    int value() default 0;

    String name() default "";
    // 注解嵌套
    Constraints constraints() default @Constraints;
}

Constraints.java

package com.rainmonth.annotation.database;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by RandyZhang on 2017/9/28.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

定义一个JavaBean,并使用上面定义的注解对它的域进行注解

package com.rainmonth.annotation.database;

/**
 * Created by RandyZhang on 2017/9/28.
 */
@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 getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Integer getAge() {
        return age;
    }

    public String getHandle() {
        return handle;
    }

    public String toString() {
        return handle;
    }
}

注意,注解中的元素一般是采用name-value的形式来赋值的,但由于SQLString的元素中,有一个value元素,所以就可以只写值而不写name(必须定义value这个元素才能这样写。

实现注解处理器,即利用上面定义好的注解和JavaBean,来生成创建数据表的sql命令。代码如下:

TableCreator.java

package com.rainmonth.annotation.database;

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

/**
 * 创建表的命令
 * Created by RandyZhang on 2017/9/28.
 */
public class TableCreator {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("arguments: annotated classes");
            System.exit(0);
        }

        for (String className : args) {
            try {
                Class<?> cl = Class.forName(className);
                DBTable dbTable = cl.getAnnotation(DBTable.class);
                if (dbTable == null) {
                    System.out.println("No DBTable annotation in class " + className);
                    continue;
                }
                String tableName = dbTable.name();
                if (tableName.length() < 0) {
                    tableName = cl.getName().toUpperCase();
                }
                List<String> columnDefList = new ArrayList<>();
                for (Field field : cl.getDeclaredFields()) {
                    String columnName;
                    Annotation[] ann = field.getDeclaredAnnotations();
                    if (ann.length < 1) {
                        continue;
                    }
                    if (ann[0] instanceof SQLInteger) {
                        SQLInteger sInt = (SQLInteger) ann[0];
                        if (sInt.name().length() < 1) {
                            columnName = field.getName().toUpperCase();
                        } else {
                            columnName = sInt.name();
                        }
                        columnDefList.add(columnName + " INT" + getConstraints(sInt.constraints()));
                    }

                    if (ann[0] instanceof SQLString) {
                        SQLString sString = (SQLString) ann[0];
                        if (sString.name().length() < 1) {
                            columnName = field.getName().toUpperCase();
                        } else {
                            columnName = sString.name();
                        }
                        columnDefList.add(columnName + " VARCHAR(" + sString.value() + ")"
                                + getConstraints(sString.constraints()));
                    }
                }
                StringBuilder createCommand = new StringBuilder("CREATE TABLE " +
                        tableName + "(");
                for (String columnDef : columnDefList) {
                    createCommand.append("\n    ").append(columnDef).append(",");
                }
                String tableCreate =
                        createCommand.substring(0, createCommand.length() - 1) + ");";
                System.out.println("Table Creation SQL for " + className + " is:\n" +
                        tableCreate);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    private static String getConstraints(Constraints con) {
        String constrains = "";
        if (!con.allowNull()) {
            constrains += " NOT NULL";
        }
        if (con.primaryKey()) {
            constrains += " PRIMARY KEY";
        }
        if (con.unique()) {
            constrains += " UNIQUE";
        }
        return constrains;
    }
}

上述main函数在运行时需要传递一个参数,即Member的具体路径(在Idea这个IDE具体操作为找到要运行的Main函数,在Configuration选项卡下面的Program arguments中添加如下参数:

com.rainmonth.annotation.database.Member

然后运行即可。会看到如下输出:

Table Creation SQL for com.rainmonth.annotation.database.Member is:
CREATE TABLE MEMBER(
    FIRSTNAME VARCHAR(30),
    LASTNAME VARCHAR(50),
    AGE INT,
    HANDLE VARCHAR(30) PRIMARY KEY);

Process finished with exit code 0
使用apt处理注解

由于Java 8 已经把apt即相关的Api移除了,这里只做大致的描述。使用apt的一般步骤:

  1. 定义要被处理的注解;

  2. 定义一个Processor(继承自AnnotationProcessor),处理注解的核心工作都在这;

  3. 指明一个工厂类(继承自AnnotationProcessorFactory),该工厂类用来为apt工具指定一个正确的处理器(即2中定义的Processor的实例)

  4. 执行apt命令开始处理注解

    apt -factory [工厂类][含有要处理注解的java文件]

总结

首先注解的引入将我们的源代码和文档紧密的连接起来,可在编译期对代码进行检查,并使代码变的简洁干净易读,这就是为什么在开源库中注解使用越来越广泛的原因了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值