浅谈Java之注解

注解 (又被称为 元数据 ) ,是 Java 提供的一种让我们可以为代码添加信息的一种方式。

元数据 : 中介数据,为描述数据的数据,主要是描述数据属性的信息。

通过注解可以有效的减少样板式代码 ,所以注解式开发是现在框架的主流趋势。除此之外,注解还可以进行类型检查 和 注释说明 等作用。

认识注解

Class类 可以看出,对于 JVM 而言,注解是一种特殊的接口,并且所有的注解类型都继承了 Annotation接口

我们知道一些框架需要一些额外信息才能与源代码协同工作。以前一般是配置文件,现在更喜欢使用注解。因为源代码已经提供了一些有用的信息,例如类名,包名等,所以使用注解会有效的减少配置。

元注解

元注解 : Java 中内置了一些元注解,元注解专职负责注释其他注解。

涉及 说明
@Target 目标,指注解的作用位置。
@Retention 保留期,指注解的生命周期。
@Inherited 继承,允许子类继承父类的该注解。只会继承作用在 ElementType.TYPE 的注解。
@Documented 文档,生成 javadoc 时会包含注解信息。
@Repeatable 可重复,表示可多次使用,JDK8新增

@Target

@Target : 表示注解的作用位置,需要设置元素类型集 ElementType[]

ElementType枚举值 说明
ElementType.TYPE 类,接口(包括注释类型)或枚举声明。
ElementType.FIELD 字段声明(包括枚举常量)。
ElementType.METHOD 方法声明。
ElementType.PARAMETER 正式参数声明。
ElementType.CONSTRUCTOR 构造函数声明。
ElementType.LOCAL_VARIABLE 局部变量声明。
ElementType.ANNOTATION_TYPE 注释类型声明。
ElementType.PACKAGE 包声明。
ElementType.TYPE_PARAMETER 输入参数声明, JDK8新增
ElementType.TYPE_USE 使用一种类型, JDK8新增

@Retention

@Retention : 表示注解的生命周期,需要设置存活策略 RetentionPolicy

RetentionPolicy枚举值 说明
RetentionPolicy.SOURCE 只存在于源码,编译器将丢弃注释。
RetentionPolicy.CLASS 注释将由编译器*记录在类文件中,但在运行时不需要由VM保留。 这是默认的行为
RetentionPolicy.RUNTIME 注释将由编译器记录在类文件中,并且在运行时由VM保留,因此 可以反射性地读取它们

@Inherited

@Inherited : 表示被修饰的注解作用于超类后,超类的子类会继承该注解。但只会继承作用在 ElementType.TYPE 的注解。

这种继承是隐蔽的,光从子类无法看出,但可以通过反射获取所继承注解。

@Documented

@Documented : 表示在生成 javadoc 时,被修饰的注解会被包含在文档中。

@Repeatable

@Repeatable : 表示被修饰的注解可多次使用,是 JDK8 新增的注解,例如 Spring 的 @Scheduled 可以设置多种定时策略。

注解分类

按注解的生命周期分类:

按生命周期分类 说明 应用
源码注解 注解只在源码中存在,编译成 *.class 文件就不存在了。
编译时注解 注解在源码和 *.class 文件中都存在。 TODO一般用于编译期检查。
运行时注解 注解在运行阶段还会起作用,甚至会影响运行逻辑。 第三方技术喜欢用其实现功能逻辑。

按注解来源分类:

按来源分类 示例
JDK自带 @Override , @Deprecated , @SuppressWarnings
来自第三方的注解 @Service , @Autowired 等Spring常用注解。
自定义的注解 可以使用 @interface 等来创建自定义注解。

JDK注解

这里介绍一下,日常开发时常会使用到的 JDK注解 。

注解是 JDK1.5 时添加的特性,JDK5只提供了这3个注解。

涉及 说明
@Override 覆盖,覆盖来自超类型(父类或接口)的方法。
@Deprecated 标识元素过时,使用处编译器会显示警告信息。
@SuppressWarnings 抑制编译器警告,可以制定警告集。例如 @SuppressWarnings("unchecked") 抑制未检查的警告。

@Override

@Override : 可以使用它来表示覆盖超类型(父类或接口)的方法,它是一个源码注解。

实际上, @Override 是可选的,不写编译器不会报错,但使用在非继承来的方法上会报错。

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Deprecated

@Deprecated : 标识元素过时,使用处编译器会显示警告信息,它是一个运行时注解。

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@SuppressWarnings

@SuppressWarnings : 抑制编译器警告,可以制定警告集,它是一个源码注解。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @jls 4.8 Raw Types
* @jls 4.12.2 Variables of Reference Type
* @jls 5.1.9 Unchecked Conversion
* @jls 5.5.2 Checked Casts and Unchecked Casts
* @jls 9.6.3.5 @SuppressWarnings
*/
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}

这里仅列出几个常用的警告关键字,详情可以查看 JLS

抑制警告的关键字 用途
all 压制所有警告。
unchecked 禁止相对于未经检查的操作的警告。
cast 禁止相对于强制转换操作的警告。
rawtypes 在类params上使用泛型时,禁止相对于非特定类型的警告。
deprecation 禁止相对于弃用的警告。

使用注解

除去 JDK 和 框架技术外,大多数时候需要 自定义注解 并 编写处理器来解析它们。

自定义注解

基础语法

  • 定义注解需要使用关键字 @interface ,其语法类似接口 ,但不允许继承。

  • 注解中可以添加 元素 ,看起来十分像接口的方法。

    注解类可以没有元素,没有元素的注解称为 标识注解

  • 元素 的类型受限,只能使用 : 基本类型 , String , Class , enum , Annotation以上类型的数组

    不能使用基本类型的包装类型,但支持自动装箱。

  • 虽然看上去像抽象方法,但 元素 是不允许有 入参 和 异常声明的。

  • 元素 不能有不确定的值,即要么使用 default 设置默认值,要么使用时提供元素值。

    默认值不允许使用 null ,为绕开这个约束,可以使用 空字符串 或 负数,来表示不存在。

约定俗成的规范 : 如果只有一个注解元素,则取名为 value() ,这样在使用时可以忽略成员名和赋值号 =

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
package tk.gushizone.java.basic.annotation;

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

@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Description {

/**
* 成员以无参无异常方式声明
*/
String value();

/**
* 可以用default指定默认值
*/
int id() default 100;
}

自定义注解 @Description 注解可以作用于 类型 和 方法 上。

注解的元素赋值一般采用 元素名=元素值 的方式设置,符合约定俗称的规范除外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package tk.gushizone.java.basic.annotation.entity;

import tk.gushizone.java.basic.annotation.Description;

@Description(value = "It's BaseItem class annotation.", id = 10)
public class BaseItem {

private int id;
private String name;

@Override
@Description("It's BaseItem method annotation.")
public String toString() {
return "BaseItem{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

@Description 注解可以被继承,可以看出注解的继承是比较隐蔽的,无法直接看出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package tk.gushizone.java.basic.annotation.entity;

import lombok.Getter;
import lombok.Setter;
import tk.gushizone.java.basic.annotation.Description;

@Getter
@Setter
public class Item extends BaseItem {

private String remark;

@Override
@Description("It's Item method annotation.")
public String toString() {
return "Item{" +
"remark='" + remark + '\'' +
'}';
}
}

编写注解处理器

如果没有用来读取注解的工具,那注解也不会比注释更有用。 使用 反射 可以获取 注解 提供的 元数据

从没有打印 getName:It's User method annotation. ,可以看出 @Inherited 只会继承作用在 ElementType.TYPE 的注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test(){
Class clazz = User.class;
boolean isExist = clazz.isAnnotationPresent(Description.class);
if (isExist){
Description d1 = (Description) clazz.getAnnotation(Description.class);

System.out.println(d1.value());
// It's Person class annotation.
}

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
boolean isMExist = method.isAnnotationPresent(Description.class);
if (isMExist){
Description d2 = method.getAnnotation(Description.class);

System.out.println(method.getName() + ":" + d2.value());
// getAge:It's User method annotation.
}
}
}
1
2
3
It's BaseItem class annotation.
toString:It's Item method annotation.
toString:It's Item method annotation.

常见应用

ORM

这里用注解简单实现一个ORM框架的查询功能,类似于 Hibernate。

定义注解

先定义两个注解,用于表示 数据表 和 字段 。

1
2
3
4
5
6
7
8
package tk.gushizone.java.basic.annotation.orm;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {

String value();
}
1
2
3
4
5
6
7
8
package tk.gushizone.java.basic.annotation.orm;

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

String value();
}

定义注解解析器

定义一个工具类用于解析注解,并完成简单查询的 SQL 拼接。

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
public class ORM {

private static final String SELECT = "SELECT";

private static final String FROM = "FROM";

private static final String WHERE = "WHERE";

private static final String SPLIT_BLANK = " ";

private static final String EQUALS = "=";

private static final String AND = "AND";

public static <T> String query(T obj) throws Exception {
StringBuilder sql = new StringBuilder();

Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();

String BASE_COLUMN_LIST = Arrays.stream(fields)
.filter(item -> item.isAnnotationPresent(Column.class))
.map(item -> item.getAnnotation(Column.class).value())
.collect(Collectors.joining(","));

String TABLE_NAME = ((Table) clazz.getAnnotation(Table.class)).value();

sql.append(SELECT).append(SPLIT_BLANK)
.append(BASE_COLUMN_LIST).append(SPLIT_BLANK)
.append(FROM).append(SPLIT_BLANK)
.append(TABLE_NAME);

StringBuilder condition = new StringBuilder();

for (Field field : fields) {
String fieldName = field.getAnnotation(Column.class).value();

PropertyDescriptor descriptor = new PropertyDescriptor(field.getName(), clazz);
Method getter = descriptor.getReadMethod();
Object fieldValue = getter.invoke(obj);

if (fieldValue == null
|| (fieldValue instanceof Integer && fieldValue.equals(0))) {
continue;
}

condition.append(SPLIT_BLANK).append(AND).append(SPLIT_BLANK)
.append(fieldName).append(EQUALS);
if (fieldValue instanceof String) {
condition.append("'").append((String) fieldValue).append("'");
} else if (fieldValue instanceof Integer) {
condition.append((Integer) fieldValue);
}
}

if (condition.length() > 0) {
sql.append(SPLIT_BLANK).append(WHERE)
.append(SPLIT_BLANK).append(condition.substring(4));
}

return sql.toString();
}
}

注解的使用

将两个注解用于实体类上,形成对象和数据表的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package tk.gushizone.java.basic.annotation.entity;

import lombok.Getter;
import lombok.Setter;
import tk.gushizone.java.basic.annotation.orm.Column;
import tk.gushizone.java.basic.annotation.orm.Table;

@Getter
@Setter
@Table("ITEM")
public class Filter {

@Column("ID")
private int id;

@Column("NAME")
private String name;

}

如此这般,一个简单的动态的条件查询就完成了。

1
2
3
4
5
6
7
8
9
@Test
public void test2() throws Exception {
Filter filter = new Filter();
filter.setId(1);
filter.setName("foo");

System.out.println(ORM.query(filter));
// SELECT ID,NAME FROM ITEM WHERE ID=1 AND NAME='foo'
}