The Thinking Logic of Computer Programs (85) - Annotations

Upper segment We discussed how to get annotation information in reflective and reflective related classes. We also mentioned annotations many times in the previous chapters. What exactly are annotations?

In Java, annotation is to add some information to a program, beginning with the character @, which is used to modify other code elements immediately behind it, such as classes, interfaces, fields, methods, parameters in methods, construction methods, etc. Annotations can be used by compilers, program runtime, and other tools to enhance. Or modify procedural behavior, etc. That's abstract. Let's take a look at some of Java's built-in annotations.

Built-in annotations

Java has built-in annotations such as @Override, @Deprecated, @SuppressWarnings, which we will briefly introduce.

@Override

@ Override modifies a method that is not declared first by the current class, but declared in a parent class or an interface implemented. The current class "overrides" the method, such as:

static class Base {
    public void action() {};
}

static class Child extends Base {
    @Override
    public void action(){
        System.out.println("child action");
    }

    @Override
    public String toString() {
        return "child";
    }
}

Child's action() overrides action() in the parent class Base, and toString() overrides toString() in the Object class. This commentary does not change the nature of these methods as "rewriting". What's the use? It can reduce some programming errors. If the method has an Override annotation, but no parent class or implemented interface declares the method, the compiler will report an error and force the programmer to fix the problem. For example, in the above example, if a programmer changes the action method definition in the Base method, it becomes:

static class Base {
    public void doAction() {};
}

However, the programmer forgot to modify the Child method. Without the Override annotation, the compiler would not report any errors. It would think that the action method is a new method added by Child, and the doAction would call the method of the parent class, which is not in line with the programmer's expectations. With the Override annotation, the compiler would report errors. So, if the method is defined in the parent class or interface, add @Override, and let the compiler help you reduce errors.

@Deprecated

@ Deprecated can be decorated in a wide range, including classes, methods, fields, parameters, etc. It means that the corresponding code is outdated and should not be used by programmers. However, it is a warning, not a mandatory one. In IDE s such as Eclipse, a deletion line is added to the Deprecated element to indicate a warning, such as Date. Many of them are outdated:

@Deprecated
public Date(int year, int month, int date)
@Deprecated
public int getYear()

By invoking these methods, the compiler also displays deletion lines and warnings, such as:

When declaring an element as @Deprecated, you should annotate the alternative in a Java document at the same time. Like API documents in Date, when calling the @Deprecated method, you should first consider the proposed alternative.

@SuppressWarnings

@ SuppressWarnings means suppressing Java compilation warnings. It has a mandatory parameter to indicate which type of warning to suppress. It can also modify most code elements. More extensive modifications also work on internal elements. For example, annotations on classes affect methods, and annotations on methods affect code. That's ok. For the call to the Date method above, if you don't want to display a warning, you can do this:

@SuppressWarnings({"deprecation","unused"})
public static void main(String[] args) {
    Date date = new Date(2017, 4, 12);
    int year = date.getYear();
}

In addition to these built-in annotations, Java does not provide us with more annotations that can be used directly. The annotations we use in our daily development are basically customized. However, they are not generally defined by us, but by various frameworks and libraries. We mainly use them directly according to their documents.

Annotations to frameworks and libraries

Various frameworks and libraries define a large number of annotations. Programmers use these annotations to configure frameworks and libraries and interact with them. Let's look at some examples.  

Jackson

stay 63 quarter We introduced the general serialization library Jackson and how to customize serialization with annotations, such as:

  • Ignore fields using @JsonIgnore and @JsonIgnore Properties configurations
  • Use @JsonManagedReference and @JsonBackReference to configure cross-reference relationships
  • Configure the name and format of fields using @JsonProperty and @JsonFormat, etc.

Before Java provides annotation function, the same configuration function can also be achieved, generally through configuration files, but the configuration items and the program elements to be configured are not in one place, it is difficult to manage and maintain, the use of annotations is much simpler, code and configuration together, at a glance, easy to understand and maintain.

Dependency injection container

Modern Java development often uses a framework to manage the life cycle of objects and their dependencies. This framework is generally called DI(Dependency Injection) container. DI refers to dependency injection. The popular frameworks include Spring, Guice and so on. When using these frameworks, programmers usually create objects not through new, but by container management. The creation of images does not require self-management for dependent services, but uses annotations to express dependencies. There are many advantages of this method. The code is simpler and more flexible. For example, the container can return a dynamic proxy according to its configuration to implement AOP, which will be introduced in the following chapters.

For a simple example, Guice defines the Inject annotation, which can be used to express dependencies, such as the following:

public class OrderService {
    
    @Inject
    UserService userService;
    
    @Inject
    ProductService productService;
    
    //....
}

Servlet 3.0

Servlet is Java's technical framework for Web applications. Early Servlets were configurable only in web.xml, while Servlet 3.0 began to support annotations. You can configure a class as Servlet using @WebServlet, for example:

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {...}

Web application framework

In Web development, the typical architecture is MVC(Model-View-Controller). The typical requirement is to configure which method handles which URL and which HTTP method, and then map HTTP request parameters to Java method parameters. Various frameworks such as Spring MVC, Jersey and so on support the use of annotations for configuration, for example, using one of Jersey's. The configuration example is:

@Path("/hello")
public class HelloResource {
    
    @GET
    @Path("test")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> test(
            @QueryParam("a") String a) {
        Map<String, Object> map = new HashMap<>();
        map.put("status", "ok");
        return map;
    }
}

The HelloResource class handles all requests under the root path / Hello of the Jersey configuration, while the test method handles the GET request of / hello/test in JSON format, automatically mapping the HTTP request parameter a to the method parameter String a.

Magic Notes

Through the above examples, we can see that annotations seem to have some magical power, through a simple statement, you can achieve some effect. In some ways, it's similar to ours. 62 quarter The introduction of serialization, serialization mechanism through a simple Serializable interface, Java can automatically handle many complex things. It's also similar to what we introduced in the concurrency section. synchronized keyword It can automatically achieve synchronous access.

These are declarative programming styles, in which the program consists of three components:

  1. Keyword and grammar of declaration itself
  2. Systems/frameworks/libraries, which interpret and execute declarative statements
  3. Application, written in a declarative style

In the world of programming, SQL language for accessing database, CSS for writing web page style, regular expression and functional programming, which will be introduced in the following chapters, are all this style. This style reduces the difficulty of programming, provides more advanced language for programmers, and enables programmers to be more abstract. Think and solve problems at the level, not at the bottom of the details.

Create Note

How do frameworks and libraries implement annotations? Let's look at the creation of annotations.  

@ Definition of Override

Let's start with the definition of @Override:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

Defining annotations is somewhat similar to defining interfaces, using interfaces, but annotated interfaces are much earlier than @ and there are two meta-annotations @Target and @Retention, which are dedicated to defining annotations themselves.

@Target

@ Target represents the goal of the annotation, @Override's goal is the method (ElementType.METHOD), ElementType is an enumeration, and other optional values are:

  • TYPE: Represents classes, interfaces (including annotations), or enumeration declarations
  • FIELD: Fields, including enumeration constants
  • METHOD: METHOD
  • PARAMETER: Parameters in the Method
  • CONSTRUCTOR: Construction Method
  • LOCAL_VARIABLE: Local variable
  • ANNOTATION_TYPE: Annotation Type
  • PACKAGE:Pack

Targets can be multiple, expressed in {}, such as @Target of @SuppressWarnings, defined as:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

If @Target is not declared, it defaults to apply to all types.

@Retention

@ Retention denotes when annotation information is retained, and only one value can be taken. The type is Retention Policy. It is an enumeration with three values:

  • SOURCE: Keep only in source code, and the compiler will discard the code when it compiles it into a bytecode file
  • CLASS: Keep in bytecode files, but Java virtual machines do not always keep class files in memory when they load them into memory
  • RUNTIME: Reserved until runtime

If @Retention is not declared, the default is CLASS.

@ Override and @SuppressWarnings are for compilers, so @Retention is RetentionPolicy.SOURCE.

Define parameters

You can define parameters for annotations by defining methods within the annotations, such as the method value defined in @SuppressWarnings, and the return value type represents the type of the parameter. Here is String []. Value must be provided when using @SuppressWarnings, for example:

@SuppressWarnings(value={"deprecation","unused"})

When there is only one parameter and the name is value, the "value=" can be omitted when the parameter value is provided, that is, the code above can be abbreviated as:

@SuppressWarnings({"deprecation","unused"})

The types of parameters in annotations are not everything. The legitimate types are basic types, String, Class, enumeration, annotations, and arrays of these types.

When defining parameters, default can be used to specify a default value, such as the definition of Inject annotation in Guice:

@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {
  boolean optional() default false;
}

It has an optional parameter with a default value of false. If the type is String, the default value can be ", but not null. If parameters are defined and default values are not provided, specific values must be provided when annotations are used, not null.

@ Inject has an additional meta-annotation @Documented, which indicates that annotation information is included in Javadoc.

@Inherited

Unlike interfaces and classes, annotations cannot be inherited. But what does annotation mean when it has a meta annotation @Inherited related to inheritance? Let's take an example:

public class InheritDemo {
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    static @interface Test {
    }
    
    @Test
    static class Base {
    }
    
    static class Child extends Base {
    }
    
    public static void main(String[] args) {
        System.out.println(Child.class.isAnnotationPresent(Test.class));
    }
}

Test is a comment. The class Base has the comment. Child inherits Base but does not declare the comment. The main method checks whether the Child class has a test comment. The output is true because Test has the comment @Inherited. If it is removed, the output becomes false.

View Annotation Information

If you create annotations, you can use them in the program, annotate the specified target, and provide the required parameters, but this will not affect the operation of the program. To influence the program, we need to be able to view this information first. We mainly consider the @Retention annotation for RetentionPolicy.RUNTIME, which uses reflection mechanism to view and utilize this information at runtime.

stay Upper segment In this article, we refer to the methods related to annotations in reflective related classes. In summary, there are the following methods in Class, Field, Method, Constructor:

//Get all the annotations
public Annotation[] getAnnotations()
//Get all annotations declared directly on this element, ignore inherited Coming
public Annotation[] getDeclaredAnnotations()
//Gets a comment of the specified type, but does not return null
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
//Determine whether there are specified types of annotations
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)

Annotation is an interface that represents annotations, specifically defined as:

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    //Returns the true annotation type
    Class<? extends Annotation> annotationType();
}

In fact, all Annotation types, when implemented internally, are extended Annotations.

For Method and Constructor, they both have method parameters and parameters can be annotated, so they all have the following methods:

public Annotation[][] getParameterAnnotations()

The return value is a two-dimensional array. Each parameter corresponds to a one-dimensional array. Let's take a simple example.

public class MethodAnnotations {
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    static @interface QueryParam {
        String value();
    }
    
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    static @interface DefaultValue {
        String value() default "";
    }
    
    public void hello(@QueryParam("action") String action,
            @QueryParam("sort") @DefaultValue("asc") String sort){
        // ...
    }
    
    public static void main(String[] args) throws Exception {
        Class<?> cls = MethodAnnotations.class;
        Method method = cls.getMethod("hello", new Class[]{String.class, String.class});
        
        Annotation[][] annts = method.getParameterAnnotations();
        for(int i=0; i<annts.length; i++){
            System.out.println("annotations for paramter " + (i+1));
            Annotation[] anntArr = annts[i];
            for(Annotation annt : anntArr){
                if(annt instanceof QueryParam){
                    QueryParam qp = (QueryParam)annt;
                    System.out.println(qp.annotationType().getSimpleName()+":"+ qp.value());
                }else if(annt instanceof DefaultValue){
                    DefaultValue dv = (DefaultValue)annt;
                    System.out.println(dv.annotationType().getSimpleName()+":"+ dv.value());
                }
            }
        }
    }
}

Two annotations @QueryParam and @DefaultValue are defined to modify method parameters. Method hello uses these two annotations. In main method, we demonstrate how to obtain annotation information for method parameters. The output is:

annotations for paramter 1
QueryParam:action
annotations for paramter 2
QueryParam:sort
DefaultValue:asc

The code is relatively simple, let's not go into details.

Annotations are defined, and annotation information is retrieved by reflection, but how can we use this information? Let's look at two simple examples, one is custom serialization and the other is DI containers.

Application Annotation-Customization Serialization

Definition Notes

Upper segment We demonstrate a simple generic serialization class, SimpleMapper, in which the format is fixed when converting objects into strings. This section demonstrates how to customize the output format. We implement a simple SimpleFormatter class, which has a method:

public static String format(Object obj)

We define two annotations, @Label and @Format, @Label for customizing the name of the output field, and @Format for defining the output format of the date type. They are defined as follows:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Label {
    String value() default "";
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface Format {
    String pattern() default "yyyy-MM-dd HH:mm:ss";
    String timezone() default "GMT+8";
}

Use annotations

These two annotations can be used to modify the class fields to be serialized, such as:

static class Student {
    @Label("Full name")
    String name;
    
    @Label("Date of birth")
    @Format(pattern="yyyy/MM/dd")
    Date born;
    
    @Label("Fraction")
    double score;

    public Student() {
    }

    public Student(String name, Date born, Double score) {
        super();
        this.name = name;
        this.born = born;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", born=" + born + ", score=" + score + "]";
    }
}

We can use SimpleFormatter in this way:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Student zhangsan = new Student("Zhang San", sdf.parse("1990-12-12"), 80.9d);
System.out.println(SimpleFormatter.format(zhangsan));

The output is:

Name: Zhang San
 Date of birth: 1990/12/12
 Score: 80.9

Using annotation information

As you can see, the output uses a custom field name and date format. How does SimpleFormatter.format() use these annotations? Let's look at the code:

public static String format(Object obj) {
    try {
        Class<?> cls = obj.getClass();
        StringBuilder sb = new StringBuilder();
        for (Field f : cls.getDeclaredFields()) {
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }
            Label label = f.getAnnotation(Label.class);
            String name = label != null ? label.value() : f.getName();
            Object value = f.get(obj);
            if (value != null && f.getType() == Date.class) {
                value = formatDate(f, value);
            }
            sb.append(name + ": " + value + "\n");
        }
        return sb.toString();
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

For a field of date type, formatDate is called with the code:

private static Object formatDate(Field f, Object value) {
    Format format = f.getAnnotation(Format.class);
    if (format != null) {
        SimpleDateFormat sdf = new SimpleDateFormat(format.pattern());
        sdf.setTimeZone(TimeZone.getTimeZone(format.timezone()));
        return sdf.format(value);
    }
    return value;
}

These codes are relatively simple, so we won't explain them.

Application Annotation - DI Container

Define @SimpleInject

Let's take another example of a simple DI container. We introduce a comment @SimpleInject to modify the fields in the class to express dependencies, which is defined as:

@Retention(RUNTIME)
@Target(FIELD)
public @interface SimpleInject {
}

Use @SimpleInject

Let's look at two simple services, Service A and Service B, which depend on Service B and are defined as:

public class ServiceA {

    @SimpleInject
    ServiceB b;
    
    public void callB(){
        b.action();
    }
}

public class ServiceB {

    public void action(){
        System.out.println("I'm B");
    }
}

Service A uses @SimpleInject to express its dependency on Service B.

The class of DI container is SimpleContainer, which provides a method:

public static <T> T getInstance(Class<T> cls) 

The application uses this method to obtain an object instance, rather than its own new, as follows:

ServiceA a = SimpleContainer.getInstance(ServiceA.class);
a.callB();

Using @SimpleInject

SimpleContainer.getInstance creates the required objects and configures the dependencies. The code is:

public static <T> T getInstance(Class<T> cls) {
    try {
        T obj = cls.newInstance();
        Field[] fields = cls.getDeclaredFields();
        for (Field f : fields) {
            if (f.isAnnotationPresent(SimpleInject.class)) {
                if (!f.isAccessible()) {
                    f.setAccessible(true);
                }
                Class<?> fieldCls = f.getType();
                f.set(obj, getInstance(fieldCls));
            }
        }
        return obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

The code assumes that each type has a public default constructor, uses it to create objects, then looks at each field, and if there is a SimpleInject annotation, gets an instance of that type based on the field type and sets the value of the field.

Define @SimpleSingleton

In the above code, every time you get a type of object, you will create a new object. In actual development, this may not be the desired result. The expected pattern may be a singleton, that is, each type only creates an object, which is shared by all the accessed code. How to meet this requirement? We add an annotation @SimpleSingleton to modify the class to indicate that the type is singleton, defined as follows:

@Retention(RUNTIME)
@Target(TYPE)
public @interface SimpleSingleton {
}

Use @SimpleSingleton

We can modify Service B in this way:

@SimpleSingleton
public class ServiceB {

    public void action(){
        System.out.println("I'm B");
    }
}

Using @SimpleSingleton

SimpleContainer also needs to be modified by adding a static variable to cache the created singleton object:

private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();

getInstance also needs to be modified, as follows:

public static <T> T getInstance(Class<T> cls) {
    try {
        boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class);
        if (!singleton) {
            return createInstance(cls);
        }
        Object obj = instances.get(cls);
        if (obj != null) {
            return (T) obj;
        }
        synchronized (cls) {
            obj = instances.get(cls);
            if (obj == null) {
                obj = createInstance(cls);
                instances.put(cls, obj);
            }
        }
        return (T) obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

First, check whether the type is singleton, and if not, call createInstance directly to create the object. Otherwise, check the cache, if any, return directly, and if not, call createInstance to create the object and put it in the cache.

createInstance is similar to the first version of getInstance in that the code is:

private static <T> T createInstance(Class<T> cls) throws Exception {
    T obj = cls.newInstance();
    Field[] fields = cls.getDeclaredFields();
    for (Field f : fields) {
        if (f.isAnnotationPresent(SimpleInject.class)) {
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }
            Class<?> fieldCls = f.getType();
            f.set(obj, getInstance(fieldCls));
        }
    }
    return obj;
}

Summary

This section describes annotations in Java, including their use, custom annotations, and application examples.

Annotation improves the expressive ability of Java language, effectively realizes the separation of application function and underlying function. Framework/library programmers can concentrate on the underlying implementation, use reflection to achieve common functions, provide annotations for application programmers to use, and application programmers can concentrate on application function through simple declarative annotations. Collaborate with frameworks/libraries.

In the next section, we will explore a more dynamic and flexible mechanism in Java, dynamic proxy.

(As in other chapters, all of the code in this section is located in https://github.com/swiftma/program-logic Located under package shuo.laoma.dynamic.c85)

----------------

To be continued, check the latest articles, please pay attention to the Wechat public number "Lao Ma Says Programming" (scanning the two-dimensional code below), from the entry to advanced, in-depth shallow, Lao Ma and you explore the essence of Java programming and computer technology. Be original and reserve all copyright.

Keywords: Java Programming Spring Eclipse

Added by tave on Tue, 02 Jul 2019 02:36:09 +0300