如何在Springboot WebFlux中返回验证错误消息

huangapple go评论63阅读模式
英文:

How to return validation error messages with Springboot WebFlux

问题

如何在Springboot 3.0中使用WebFlux返回自定义验证错误?

我已经配置了以下的控制器

import jakarta.validation.Valid;
//...

@Validated
@RestController
@RequestMapping("/organizations")
@RequiredArgsConstructor
public class OrganizationController {

    private final OrganizationService organizationService;

    @PostMapping("/create")
    public Mono<ResponseEntity<Organization>> create(@Valid @RequestBody final OrganizationDto organizationDto) {

        return organizationService.create(organizationDto).map(ResponseEntity::ok);
    }
}

OrganizationDto 被设置为:

import jakarta.validation.constraints.NotNull;
//...

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public final class OrganizationDto {

    @NotNull    
    private String name;

    //...
}

最后,我认为以下的 ValidationHandler/ErrorController 是正确的

@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class ErrorController {

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidationExceptions(final MethodArgumentNotValidException ex) {

        final BindingResult bindingResult = ex.getBindingResult();
        final List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        final Map<String, String> errors = new HashMap<>();
        fieldErrors.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
        
        return errors;
    }
}

然而,如果我向控制器的端点发送一个负载:

{
  "name": null
}

我得到了以下响应:

{
    "timestamp": 1677430410704,
    "path": "/organizations/create",
    "status": 400,
    "error": "Bad Request",
    "requestId": "2050221b-2"
}

这几乎是我想要的,但我尝试将验证失败的原因包含在响应中,但没有成功。

我在 handleValidationExceptions 上设置了断点,但看起来我从未进入它,而且在服务器端日志中也没有看到任何指示发生了什么的内容。

我在类路径中有 org.springframework.boot:spring-boot-starter-validation,并且我正在使用最新的Springboot 3.0.3。

我是否漏掉了某个步骤或注解?

英文:

How do I return the custom validation errors for Springboot 3.0 with WebFlux?

I have wired up the following controller

import jakarta.validation.Valid;
//...

@Validated
@RestController
@RequestMapping(&quot;/organizations&quot;)
@RequiredArgsConstructor
public class OrganizationController {

    private final OrganizationService organizationService;

    @PostMapping(&quot;/create&quot;)
    public Mono&lt;ResponseEntity&lt;Organization&gt;&gt; create(@Valid @RequestBody final OrganizationDto organizationDto) {

        return organizationService.create(organizationDto).map(ResponseEntity::ok);
    }
}

The OrganizationDto has been setup as:

import jakarta.validation.constraints.NotNull;
//...

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public final class OrganizationDto {

    @NotNull    
    private String name;

    //...
}

And finally I have what I thought was a correct ValidationHandler/ErrorController

@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class ErrorController {

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map&lt;String, String&gt; handleValidationExceptions(final MethodArgumentNotValidException ex) {

        final BindingResult bindingResult = ex.getBindingResult();
        final List&lt;FieldError&gt; fieldErrors = bindingResult.getFieldErrors();
        final Map&lt;String, String&gt; errors = new HashMap&lt;&gt;();
        fieldErrors.forEach(error -&gt; errors.put(error.getField(), error.getDefaultMessage()));
        
        return errors;
    }

}

However if I send a payload to the endpoint in the controller of

{
  &quot;name&quot;: null
}

I get back

{
    &quot;timestamp&quot;: 1677430410704,
    &quot;path&quot;: &quot;/organizations/create&quot;,
    &quot;status&quot;: 400,
    &quot;error&quot;: &quot;Bad Request&quot;,
    &quot;requestId&quot;: &quot;2050221b-2&quot;
}

Which is almost what I want, but I'm trying to get the reason why it failed validation into the response but not having any luck

I've put breakpoints on the handleValidationExceptions but looks like I'm never getting into it, and I'm also not seeing anything in the server side logs which points to whats going on.

I do have org.springframework.boot:spring-boot-starter-validation on my classpath, and I'm using the latest Springboot 3.0.3

Have I missed a step or annotation here?

答案1

得分: 1

以下是您要翻译的内容:

I was able to solve this by deleting the ErrorController and going back to what I had previously tried which was writing a custom implementation of WebExceptionHandler

The important thing to note here is that you must set the @Order value otherwise this implementation is skipped over and never called (Which is what lead me to ignore this solution originally).

My version of the WebExceptionHandler

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http MediaType;
import org.springframework.validation FieldError;
import org.springframework.web bind.annotation.RestControllerAdvice;
import org.springframework.web bind.support WebExchangeBindException;
import org.springframework.web server ServerWebExchange;
import org.springframework.web server WebExceptionHandler;
import reactor core publisher Mono;

import java.util Map;
import java.util stream Collectors;

@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@RequiredArgsConstructor
public class ValidationHandler implements WebExceptionHandler {

    private final ObjectMapper objectMapper;

    @Override
    @SneakyThrows
    public Mono&lt;Void&gt; handle(final ServerWebExchange exchange, final Throwable throwable) {

        if (throwable instanceof WebExchangeBindException validationEx) {
            final Map&lt;String, String&gt; errors = getValidationErrors(validationEx);

            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

            return writeResponse(exchange, objectMapper.writeValueAsBytes(errors));
        } else {
            return Mono.error(throwable);
        }
    }

    private Map&lt;String, String&gt; getValidationErrors(final WebExchangeBindException validationEx) {

        return validationEx.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,
                error -&gt; Optional.ofNullable(error.getDefaultMessage()).orElse(&quot;&quot;)));
    }

    private Mono&lt;Void&gt; writeResponse(final ServerWebExchange exchange, final byte[] responseBytes) {

        return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(responseBytes)));
    }

}

This will correctly return the response of:

{
    &quot;name&quot;: &quot;must not be null&quot;
}

When passing in the request used in my question.

Note: If anyone uses this, be careful with the @SneakyThrows

英文:

I was able to solve this by deleting the ErrorController and going back to what I had previously tried which was writing a custom implementation of WebExceptionHandler

The important thing to note here is that you must set the @Order value otherwise this implementation is skipped over and never called (Which is what lead me to ignore this solution originally).

My version of the WebExceptionHandler

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@RequiredArgsConstructor
public class ValidationHandler implements WebExceptionHandler {

    private final ObjectMapper objectMapper;

    @Override
    @SneakyThrows
    public Mono&lt;Void&gt; handle(final ServerWebExchange exchange, final Throwable throwable) {

        if (throwable instanceof WebExchangeBindException validationEx) {
            final Map&lt;String, String&gt; errors = getValidationErrors(validationEx);

            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

            return writeResponse(exchange, objectMapper.writeValueAsBytes(errors));
        } else {
            return Mono.error(throwable);
        }
    }

    private Map&lt;String, String&gt; getValidationErrors(final WebExchangeBindException validationEx) {

        return validationEx.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,
                error -&gt; Optional.ofNullable(error.getDefaultMessage()).orElse(&quot;&quot;)));
    }

    private Mono&lt;Void&gt; writeResponse(final ServerWebExchange exchange, final byte[] responseBytes) {

        return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(responseBytes)));
    }

}

This will correctly return the response of:

{
    &quot;name&quot;: &quot;must not be null&quot;
}

When passing in the request used in my question.

Note: If anyone uses this, be careful with the @SneakyThrows

huangapple
  • 本文由 发表于 2023年2月27日 01:01:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/75573603.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定