Java中的注解(annotation)

注解的作用

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

注解在一定程度上是把元数据和源代码文件结合在一起,而不是保存在外部文档中。它可以使得我们能够让编译器来测试和验证格式,存储有关程序的额外信息。注解还可以生成描述符文件,甚至或是新的类定义,并且有助于减轻编写“样板”代码的负担。

注解是在实际的源代码级别保存所有的信息,而不是某种注释性的文字,这使得代码更整洁且便于维护。

在java.lang中包含三种内置注解:

  • @Override,表示当前方法定义将覆盖超类中的方法,如果签名出错会收到编译器的提示;
  • @Deprecated,表示过期或不推荐的方法;
  • @SuppressWarnings,关闭不当的编译器警告信息。

Java还另外提供了四种注解,专门负责新注解的创建。当创建描述符性质的类或者接口时,并且其中包含重复性工作,可以考虑使用注解来简化与自动化该过程。

定义注解

定义注解和定义接口很像,它会需要一些元注解(meta-annotation),如@Target和@Retention。使用@Target可以表明注解可应用于什么地方,@Retention表明用在哪个级别。

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

元注解

Java目前只内置了三种标准注解(@Override、@Deprecated、@SuppressWarnings)和四种元注解。

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

注解处理器

如果没有用来读取注解的工具,那么注解也就和注释没有任何区别,在使用注解的过程中,很重要的一部分就是创建与使用注解工具。

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> prevPasswords, String password) {
		return !prevPasswords.contains(password);
	}
}
public class UseClassTracker {
	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.description());
				useCases.remove(new Integer(uc.id()));
			}
		}
		for (int i : useCases) {
			System.out.println("Warning: 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);
	}
}

UseClassTracker类用于读取PasswordUtils类,并使用反射机制查找@UseCase标志。其中使用了两个反射方法getDeclaredMethods()和getAnnotation(),它们都属于AnnotatedElement接口(Class,Method与Field等类都实现了该接口)。getAnnotation()返回了指定类型的注解对象,这里是UseCase,如果被注解的方法上没有该类型的注解,则会返回null。然后我们通过调用id()和description()方法从返回的UseCase对象中提取元素的值。encryptPassword()方法在注解的时候没有指定description的值,因此它会使用默认值“no description”。

注解元素

注解元素的所有可用类型包括:

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

例如,@UseCase使用了int类型的id,String类型的description。如果你使用了其他类型,那么会编译出错。注意这里也不允许使用包装类型。

默认值限制

编译器要求,元素不能有不确定的值,也就是说元素必须具有默认值,或者在使用注解时提供元素的值。另外,对于非基本类型的元素,无论在源代码中声明时,或者在注解接口中定义默认值时,都不能以null为其值。因此对于某些表示元素存在或者缺失的状态时,我们只能定义一些特殊的值,如空字符串或负数来表示。

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

注解的应用-ORM工具

生成外部文件

在许多对象/关系映射工具(如hibernate)中,一般会需要提供除了java bean之外的XML描述文件,而这些文件脱离于源代码之外,这经常会导致同步问题。同时它也要求为项目工作的程序员,必须同时知道如何编写java程序,以及如何编辑描述文件。然而,如果使用注解,就可以将所有信息保存在java bean源文件中。

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

@DBTable中有name()元素,该注解为元素处理器提供创建数据库的表名。

// Constraints
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
	boolean primaryKey() default false;
	boolean allowNull() default true;
	boolean unique() default false;
}
//SQLString
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
	int value() default 0;
	String name() default "";
	Constraints constraints() default @Constraints;
}
//SQLInteger
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints;
}

@Constraints表示数据库能提供的部分约束。@SQLString和@SQLInteger提供两种基本数据类型的约束。这两种基本数据类型都提供了约束默认值@Constraints,它使用了所有元素都是默认值的@Constraints注解。如果要使用unique()元素为true的@Constraints注解,则需要定义如下类型的注解:

public @interface Uniqueness {
	Constraints constraints() 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 numberCount;
	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; }
}

@DBTable定义了表名MEMBER。firstName和lastName被注解为@SQLString,并且限制长度分别为30和50。这个注解有个特点:1.它们都使用了嵌套的@Constraints注解的默认值;2.它们都是用了快捷方式。所谓的快捷方式是指,在注解中定义了value()的元素,并且在应用注解的时候value()是唯一需要复制的元素,则可以直接为它赋值,而不需要使用key=value的形式,这可以应用于任意的合法类型的元素上。

变通之道

以上功能只是实现方式的一种,还有其他的方式实现类似功能:

  • 使用单一注解类@TableColumn,它带有一个enum元素,它可以枚举STRING、INTEGER、以及FLOAT等实例,这样就可以不用定义许多@interface来管理数据类型,不过这样会使得无法精确控制每种类型,如控制长度或者精度等。
  • 使用String元素描述实际的SQL类型,如VARCHAR(30)或INTEGER,这样就可以灵活的使用SQL中的类型,但是这么做会使得Java类和数据库相互绑定,从而为数据库迁移造成麻烦。
  • 使用多种注解来注释同一个域,如使用@Constants和相应的SQL类型(如@SQLInteger),这种方式代码可能会比较多,但是编译器允许这样做,需要注意的是,使用多个注解时,同一个注解不能重复使用。

最后注解是不支持继承的,所以不能使用extends来继承某个@interface。

实现数据库注解处理器

public class TableCreator {
	public static void main(String[] args) throws Exception {
		String names[] = { "Member" };
		for (String className : names) {
			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 == null || anns.length < 1) {
					continue;// 不是db table列
				}
				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\t" + 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;
	}
}/*output:
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
	FIRSTNAME VARCHAR(30));
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
	FIRSTNAME VARCHAR(30),
	LASTNAME VARCHAR(50));
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
	FIRSTNAME VARCHAR(30),
	LASTNAME VARCHAR(50),
	AGE INT);
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
	FIRSTNAME VARCHAR(30),
	LASTNAME VARCHAR(50),
	AGE INT,
	HANDLE VARCHAR(30) PRIMARY KEY );
*///:~

以上代码通过forName()方法加载每个类,然后用getAnnotation(DBTable.class)检查该类是否带有@DBTable注解,如果有,则将发现的表名保存下来,然后读取这个类的所有域,并用getDeclaredAnnotation()进行检查,最后用instanceof操作符判断这些注解的具体类型。由于没有继承机制,要获得近似多态的行为只能使用getDeclaredAnnotation()方法。

从以上可以看出,注解可以在源码中提供额外信息,针对这些信息我们可以编写注解处理器来处理它们,从而实现各种自动化功能(比如JUnit自动测试,ORM自动映射对象和实体等)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值