Yuan notes

To customize the development of annotations, the first thing to know is meta-annotations. Meta-annotations can be simply understood as annotations that modify annotations, mainly including the following four:

@Documented

Documented is a marked annotation with no member. Annotations decorated with this annotation are logged by the Javadoc tool. By default, documentation generated using the Javadoc command does not include information about annotations.

@Target

The @target annotation is used to specify the scope of the annotation that it modifies. Its value range is defined in the ElementType enumeration, which can refer to the annotation in the enumerated class:

public enum ElementType {
 
    TYPE, // Class, interface, enumeration class
 
    FIELD, // Member variables (including enumeration constants)
 
    METHOD, // Member methods
 
    PARAMETER, // Method parameters
 
    CONSTRUCTOR, // constructor
 
    LOCAL_VARIABLE, // Local variables
 
    ANNOTATION_TYPE, // Annotation class - somewhat similar to the scope of a meta-annotation
 
    PACKAGE, // Can be used to modify: package
 
    TYPE_PARAMETER, // Type parameter, new in JDK 1.8
 
    TYPE_USE // Use type anywhere, new in JDK 1.8
 
}
Copy the code

@ Retention annotations

The @reteniton annotation defines the lifetime of the annotation class it modifies. There are three policies, defined in the RetentionPolicy enumeration.

public enum RetentionPolicy {
    
    /** * Annotations are to be discarded by the compiler. Annotations remain in the source file only, and are discarded when a Java file is compiled into a class file. * /
    SOURCE,
 
    /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time.  This is the default * behavior. Annotations are kept in the class file, but are discarded when the JVM loads the class file, which is the default lifecycle; * /
    CLASS,
 
    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @seeJava. Lang. Reflect. AnnotatedElement annotation not only be saved to a class file, the JVM to load the class files, still existed; * /
    RUNTIME
        
    // These three life cycles correspond to: Java source files (.java files) -->.class files --> bytecode in memory.
        
}
Copy the code

@Inheritedannotations

The @Inherited annotations are used to make the annotations Inherited. If a class uses the annotations modified by @Inherited, its child classes inherit the annotations.

Custom @valid related annotation

Refer to the @valid annotation

The spring-boot-starter dependency will be included in the following version and will be introduced separately in the new version.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Copy the code

Code sample

// ==== create a User class =====

@Data
public class User {
 
    private String username;

    @notblank (message = "password cannot be blank ")
    private String password;

    private Integer id;

    @past (message = "date of birth cannot be in the future ")
    private Date birthday;
}


//====== verifies that the attribute of the user object ======= is received

@RestController 
@Slf4j 
public class UserController {
 
    @PostMapping("/user")
    public Boolean postUser(@Valid @RequestBody User user, BindingResult bindingResult){
        
        // Validation failed to print an error message
        if(bindingResult.hasErrors()){
            // A typo
            log.error("error={}",bindingResult.getFieldError().getDefaultMessage());
        }
       
        returnbindingResult.hasErrors(); }}// ==== The parameter passed in =======

{
  "birthday": "The 2025-09-13 T01: but 148 z".// Greater than the current date
  "id": 0."password": "".// The password is empty
  "username": "string"
}

// ===== Logs printed ======

2020- 09 -10 09:24:49.517 ERROR 21968 --- [nio-7777-exec-9] C.A.C ontroller. CreateDataController: error = date of birth cannot be in the futureCopy the code

As you can see from the sample code, the @VALID annotation can be used in conjunction with the BindingResult object to validate the parameters of the passed object. Another annotation with similar functions is @Validated. The major difference between the two annotations is that the former does not support group verification and the latter does. If in doubt can refer to: https://www.cnblogs.com/xiaoqi/p/spring-valid.html.

Custom @valid related annotation

@Constraint

Qualify custom annotations to specify the processing logic for custom annotations.

Custom PhoneNumber annotations

package com.example.annotation;

import org.springframework.util.StringUtils;

import javax.validation.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.regex.Pattern;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/** * Custom validate annotation *@author WT
 * @date2022/01/02 13:26:42 * /

// Can be logged by javadoc
@Documented
// Specify a class for custom annotation processing logic
@Constraint(validatedBy = { PhoneNumber.PhoneNumberValidator.class })
// Life cycle to runtime
@Retention(RUNTIME)
// What can it be used for
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
public @interface PhoneNumber {

    // Failure message returned when the isValid method of the class specified by the processing logic returns false
    String message(a) default"Mobile phone number verification failed";

    // constraint groupingClass<? >[] groups()default {};

    Class<? extends Payload>[] payload() default {};

    The first argument is the name of the annotation and the second argument is the argument to the annotation validation
    class PhoneNumberValidator implements ConstraintValidator<PhoneNumber.CharSequence> {

        /** * mobile phone */
        private final static String PHONE_NUMBER_REGEX = "1[3456789]\d{9}";

        @Override
        public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
            if(! StringUtils.hasText(value)) {return false;
            }
            returnPattern.compile(PHONE_NUMBER_REGEX).matcher(value).matches(); }}}Copy the code

Use custom annotations

// === entity class === =

package com.example.pojo;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.annotation.PhoneNumber;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.math.BigDecimal;

/** *@Validate
 * @author WT
 * @date2022/01/02 12:53:41 * /
@Data
@TableName("b_coin_account_alarm_data_month_yyyymm")
public class TestTable {

    @TableId
    @notnull (message = "ID cannot be empty ")
    private Long id;
    private String accountId;
    private String strategyId;
    @PhoneNumber
    private String month;
    private String mobile;
    private String tag;
    private String measurement;
    private BigDecimal data;

}



// === used in controller

@PostMapping ("/test/valid")
public void testValid (@Valid @RequestBody TestTable testTable, BindingResult bindingResult) {

    if (bindingResult.hasErrors()) {
        // A typo
        log.error("== error: [{}]",bindingResult.getFieldError().getDefaultMessage()); }}Copy the code

Custom annotations

The previous section introduced custom validate-related annotations, but the usage scenarios are too limited. The above example is used as a brief introduction to custom annotations. Let’s customize a general annotation and show you how to use custom annotations in general.

Scene description

A developed system wants to add authentication information. When other services call the service, the verification operation needs to be performed.

The following requirements must be met:

  1. Do not change any logic of the original code
  2. You can specify which clients are accessible
  3. Check further for clients that have access permission

The solution

Use AOP in conjunction with custom annotations to fulfill this requirement.

Maven rely on

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Copy the code

Yaml configuration

auth:
  Client 1 configuration
  client1:
    Example: md5crypt.apr1crypt ("client1-secret", "salt1")
    appSecret: $apr1$salt1$EMKCT8e1YK6HkS0lR2Onv/
    # salt used for MD5 encryption
    salt: salt1
  Client 2 configuration
  client2:
    Add salt to md5 for client2-secret
    appSecret: $apr1$salt2$JnMmVb96AOcSz2kObzLc90
    # salt used for MD5 encryption
    salt: salt2
Copy the code

The configuration class

package com.example.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/** * Get information about the auth in the configuration file **@author WT
 * @date2022/01/02 15:10:59 * /
@Data
@Configuration
@ConfigurationProperties("auth")
public class AuthConfig {

    private ClientConfig client1;
    private ClientConfig client2;

    @Data
    public static class ClientConfig {
        private String appSecret;
        privateString salt; }}Copy the code

Custom annotations

package com.example.annotation;

import java.lang.annotation.*;

/** * Custom annotation, when marked by this annotation, requires authentication to access the corresponding interface * authentication rule: Specifying appKeyArray in annotations indicates which clients have access to this interface, passing appKey and Secret in the header, using AOP to verify that secret in the request header is correct, and allowing access to * * if it is@author WT
 * @date2022/01/02 13:45:29 * /
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface EnableAuth {

    // Specify which clients have access to the interface being decorated
    String[] appKeyArray() default "";

}
Copy the code

AOP handles logical classes

package com.example.aop;

import com.example.annotation.EnableAuth;
import com.example.aop.exception.AuthException;
import com.example.config.AuthConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.Md5Crypt;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/** * Specifies the section class **@author WT
 * @date2022/01/02 14:34:25 * /
@Slf4j
@Aspect
@Component
public class AuthAspect {

    private final AuthConfig authConfig;

    public AuthAspect (AuthConfig authConfig) {
        this.authConfig = authConfig;
    }

    /** * Specifies the pointcut that defines the place where Advice occurs as being@EnableAuthMethod of modification */
    @Pointcut("@annotation(com.example.annotation.EnableAuth)")
    public void auth(a) {}/** * Specify pre-advice enhancement *@paramThe enableAuth flag requires the annotation of the enhanced method */
    @Before("@annotation(enableAuth)")
    public void beforeAdvice(EnableAuth enableAuth) {

        log.info("Go to the beforeAdvice method and start auth operation");

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (requestAttributes == null) {
            throw new AuthException("Non-httpservletrequest request");
        }

        HttpServletRequest request = requestAttributes.getRequest();

        String[] appKeyArray = enableAuth.appKeyArray();
        if (appKeyArray == null || appKeyArray.length == 0) {
            throw new AuthException(AppKeyArray cannot be empty);
        }

        String headerAppKey = request.getHeader("appKey");
        String headerSecret = request.getHeader("secret");

        if(! StringUtils.hasText(headerAppKey) || ! StringUtils.hasText(headerSecret)) {throw new AuthException("Header header headerAppKey or headerSecret is empty.");
        }

        boolean flag;

        // Verify that the client requesting this interface has access permission
        if(! Arrays.asList(appKeyArray).contains(headerAppKey)) { log.error("Clients with appKey: {} do not have permission to access this interface", headerAppKey);
            throw new AuthException("AppKey is:" + headerAppKey + "The client does not have permission to access this interface");
        }

        / / check appSecret
        switch (headerAppKey) {
            case "client1": {
                AuthConfig.ClientConfig client1 = authConfig.getClient1();

                flag = validateSecret(headerSecret, authConfig.getClient1().getSalt(), authConfig.getClient1().getAppSecret());
                break;
            }
            case "client2": {
                flag = validateSecret(headerSecret, authConfig.getClient1().getSalt(), authConfig.getClient2().getAppSecret());
                break;
            }
            default: flag = false;
        }

        if(! flag) { log.error("Failed to verify headerSecret for {}", headerAppKey);
            throw new AuthException("Check" + headerAppKey + "HeaderSecret failed");
        }

        log.info("== check {} headerSecret successfully", headerAppKey);

    }

    private static boolean validateSecret (String secret, String salt, String appSecret) {
        returnappSecret.equals(Md5Crypt.apr1Crypt(secret, salt)); }}Copy the code

Use custom annotations for permission control

@EnableAuth(appKeyArray = {"client1", "client2"})
@GetMapping("/test/auth")
public void testAuth (a) {
    log.info("== successful call to testAuth method");
}
Copy the code