什么是注解 注解(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 内置的注解不能满足我们的使用需求,就要考虑引入第三方的注解,或是定义自己的注解类型。 自定义注解还是比较简单的。大致步骤如下:
首先,确定注解的名称。
考虑是否需要属性,如果需要配置属性,则进一步考虑属性的类型,以及默认值。
指定 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
。
确认该注解的 Retention ,我理解为作用范围(或者是生命周期),也就是要保留多久。可选择的范围有:
RetentionPolicy.SOURCE
:编译器会丢弃的注解,也就是在 class 文件中不会保留它们的信息。
RetentionPolicy.CLASS
:编译器会将注解保留在 class 文件中,但在运行时 虚拟机不会加载它们。这是默认的行为。
RetentionPolicy.RUNTIME
:编译器会将注解保留在 class 文件中,并且在运行时虚拟机也会加载。因此可以通过反射 API 读取它们。
最后,确认是否需要将注解的信息加入到 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; }
其对应的数据库表是:
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; }
这样,元数据就有了,接下来就是如何去读取并利用它们:
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 { 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); 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++; } 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 文档,搞清楚每个注解的含义,就可以了。
在有些情况下(例如,开发组件或框架),我们可能需要开发自己的注解。这时候不妨参考一下本文上述内容,相信这也难不倒我们。
虽然现在非常流行使用注解,但是,最终是选择使用注解还是配置文件,这不是由技术驱动的,而是要取决于我们的团队和具体的项目。