Java漫游笔记-13-注解

什么是注解

注解(Annotation,有的地方也翻译为注释,但是我觉得这样会和我们一般理解的注释混淆) 是 JDK 1.5 引入的新特性。
在 API 文档中有写到:注解是一种接口。其实,这并不好理解。但我们可以从它的声明方式中看出一二:

1
2
3
public @interface AnnotationName {
String value() default ""
}

实际上,我更喜欢这样理解:注解是一种元数据工具,或者说是我们给代码打的一个标记。

注解的作用

如今,在基于 Java 的应用程序开发中,注解已经被广泛使用。
在我看来,注解的主要好处在于:提供编译器检查和代码分析功能。
JDK 内置了一些注解,比较常见的有 java.lang 包中的:

  • @Deprecated :表明这个元素已经被弃用,如果使用它,编译器会发出警告。
  • @Override :被标记的方法必须是父类或接口中已定义的方法,否则编译器会生成一条错误消息。
  • @SuppressWarnings :可以用来消除显示指定的编译器警告。

这类注解,我们主要用它们来执行一些的编译器的检查。

而另外一类注解,我们则是用它们来给代码添加一些信息,以便实现额外的功能。例如:

  • javax.annotation 中的:@Resource
  • JPA 中的:@Entity@Table(name = "user")
  • Spring 中的:@Component@RequestMapping(value = "/index", method = RequestMethod.GET)

就像前面所说的,注解只是一个标记,它本身并不会做任何事情,往往需要借助其它帮手才能发挥作用。注解可以选择只在源代码中有效,也可以供编译器使用,或者是在运行期再通过程序读取。
我们要做的,就是找到它,分析它,然后再来决定做点什么。

自定义注解

如果 JDK 内置的注解不能满足我们的使用需求,就要考虑引入第三方的注解,或是定义自己的注解类型。
自定义注解还是比较简单的。大致步骤如下:

  1. 首先,确定注解的名称。
  2. 考虑是否需要属性,如果需要配置属性,则进一步考虑属性的类型,以及默认值。
  3. 指定 Target ,也就是确定该注解适用于哪种程序元素(如果不指定,则可以用于任意元素)。可选择的元素有:
    • ElementType.TYPE :类、接口(包括注解)或枚举声明。
    • ElementType.FIELD :字段(包括枚举常量)声明。
    • ElementType.METHOD :方法声明。
    • ElementType.PARAMETER :方法参数声明。
    • ElementType.CONSTRUCTOR :构造方法声明。
    • ElementType.LOCAL_VARIABLE :局部变量声明。
    • ElementType.ANNOTATION_TYPE :注解声明。
    • ElementType.PACKAGE :包声明。
    • ElementType.TYPE_PARAMETER :类型参数声明(例如:class Ccc<@Xxx T> {}),JDK 1.8 新增。
    • ElementType.TYPE_USE :类型使用(例如:new @Xxx Object() 或者是 void method() throws @Xxx Exception {}),JDK 1.8 新增。TYPE_USE 包含 TYPE_PARAMETER
  4. 确认该注解的 Retention ,我理解为作用范围(或者是生命周期),也就是要保留多久。可选择的范围有:
    • RetentionPolicy.SOURCE :编译器会丢弃的注解,也就是在 class 文件中不会保留它们的信息。
    • RetentionPolicy.CLASS :编译器会将注解保留在 class 文件中,但在运行时 虚拟机不会加载它们。这是默认的行为。
    • RetentionPolicy.RUNTIME :编译器会将注解保留在 class 文件中,并且在运行时虚拟机也会加载。因此可以通过反射 API 读取它们。
  5. 最后,确认是否需要将注解的信息加入到 Java Doc 中。如果需要加上 @Documented 即可。

具体可以参考后面的代码。

需要注意的是:
注解中属性声明实际上就是方法声明。
但是,与普通接口也略有不同,注解中的方法声明不能有任何参数,不能使用泛型,也不能抛出异常。

让注解发挥作用

单单定义注解是没有什么实际意义的,它只不过是给我们的代码添加了一点点元数据而已。
下面我将通过一个小型案例来演示注解的使用。

假设我们正在开发一个简单的 ORM 组件,其中涉及自动生成 SQL 语句的功能。
那么,我们首先要做的就是,弄清楚对象到数据库表之间的映射关系。
例如,我们定义了一个实体类:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Date;

public class User {

private long id;
private String name;
private String password;
private String email;
private Date createTime;

// getter & setter
}

其对应的数据库表是:

id username password email create_time
1 franky 6d17840a franky@codinglike.com 2015-08-01 15:06:56

二者映射关系为:

类名/表名 User user
属性名/字段名 id id
属性名/字段名 name username
属性名/字段名 password password
属性名/字段名 email email
属性名/字段名 createTime create_time

这种映射关系,就是一种元数据。
在没有注解之前,我们可能考虑使用配置文件(例如:xml 文件、properties 文件)来存储这种信息。例如:

1
2
3
4
5
6
7
<class name="User" table="user">
<id name="id" column="id" />
<property name="name" column="username" />
<property name="password" column="password" />
<property name="email" column="email" />
<property name="createTime" column="create_time" />
</class>

但是,现在,我们更倾向于使用注解来解决这个问题。
首先,我们需要定义一些注解,用来标记实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.annotation.Documented;
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)
@Documented
public @interface Table {

String name();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.annotation.Documented;
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)
@Documented
public @interface Column {

String name();

}
1
2
3
4
5
6
7
8
9
10
11
import java.lang.annotation.Documented;
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)
@Documented
public @interface Id {
}

标记后的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Table(name = "user")
public class User {

@Id
private long id;

@Column(name = "username")
private String name;

private String password;

private String email;

@Column(name = "create_time")
private Date createTime;

// getter & setter
}

这样,元数据就有了,接下来就是如何去读取并利用它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.lang.reflect.Field;

public class SqlBuilder {

/** 默认主键列名为 id */
private static final String DEFAULT_ID = "id";

/**
* 构建查询一个实体对象的 SELECT 语句
*
* @param clazz
* @return SQL 语句
*/

public static String buildSelect(Class<?> clazz) {
StringBuilder sql = new StringBuilder("SELECT ");
// 拼接查询条件
StringBuilder id = new StringBuilder(" FROM ").append(
getTableName(clazz)).append(" WHERE 1 = 1");
String fieldName = null;
String columnName = null;
int idCount = 0;
int columnCount = 0;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
fieldName = field.getName();
columnName = getColumnName(field);
// 判断属性是否标注为 @Id
boolean isId = field.isAnnotationPresent(Id.class);
if (isId) {
id.append(" AND ").append(columnName).append(" = ?");
idCount++;
}
if (columnCount == 0) {
sql.append(columnName);
} else {
sql.append(", ").append(columnName);
}
columnCount++;
}
// 如果没有标注 Id ,则使用默认的 id 列名
if (idCount == 0) {
id.append(" AND ").append(DEFAULT_ID).append(" = :")
.append(DEFAULT_ID);
}
return sql.append(id).toString();
}

/**
* 判断实体属性是否标注了列名,如果已标注,返回标注的名称,否则直接返回属性名。
*
* @param field
* 实体类属性
* @return 列名
*/

private static String getColumnName(Field field) {
String columnName = null;
boolean isColumn = field.isAnnotationPresent(Column.class);
if (isColumn) {
Column column = field.getAnnotation(Column.class);
columnName = column.name();
} else {
columnName = field.getName();
}
return columnName;
}

/**
* 判断实体是否标注了表名,如果已标注,返回标注的名称,否则直接返回类名。
*
* @param clazz
* 实体类
* @return 表名
*/

private static String getTableName(Class<?> clazz) {
String tableName = null;
boolean isTable = clazz.isAnnotationPresent(Table.class);
if (isTable) {
Table table = clazz.getAnnotation(Table.class);
tableName = table.name();
} else {
tableName = clazz.getSimpleName();
}
return tableName;
}

}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;

public class SqlBuilderTest {

@Test
public void testBuildSelectById() {
String sql = SqlBuilder.buildSelectById(User.class);
assertThat(sql)
.isEqualTo(
"SELECT id, username, password, email, create_time FROM user WHERE 1 = 1 AND id = ?");
}

}

从上面的代码可以看到,使用注解来表示元数据,可以精简配置。这就是它相对于配置文件的优势之一。除此之外,在使用注解的时候编译器还能够进行校验,减少出错,并且,由于注解是和代码放在一起的,这也有助于增强程序的内聚性,在一些情况更便于理解和维护。
但是,从另外一个角度来看,这又成了它的缺点,一旦我们修改了注解,那么,就必须重新编译代码,同时,注解相对于配置文件往往更加分散,不便于集中式的管理。例如,数据源这样通用的配置(可能还会经常改动),有的人就更喜欢使用配置文件。
因此,究竟采用那种方式,还是具体情况具体分析吧。

Java Doc 注释

在我们编写 Java Doc 注释的时候,也会看到一些跟注解长得很像的家伙,例如:

  • @author
  • @param
  • @return
  • @throws
  • @deprecated
  • @see
  • @since

但是它们并不是注解,这些只是 Java Doc 工具内置的一些标签而已。

总结

  • 一般来说,在实际的开发中,我们更多的只是去使用注解,这时候只需要认真阅读相关的 API 文档,搞清楚每个注解的含义,就可以了。
  • 在有些情况下(例如,开发组件或框架),我们可能需要开发自己的注解。这时候不妨参考一下本文上述内容,相信这也难不倒我们。
  • 虽然现在非常流行使用注解,但是,最终是选择使用注解还是配置文件,这不是由技术驱动的,而是要取决于我们的团队和具体的项目。