Java中如何創建自定義的註解學習筆記(MD版)

概要

Java中如何創建自定義的註解學習筆記(MD版)。

博客

博客地址:IT老兵驛站

前言

記得這篇筆記還是在泉州的龍玲酒店記錄的,是一個週六的晚上,坐飛機從上海到泉州,從筆記中能勾起一些旅遊的回憶,感覺很豐富。

這次重新修改它,是因爲感覺對 Java 的註解還是沒有搞明白,還需要再花點氣力。

這篇文章之前使用的是富文本編輯的,現在感覺遷移起來太痛苦,所以改爲 Markdown 來記錄。

關於 Java 的註解,我一直在用,沒有太搞明白它的原理,至於如何自定義一個註解,就更不明白了。其實參考的這篇文章,之前看過一遍,當時以爲看懂了,但是最近在工作中去印證的時候,發現對註解還是不理解,所以這兩天又再看了一遍,感覺這下又懂了一些。

本文針對着原文的段落進行備註和記錄筆記。

正文

This comprehensive look at annotations in Java not only goes into how to create them but also advise on how to use them and how they’re processed by the JVM.

這篇文章主旨不光是講如何創建註解,還包括如何使用它們,以及它們在 JVM 上是如何處理的。

Annotations are a powerful part of Java, but most times we tend to be the users rather than the creators of annotations. For example, it is not difficult to find Java source code that includes the @Override annotation processed by the Java compiler, the @Autowired annotation used by the Spring framework, or the @Entity annotation used by the Hibernate framework, but rarely do we see custom annotations. While custom annotations are an often-overlooked aspect of the Java language, they can be a very useful asset in developing readable code and just as importantly, useful in understanding how many common frameworks, such as Spring or Hibernate, succinctly accomplish their goals.

自定義註解在實際的使用中,其實沒有得到足夠的重視。

In this article, we will cover the basics of annotations, including what annotations are, how they are useful in large-than-academic examples, and how to process them. In order to demonstrate how annotations work in practice, we will create a Javascript Object Notation (JSON) serializer that processes annotated objects and produces a JSON string representing each object. Along the way, we will cover many of the common stumbling blocks of annotations, including the quirks of the Java reflection framework and visibility concerns for annotation consumers. The interested reader can find the source code for the completed JSON serializer on GitHub.

這篇文章以一個 JSON 序列化器的註解爲例來介紹。

What Are Annotations? 註解是什麼

Annotations are decorators that are applied to Java constructs, such as classes, methods, or fields, that associate metadata with the construct. These decorators are benign and do not execute any code in-and-of-themselves, but can be used by runtime frameworks or the compiler to perform certain actions. Stated more formally, the Java Language Specification (JLS), Section 9.7, provides the following definition:

An annotation is a marker which associates information with a program construct, but has no effect at run time.

It is important to note the last clause in this definition: Annotations have no effect on a program at runtime. This is not to say that a framework may not change its behavior based on the presence of an annotation at runtime, but that the inclusion of an annotation does not itself change the runtime behavior of a program. While this may appear to be a nuanced distinction, it is a very important one that must be understood in order to grasp the usefulness of annotations.

For example, adding the @Autowired annotation to an instance field does not in-and-of-itself change the runtime behavior of a program: The compiler simply includes the annotation at runtime, but the annotation does not execute any code or inject any logic that alters the normal behavior of the program (the behavior expected when the annotation is omitted). Once we introduce the Spring framework at runtime, we are able to gain powerful Dependency Injection (DI) functionality when our program is parsed. By including the annotation, we have instructed the Spring framework to inject an appropriate dependency into our field. We will see shortly (when we create our JSON serializer) that the annotation itself does not accomplish this, but rather, the annotation acts as a marker, informing the Spring framework that we desire a dependency to be injected into the annotated field.

註解是一種裝飾器,那麼參考裝飾器設計模式的概念,它其實是給被裝飾者增加功能用的。

Retention and Target 保存和目標

Creating an annotation requires two pieces of information: (1) a retention policy and (2) a target. A retention policy specifies how long, in terms of the program lifecycle, the annotation should be retained for. For example, annotations may be retained during compile-time or runtime, depending on the retention policy associated with the annotation. As of Java 9, there are three standard retention policies, as summarized below:

創建一個註解,需要兩方面的信息:

  1. 一個保留策略
  2. 一個目標

保留策略指定了多長時間,用術語來說就是程序的生命週期,這個註解可以被保留。參考下面:

POLICY DESCRIPTION
Source Annotations are discarded by the compiler
Class Annotations are recorded in the class file generated by the compiler but are not required to be retained by the Java Virtual Machine (JVM) that processes the class file at runtime
Runtime Annotations are recorded in the class file by the compiler and retained at runtime by the JVM

As we will see shortly, the runtime option for annotation retention is one of the most common, as it allows for Java programs to reflectively access the annotation and execute code based on the presence of an annotation, as well as access the data associated with an annotation. Note that an annotation has exactly one associated retention policy.

The target of an annotation specifies which Java constructs an annotation can be applied to. For example, some annotations may be valid for methods only, while others may be valid for both classes and fields. As of Java 9, there are eleven standard annotation targets, as summarized in the following table:

註解的 target 指定了一個註解將會被用於哪一個 Java 構造器,下面表格進行了總結:

TARGET DESCRIPTION
Annotation Type Annotates another annotation
Constructor Annotates a constructor
Field Annotates a field, such as an instance variable of a class or an enum constant
Local variable Annotates a local variable
Method Annotates a method of a class
Module Annotates a module (new in Java 9)
Package Annotates a package
Parameter Annotates a parameter to a method or constructor
Type Annotates a type, such as a class, interfaces, annotation types, or enum declarations
Type Parameter Annotates a type parameter, such as those used as formal generic parameters
Type Use Annotates the use of a type, such as when an object of a type is created using the newkeyword, when an object is cast to a specified type, when a class implements an interface, or when the type of a throwable object is declared using the throws keyword (for more information, see the [Type Annotations and Pluggable Type Systems Oracle tutorial]

For more information on these targets, see Section 9.7.4 of the JLS. It is important to note that one or more targets may be associated with an annotation. For example, if the field and constructor targets are associated with an annotation, then the annotation may be used on either fields or constructors. If on the other hand, an annotation only has an associated target of method, then applying the annotation to any construct other than a method results in an error during compilation.

一個註解可以被關聯上一個或者多個 target。

Annotation Parameters 註解參數

Annotations may also have associated parameters. These parameters may be a primitive (such as int or double), String, class, enum, annotation, or an array of any of the five preceding types (see Section 9.6.1 of the JLS). Associating parameters with an annotation allows for an annotation to provide contextual information or can parameterize a processor of an annotation. For example, in our JSON serializer implementation, we will allow for an optional annotation parameter that specifies the name of a field when it is serialized (or use the variable name of the field by default if no name is specified).

註解可以有關聯的參數,這些參數可以是一個原語(例如 int 或者 double),這裏原語的意思是第一級的類型定義,String, class, enum, annotation,或者前面這五種類型的數據。把參數和一個註解關聯起來,這樣可以允許一個註解提供上下文的一些信息或者可以參數化一個註解的一個處理器。舉個例子,在我們的 JSON 序列化器的實現中,我們將會允許一個作爲可選項的註解參數來指定將要被序列化的字段的名字(或者,如果沒有名稱被指定,那麼使用這個字段的默認名字)。

How Are Annotations Created? 註解是怎麼創建的

For our JSON serializer, we will create a field annotation that allows a developer to mark a field to be included when serializing an object. For example, if we create a car class, we can annotate the fields of the car (such as make and model) with our annotation. When we serialize a car object, the resulting JSON will include make and model keys, where the values represent the value of the make and model fields, respectively. For the sake of simplicity, we will assume that this annotation will be used only for fields of type String, ensuring that the value of the field can be directly serialized as a string.

下面要開始舉例說明如何創建一個註解。

To create such a field annotation, we declare a new annotation using the @interface keyword:

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

這個地方以前沒有看仔細,這裏是@interface,不是 interface

The core of our declaration is the public @interface JsonField, which declares an annotation type with a public modifier, allowing our annotation to be used in any package (assuming the package is properly imported if in another module). The body of the annotation declares a single String parameter, named value, that has a type of String and a default value of an empty string.

上面這段代碼聲明的核心是 public @interface JsonField,這聲明瞭一個帶有 public 修飾符的註解,允許我們的註解在任意的包被使用(假設這個包在任何模塊中正確地被引入----這句話有點看不懂)。聲明的 body 部分聲明瞭一個 String 的參數,名字是 value ,類型是 String,默認值是空的 String。

Note that the variable name value has a special meaning: It defines a Single-Element Annotation (Section 9.7.3. of the JLS) and allows users of our annotation to supply a single parameter to the annotation without specifying the name of the parameter. For example, a user can annotate a field using @JsonField("someFieldName") and is not required to declare the annotation as @JsonField(value = "someFieldName"), although the latter may still be used (but it is not required). The inclusion of a default value of empty string allows for the value to be omitted, resulting in value holding an empty string if no value is explicitly specified. For example, if a user declares the above annotation using the form @JsonField, then the value parameter is set to an empty string.

注意這個 value 有一個特殊的意義,它定義了一個單元素的註解,允許註解的使用者給一個註解提供一個單一的參數,而不用指定參數的名字。例如用戶可以@JsonField("someFieldName") 這樣去對一個字段進行註解,而不需要@JsonField(value = "someFieldName") 這樣去聲明這個註解,當然後面這種方式還是可以被使用的。默認值的包含允許這個 value 值被忽略,而沒有明確的 value 被指定的時候,就會使用這個空字符串作爲默認值,例如,一個用戶是用@JsonField 來聲明的,那麼這個 value 就會被設置成空串。

The retention policy and target of the annotation declaration are specified using the @Retention and @Targetannotations, respectively. The retention policy is specified using the [java.lang.annotation.RetentionPolicy](https://docs.oracle.com/javase/9/docs/api/java/lang/annotation/RetentionPolicy.html)enum and includes constants for each of the three standard retention policies. Likewise, the target is specified using the [java.lang.annotation.ElementType](https://docs.oracle.com/javase/9/docs/api/java/lang/annotation/ElementType.html)enum, which includes constants for each of the eleven standard target types.

In summary, we created a public, single-element annotation named JsonField, which is retained by the JVM during runtime and may only be applied to fields. This annotation has a single parameter, value, of type String with a default value of an empty string. With our annotation created, we can now annotate fields to be serialized.

How Are Annotations Used? 註解是怎麼被使用的

Using an annotation requires only that the annotation is placed before an appropriate construct (any valid target for the annotation). For example, we can create a Carclass using the following class declaration:

下面結合一個例子來講,註解是如何被使用的。

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;
    }
}

This class exercises the two major uses of the @JsonField annotation: (1) with an explicit value and (2) with a default value. We could have also annotated a field using the form @JsonField(value = "someName"), but this style is overly verbose and does not aid in the readability of our code. Therefore, unless the inclusion of an annotation parameter name in a single-element annotation adds to the readability of code, it should be omitted. For annotations with more than one parameter, the name of each parameter is required to differentiate between parameters (unless only one argument is provided, in which case, the argument is mapped to the value parameter if no name is explicitly provided).

上面的例子實踐了註解的兩個用法,一個是帶有明確的值,一個是帶有一個默認值。對於單一參數的註解,可以忽略掉參數名;反之,則不行。

Given the above uses of the @JsonField annotation, we would expect that a Car ject is serialized into a JSON string of the form {"manufacturer":"someMake", "model":"someModel"} (note, as we will see later, we will disregard the order of the keys–manufacturer and model–in this JSON string). Before we proceed, it is important to note that adding the @JsonField annotations does not change the runtime behavior of the Carclass. If we compile this class, the inclusion of @JsonField annotations does not enhance the behavior of the Car class anymore than had we omitted the annotations. These annotations are simply recorded, along with the value of the value parameter, in the class file for the Car class. Altering the runtime behavior of our system requires that we process these annotations.

基於上面的 @JsonField 的註解使用,我們可以期望一個 Car 被序列化成一個 JSON 字符串。
後面這段是講,這個註解並沒有改變 Car 的運行時的行爲。

How are Annotations Processed? 註解是怎麼被處理的

Processing annotations is accomplished through the Java Reflection Application Programming Interface (API). Sidelining the technical nature of the reflection API for a moment, the reflection API allows us to write code that will inspect the class, methods, fields, etc. of an object. For example, if we create a method that accepts a Car object, we can inspect the class of this object (namely, Car) and discover that this class has three fields: (1) make, (2) model, and (3) year. Furthermore, we can inspect these fields to discover if each is annotated with a specific annotation.

註解的處理是由反射的 API 來完成的,反射的 API 允許我們寫一些代碼去檢視一個對象的類、方法、域等。

Using this capability, we can iterate through each field of the class associated with the object passed to our method and discover which of these fields are annotated with the @JsonField annotation. If the field is annotated with the @JsonField annotation, we record the name of the field and its value. Once all the fields have been processed, then we can create the JSON string using these field names and values.

使用這個能力,我們可以遍歷傳遞給我們的方法的對象的類的每一個字段,並且去發現那些字段是被 @JsonField 來註解的。前半句有些拗口,“傳遞給我們的方法的對象”,這裏是指什麼呢?如果一個域被用 @JsonField 註解,我們記錄下來它的域的名稱和它的值。

Determining the name of the field requires more complex logic than determining the value. If the @JsonFieldincludes a provided value for the value parameter (such as "manufacturer" in the previous @JsonField("manufacturer") use), we will use this provided field name. If the value of the value parameter is an empty string, we know that no field name was explicitly provided (since this is the default value for the value parameter), or else, an empty string was explicitly provided. In either case, we will use the variable name of the field as the field name (for example, model in the private final String model declaration).

確定域的名字比確定值,邏輯要更加複雜。如果 @JsonField 包含一個給 value 參數提供一個值(例如之前的 @JsonField("manufacturer") 裏面的"manufacturer" ),我們將那個作爲域的值。如果 value 參數的值是一個空串,那麼我們知道域的名稱沒有被明確地提供(因爲這是 value 參數的默認值),或者提供了一個空串。這種情況下,我們將使用這個域的變量名作爲域的名(例如,在 private final String model 聲明裏面的 model)。

這個註解其實是爲了確認這個域的名稱,這裏的 value 是這個域的名稱的 value,不是這個域的值,這裏容易混淆,之前就沒搞明白這裏。

Combining this logic into a JsonSerializer class, we can create the following class declaration:

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;
        }
    }
}

Note that multiple responsibilities have been combined into this class for the sake of brevity. For a refactored version of this serializer class, see this branch in the codebase repository. We also create an exception that will be used to denote if an error has occurred while processing the object supplied to our serialize method:

public class JsonSerializeException extends Exception {

    private static final long serialVersionUID = -8845242379503538623L;

    public JsonSerializeException(String message) {
        super(message);
    }
}

Although the JsonSerializer class appears complex, it consists of three main tasks: (1) finding all fields of the supplied class annotated with the @JsonField annotation, (2) recording the field name (or the explicitly provided field name) and value for all fields that include the @JsonField annotation, and (3) converting the recorded field name and value pairs into a JSON string.

上面這個 JsonSerializer 類有一些複雜,它包含了3個主要任務:

1\. 發現所有被@JsonField註解的域。

2\. 記錄所有包含在@JsonField註解的域的名字,或者明確提供域的名字和所有域的值。

3\. 轉換這些域的名和值到一個JSON字符串。

The line requireNonNull(object).getClass() simply checks that the supplied object is not null (and throws a NullPointerException if it is) and obtains the [Class](https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html) object associated with the supplied object. We will use this Class object shortly to obtain the fields associated with the class. Next, we create a Map of Strings to Strings, which will be used store the field name and value pairs.

這個代碼裏面涉及到了一些新的 Java 的功能,我以前沒有接觸過,例如 requireNonNull(object).getClass() 可以檢查一個對象是否是 null, 如果不是,則去獲取這個對象的 [Class] 信息。

With our data structures established, we next iterate through each field declared in the class of the supplied object. For each field, we configure the field to suppress Java language access checking when accessing the field. This is a very important step since the fields we annotated are private. In the standard case, we would be unable to access these fields, and attempting to obtain the value of the private field would result in an IllegalAccessException being thrown. In order to access these private fields, we must instruct the reflection API to suppress the standard Java access checking for this field using the setAccessible method. The setAccessible(boolean) documentation defines the meaning of the supplied boolean flag as follows:

這裏涉及到了一個比較關鍵的地方,即權限問題,本來 Java 的這些私有域都是不能被訪問的,爲了訪問這些私有域,我們必須通知反射的 API 去抑制住標準的 Java 訪問檢查,即使用 setAccessible 這個方法。

A value of true indicates that the reflected object should suppress Java language access checking when it is used. A value of false indicates that the reflected object should enforce Java language access checks.

Note that with the introduction of modules in Java 9, using the setAccessible method requires that the package containing the class whose private fields will be accessed should be declared open in its module definition. For more information, see this explanation by Michał Szewczyk and Accessing Private State of Java 9 Modules by Gunnar Morling.

這裏會有一個訪問權限的問題,因爲註解關聯的域是私有域,那麼想要訪問,需要修改反射時的訪問權限。

After gaining access to the field, we check if the field is annotated with the @JsonField. If it is, we determine the name of the field (either through an explicit name provided in the @JsonField annotation or the default name, which equals the variable name of the field) and record the name and field value in our previously constructed map. Once all fields have been processed, we then convert the map of field names to field values (jsonElements) into a JSON string.

We accomplish by converting the map into a stream of entries (key-value pairs for each entry in the map), mapping each entry to a string of the form "<fieldName>":"<fieldValue>", where <fieldName> is the key for the entry and <fieldValue> is the value for the entry. Once all entries have been processed, we combine all of these entry strings with a comma. This results in a string of the form "<fieldName1>":"<fieldValue1>","<fieldName2>":"<fieldValue2>",.... Once this terminal string has been joined, we surround it with curly braces, creating a valid JSON string.

In order to test this serializer, we can execute the following code:

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

This results in the following output:

{"model":"F150","manufacturer":"Ford"}

As expected, the maker and model fields of the Car object have been serialized, using the name of the field (or the explicitly supplied name in the case of the maker field) as the key and the value of the field as the value. Note that the order of JSON elements may be reversed from the output seen above. This occurs because there is no definite ordering for the array of declared fields for a class, as stated in the getDeclaredFieldsdocumentation:

The elements in the returned array are not sorted and are not in any particular order.

Due to this limitation, the order of the elements in the JSON string may vary. To make the order of the elements deterministic, we would have to impose ordering ourselves (such as by sorting the map of field names to field values). Since a JSON object is defined as an unordered set of name-value pairs, as per the JSON standard, imposing ordering is unneeded. Note, however, a test case for the serialize method should pass for either {"model":"F150","manufacturer":"Ford"} or {"manufacturer":"Ford","model":"F150"}.

上面輸出的 JSON 並沒有排序,這並沒有什麼問題,規則中並沒有要求有順序。

Conclusion 結論

Java annotations are a very powerful feature in the Java language, but most often, we are the users of standard annotations (such as @Override) or common framework annotations (such as @Autowired), rather than their developers. While annotations should not be used in place of interfaces or other language constructs that properly accomplish a task in an object-oriented manner, they can greatly simplify repetitive logic. For example, rather than creating a toJsonStringmethod within an interface and having all classes that can be serialized implement this interface, we can annotate each serializable field. This takes the repetitive logic of the serialization process (mapping field names to fields values) and places it into a single serializer class. It also decouples the serialization logic from the domain logic, removing the clutter of manual serialization from the conciseness of the domain logic.

While custom annotations are not frequently used in most Java applications, knowledge of this feature is a requirement for any intermediate or advanced user of the Java language. Not only will knowledge of this feature enhance the toolbox of a developer, which is just as important, but it will also aid in the understanding of the common annotations in the most popular Java frameworks.

總結

本文以一個實際的例子,描述了怎麼去寫一個 Java 的註解,反覆理解這個過程,對於理解註解,有很好的幫助。

其實通過一些特定的類,可以反向獲取到很多 Java 對象類的信息,例如域,例如註解,這樣來給原本的對象類增加了一些功能。

這裏面涉及到的類、方法、用法:
Class 這個類可以獲取對象類的基本信息,從 JDK 1.0 就開始存在了,以前研究反射應該是用過,但是沒有仔細琢磨過這個類。
requireNonNull 這個是 Objects 的一個靜態方法,1.7 之後引入的,這個類可以理解爲一個工具類,它的註釋裏面也寫着 utilities。
Field 用來動態訪問類或者接口的類
@Test JUnit 的註解
NullPointerException
靜態 import 參考底下的鏈接,1.5 版本引入的,不再需要類名,直接可以使用靜態方法,不過不推薦使用

參考

https://dzone.com/articles/creating-custom-annotations-in-java
https://www.geeksforgeeks.org/static-import-java/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章