Spring Boot элегантно реализует проверку параметров интерфейса

Java
Spring Boot элегантно реализует проверку параметров интерфейса

Сегодня я продолжу делиться с вами тем, как элегантно проверить правильность параметров интерфейса на работе и как единообразно обрабатывать формат json, возвращаемый интерфейсом. Каждое слово — галантерея, оригинальность непроста, делиться не просто.

Кроме того, я также разобрал некоторые материалы по Java, и вы можете получить их сами, если они вам нужны!Наиболее полное исследование отмечает Dachang Zhenti + Microservices + MySQL + Distributed + SSM Framework + Java + Redis + Структура данных и алгоритм + Сеть + Linux + Spring Bucket Family + JVM + High Concurrency + Major Learning Thinking Brain Maps + Сборник интервью

Валидация в основном предназначена для проверки законности данных, представленных пользователем, например, является ли она пустой, соответствует ли пароль правилам, является ли формат электронной почты правильным и т. д. Существует множество платформ проверки, и наиболее часто используемой является hibernate-validator, который также поддерживает интернационализацию, вы также можете настроить аннотацию типа проверки.Вот просто демонстрация простой интеграции среды проверки в Spring Boot.Для получения дополнительной информации обратитесь к hibernate-validator.

1. pom.xml

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

2. dto

public class UserInfoIDto {

    private Long id;

    @NotBlank
    @Length(min=3, max=10)
    private String username;

    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,3-9]))\\d{8}$", message="手机号格式不正确")
    private String phone;

    @Min(value=18)
    @Max(value = 200)
    private int age;

    @NotBlank
    @Length(min=6, max=12, message="昵称长度为6到12位")
    private String nickname;

     // Getter & Setter
}

3. controller

import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

@RestController
public class SimpleController {

    @PostMapping("/users")
    public String register(@Valid @RequestBody UserInfoIDto userInfoIDto, BindingResult result){
        if (result.hasErrors()) {
            FieldError fieldError = result.getFieldError();
            String field = fieldError.getField();
            String msg = fieldError.getDefaultMessage();

            return field + ":" + msg;
        }
        System.out.println("开始注册用户...");

        return "success";
    }
}

4. Удалите параметр BindingResult.

Для каждого интерфейса требуется параметр BindingResult, и каждый интерфейс должен обрабатывать информацию об ошибках, поэтому добавление параметра не является элегантным, а объем кода для обработки информации об ошибках также очень повторяющийся. Если параметр BindingResult будет удален, система сообщит об ошибке MethodArgumentNotValidException.Нам нужно использовать только глобальное исключение, чтобы поймать ошибку, а параметр BindingResult можно опустить после обработки.

@RestController
public class SimpleController {

    @PostMapping("/users")
    public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
        System.out.println("开始注册用户...");
        return "success";
    }
}

@RestControllerAdvice используется для перехвата всех @RestController

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String methodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return objectError.getDefaultMessage();
    }
}

5. Единый формат возврата

Перечисление кодов ошибок

@Getter
public enum ErrorCodeEnum {
    SUCCESS(1000, "成功"),
    FAILED(1001, "响应失败"),
    VALIDATE_FAILED(1002, "参数校验失败"),
    ERROR(5000, "未知错误");

    private Integer code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

Пользовательское исключение.

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    public APIException(ErrorCodeEnum errorCodeEnum) {
        super(errorCodeEnum.getMsg());
        this.code = errorCodeEnum.getCode();
        this.msg = errorCodeEnum.getMsg();
    }
}

Определите формат возврата.

@Getter
public class Response<T> {
    /**
     * 状态码,比如1000代表响应成功
     */
    private int code;

    /**
     * 响应信息,用来说明响应情况
     */
    private String msg;

    /**
     * 响应的具体数据
     */
    private T data;

    public Response(T data) {
        this.code = ErrorCodeEnum.SUCCESS.getCode();
        this.msg = ErrorCodeEnum.SUCCESS.getMsg();
        this.data = data;
    }

    public Response(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

Глобальный обработчик исключений усиливает перехват APIException и изменяет формат данных, возвращаемых при возникновении исключения.

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
    }

    @ExceptionHandler(APIException.class)
    public Response<String> APIExceptionHandler(APIException e) {
        return new Response<>(e.getCode(), e.getMsg());
    }
}

SimpleController добавляет метод, который генерирует исключение.

@RestController
public class SimpleController {

    @PostMapping("/users")
    public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
        System.out.println("开始注册用户...");
        return "success";
    }

    @GetMapping("/users")
    public Response<UserInfoIDto> list() {
        UserInfoIDto userInfoIDto = new UserInfoIDto();
        userInfoIDto.setUsername("monday");
        userInfoIDto.setAge(30);
        userInfoIDto.setPhone("123456789");
        if (true) {
            throw new APIException(ErrorCodeEnum.ERROR);
        }
        // 为了保持数据格式统一,必须使用Response包装一下
        return new Response<>(userInfoIDto);
    }
}

Формат возврата ошибки.

Об ошибке не сообщается, формат возвращается.

6. Удалите оболочку Response в интерфейсе

@RestControllerAdvice может глобально перехватывать как исключения, так и обычные возвращаемые значения в указанном пакете, а также может изменять возвращаемое значение.

@RestControllerAdvice(basePackages = {"com.example.validator.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 对那些方法需要包装,如果接口直接返回Response就没有必要再包装了
     *
     * @param returnType
     * @param aClass
     * @return 如果为true才会执行beforeBodyWrite
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
        return !returnType.getParameterType().equals(Response.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String类型不能直接包装,所以要进行些特别的处理
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 将数据包装在Response里后,再转换为json字符串响应给前端
                return objectMapper.writeValueAsString(new Response<>(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ErrorCodeEnum.ERROR);
            }
        }
        // 这里统一包装
        return new Response<>(data);
    }
}

@RestController
public class SimpleController {

    @GetMapping("/users")
    public UserInfoIDto list() {
        UserInfoIDto userInfoIDto = new UserInfoIDto();
        userInfoIDto.setUsername("monday");
        userInfoIDto.setAge(30);
        userInfoIDto.setPhone("123456789");
        // 直接返回值,不需要再使用Response包装
        return userInfoIDto;
    }
}

7. Каждой ошибке проверки соответствует свой код ошибки.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ValidateErrorCode {
    /** 校验错误码 code */
    int value() default 100000;
}

@Data
public class UserInfoIDto {
    @NotBlank
    @Email
    @ValidateErrorCode(value = 20000)
    private String email;

    @NotBlank
    @Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,3-9]))\\d{8}$", message="手机号格式不正确")
    @ValidateErrorCode(value = 30000)
    private String phone;
}

Проверьте исключение, чтобы получить код ошибки в аннотации.

@ExceptionHandler(MethodArgumentNotValidException.class)
    public Response<String> methodArgumentNotValidException(MethodArgumentNotValidException e) throws NoSuchFieldException {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);

        // 参数的Class对象,等下好通过字段名称获取Field对象
        Class<?> parameterType = e.getParameter().getParameterType();
        // 拿到错误的字段名称
        String fieldName = e.getBindingResult().getFieldError().getField();
        Field field = parameterType.getDeclaredField(fieldName);
        // 获取Field对象上的自定义注解
        ValidateErrorCode annotation = field.getAnnotation(ValidateErrorCode.class);
        if (annotation != null) {
            return new Response<>(annotation.value(),objectError.getDefaultMessage());
        }

        // 然后提取错误提示信息进行返回
        return new Response<>(ErrorCodeEnum.VALIDATE_FAILED.getCode(), objectError.getDefaultMessage());
    }

8. Отдельные интерфейсы не всегда упаковывают ответы

Иногда сторонний интерфейс вызывает наш интерфейс, и наш интерфейс должен следовать формату возврата, определенному третьей стороной.В настоящее время третья сторона не обязательно совпадает с нашим собственным форматом возврата, поэтому нам нужно предоставить способ обойти унифицированную упаковку.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseWrap {
}

@RestController
public class SimpleController {

    @NotResponseWrap
    @PostMapping("/users")
    public String register(@Valid @RequestBody UserInfoIDto userInfoIDto){
        System.out.println("开始注册用户...");
        return "success";
    }
}

ResponseControllerAdvice добавляет условие отсутствия упаковки, и упаковка пропускается, если настроена аннотация @NotResponseWrap.

@Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
        return !(returnType.getParameterType().equals(Response.class) || returnType.hasMethodAnnotation(NotResponseWrap.class));
    }