Annotations are a powerful part of Java, but most of the time we tend to use them rather than create them. For example, it’s not hard to find the @Override annotation handled by the Java compiler, the @AutoWired annotation in the Spring framework, or the @Entity annotation used by the Hibernate framework in Java source code, but we rarely 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, as well as in understanding how common frameworks such as Spring or Hibernate succinctly achieve their goals.

In this article, we’ll cover the basics of annotations, including what annotations are, how they are used in examples, and how to handle them. To demonstrate how annotations work in practice, we will create a Javascript Object Notation (JSON) serializer that works with annotated objects and generates a JSON string representing each Object. Along the way, we’ll cover many common annotation blocks, including the Java reflection framework and annotation visibility issues. Interested readers can find the source code for the completed JSON serializer on GitHub.

What are annotations?

Annotations are decorators that apply to Java structures, such as associating metadata with classes, methods, or fields. These decorators are benign and do not execute any code on their own, but at run time they can be used by the framework or compiler to perform certain operations. More formally, section 9.7 of the Java Language Specification (JLS) provides the following definition:

Annotations are markers that relate information to the program structure, but have no impact at run time.

Be sure to note the last sentence in this definition: Annotations have no effect on the program at run time. This is not to say that frameworks do not change their runtime behavior based on the existence of annotations, but that programs that contain annotations themselves do not change their runtime behavior. While this may seem like a nuance, it is important to understand in order to grasp the usefulness of annotations.

For example, the @Autowired annotation added to a field in an instance does not, by itself, change the program’s runtime behavior: the compiler simply includes the annotation at runtime, but the annotation does not execute any code or inject any logic to change the program’s normal behavior (ignoring the expected behavior at annotation time). Once we introduce the Spring framework at run time, we can get powerful dependency injection (DI) functionality when parsing programs. By introducing annotations, we’ve instructed the Spring framework to inject the appropriate dependencies into our fields. As we will soon see (when we create the JSON serializer), the annotations themselves do not do this, but rather serve as tokens to inform the Spring framework that we want to inject the dependencies into the annotated fields.

The Retention and Target

Creating annotations requires two pieces of information :(1) retention policy and (2) target. Retention policies specify how long annotations should be retained for the lifetime of a program. For example, annotations can be retained at compile time or runtime, depending on the retention policy associated with the annotation. Starting with Java 9, there are three standard retention strategies, summarized as follows:

strategy

describe

Source

The compiler discards annotations

Class

Annotations are recorded in class files generated by the compiler, but do not require the Java Virtual Machine (JVM) reservation that processes class files at run time.

Runtime

Annotations are recorded in class files by the compiler and retained by the JVM at run time

As we’ll see later, the runtime option for annotations reservation is one of the most common, because it allows Java programs to reflectively access annotations and execute code based on existing annotations, as well as access the data associated with the annotations. Note that annotations have only one associated retention policy.

The target of the annotation specifies which Java structure the annotation can be applied to. For example, some annotations may be valid only for methods, while others may be valid for both classes and fields. Starting with Java 9, there are 11 standard annotation targets, as shown in the following table:



The target

describe

Annotation Type

Annotation another annotation

Constructor

Annotation constructor

Field

Annotate a field, such as a class instance variable or enumeration constant

Local variable

Annotation local variable

Method

Annotation class methods

Module

Annotation module (new in Java 9)

Package

Annotations package

Parameter

Annotated arguments to a method or constructor

Type

Annotate a type, such as a class, interface, annotation type, or enumeration declaration

Type Parameter

Annotate type parameters, such as those used as generic parameter forms

Type Use

The use of annotation types, such as when creating an object of type using the new keyword, when casting an object to a specified type, when a class implements an interface, or when declaring the type of a throwable object using the throws keyword (for more information, See Type Annotations and Pluggable Type Systems Oracle Tutorial)

For more information on these goals, see section 9.7.4 of the JLS. Note that annotations can be associated with one or more targets. For example, annotations can be used on a field or constructor if the field and constructor target are associated with an annotation. On the other hand, if the annotation is associated only with the method target, applying the annotation to any construct other than the method will result in an error at compile time.

Annotation parameters

Annotations can also have parameters. These arguments can be primitive types (such as int or double), strings, classes, enumerations, annotations, or arrays of any of the first five types (see Section 9.6.1 of JLS). Associating parameters with annotations allows annotations to provide contextual information or handlers that can parameterize annotations. For example, in our JSON serializer implementation, we will allow an optional annotation parameter that specifies the name of the field at serialization (if no name is specified, the variable name of the field is used by default).

How do I create annotations?

For our JSON serializer, we will create a field annotation that allows the developer to mark the field name to be converted when serializing the object. For example, if we create the car class, we can annotate the car fields (such as make and model) with our annotations. When we serialize the car object, the resulting JSON will include the make and Model keys, where the values represent the values of the make and Model fields, respectively. For simplicity, we assume that this annotation only applies to fields of type String, ensuring that the value of the field can be serialized directly into 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(a) default "";
}Copy the code

The core of our declaration is the public @Interface JsonField, which declares annotations with the public modifier — allowing our annotations to be used in any package (assuming the package is imported correctly in another module). The annotation declares a parameter of type String value. The default value is an empty String.

Note that the variable name value has special meaning: it defines a single-element annotation (Section 9.7.3 of THE JLS) and allows our annotation user to provide a single parameter to the annotation without specifying the name of the parameter. For example, a user can use @jsonField (“someFieldName”) and do not need to declare the annotation as @jsonfield (value = “someFieldName”), although the latter can still be used (but is not required). An empty string that contains a default value allows the value to be omitted. Value If no value is explicitly specified, the value is an empty string. For example, if the user declares the above annotation @jsonField with a form, the value parameter is set to an empty string.

The Retention policy and Target of the annotation declaration are specified using the @Retention and @target annotations, respectively. Reserve strategy using Java. Lang. The annotation. Specify RetentionPolicy enumeration, and contains three standard constant retention strategy. Also, specify the target for Java. Lang. The annotation. ElementType enumeration, including 11 types of standard target in each type of constant.

In summary, we created a public single-element annotation called JsonField, which is reserved by the JVM at runtime and applies only to fields. This annotation takes a single argument, of type String value, which defaults to an empty String. By creating annotations, we can now annotate the fields to serialize.

How are annotations used?

Using annotations simply precedes the appropriate structure (any valid target for annotations). For example, we could create a Car class:

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(a) {
        return make;
    }

    public String getModel(a) {
        return model;
    }

    public String getYear(a) {
        return year;
    }

    @Override
    public String toString(a) {
        return year + "" + make + ""+ model; }}Copy the code

 

This class uses the @jsonfield annotation for two main purposes :(1) having explicit values, and (2) having default values. We could also annotate a field with @jsonField (value = “someName”), but this style is verbose and does not help readability. Therefore, unless the annotation parameter name is included in the single-element annotation to increase the readability of the code, it should be omitted. For annotations with multiple parameters, you need to explicitly specify the name of each parameter to distinguish the parameters (unless only one parameter is provided, in which case the parameter is mapped to the value parameter if the name is not explicitly provided).

Given the above use of the @jsonField annotation, we want to serialize Car as a JSON string {“manufacturer”:”someMake”, “model”:”someModel”} (note that we will see later, We will ignore the order of the manufacturer and model keys in this JSON string. Before we do that, it’s important to note that adding @JsonField annotations does not change the run-time behavior of the Car class. If the class is compiled, including the @JsonField annotation does not enhance the behavior of the class compared to omitting the annotation. Class class files simply record these annotations and the values of the parameters. Changing the runtime behavior of the system requires us to deal with these annotations.

How are annotations handled?

Annotation processing is done through the Java Reflection Application Programming Interface (API). The reflection API allows us to write code to access an object’s classes, methods, fields, and so on. For example, if we create a method that accepts a Car object, we can examine the object’s class (that is, Car) and find that the class has three fields :(1) make, (2) model, and (3) year. In addition, we can examine these fields to see if each field is annotated with a specific annotation.

This way, we can iterate over each field of the parameter object associated class passed to the method and discover which fields use the @JsonField annotation. If the field uses the @jsonField annotation, we record the name of the field and its value. After processing all the fields, we can create JSON strings using the field names and values.

Determining the name of a field requires more complex logic than determining the value. If @jsonField contains the supplied value for the value parameter (such as @jsonfield (“manufacturer”) used before “manufacturer”), we will use the supplied field name. If the value of the value argument is an empty string, we know that the field name is not explicitly provided (because that is the default value for the value argument), otherwise, an empty string is explicitly provided. In each of the latter cases, we will use the variable name of the field as the field name (for example, in the private Final String Model declaration).

Combine this logic into a JsonSerializer class:

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 newJsonSerializeException(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 {
            returnannotationValue; }}}Copy the code

Note that multiple functions have been merged into this class for brevity. About the serializer class refactoring version, please refer to the codebase repository of the branch (https://github.com/albanoj2/dzone-json-serializer/tree/srp_generalization). We also create an exception to indicate whether an error occurred when the serialize method processed an object:

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

    public JsonSerializeException(String message) {
        super(message); }}Copy the code

Although the JsonSerializer class may seem complex, it contains three main tasks: (1) Find all fields that use the @JsonField annotation, (2) record the names (or explicitly supplied field names) and values of all fields that contain the @JsonField annotation, and (3) convert the key-value pairs of the recorded field names and values to JSON strings.

RequireNonNull (object).getClass() checks that the supplied object is not NULL (if so, throws a NullPointerException) and obtains the Class object associated with the supplied object. And use the class associated with this object to get the associated field. Next, we create a string-to-string Map that stores key-value pairs of field names and values.

As the data structure is set up, each field declared in the class is iterated. For each field, we configure to disable Java language access checking when the field is accessed. This is an important step because the fields we annotate are private. In the standard case, we would not be able to access these fields, and trying to get the value of a private field would result in an IllegalAccessException thrown. To access these private fields, we must disable standard Java access checks on the field. SetAccessible (Boolean) Defined as follows:

The return value true indicates that the reflection object should disable Java language access checks. False indicates that the reflection object should enforce Java language access checks.

Note that with the introduction of modules in Java 9, using the setAccessible method requires that packages containing classes that access their private fields be declared open in their module definitions. 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 to see if the field uses the annotation @jsonField. If so, we determine the name of the field (either by explicit name or default name provided in the @JsonField annotation) and record the name and field value in the map we constructed earlier. After processing all the fields, we transform the field name mapping into A JSON string.

After processing all the records, we combine all of these strings with commas. This produces a string “< fieldName1 >” : “< fieldValue1 >”, “< fieldName2 >” : “< fieldValue2 >”,… . Once the string is concatenated, we enclose it in curly braces to create a valid JSON string.

To test the serializer, we can execute the following code:

Car car=new Car("Ford"."F150"."2018");
JsonSerializer serializer=new JsonSerializer();
serializer.serialize(car); Copy the code

Output:

{"model":"F150"."manufacturer":"Ford"}Copy the code

As expected, the MAKER and Model fields of the Car object have been serialized, using the name of the field as the key and the value of the field as the value. Note that the order of the JSON elements might be reversed from the output seen above. This happens because there is no clear sort for the class’s array of declared fields, as described in the getDeclaredFields document:

Returns an array in which the elements are unsorted and not in any particular order.

Because of this limitation, the order of elements in a JSON string may differ. To make the order of elements deterministic, we must impose our own ordering. Because JSON objects are defined as an unordered set of key-value pairs, there is no mandatory ordering according to the JSON standard. Note, however, that the test case for the serialization method should output {“model”:”F150″,”manufacturer”:”Ford”} or {“manufacturer”:”Ford”,”model”:”F150″}.

conclusion

Java annotations are a very powerful feature of the Java language, but for the most part, we use standard annotations (such as @Override) or generic framework annotations (such as @AutoWired), not developers. While annotations should not be used instead of an object-oriented approach, they can greatly simplify repeating logic. For example, we could annotate each serializable field instead of creating a toJsonString method in the interface and all the serializable classes that implement the interface. It also separates the serialization logic from the domain logic, removing the clutter of manual serialization from the simplicity of the domain logic.

Although custom annotations are not often used in most Java applications, it is a feature that any intermediate or advanced user of the Java language needs to be aware of. Knowledge of this feature not only strengthens the developer’s knowledge base, but also helps to understand common annotations found in the most popular Java frameworks.

Click on the link to the original English text

More articles are welcome to visit http://www.apexyun.com

Public id: Galaxy 1

Contact email: [email protected]

(Please do not reprint without permission)