Java建立Annotation

注解是Java很强大的部分,但大多数时候咱们倾向于使用而不是去建立注解。例如,在Java源代码里不难找到Java编译器处理的@Override注解, Spring框架的@Autowired注解, 或 Hibernate框架使用的@Entity 注解,但咱们不多看到自定义注解。虽然自定义注解是Java语言中常常被忽视的一个方面,但在开发可读性代码时它多是很是有用的资产,一样有助于理解常见框架(如Spring或Hibernate)如何简洁地实现其目标。
在本文中,咱们将介绍注解的基础知识,包括注解是什么,它们如何在示例中使用,以及如何处理它们。为了演示注解在实践中的工做原理,咱们将建立一个Javascript Object Notation(JSON)序列化程序,用于处理带注解的对象并生成表示每一个对象的JSON字符串。在此过程当中,咱们将介绍许多常见的注解块,包括Java反射框架和注解可见性问题。感兴趣的读者能够在 GitHub找到已完成的JSON序列化程序的源代码。

什么是注解?

注解是应用于Java结构的装饰器,例如将元数据与类,方法或字段相关联。这些装饰器是良性的,不会自行执行任何代码,但运行时,框架或编译器可使用它们来执行某些操做。更正式地说,Java语言规范(JLS) 第9.7节提供了如下定义:
注解是信息与程序结构相关联的标记,但在运行时没有任何影响。
请务必注意此定义中的最后一句:注解在运行时对程序没有影响。这并非说框架不会基于注解的存在而改变其运行时行为,而是包含注解自己的程序不会改变其运行时行为。虽然这可能看起来是细微差异,但为了掌握注解的实用性,理解这一点很是重要。
例如,某个实例的字段添加了@Autowired注解,其自己不会改变程序的运行时行为:编译器只是在运行时包含注解,但注解不执行任何代码或注入任何逻辑来改变程序的正常行为(忽略注解时的预期行为)。一旦咱们在运行时引入Spring框架,咱们就能够在解析程序时得到强大的依赖注入(DI)功能。经过引入注解,咱们已经指示Spring框架向咱们的字段注入适当的依赖项。咱们将很快看到(当咱们建立JSON序列化程序时)注解自己并无完成此操做,而是充当标记,通知Spring框架咱们但愿将依赖项注入到带注解的字段中。

Retention和Target

建立注解须要两条信息:(1)retention策略和(2)target。保留策略(retention)指定了在程序的生命周期注解应该被保留多长时间。例如,注解能够在编译时或运行时期间保留,具体取决于与注解关联的保留策略。从Java 9开始,有 三种标准保留策略,总结以下:

策略html

描述java

Sourcegit

编译器会丢弃注解github

Classspring

注解是在编译器生成的类文件中记录的,但不须要在运行时处理类文件的Java虚拟机(JVM)保留。编程

Runtimejson

注解由编译器记录在类文件中,并由JVM在运行时保留api

正如咱们稍后将看到的,注解保留的运行时选项是最多见的选项之一,由于它容许Java程序反射访问注解并基于存在的注解执行代码,以及访问与注解相关联的数据。请注意,注解只有一个关联的保留策略。
注解的目标(target)指定注解能够应用于哪一个Java结构。例如,某些注解可能仅对方法有效,而其余注解可能对类和字段都有效。从Java 9开始,有 11个标准注解目标,以下表所示:

目标数组

描述bash

Annotation Type

注解另外一个注解

Constructor

注解构造函数

Field

注解一个字段,例如类的实例变量或枚举常量

Local variable

注解局部变量

Method

注解类的方法

Module

注解模块(Java 9中的新增功能)

Package

注解包

Parameter

注解到方法或构造函数的参数

Type

注解一个类型,例如类,接口,注解类型或枚举声明

Type Parameter

注解类型参数,例如用做通用参数形式的参数

Type Use

注解类型的使用,例如当使用new关键字建立类型的对象时 ,当对象强制转换为指定类型时,类实现接口时,或者使用throws关键字声明throwable对象的类型时(有关更多信息,请参阅Type Annotations and Pluggable Type Systems Oracle tutorial)

有关这些目标的更多信息,请参见 JLS的第9.7.4节。要注意,注解能够关联一个或多个目标。例如,若是字段和构造函数目标与注解相关联,则能够在字段或构造函数上使用注解。另外一方面,若是注解仅关联方法目标,则将注解应用于除方法以外的任何构造都会在编译期间致使错误。

注解参数

注解也能够具备参数。这些参数能够是基本类型(例如int或double),String,类,枚举,注解或前五种类型中任何一种的数组(参见 JLS的第9.6.1节)。将参数与注解相关联容许注解提供上下文信息或者能够参数化注解的处理器。例如,在咱们的JSON序列化程序实现中,咱们将容许一个可选的注解参数,该参数在序列化时指定字段的名称(若是没有指定名称,则默认使用字段的变量名称)。

如何建立注解?

对于咱们的JSON序列化程序,咱们将建立一个字段注解,容许开发人员在序列化对象时标记要转换的字段名。例如,若是咱们建立汽车类,咱们可使用咱们的注解来注解汽车的字段(例如品牌和型号)。当咱们序列化汽车对象时,生成的JSON将包括make和model键,其中值分别表明make和model字段的值。为简单起见,咱们假设此注解仅用于String类型的字段,确保字段的值能够直接序列化为字符串。
要建立这样的字段注解,咱们使用@interface 关键字声明一个新的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}复制代码

咱们声明的核心是public @interface JsonField,声明带有public修饰符的注解——容许咱们的注解在任何包中使用(假设在另外一个模块中正确导入包)。注解声明一个String类型value的参数,默认值为空字符串。

请注意,变量名称value具备特殊含义:它定义单元素注解( JLS的第9.7.3节),并容许咱们的注解用户向注解提供单个参数,而无需指定参数的名称。例如,用户可使用@JsonField("someFieldName")而且不须要将注解声明为注解@JsonField(value = "someFieldName"),尽管后者仍然可使用(但不是必需的)。包含默认值空字符串容许省略该值,value若是没有显式指定值,则致使值为空字符串。例如,若是用户使用表单声明上述注解@JsonField,则该value参数设置为空字符串。
注解声明的保留策略和目标分别使用@Retention和@Target注解指定。保留策略使用 java.lang.annotation.RetentionPolicy枚举指定,并包含三个标准保留策略的常量。一样,指定目标为 java.lang.annotation.ElementType枚举,包括11种标准目标类型中每种类型的常量。
总之,咱们建立了一个名为JsonField的public单元素注解,它在运行时由JVM保留,而且只能应用于字段。此注解只有单个参数,类型String的value,默认值为空字符串。经过建立注解,咱们如今能够注解要序列化的字段。

如何使用注解?

使用注解仅须要将注解放在适当的结构(注解的任何有效目标)以前。例如,咱们能够建立一个Car类:

public class Car {
    @JsonField("manufacturer")
    private final String make;
    @JsonField
    private final String model;
    private final String year;

    public Car(String make, String model, String year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public String getYear() {
        return year;
    }

    @Override
    public String toString() {
        return year + " " + make + " " + model;
    }
} 复制代码

 

该类使用@JsonField注解的两个主要用途:(1)具备显式值,(2)具备默认值。咱们也可使用@JsonField(value = "someName")注解一个字段,但这种样式过于冗长,并无助于代码的可读性。所以,除非在单元素注解中包含注解参数名称能够增长代码的可读性,不然应该省略它。对于具备多个参数的注解,须要显式指定每一个参数的名称来区分参数(除非仅提供一个参数,在这种状况下,若是未显式提供名称,则参数将映射到value参数)。

鉴于@JsonField注解的上述用法,咱们但愿将Car序列化为JSON字符串{"manufacturer":"someMake", "model":"someModel"} (注意,咱们稍后将会看到,咱们将忽略键manufacturer 和model在此JSON字符串的顺序)。在这以前,重要的是要注意添加@JsonField注解不会改变类Car的运行时行为。若是编译这个类,包含@JsonField注解不会比省略注解时加强类的行为。类的类文件中只是简单地记录这些注解以及参数的值。改变系统的运行时行为须要咱们处理这些注解。

如何处理注解?

处理注解是经过 Java反射应用程序编程接口(API)完成的。反射API容许咱们编写代码来访问对象的类、方法、字段等。例如,若是咱们建立一个接受Car对象的方法,咱们能够检查该对象的类(即Car),并发现该类有三个字段:(1)make,(2)model和(3)year。此外,咱们能够检查这些字段以发现每一个字段是否都使用特定注解进行注解。
这样,咱们能够遍历传递给方法的参数对象关联类的每一个字段,并发现哪些字段使用@JsonField注解。若是该字段使用了@JsonField注解,咱们将记录该字段的名称及其值。处理完全部字段后,咱们就可使用这些字段名称和值建立JSON字符串。
肯定字段的名称须要比肯定值更复杂的逻辑。若是@JsonField包含value参数的提供值(例如"manufacturer"以前使用的@JsonField("manufacturer")),咱们将使用提供的字段名称。若是value参数的值是空字符串,咱们知道没有显式提供字段名称(由于这是value参数的默认值),不然,显式提供了一个空字符串。后面这几种状况下,咱们都将使用字段的变量名做为字段名称(例如,在private final String model声明中)。
将此逻辑组合到一个JsonSerializer类中:

public class JsonSerializer {
    public String serialize(Object object) throws JsonSerializeException {
        try {
            Class<?> objectClass = requireNonNull(object).getClass();
            Map<String, String> jsonElements = new HashMap<>();
            for (Field field : objectClass.getDeclaredFields()) {
                field.setAccessible(true);
                if (field.isAnnotationPresent(JsonField.class)) {
                    jsonElements.put(getSerializedKey(field), (String) field.get(object));
                }
            }
            System.out.println(toJsonString(jsonElements));
            return toJsonString(jsonElements);
        } catch (IllegalAccessException e) {
            throw new JsonSerializeException(e.getMessage());
        }
    }

    private String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet().stream().map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"").collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }
} 复制代码

请注意,为简洁起见,已将多个功能合并到该类中。有关此序列化程序类的重构版本,请参阅codebase存储库中的此分支(https://github.com/albanoj2/dzone-json-serializer/tree/srp_generalization)。咱们还建立了一个异常,用于表示在serialize方法处理对象时是否发生了错误:

public class JsonSerializeException extends Exception {
    private static final long serialVersionUID = -8845242379503538623L;

    public JsonSerializeException(String message) {
        super(message);
    }
} 复制代码

尽管JsonSerializer该类看起来很复杂,但它包含三个主要任务:(1)查找使用@JsonField注解的全部字段,(2)记录包含@JsonField注解的全部字段的名称(或显式提供的字段名称)和值,以及(3)将所记录的字段名称和值的键值对转换成JSON字符串。

requireNonNull(object).getClass()检查提供的对象不是null (若是是,则抛出一个NullPointerException)并得到与提供的对象关联的 Class对象。并使用此对象关联的类来获取关联的字段。接下来,咱们建立String到String的Map,存储字段名和值的键值对。
随着数据结构的创建,接下来遍历类中声明的每一个字段。对于每一个字段,咱们配置为在访问字段时禁止Java语言访问检查。这是很是重要的一步,由于咱们注解的字段是私有的。在标准状况下,咱们将没法访问这些字段,而且尝试获取私有字段的值将致使IllegalAccessException抛出。为了访问这些私有字段,咱们必须禁止对该字段的标准Java访问检查。setAccessible(boolean) 定义以下:
返回值true 表示反射对象应禁止Java语言访问检查。false 表示反射对象应强制执行Java语言访问检查。
请注意,随着Java 9中模块的引入,使用setAccessible 方法要求将包含访问其私有字段的类的包在其模块定义中声明为open。有关更多信息,请参阅 this explanation by Michał SzewczykAccessing Private State of Java 9 Modules by Gunnar Morling
在得到对该字段的访问权限以后,咱们检查该字段是否使用了注解@JsonField。若是是,咱们肯定字段的名称(经过@JsonField注解中提供的显式名称或默认名称),并在咱们先前构造的map中记录名称和字段值。处理完全部字段后,咱们将字段名称映射转换为JSON字符串。
处理完全部记录后,咱们将全部这些字符串与逗号组合在一块儿。这会产生一个字符串"<fieldName1>":"<fieldValue1>","<fieldName2>":"<fieldValue2>",...。一旦这个字符串被链接起来,咱们用花括号括起来,建立一个有效的JSON字符串。
为了测试这个序列化器,咱们能够执行如下代码:

Car car=new Car("Ford","F150","2018");
JsonSerializer serializer=new JsonSerializer();
serializer.serialize(car); 复制代码

输出:

{"model":"F150","manufacturer":"Ford"}复制代码
正如预期的那样,Car对象的maker和model字段已经被序列化,使用字段的名称做为键,字段的值做为值。请注意,JSON元素的顺序可能与上面看到的输出相反。发生这种状况是由于对于类的声明字段数组没有明确的排序,如 getDeclaredFields文档中所述:
返回数组中的元素未排序,而且不按任何特定顺序排列。
因为此限制,JSON字符串中元素的顺序可能会有所不一样。为了使元素的顺序具备肯定性,咱们必须本身强加排序。因为JSON对象被定义为一组无序的键值对,所以根据 JSON标准,不须要强制排序。但请注意,序列化方法的测试用例应该输出{"model":"F150","manufacturer":"Ford"} 或者{"manufacturer":"Ford","model":"F150"}。

结论

Java注解是Java语言中很是强大的功能,但大多数状况下,咱们使用标准注解(例如@Override)或通用框架注解(例如@Autowired),而不是开发人员。虽然不该使用注解来代替以面向对象的方式,但它们能够极大地简化重复逻辑。例如,咱们能够注解每一个可序列化字段而不是在接口中的方法建立一个toJsonString以及全部能够序列化的类实现此接口。它还将序列化逻辑与域逻辑分离,从域逻辑的简洁性中消除了手动序列化的混乱。
虽然在大多数Java应用程序中不常用自定义注解,可是对于Java语言的任何中级或高级用户来讲,须要了解此功能。这个特性的知识不只加强了开发人员的知识储备,一样也有助于理解最流行的Java框架中的常见注解。
更多文章欢迎访问: http://www.apexyun.com
公众号:银河系1号
联系邮箱:public@space-explore.com
(未经赞成,请勿转载)   
相关文章
相关标签/搜索