The Controller layer code is written like this, concise and elegant!

An excellent Controller layer logic

Speaking of Controller, I believe everyone is familiar with it. It can easily provide data interfaces to the outside world. Its positioning, I think, is an “indispensable supporting role”.

It is said that it is indispensable because whether it is the traditional three-tier architecture or the current COLA architecture, the Controller layer still has a place, which shows its necessity.

It is said that it is a supporting role because the code of the Controller layer is generally not responsible for the implementation of specific logic business logic, but it is responsible for receiving and responding to requests.

Looking at the problem from the status quo

The main tasks of the Controller are as follows:

  • Receive request and parse parameters

  • Call Service to execute specific business code (may include parameter verification)

  • Catch business logic exceptions and give feedback

  • The business logic executes successfully and responds

//DTO
@Data
public class TestDTO {
    private Integer num;
    private String type;
}


//Service
@Service
public class TestService {

    public  Double  service (TestDTO testDTO)  throws  Exception  {
         if  (testDTO.getNum() <=  0 ) {
             throw  new  Exception( "The input number needs to be greater than 0" );
        }
        if (testDTO.getType().equals("square")) {
            return Math.pow(testDTO.getNum(), 2);
        }
        if (testDTO.getType().equals("factorial")) {
            double result = 1;
            int num = testDTO.getNum();
            while (num > 1) {
                result = result * num;
                num -= 1;
            }
            return result;
        }
        throw  new  Exception( "Unrecognized algorithm" );
    }
}


//Controller
@RestController
public class TestController {

    private TestService testService;

    @PostMapping("/test")
    public Double test(@RequestBody TestDTO testDTO) {
        try {
            Double result = this.testService.service(testDTO);
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Autowired
    public DTOid setTestService(TestService testService) {
        this.testService = testService;
    }
}

There are several problems with developing Controller code according to the work items listed above:

  • Parameter validation couples business code too much and violates the single responsibility principle

  • The same exception may be thrown in multiple businesses, resulting in code duplication

  • Various abnormal feedback and successful response formats are not uniform, and the interface connection is not friendly

Retrofit Controller layer logic

Unified return structure

It is very necessary to unify the return value type regardless of whether the front-end and back-end of the project are separated, so that developers who connect to the interface can know more clearly whether the call of this interface is successful (you cannot judge whether the call is successful or not simply by looking at whether the return value is null, because Some interfaces are designed that way).

Use a status code and status information to clearly understand the interface invocation:

//Define the return data structure 
public  interface  IResult  {
     Integer  getCode () ;
     String  getMessage () ;
}

//Enumeration of common results 
public  enum  ResultEnum implements IResult {
    SUCCESS( 2001 ,  "Interface call succeeded" ),
    VALIDATE_FAILED( 2002 ,  "Parameter verification failed" ),
    COMMON_FAILED( 2003 ,  "Interface call failed" ),
    FORBIDDEN( 2004 ,  "No permission to access resource" );

    private Integer code;
    private String message;

    //Omit get, set methods and constructors
}

//Unified return data structure 
@Data 
@NoArgsConstructor 
@AllArgsConstructor 
public  class  Result < T >  {
     private  Integer code;
     private  String message;
     private  T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }

    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
    }

    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }

    public static Result<?> failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }

    public static Result<?> failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }

    public static <T> Result<T> instance(Integer code, String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

After the unified return structure, it can be used in the Controller, but each Controller writes such a piece of final encapsulation logic, which is very repetitive work, so it is necessary to continue to find ways to further process the unified return structure.

unified packaging

Spring provides a class ResponseBodyAdvice that can help us achieve the above requirements:

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

ResponseBodyAdvice intercepts the content returned by the Controller before HttpMessageConverter performs type conversion, and then returns the result to the client after the corresponding processing operation.

Then you can put the unified packaging work into this class:

  • supports:  determine whether to hand over to the beforeBodyWrite method for execution, true: required; false: not required

  • beforeBodyWrite:  specific processing of the response

// If the document generation component of swagger or knife4j is introduced, only the package of your own project needs to be scanned here, otherwise the document cannot be generated normally 
@RestControllerAdvice(basePackages =  "com.example.demo" )
 public  class  ResponseAdvice  implements  ResponseBodyAdvice < Object >  {
    @Override
    public  boolean supports(MethodParameter returnType,  Class <?  extends  HttpMessageConverter <?>>  converterType )  {
         // If you don't need to encapsulate, you can add some verification methods, such as adding an annotation to mark exclusion 
        return  true ;
    }


    @Override
    public  Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,  Class <?  extends  HttpMessageConverter <?>>  selectedConverterType ,  ServerHttpRequest  request ,  ServerHttpResponse  response )  {
         // Provide a certain degree of flexibility, if the body has been wrapped, it will not be wrapped 
        if  (body  instanceof  Result) {
             return  body;
        }
        return Result.success(body);
    }
}

After such transformation, the unified packaging of the data returned by the Controller can be achieved without the need to make a lot of changes to the original code.

parameter verification

The Java API specification JSR303 defines the standard validation-api for validation, and one of the more well-known implementations is hibernate validation.

Spring validation is a secondary encapsulation of it. It is often used for automatic parameter verification of SpringMVC. The code of parameter verification does not need to be coupled with the business logic code.

①@PathVariable and @RequestParam parameter verification

The parameter reception of the Get request generally depends on these two annotations, but due to the length limit of the url and the maintainability of the code, if there are more than 5 parameters, try to use the entity to pass the parameters.

Validation of @PathVariable and @RequestParam parameters requires annotations that declare constraints on the input parameters.

If validation fails, a MethodArgumentNotValidException will be thrown.

@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {

    private TestService testService;

    @GetMapping ( "/{num}" )
     public  Integer  detail (@PathVariable( "num" )  @ Min ( 1 )  @ Max ( 20 )  Integer num)  {
         return  num * num;
    }

    @GetMapping("/getByEmail")
    public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
        TestDTO testDTO = new TestDTO();
        testDTO.setEmail(email);
        return testDTO;
    }

    @Autowired
    public void setTestService(TestService prettyTestService) {
        this.testService = prettyTestService;
    }
}

Calibration principle

In SpringMVC, there is a class called RequestResponseBodyMethodProcessor, which has two functions (actually you can get a little inspiration from the name)

  • Parameters for parsing @RequestBody annotations

  • Handling the return value of the @ResponseBody annotated method

The method to resolve @RequestBoyd annotated parameters is resolveArgument.

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
      /**
     * Throws MethodArgumentNotValidException if validation fails.
     * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
     * is {@code true} and there is no body content or if there is no suitable
     * converter to read the content with.
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

      parameter = parameter.nestedIfOptional();
      //Encapsulate the request data into a marked DTO object
      Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
      String name = Conventions.getVariableNameForParameter(parameter);

      if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if  (arg !=  null ) {
           //Perform data verification
          validateIfApplicable(binder, parameter);
          //If the verification fails, throw a MethodArgumentNotValidException exception 
          //If we don't catch it ourselves, it will eventually be captured and processed by the DefaultHandlerExceptionResolver 
          if  (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
             throw  new  MethodArgumentNotValidException(parameter, binder.getBindingResult());
          }
        }
        if (mavContainer != null) {
          mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
      }

      return adaptArgumentIfNecessary(arg, parameter);
    }
}

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  /**
    * Validate the binding target if applicable.
    * <p>The default implementation checks for {@code @javax.validation.Valid},
    * Spring's {@link org.springframework.validation.annotation.Validated},
    * and custom annotations whose name starts with "Valid".
    * @param binder the DataBinder to be used
    * @param parameter the method parameter descriptor
    * @since 4.1.5
    * @see #isBindExceptionRequired
    */ 
   protected  void  validateIfApplicable (WebDataBinder binder, MethodParameter parameter)  {
     //Get all annotations on the parameter
      Annotation[] annotations = parameter.getParameterAnnotations();
      for  (Annotation ann : annotations) {
       //If the annotation contains @Valid, @Validated or an annotation whose name starts with Valid, parameter validation will be performed
         Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
         if  (validationHints !=  null ) {
         //The actual validation logic will eventually call Hibernate Validator to perform the real validation 
        //So Spring Validation is a secondary encapsulation of Hibernate Validation
            binder.validate(validationHints);
            break;
         }
      }
   }
}

②@RequestBody parameter verification

The parameters of Post and Put requests are recommended to use @RequestBody request body parameters.

To verify @RequestBody parameters, you need to add verification conditions to the DTO object, and then use @Validated to complete automatic verification.

If validation fails, ConstraintViolationException will be thrown.

//DTO
@Data
public class TestDTO {
    @NotBlank
    private String userName;

    @NotBlank
    @Length(min = 6, max = 20)
    private String password;

    @NotNull
    @Email
    private String email;
}

//Controller
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {

    private TestService testService;

    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        this.testService.save(testDTO);
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

Calibration principle

The way of declaring constraints, annotations are added to the parameters, it is easier to guess that AOP is used to enhance the method.

In fact, Spring also dynamically registers the AOP aspect through the MethodValidationPostProcessor, and then uses the MethodValidationInterceptor to weave and enhance the pointcut method.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {

    //Specifies the annotation of the Bean that creates the aspect 
   private  Class<? extends Annotation> validatedAnnotationType = Validated.class;

    @Override 
    public  void  afterPropertiesSet ()  {
         //Create a facet for all @Validated marked beans 
        Pointcut pointcut =  new  AnnotationMatchingPointcut( this .validatedAnnotationType,  true );
         //Create an Advisor for enhancement 
        this .advisor =  new  DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice( this .validator));
    }

    //Creating Advice is essentially a method interceptor 
    protected  Advice  createMethodValidationAdvice (@Nullable Validator validator)  {
         return  (validator !=  null  ?  new  MethodValidationInterceptor(validator) :  new  MethodValidationInterceptor());
    }
}

public  class  MethodValidationInterceptor  implements  MethodInterceptor  {
     @Override 
    public  Object  invoke (MethodInvocation invocation)  throws  Throwable  {
         //No need for enhanced methods, just skip 
        if  (isFactoryBeanMetadataMethod(invocation.getMethod())) {
             return  invocation.proceed();
        }

        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try  {
             //Method input parameter verification, and finally entrusted to Hibernate Validator to verify 
             //So Spring Validation is a secondary encapsulation of Hibernate Validation
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //The verification fails to throw ConstraintViolationException 
        if  (!result.isEmpty()) {
             throw  new  ConstraintViolationException(result);
        }
        //Controller method call
        Object returnValue = invocation.proceed();
        //The following is to verify the return value, the process is roughly the same as above
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

③Custom verification rules

Sometimes the validation rules provided in the JSR303 standard do not meet complex business requirements, and you can also customize the validation rules.

Custom validation rules need to do two things:

  • Customize the annotation class, define error messages and some other required content

  • Annotation validator, defining decision rules

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    /**
     * Is it allowed to be empty
     */
    boolean required() default true;

    /**
     * Prompt information returned if the verification fails
     */ 
    String  message ()  default  "Not a phone number format" ;

    /**
     * Attribute required by Constraint, used for grouping checksum expansion, just leave it blank
     */
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

//Annotation validator 
public  class  MobileValidator  implements  ConstraintValidator < Mobile ,  CharSequence >  {

    private boolean required = false;

    private  final  Pattern pattern = Pattern.compile( "^1[34578][0-9]{9}$" );  // Verify phone number

    /**
     * Call the method in the annotation before the verification starts, so as to get some parameters in the annotation
     *
     * @param constraintAnnotation annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(Mobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }

    /**
     * Determine whether the parameter is legal
     *
     * @param value   object to validate
     * @param context context in which the constraint is evaluated
     */
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (this.required) {
            return isMobile(value);
        }
        if (StringUtils.hasText(value)) {
            return isMobile(value);
        }
        return true;
    }

    private boolean isMobile(final CharSequence str) {
        Matcher m = pattern.matcher(str);
        return m.matches();
    }
}

Automatically verifying parameters is really a very necessary and meaningful work. JSR303 provides a wealth of parameter verification rules, plus custom verification rules for complex businesses, which completely decouples parameter verification and business logic, making the code more concise and in line with the single responsibility principle.

Custom exception and unified interception exception

There are several problems that can be seen in the original code:

  • The exception thrown is not specific enough, just simply put the error message in Exception

  • After an exception is thrown, the Controller cannot give feedback specifically based on the exception

  • Although automatic parameter verification is done, the abnormal return structure is inconsistent with the normal return structure

The purpose of custom exceptions is to differentiate the exceptions in the business in a more fine-grained manner when the exceptions are intercepted uniformly later, and make different responses to different exceptions during interception.

The purpose of unified interception of exceptions is to correspond to the unified packaging and return structure defined above, and the other is that we hope that no matter what abnormality occurs in the system, the Http status code should be 200, and the system should be distinguished by business as much as possible. abnormal.

//Custom exception 
public  class  ForbiddenException  extends  RuntimeException  {
     public  ForbiddenException(String message) {
        super(message);
    }
}

//Custom exception 
public  class  BusinessException  extends  RuntimeException  {
     public  BusinessException(String message) {
        super(message);
    }
}

//Unified interception exception 
@RestControllerAdvice(basePackages =  "com.example.demo" )
 public  class  ExceptionAdvice  {

    /**
     * Catch { @code  BusinessException} exception
     */
    @ExceptionHandler({BusinessException.class})
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }

    /**
     * Catch { @code  ForbiddenException} exception
     */
    @ExceptionHandler({ForbiddenException.class})
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }

    /**
     * { @code  @RequestBody } Exception handling when parameter validation fails
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb =  new  StringBuilder( "Validation failed:" );
         for  (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }

    /**
     * Exception handling thrown when { @code  @PathVariable } and { @code  @RequestParam } parameter validation fails
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }

    /**
     * Top-level exceptions are captured and handled uniformly, when other exceptions cannot be handled, choose to use
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }

}

Summarize

After making all these changes, you can find that the code of the Controller has become very concise, you can clearly know the verification rules of each parameter and each DTO, and you can clearly see what data each Controller method returns. , it can also facilitate how each exception should be fed back.

After this set of operations, we can focus more on the development of business logic, code introduction, and complete functions. Why not do it?

If this article is helpful to you, don’t forget to give me a 3-link, like, forward, comment,

Learn more JAVA knowledge and skills, pay attention to bloggers to learn JAVA courseware, source code, installation package, as well as the latest interview materials of large factories, etc.

See you next time.

Favorites are equal to prostitution, and liking is the truth.

Leave a Comment

Your email address will not be published. Required fields are marked *