Java 注解


近日在项目中遇到了 Java 的原生态的注解,但是忘记了用法,于是特意翻看了 Java 编程思想,在此总结一下.

注解概述

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

注解是众多引入到 JavaSE5 中的重要的语言变化之一.他们可以提供用来完整的描述程序所需的信息,而这些信息是无法用 Java 来表达的.因此,注解使得我们能够以将由编译器来测试和验证的格式.存储有关程序的额外信息.注解可以用来生成描述符文件,甚至或是新的类定义.并且有助于减轻编写样板代码的负担.通过使用注解,我们可以将这些元数据保存在 Java 源代码中,并利用 annotation API 为自己的注解构造处理工具,注解的还有以下的优点:更加干净易读的代码以及编译类期类型检查等.虽然 Java SE5 预先定义了一些元数据,但是一般来说主要还是需要我们自己添加注解,按照自己的方式使用它们.

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

  • @Override 表示当前的方法定义将覆盖超类中的方法,如果出现错误,编译器就会提示错误.
  • @Deprecated 如果使用了注解为它的元素,那么编译器就会发出警告信息.
  • @SuppressWarnings 关闭不当的编译器警告信息.

此外 Java 还提供了四种注解,专门负责新注解的创建.
注解是真正的语级的概念,一旦构造出来,就享有编译期的类型检查保护.注解(annotation)是在实际的源代码级别保存所有的信息,而不是某种注释性的文字(comment),这使得代码更整洁,且便于维护,通过使用扩展的 annotation API,或外部的字节码工具类库,将拥有对源代码以及字节码强大的检查与操作能力.

基本语法

举一个例子,使用 @Test 对 testExecute() 方法进行注解,该注解本身并不做任何事情.被注解的方法与其他的方法没有区别,在这个例子中,注解 @Test 可以与任何的修饰符共同作用于方法,从语法的角度来看,注解的使用的方式几乎与修饰符的使用一模一样.

package annotations;

public class Testable {

	public void execute() {
		System.out.println("Executing...");
	}
	
	@Test void testExecute() {
		execute();
	}
}

定义注解

package annotations;

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

可以看出除了 @ 符号之外.@Test 的定义很像一个空的接口.

同时在定义注解的时候,会用到一些元注解(meta-annotation),如 @Target 和 @Retention. @Target 用来定义你的注解将应用在什么地方(比如是一个方法或者是一个域).@Retention 用来定义该注解在哪一个级别可用.

在注解当中一般都会包含一些元素以表示某些值,没有元素的注解被称为标记注解(marker annotation) 如举例的 @Test

package annotations;

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 进行类型检查,description 元素有一个 default 值,如果在注解某个方法时没有给出 description 的值,则该注解的处理器就会使用定义的默认值.

package annotations;

import java.util.List;

public class PasswordUtils {

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

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

	@UserCase(id = 49, description = "New password can't equal priviously used ones")
	public boolean checkForNewPassword(List<String> prevPasswords, String password) {
		return !prevPasswords.contains(password);
	}

}

注解的元素在使用的时候表现为键值对额形式,并需要将 @UserCase 声明之后的括号内.在 encryptPasswor() 方法的注解中,并没有给出 description 元素的值,因此处理器会使用他的默认值.

元注解

四种元注解:

	@Target		  		表示注解可以用在什么地方	
	ElementType 参数   	CONSTRUCTOR 构造器的声明
						FIELD 域声明(包括 enum 实例
						LOCAL_VARLABLE 局部变量声明
						METAOD 方法声明
						PACKAGE 包声明
						PARAMETER 参数声明
						TYPE 类,接口(包括注解类型)或者 enum 声明
	-------------------------------------------------------------
	@Retention			表示需要在什么级别保存该注解信息
	RetentionPolicy参数	SOURCE 注解将被编译器丢弃
						CLASS 注解在 class 文件中可用,但是会被 JVM 丢弃
						RUNTIME JVM在运行期也会保留该注解,因此可以通过反射机制读取注解的信息.
	-------------------------------------------------------------
	@Documented 		将此注解包含在 javadoc 中
	-------------------------------------------------------------
	@Inherited			允许子类继承父类中的注解

编写注解处理器

大多数的时候都是我们自己定义注解,并且需要自己编写处理期来处理他们.
Java SE5 扩展了反射机制的API,可以帮助我们构造这类工具,同时还提供了一个外部工具 apt 帮助解析带有注解的 Java 源代码.

package annotations;

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

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

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

这个程序用到了两个反射的方法: getDeclaredMethods() 和 getAnnotation(),它们属于 AnnotatedElement 接口(Class, Method 和 FieId 等类都实现了该方接口)

getAnnotation() 方法返回指定类型的注解对象,这里指的的 UserCase().如果备注的方法上没有该类型的注解,则返回 null 值. 然后我们可以通过调用 id() 和 description() 方法从返回的 UserCase 对象中提取元素的值,其中 encyrptPassword() 方法在注解的时候没有指定 description 的值,因此处理器在处理它对应的注解的时候获取到的是默认的值.

执行结果:
在这里插入图片描述

注解元素:

标签 @UserCase 由 UserCase.java 定义,其中包括 int 元素的 id,以及一个 String 的 description.

注解元素的可用的类型如下:

  • 所有的基本类型(int, float, boolean 等)
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组
    如果使用其他的类型,那么编译器就会进行报错.

注意:也不允许使用任何的包装类型,但由于有自动拆装箱的的存在,这个限制不大.此外注解也可以作为元素的类型,也就是说注解可以进行嵌套.

默认值限制

  • 元素不能有不确定的值,也就是元素必须有一个默认的值,要么使用注解的提供给元素的值.
  • 对于非基本类型的元素,无论是在源代码声明的时候,还是在注解接口中定义默认值时,都不可以以 null 作为其值.
package annotations;

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 SimulatingNull {

	public int id() default -1;
	public String description() default " ";
}

对此我们可以自己定义一些默认的值,用来表示某个元素不存在等信息.

生成外部文件

假设我们希望提供一些基本的对象/关系映射功能,能够自动生成数据库表,用以存储 javaBean 对象,我们可以选择 XML 描述文件,指明类的名称,每个成员以及数据库映射的相关信息.

如果我们使用注解的话,就可以将所有的信息都保存在 javaBean 源文件中,为此我们会需要一些新的注解,用于定义与 Bean 关联的数据库表的名字,以及与 Bean 属性关联的列名字和 SQL 类型.

package annotations.database;

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

@Target(ElementType.TYPE) // 只适用于类
@Retention(RetentionPolicy.RUNTIME)
public @interface DBtable {

	public String name() default " ";
}

在 @Target 注解中指定的每一个 ElementType 就是一个约束,编译器会把这个自定义的注解应用到指定的该类型,可以只指定 enum ElementType 中的某一个值,或者以逗号分隔的形式指定多个值,如果想将注解应用到所有的 ElementType,那么可以省去 @Target 元注解(这种做法很少见).

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

为修饰 JavaBean 域准备的注解.

package annotations.database;

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

@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {

	boolean primaryKey() default false;

	boolean allowNull() default true;
	
	boolean unique() default false;
}

package annotations.database;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {

	int value() default 0;

	String name() default "";

	Constraints constraints() default @Constraints;
}

package annotations.database;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {

	String name() default "";

	Constraints constraints() default @Constraints;
}

注解处理器通过 @Constraints 注解提取出数据库表的元数据,对于数据库所能提供的所有约束而言, @Constraints 注解只表示了它的一个很小的子集,不过它表达的思想很清楚. primaryKey() allowNull() 和 unique() 元素提供了默认值,使用该注解,就无需输入太多的东西.

另外两个 @interface 定义的是 SQL 类型,

这些 SQL 类型具有 name() 和 constraints() 元素,后者利用了嵌套注解的功能,将 column 类型的数据库约束信息嵌入其中.
注意: constraints() 元素的默认值是 @Constrains,由于在 @Constraints 注解类型之后,没有在括号中指明 @Constraints 中的元素的值,因此, constraints() 元素的默认值实际上就是一个所有元素都为 @Constraints 注解,如果要嵌入 @Constraints 注解中的 unique() 元素为 true,并以此为 constraints() 元素的默认值,则需要这样定义该元素:

package annotations.database;

public @interface Uniqueness {

	Constraints constraints() default @Constraints(unique = true);
}

应用这些注解:

package annotations.database;

@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 Integer getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "Member [handle=" + handle + "]";
	}

}

类的注解 @DBTable 给定了值 MEMBER,它将会用来作为表的名字. Bean 的属性 firstName 和 lastName,都被注解为 @SQLString 类型,并且其元素值分别为 30 和 50.这些注解有两个有趣的地方: 第一,他们都使用了嵌入式的 @Constraints 注解的默认值;第二,它们都使用了快捷方式(如果我们注解中定义了名为 value 的元素,并且在应用该注解的时候,如果该元素的是唯一需要赋值的一个元素,那么此时无需使用键–值对的这样的语法,只需要在括号内给出 value 元素的值即可.这可以应用于任何合法的类型的元素,但这样也限制了我们必须将此元素命名为 value, 不过在上面的例子中,这不但使得语义更清晰,而且这样的注解语句也更易于理解 @SQLString(30) 处理器将在创建表的时候使用该值设置 SQL 列的大小).

注意 : 默认值的语法虽然很灵巧,但它很快就变的复杂起来.以 handle 域的注解为例.这是一个 @SQLString 注解,同时该域将成为表的主键,因此在嵌入的 @Constraints 注解的中,必须对 pimaryKey 元素进行设定.这时事情就变得麻烦了,就必须不得不使用很长很长的键值对的形式,重写元素名和 @interface 的名字.与此同时,由于有特殊命名的 value 元素已经不再是唯一需要赋值的元素,所以也就不能在使用快捷方式为其赋值了,最终结果就不会在那么的清晰易懂了.

变通之道: 可以使用多种不同的方式来定义自己的注解,以上实现的例子的功能.例如,可以使用一个单一的注解类 @TableColumn,它带有一个 enum 元素,该枚举类定义了 STRING, INTEGER 以及 FLOAT等枚举实例.这就消除了每个 SQL 类型都需要一个 @interface 定义的负担,不过也就使得以额外的信息修饰 SQL 类型的需求变得不可能,而这些额外的信息,例如长度或者精度等,可能是非常有必要的需求.

我们也可以使用 String 元素来描述实际的 SQL 类型,比如 VARCHAR(30) 或者 INTEGER.这使得我们可以修饰 SQL 类型.但是,它同时也将 java 类型到 SQL 类型的映射绑定在了一起,但这样在更换数据库就必须修改代码并且重新编译.

第三种可行的方案是同时使用两个注解类型来注解一个域, @Constraints 和相应的 SQL 类型(例如 @SQLIntger),这样代码可能会有点乱,不过编译器是允许程序员对一个目标同时使用多个注解,但同一个注解不能重复使用.

注解不支持继承

不能使用关键字 extends 来继承某个 @interface,如果可以定义一个 @TableColumu 注解(参考前面的建议),同时在其中嵌套一个 @SQLType 类型额注解,啊么这将是一个优雅的设计.按照这种方式,我们可以继承 @SQLType,从而创建出各种 SQL 类型,例如 @SQLInteger 和 @SQLString 等.

实现处理器

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

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 Exception {
		if (args.length < 1) {
			System.out.println("参数: 有注解的级别.");
			System.exit(0);
		}

		for (String className : args) {
			Class<?> cl = Class.forName(className);
			DBtable dbTable = cl.getAnnotation(DBtable.class);
			if (dbTable == null) {
				System.out.println("没有 DBTable 注解的类: " + className);
				continue;
			}

			String tableName = dbTable.name();
			// 判断这个名字是不是空的,有没有使用这个类名.
			if (tableName.length() < 1) {
				tableName = cl.getName().toUpperCase(); // 转换成大写
				List<String> columnDefs = new ArrayList<String>();
				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.primaryKey()) {
			constraints += "NOT NULL";
		} else if (con.primaryKey()) {
			constraints += "PRIMARY KEY";
		} else if (con.unique()) {
			constraints += "UNIQUE";
		}
		return constraints;
	}

}

其中 main 方法会处理命令行传入的每一个类名.使用 forName() 方法加载每一个类,并使用 getAnnotation(DBTable.class) 检查该类是否带有 @DBTable 注解.如果有,就将发现的表名保存下来,然后读取这个类的所有域,并用 getDeclareAnnotation() 进行检查.该方法返回一个包含一个域上的所有注解的数组.最后用 instanceof 操作符来判断这些注解是否是 @SQLInteger 或者 @SQLString 类型,如果是的,在对应的处理块中将构造出来的相应的 column 名的字符串片段.

注意: 由于注解没有继承机制,所以要获得近似多态的行为,使用 getDeclaredAnnotation() 是唯一的办法.嵌套中的 @Constraint 注解被传递给 getConstraints() 方法,由它负责构造一个包含 SQL 约束的 String 对象.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值