Java中如何创建自定义的注解

前言


关于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.

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.

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.一个目标。

保留策略指定了多长时间,用术语来说就是程序的生命周期,这个注解可以被保留。参考下面:

 

POLICYDESCRIPTION
SourceAnnotations are discarded by the compiler
ClassAnnotations 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
RuntimeAnnotations 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构造器,下面表格进行了总结:

TARGETDESCRIPTION
Annotation TypeAnnotates another annotation
ConstructorAnnotates a constructor
FieldAnnotates a field, such as an instance variable of a class or an enum constant
Local variableAnnotates a local variable
MethodAnnotates a method of a class
ModuleAnnotates a module (new in Java 9)
PackageAnnotates a package
ParameterAnnotates a parameter to a method or constructor
TypeAnnotates a type, such as a class, interfaces, annotation types, or enum declarations
Type ParameterAnnotates a type parameter, such as those used as formal generic parameters
Type UseAnnotates 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.

一个注解可以被关联上一个或者多个targe。

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).

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

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.

上面的代码声明了一个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")这样去声明这个注解。

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.RetentionPolicyenum and includes constants for each of the three standard retention policies. Likewise, the target is specified using the java.lang.annotation.ElementTypeenum, 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.

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.

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.

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).

上面这段终于看明白了,整个处理过程是由JVM通过反射机制来实现的,它在加载完类、方法之后,然后开始检测是否存在注解,然后根据注解的定义,来对相应的单位进行处理。

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 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.

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:

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.

参考:

 https://dzone.com/articles/creating-custom-annotations-in-java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值