Java business verification tool implementation

Posted by mahenderp on Wed, 17 Jun 2020 04:34:10 +0200

1, Background

In our daily interface development process, we may have to face some slightly more complex business logic code writing. Before executing the real business logic, we often need to carry out a series of pre verification work, which can be divided into parameter validity verification and business data verification.

Parameter validity verification, such as the most common verification parameter value non null verification, format verification, maximum value minimum value verification, can be realized through the Hibernate Validator framework, which is not specifically explained in this article. Business data verification is usually related to the actual business, such as the order submission interface. We may need to verify whether the goods are legal, whether the inventory is sufficient, whether the customer balance is sufficient, and some other risk control verification. Our code might look like this:

public ApiResult<OrderSubmitVo> submitOrder(OrderSubmitDto orderSubmitDto) {
    // Business verification 1

    // Business verification 2

    // Business verification 3

    // Service check n

    // Execute real business logic

    return ApiResult.success();
}

2, Questions

  • Not elegant enough

In the process of version iteration, the above codes may continue to add / modify some verification logic. If the business logic verification codes are all coupled in the core business logic, this implementation is not elegant enough, and does not conform to the single responsibility principle and open close principle of the design principle.

  • Verification code cannot be reused

If a business verification code needs to be used in other businesses, we need to copy the same code to the business code, such as verifying the user status, which needs to be verified in many business verifications. If the verification logic has some changes, all the involved places need to be modified synchronously, which is not conducive to system maintenance.

  • The verification logic cannot be executed in order, and the subsequent data generated in the verification process is inconvenient to obtain

If we encapsulate the verification logic in the above code into independent sub methods, it is possible that business verification 2 depends on the data result of business verification 1, and the data generated in the process of business verification needs to be used in the subsequent implementation of real business logic.

3, Realization of calibration tools

The verification tool we need to write should solve at least three problems mentioned above

  • Decoupling of business verification code and core business logic code
  • The same verifier can be used for multiple services to improve code reusability and maintainability
  • The verification code can be executed in the specified order, and the data generated in the verification process can be transferred later

When using zuul as a gateway service, I got some inspiration,
filterType in zuul is used to distinguish the types before the request is routed to the target, after the target request is processed and after the target request is returned. filterOrder is used to specify the execution order of the filter. RequestContext is the request context. RequestContext inherits from ConcurrentHashMap, and is bound with ThreadLocal to ensure thread safety. The data in the request context is in all the filters of a request It can be obtained in and completes the data transmission well.

First, we need to define a validator annotation, which specifies the business type and execution order. Adding the annotation to the validator indicates that it is a validator. Define a verifier context, and the data generated during the execution of business verification can be passed through the context. Define a verifier base class. The verifier inherits the base class and implements the specific verification methods. Define a unified actuator of verifier. The actuator can find out all verifier lists with verifier annotation and specified service type according to the service type. After sorting according to the execution order in the verifier annotation, traverse all verifier lists and call the verification method. If the verification fails in the verification process, a verification exception will be thrown to interrupt the business execution.

The above is the general implementation idea, and the specific implementation code is as follows:

4, show me your code

  • Validator.java
import java.lang.annotation.*;

/**
 * @author: Dancing robot
 * @date: 2019/10/23 13:58
 * @description: Business verification notes
 */
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Validator {
    /**
     * Business type. Multiple business types can be specified for the same verifier
     *
     * @return
     */
    String[] validateTypes();

    /**
     * Execution order: the smaller the value, the earlier the execution
     *
     * @return
     */
    int validateOrder();
}

Validator verification annotation: add this annotation to the class of validator to indicate that it is a business validator. validateTypes indicates the business type. The same validator can specify multiple business types. Multiple business types can reuse the same validator. validateOrder indicates the execution sequence. The smaller the value, the earlier the execution.

  • ValidatorContext.java

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: Dancing robot
 * @date: 2019/9/11 14:56
 * @description: Validator context, bound to current thread
 */
public class ValidatorContext extends ConcurrentHashMap<String, Object> {
    /**
     * Request object
     */
    public Object requestDto;

    protected static final ThreadLocal<? extends ValidatorContext> threadLocal = ThreadLocal.withInitial(() -> new ValidatorContext());

    /**
     * Get the context of the current thread
     *
     * @return
     */
    public static ValidatorContext getCurrentContext() {
        ValidatorContext context = threadLocal.get();
        return context;
    }


    /**
     * Set value
     *
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        if (value != null) put(key, value);
        else remove(key);
    }


    /**
     * Get String value
     *
     * @param key
     * @return
     */
    public String getString(String key) {
        return (String) get(key);
    }

    /**
     * Get Integer value
     *
     * @param key
     * @return
     */
    public Integer getInteger(String key) {
        return (Integer) get(key);
    }

    /**
     * Get Boolean value
     *
     * @param key
     * @return
     */
    public Boolean getBoolean(String key) {
        return (Boolean) get(key);
    }

    /**
     * Get object
     *
     * @param key
     * @param <T>
     * @return
     */
    public <T> T getClazz(String key) {
        return (T) get(key);
    }

    /**
     * Get Long value
     *
     * @param key
     * @return
     */
    public Long getLong(String key) {
        return (Long) get(key);
    }


    public <T> T getRequestDto() {
        return (T) requestDto;
    }

    public void setRequestDto(Object requestDto) {
        this.requestDto = requestDto;
    }

ValidatorContext is the request context, bound to the current request thread and inherited from ConcurrentHashMap. The requestDto property is the interface request input parameter object. get/set method is provided to make it more convenient to obtain the request input parameter data in the context.

  • ValidatorTemplate.java

/**
 * @author: Dancing robot
 * @date: 2019/10/23 11:51
 * @description: The business validator needs to inherit the template class
 */
@Slf4j
@Component
public abstract class ValidatorTemplate {

    /**
     * Verification method
     */
    public void validate() {
        try {
            validateInner();
        } catch (ValidateException e) {
            log.error("Business verification failed", e);
            throw e;
        } catch (Exception e) {
            log.error("Business verification exception", e);
            ValidateException validateException = new ValidateException(ResultEnum.VALIDATE_ERROR);
            throw validateException;
        }
    }

    /**
     * Verification method, implemented by subclass
     *
     * @throws ValidateException
     */
    protected abstract void validateInner() throws ValidateException;
}

The verifier is an abstract class. The concrete verifier needs to inherit this class and implement the concrete validateInner verification method.

  • ValidatorTemplateProxy.java


/**
 * @author: Dancing robot
 * @date: 2019/10/25 18:03
 * @description: ValidatorTemplate proxy class
 */
@Data
@AllArgsConstructor
public class ValidatorTemplateProxy extends ValidatorTemplate implements Comparable<ValidatorTemplateProxy> {
    private ValidatorTemplate validatorTemplate;
    private String validateType;
    private int validateOrder;

    @Override
    public int compareTo(ValidatorTemplateProxy o) {
        return Integer.compare(this.getValidateOrder(), o.getValidateOrder());
    }

    @Override
    protected void validateInner() throws ValidateException {
        validatorTemplate.validateInner();
    }
}

The ValidatorTemplate class implements the Comparable sorting interface, which is convenient for the verifier to sort according to the validateOrder attribute, and converts the annotation in the verifier into two attribute fields in the agent class, which is convenient for the unified log printing in the execution process.

  • ValidateProcessor.java
import java.lang.annotation.Annotation;
import java.util.*;

/**
 * @author: Dancing robot
 * @date: 2019/10/25 18:02
 * @description: Actuator
 */
@Slf4j
@Component
public class ValidateProcessor {

    /**
     * Validator corresponding to business type
     *
     * @param validateType
     */
    public void validate(String validateType) {
        if (StringUtils.isEmpty(validateType)) {
            throw new IllegalArgumentException("validateType cannot be null");
        }
        long start = System.currentTimeMillis();
        log.info("start validate,validateType={},ValidatorContext={}", validateType, ValidatorContext.getCurrentContext().toString());
        List<ValidatorTemplateProxy> validatorList = getValidatorList(validateType);
        if (CollectionUtils.isEmpty(validatorList)) {
            log.info("validatorList is empty");
            return;
        }
        ValidatorTemplateProxy validateProcessorProxy;
        for (ValidatorTemplateProxy validatorTemplate : validatorList) {
            validateProcessorProxy = validatorTemplate;
            log.info("{} is running", validateProcessorProxy.getValidatorTemplate().getClass().getSimpleName());
            validatorTemplate.validate();
        }
        log.info("end validate,validateType={},ValidatorContext={},time consuming {} ms", validateType,
                ValidatorContext.getCurrentContext().toString(), (System.currentTimeMillis() - start));
    }


    /**
     * Get all validators with validateType of Validator annotation
     *
     * @param validateType
     * @return
     */
    private List<ValidatorTemplateProxy> getValidatorList(String validateType) {
        List<ValidatorTemplateProxy> validatorTemplateList = new LinkedList<>();
        Map<String, Object> map = SpringUtil.getApplicationContext().getBeansWithAnnotation(Validator.class);
        String[] validateTypes;
        int validateOrder;
        Annotation annotation;
        for (Map.Entry<String, Object> item : map.entrySet()) {
            annotation = item.getValue().getClass().getAnnotation(Validator.class);
            validateTypes = ((Validator) annotation).validateTypes();
            validateOrder = ((Validator) annotation).validateOrder();
            if (item.getValue() instanceof ValidatorTemplate) {
                if (Arrays.asList(validateTypes).contains(validateType)) {
                    validatorTemplateList.add(new ValidatorTemplateProxy((ValidatorTemplate) item.getValue(), validateType, validateOrder));
                }
            } else {
                log.info("{}not extend from ValidatorTemplate", item.getKey());
            }
        }
        Collections.sort(validatorTemplateList);
        return validatorTemplateList;
    }
}

The getValidatorList method gets all validators with the validateType value according to the validateType value, encapsulates them as ValidatorTemplateProxy proxy class, and then sorts them. validate is a unified business verification method.

  • ValidateException.java


/**
 * @author: Dancing robot
 * @date: 2019/4/4 6:34 PM
 * @description: Verification exception
 */
public class ValidateException extends RuntimeException {
    // Exception code
    private Integer code;

    public ValidateException() {
    }

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

    public ValidateException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

}

ValidateException is the business validation exception class thrown when validation fails.

  • ValidateTypeConstant.java

/**
 * @author: Dancing robot
 * @date: 2019/10/30 15:16
 * @description:
 */
public class ValidateTypeConstant {
    /**
     * Submit order verification
     */
    public static final String ORDER_SUBMIT = "order_submit";
}

Validatedtypeconstant is a constant class that defines the validateType business verification type.

5, Working with samples

Taking the order submission as an example, we first define two basic validators, the order product information validator and the customer status validator, which are all pseudo code implementations.

  • OrderSubmitProductValidator.java

/**
 * @author: Dancing robot
 * @date: 2019/10/30 15:34
 * @description: Commodity status and inventory verification
 */
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 1)
public class OrderSubmitProductValidator extends ValidatorTemplate {
    @Override
    protected void validateInner() throws ValidateException {
        ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
        OrderSubmitDto orderSubmitDto = validatorContext.getRequestDto();
        // Obtain product information and verify product status
        List<ProductShelfVo> productShelfVoList = new ArrayList<>();
        if (0 == 1) {
            throw new ValidateException("Goods are off the shelves");
        }
        // Set product information to context
        validatorContext.set("productShelfVoList", productShelfVoList);
    }
}
  • OrderSubmitCustomerValidator.java

/**
 * @author: Dancing robot
 * @date: 2019/10/30 19:24
 * @description:
 */
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 2)
public class OrderSubmitCustomerValidator extends ValidatorTemplate {
    @Override
    protected void validateInner() throws ValidateException {
        ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
        String customerNo = validatorContext.getString("customerNo");
        if (StringUtils.isEmpty(customerNo)) {
            throw new IllegalArgumentException("Customer number is empty");
        }
        // Obtain customer information and verify customer status
        CustomerVo customer = new CustomerVo();
        if (0 == 1) {
            throw new ValidateException("Customer restricted transactions");
        }
    }
}

Use in the code of the business logic to submit the order:

/**
 * place order
 *
 * @param orderSubmitDto
 * @return
 */
public ApiResult<OrderSubmitVo> submitOrder(OrderSubmitDto orderSubmitDto) {
    // Business verification
    ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
    validatorContext.setRequestDto(orderSubmitDto);
    validateProcessor.validate(ValidateTypeConstant.ORDER_SUBMIT);
    // Get order information from context
    List<ProductShelfVo> productShelfVoList = validatorContext.getClazz("productShelfVoList");

    // Subsequent business logic processing
    return ApiResult.success();
}

After using the above encapsulated verification tool, the business code is decoupled from the verification code. When adding / modifying the business verification logic in the future, we only need to add / modify the corresponding verifier instead of changing to the main business logic. In order to make it easier and convenient for us to find all validators corresponding to a certain business logic, we can prefix the business type when naming the validators.

6, Summary

1. In the development process, when we encounter some "annoying" problems, we should find a way to solve it, rather than ignore it. By solving problems, we can improve our technical ability.

2. We should be good at learning from other excellent technical frameworks.

3. The above verification tool is only a simple implementation, and the problem solved is only the problem encountered by the author in the development process, which may not be universal.

If the article is helpful to you, please give it a compliment.

If there is something wrong, please point out.

The first official account: dancing robot, welcome to scan code.

Topics: Java Attribute Hibernate