Spring Boot 在Sping Boot 中,使用一个@ExceptionHandler处理多个异常以进行枚举验证

oknrviil  于 2023-06-22  发布在  Spring
关注(0)|答案(1)|浏览(134)

我实现了两个枚举类,作为在RestController中作为输入接收的UserDTO类的参数

package com.example.enumhandler.enumerator;

import com.example.enumhandler.exception.NotValidEnumTypeException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.Arrays;

public enum GenderTypeEnum {

    MALE("male"),
    FEMALE("female");

    private final String value;

    private GenderTypeEnum(String value) {
        this.value = value;
    }

    private static String[] getNames(Class<? extends Enum<?>> e) {
        return Arrays.stream(e.getEnumConstants())
                .map(Enum::name)
                .toArray(String[]::new);
    }

    private static String[] getNamesValue(){
        return Arrays
                .stream(values())
                .map(e -> valueOf(e.name()).value)
                .toArray(String[]::new);
    }

    @JsonValue
    public String getGender(){
        return this.value;
    }

    @JsonCreator
    public static GenderTypeEnum fromValues(String value) {
        for (GenderTypeEnum category : GenderTypeEnum.values()) {
            if (category.getGender().equalsIgnoreCase(value)) {
                return category;
            }
        }
        throw new NotValidEnumTypeException(
                "Unknown enum type "
                        + value
                        + ", allowed values are "
                        + Arrays.toString(getNamesValue()));
    }
}

和/或

package com.example.enumhandler.enumerator;

import com.example.enumhandler.exception.NotValidEnumTypeException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.Arrays;

public enum HobbyTypeEnum {

    BIKE("bike"),
    CLIMB("climb"),
    TENNIS("tennis");

    private final String value;

    private HobbyTypeEnum(String value) {
        this.value = value;
    }

    @JsonValue
    public String getGender(){
        return this.value;
    }

    private static String[] getNames(Class<? extends Enum<?>> e) {
        return Arrays.stream(e.getEnumConstants())
                .map(Enum::name)
                .toArray(String[]::new);
    }

    private static String[] getNamesValue(){
        return Arrays
                .stream(values())
                .map(e -> valueOf(e.name()).value)
                .toArray(String[]::new);
    }

    @JsonCreator
    public static HobbyTypeEnum fromValues(String value) {
        for (HobbyTypeEnum category : HobbyTypeEnum.values()) {
            if (category.getGender().equalsIgnoreCase(value)) {
                return category;
            }
        }
        throw new NotValidEnumTypeException(
                "Unknown enum type "
                        + value
                        + ", allowed values are "
                        + Arrays.toString(getNamesValue()));
    }

}

此时,我已经创建了一个自定义Exception来处理异常,以防传递的值与各种Enum类所包含的值相比不正确

package com.example.enumhandler.exception;

import java.io.Serial;

public class NotValidEnumTypeException extends RuntimeException{

    @Serial
    private static final long serialVersionUID = 1L;

    public NotValidEnumTypeException() {
        super();

    }

    public NotValidEnumTypeException(String message, Throwable cause) {
        super(message, cause);
    }

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

    public NotValidEnumTypeException(Throwable cause) {
        super(cause);
    }

}

和/或

package com.example.enumhandler.dto;

import com.example.enumhandler.enumerator.GenderTypeEnum;
import com.example.enumhandler.enumerator.HobbyTypeEnum;
import com.example.enumhandler.validator.ValidGenderTypeEnum;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;
import lombok.*;

import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class UserDTO {

    private String name;

    @Min(18)
    @Max(25)
    private Integer age;

    @NotNull
    private GenderTypeEnum gender;

    @NotNull
    @NotEmpty
    private List<HobbyTypeEnum> hobby;

}

最后,我创建了一个@RestControllerAdvice来处理这个异常:

package com.example.enumhandler.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RestControllerAdvice
public class UserExceptionHandler {

    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = {NotValidEnumTypeException.class})
    public final ResponseEntity<?> handleInvalidEnumTypeArgument(
            NotValidEnumTypeException ex
    ){

        HttpStatus badRequest = HttpStatus.BAD_REQUEST;
        final Throwable rootCause = org
                .apache
                .commons
                .lang3
                .exception
                .ExceptionUtils
                .getRootCause(ex);

        List<String> details = new ArrayList<>();
        details.add(rootCause.getLocalizedMessage());

        ErrorResponse error = ErrorResponse
                .builder()
                .timestamp(LocalDateTime.now())
                .status(badRequest.getReasonPhrase())
                .code(badRequest.value())
                .message(details)
                .build();

        return new ResponseEntity<>(error, badRequest);
    }
}

带有自定义错误消息

package com.example.enumhandler.exception;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

@AllArgsConstructor
@Data
@Builder
class ErrorResponse {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
    private LocalDateTime timestamp;
    private String status;
    private int code;
    private List<String> message;

}

一切正常,但我希望如果两个或更多的枚举是错误的,返回一个唯一的消息,捕获所有这些异常:

0qx6xfy6

0qx6xfy61#

经过一段时间试图弄清楚Jackson如何抛出HttpMessageNotReadableException,我发现如果在反序列化当前字段时抛出异常,Jackson将不会继续反序列化下一个字段。因此,我们必须忽略并存储反序列化字段时抛出的异常。然后在反序列化完成后抛出这些异常。首先,我们必须创建一个上下文持有者来为每个用户单独存储异常。

@UtilityClass
public class RequestBodyExceptionHolder {

    public static final Object DEFAULT_VALUE_WHEN_DESERIALIZATION_FAILED = null;

    /**
     * Thread local helps us to store exceptions separately for each request.
     */
    private static final ThreadLocal<Map<String, Throwable>> context = new ThreadLocal<>();

    private static Map<String, Throwable> getExceptionsFromContext() {
        Map<String, Throwable> data = context.get();
        if (data == null) {
            data = new LinkedHashMap<>();
            context.set(data);
        }
        return data;
    }

    public static Map<String, Throwable> getStoredExceptions() {
        return Collections.unmodifiableMap(getExceptionsFromContext());
    }

    public static void clearExceptions() {
        context.remove();
    }

    public static void collectFailedFieldWhileDeserialization(final JsonParser parser, final Throwable exception) throws IOException {
        final Map<String, Throwable> exceptions = getExceptionsFromContext();
        final String fieldName = Optional.ofNullable(parser.getCurrentName()).orElseGet(() -> parser.getParsingContext().getParent().getCurrentName());
        exceptions.put(fieldName, exception);
    }

}

现在,您必须捕获反序列化时抛出的异常并存储它们。当反序列化失败时,我们应该返回null。

@Configuration
public class JacksonConfiguration {

    @Bean
    public static ObjectMapper getObjectMapper(final Jackson2ObjectMapperBuilder builder) {
        final ObjectMapper jsonMapper = JsonMapper.builder().build();
        jsonMapper.addHandler(new RequestBodySilenceDeserializationProblemHandler());

        return jsonMapper;
    }

    static class RequestBodySilenceDeserializationProblemHandler extends DeserializationProblemHandler {
        @Override
        public Object handleInstantiationProblem(final DeserializationContext context, final Class<?> instClass, final Object argument, final Throwable exception) throws IOException {
            final JsonParser parser = context.getParser();
            RequestBodyExceptionHolder.collectFailedFieldWhileDeserialization(parser, exception);
            return RequestBodyExceptionHolder.DEFAULT_VALUE_WHEN_DESERIALIZATION_FAILED;
        }
    }
}

反序列化成功后,你必须抛出我们存储的所有异常:

@ControllerAdvice
public class RequestAdvisor {

    @InitBinder
    public void initBinder(@NonNull final WebDataBinder binder) {
        final Map<String, Throwable> exceptions = RequestBodyExceptionHolder.getStoredExceptions();
        if (!exceptions.isEmpty()) {
            RequestBodyExceptionHolder.clearExceptions();
            throw new RequestBodyDeserializationException(exceptions);
        }
    }

    @Getter
    @AllArgsConstructor
    public static class RequestBodyDeserializationException extends RuntimeException {
    
        private final Map<String, Throwable> errors;
    
    }
}

然后,为RequestBodyDeserializationException添加自定义异常处理程序:

@ExceptionHandler(RequestAdvisor.RequestBodyDeserializationException.class)
    public static ResponseEntity<?> handleRequestBodyDeserializationException(
        @NonNull final RequestAdvisor.RequestBodyDeserializationException exception
    ) {
        exception.printStackTrace();

        final List<String> details = exception
            .getErrors()
            .values()
            .stream()
            .map(cause -> ExceptionUtils.getRootCause(cause).getLocalizedMessage())
            .collect(Collectors.toList());
        
        ...

    }

最后,请确保在请求继续进行后清理上下文:

@Log4j2
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        @NonNull final HttpServletRequest request,
        @NonNull final HttpServletResponse response,
        @NonNull final FilterChain filterChain
    ) throws ServletException, IOException {
        filterChain.doFilter(request, response);
        
        // Clear exceptions that stored in context
        RequestBodyExceptionHolder.clearExceptions();
    }
}

这是我得到的结果,希望对你有帮助:

相关问题