Java注解
在面对大型的Java EE项目中,常常通过注解进行地址映射,对象注入等操作,因此有必要去了解一下注解的相关的知识。
注解是 java5 引入众多语言变化之一,可以用于表达在java中无法表达且你需要完整表述程序所需的信息。注解可以让我们可以以编译器验证的格式存储程序的额外信息。通过使用注解,可以将元数据保存在Java源代码中,且具备以下优势:
- 简单易读
- 编译器类型检查
- 使用annotation API为自己的注解构造处理工具。
JAVA 5引入的注解:
@Override
:表示当前的方法定义将覆盖基类的方法。(若不小心拼写错误,或者方法签名被错误拼写,编译器会发出错误提示)@Deprecated
:如果使用该注解的元素,编译器会报错。(表示该元素已经被弃用)@SuppressWarnings:
关闭不当的编译器警告信息。@SafeVarargs
:在JAVA7中加入用于禁止对具有泛型的varargs
参数的方法或构造函数的调用发出警告@FunctionalInterface
:在JAVA8中加入用于表示类型声明为函数式接口。
一个注解的定义:
注解的定义类似于接口,与其他的java接口一样,同样会被编译成class文件。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
注解的定义也需要一些元注解:
@Target
:定义你的注解可以应用在哪里(方法还是字段)@Retention
:定义了注解在哪里可用(源代码(SOURCE),class文件(CLASS),运行时(RUNTIME))
我们也可以用注解包含一些特定值。不包含任何元素的注解称之为:标记注解(maker annotation),如上面的@Test
。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
id()
与description()
与方法的定义类似。description
带有一个默认值,当你不显式使用时,就会使用默认值。
应用:
public class AnnotationTest {
@UseCase(id = 1,description = "This a Test demo")
public void print(){
System.out.println("Test demo...");
}
@UseCase(id = 2,description = "This a Test demo")
public void print2(){
System.out.println("Test demo...");
}
}
下面的是五种元注解,上面已经讲过了5个标准注解:
@Target
: 表示注解可以用于哪些地方。可能的ElementType
参数:CONSTRUCTOR
:构造器的声明FIELD
:字段声明(包括enum
实例)LOCAL_VARIABLE
:局部变量声明METHOD
:方法声明PACKAGE
:包声明PARAMETER
:参数声明TYPE
:类、接口(包括注解类型)或者enum
声明
@Retention
表示注解信息保存的时长。可选的RetentionPolicy
参数包括:SOURCE
:注解将被编译器丢弃CLASS
:注解在class文件中可用,但是会被VM丢弃。RUNTIME
:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented
:将此注解保存在Javadoc中Inherited
:允许子类继承父类的注解Repeatable
:允许一个注解可以被使用一次或者多次(Java 8)。
注解处理器
根据名字我们就可以知道,这个是用于读取注解的工具。
一个编写的示例:
public class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases,Class<?> c1){
for (Method m : c1.getDeclaredMethods()){
UseCase us = m.getAnnotation(UseCase.class);
if(us != null){
System.out.println("Found Use Case "+us.id() + "\n" +us.description());
useCases.remove(Integer.valueOf(us.id()));
}
useCases.forEach(i-> System.out.println("Missing use case"+i));
}
}
public static void main(String[] args) {
List<Integer> useCases = IntStream.range(1,3).boxed().collect(Collectors.toList());
trackUseCases(useCases,AnnotationTest.class);
}
}
输出:
Found Use Case 1
This a Test demo
Missing use case2
Found Use Case 2
This a Test demo
getDeclaredMethods()
和getAnnotation()
是上面使用的两个反射方法,属于AnnotatedElement
接口(Class
,Method
与Field
类都实现该接口)。getAnnotation()
方法返回指定类型的注解对象。其后调用相应的方法(如id()
)返回值.
注解元素
有以下类型:
- 基本类型(
int
、float
) String
Class
enum
Annotation
- 以上的数组
注意:使用其他类型,编译器会报错
默认值限制
根据规定,元素是不能够有不确定的值。也就是说元素需提供默认值或者显式给出。同时有:任何非基本类型的元素,无论在源代码声明时还是注解接口中定义默认值都不能使用null作为其值。由此可见,通过这个限制使得注解中所有元素都时存在的,并且有相对应的值。那么为了绕开这个约束,可以自行定义一些默认值。
生成外部文件
假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 JavaBean 源文件中。为此你需要一些用于定义数据库表名称、数据库列以及将 SQL 类型映射到属性的注解。
以下注解的定义用于告诉注解处理器创建一个数据库表:
package ink.kilig.annotation;
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 {
String name() default "";
}
package ink.kilig.annotation;
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 ink.kilig.annotation;
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;
}
package ink.kilig.annotation;
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 ink.kilig.annotation;
@DBTable(name = "MEMBER") //提供表名
public class Member {
//前面的注解表示了其被注解为xxx类型,并且提供了默认值,用于表示该列的大小
@SQLString(30) String firstName;
@SQLString(50) String lastName;
@SQLInteger Integer age;
@SQLString(value = 30,constraints = @Constraints(primaryKey = true))
String reference;
@Override
public String toString() {
return reference;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Integer getAge() {
return age;
}
public String getReference() {
return reference;
}
}
因为上面reference其注解写得有些复杂了,所以我们需要有一些替代方案,使得看起来结果清晰易懂。
替代方案
- 可以使用一个单一的注解类,拥有一个
enum
元素,专门定义一些必须的类型。 - 可以使用一个
String
类型来描述实际的SQL
类型 - 建议:将上面的一个注解分解为多个注解同时使用。
注解是不支持继承的
也许你会思考,上面的例子是如何将这些注解和实际的对象绑定到一起的?那么这时候我们就要重新回到上面所说的注解处理器。它就是专门把我们的相对应的注解结合对象转化为对数据库操作的SQL语句。下面是一个例子:
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(
"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(
"No DBTable annotations in class " +
className);
continue;
}
String tableName = dbTable.name();
// If the name is empty, use the Class 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; // Not a db table column
if (anns[0] instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) anns[0];
// Use field name if name not specified
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];
// Use field name if name not specified.
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 + ",");
// 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;
}
}
其输出:
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
REFERENCE VARCHAR(30) PRIMARY KEY);
至此,本文的内容已经结束,如果你还是意犹未尽,请移步参考文章内容,将会有关注解器的一个实现。