如何建议 (AOP) spring webflux 网络处理程序以捕获和转换反应性错误

How to advise (AOP) spring webflux web handlers to catch and transform reactive error

[更新 2021-10-11] 添加了 MCVE

https://github.com/SalathielGenese/issue-spring-webflux-reactive-error-advice


出于可重用性方面的考虑,我 运行 我在服务层上进行了验证,returns Mono.error( constraintViolationException )...

以便我的 Web 处理程序仅将未编组的域转发到服务层。

到目前为止,太棒了。


但是我如何建议 (AOP) 我的 Web 处理程序,以便它 returns HTTP 422 违反格式化约束?

WebExchangeBindException 只处理同步抛出的异常(我不希望同步验证破坏反应流)。

我的 AOP 建议触发器和错误 b/c :

并且如果我将我的响应实体(来自建议)包装到 Mono<ResponseEntity> 中,我将响应实体序列化为 HTTP 200 OK:(

代码摘录

@Aspect
@Component
class CoreWebAspect {
    @Pointcut("withinApiCorePackage() && @annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void postMappingWebHandler() {
    }

    @Pointcut("within(project.package.prefix.*)")
    public void withinApiCorePackage() {
    }

    @Around("postMappingWebHandler()")
    public Object aroundWebHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        try {
            final var proceed = proceedingJoinPoint.proceed();

            if (proceed instanceof Mono<?> mono) {
                try {
                    return Mono.just(mono.toFuture().get());
                } catch (ExecutionException exception) {
                    if (exception.getCause() instanceof ConstraintViolationException constraintViolationException) {
                        return Mono.just(getResponseEntity(constraintViolationException));
                    }

                    throw exception.getCause();
                }
            }

            return proceed;
        } catch (ConstraintViolationException constraintViolationException) {
            return getResponseEntity(constraintViolationException);
        }
    }

    private ResponseEntity<Set<Violation>> getResponseEntity(final ConstraintViolationException constraintViolationException) {
        final var violations = constraintViolationException.getConstraintViolations().stream().map(violation -> new Violation(
                stream(violation.getPropertyPath().spliterator(), false).map(Node::getName).collect(toList()),
                violation.getMessageTemplate().replaceFirst("^\{(.*)\}$", ""))
        ).collect(Collectors.toSet());

        return status(UNPROCESSABLE_ENTITY).body(violations);
    }


    @Getter
    @AllArgsConstructor
    private static class Violation {
        private final List<String> path;
        private final String template;
    }
}

根据观察(我没有在文档中找到任何证据),无论内容如何,​​响应中的 Mono.just() 都会自动翻译成 200 OK。因此,需要 Mono.error()。然而,它的构造函数需要 Throwable 所以 ResponseStatusException 开始发挥作用。

return Mono.error(new ResponseStatusException(UNPROCESSABLE_ENTITY));
  • 要求:
    curl -i --request POST --url http://localhost:8080/welcome \
    --header 'Content-Type: application/json' \
    --data '{}'
    
  • 响应(格式化):
    HTTP/1.1 422 Unprocessable Entity
    Content-Type: application/json
    Content-Length: 147
    
    {
      "error": "Unprocessable Entity",
      "message": null,
      "path": "/welcome",
      "requestId": "7a3a464e-1",
      "status": 422,
      "timestamp": "2021-10-13T16:44:18.225+00:00"
    }
    

终于返回422 Unprocessable Entity

可悲的是,所需的 List<Violation> 作为正文只能作为 String reason 传递到 ResponseStatusException 中,最终得到一个丑陋的响应:

return Mono.error(new ResponseStatusException(UNPROCESSABLE_ENTITY, violations.toString()));
  • 同样的请求
  • 响应(格式化):
    HTTP/1.1 422 Unprocessable Entity
    Content-Type: application/json
    Content-Length: 300
    
    {
      "timestamp": "2021-10-13T16:55:30.927+00:00",
      "path": "/welcome",
      "status": 422,
      "error": "Unprocessable Entity",
      "message": "[IssueSpringWebfluxReactiveErrorAdviceApplication.AroundReactiveWebHandler.Violation(template={javax.validation.constraints.NotNull.message}, path=[name])]",
      "requestId": "de92dcbd-1"
    }
    

但是有一个解决方案定义了 ErrorAttributes bean 并将违规添加到正文中。从自定义异常开始,不要忘记用 @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) 注释它以定义正确的响应状态代码:

@Getter
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class ViolationException extends RuntimeException {

    private final List<Violation> violations;
}

现在定义 ErrorAttributes bean,获取违规并将其添加到正文中:

@Bean
public ErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {
        @Override
        public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
            Throwable error = getError(request);
            if (error instanceof ViolationException) {
                ViolationException violationException = (ViolationException) error;
                errorAttributes.put("violations", violationException .getViolations());
            }
            return errorAttributes;
        }
    };
}

最后,在你的方面做到这一点:

return Mono.error(new ViolationException(violations));

并进行测试:

  • 同样的请求
  • 响应(格式化):
    HTTP/1.1 422 Unprocessable Entity
    Content-Type: application/json
    Content-Length: 238
    
    {
      "timestamp": "2021-10-13T17:07:07.668+00:00",
      "path": "/welcome",
      "status": 422,
      "error": "Unprocessable Entity",
      "message": "",
      "requestId": "a80b54d9-1",
      "violations": [
        {
          "template": "{javax.validation.constraints.NotNull.message}",
          "path": [
            "name"
          ]
        }
      ]
    }
    

测试将通过。不要忘记一些 类 是来自反应包的新内容:

  • org.springframework.boot.web.reactive.error.ErrorAttributes
  • org.springframework.boot.web.reactive.error.DefaultErrorAttributes
  • org.springframework.web.reactive.function.server.ServerRequest

用包含 @ExceptionHandler@ControllerAdvice 替换方面怎么样?但是让我们清理主应用程序 class,从中提取内部 classes 到一个额外的 class:

package name.genese.salathiel.issuespringwebfluxreactiveerroradvice;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import static org.springframework.boot.SpringApplication.run;

@SpringBootApplication
@EnableAspectJAutoProxy
public class IssueSpringWebfluxReactiveErrorAdviceApplication {
  public static void main(String[] args) {
    run(IssueSpringWebfluxReactiveErrorAdviceApplication.class, args);
  }
}
package name.genese.salathiel.issuespringwebfluxreactiveerroradvice;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.stream.Collectors;

import static java.util.stream.StreamSupport.stream;

@ControllerAdvice
public class ConstraintViolationExceptionHandler {
  @ExceptionHandler(ConstraintViolationException.class)
  public ResponseEntity<List<Violation>> handleException(ConstraintViolationException constraintViolationException) {
    final List<Violation> violations = constraintViolationException.getConstraintViolations().stream()
      .map(violation -> new Violation(
        violation.getMessageTemplate(),
        stream(violation.getPropertyPath().spliterator(), false)
          .map(Path.Node::getName)
          .collect(Collectors.toList())
      )).collect(Collectors.toList());

    return ResponseEntity.unprocessableEntity().body(violations);
  }

  @Getter
  @RequiredArgsConstructor
  static class Violation {
    private final String template;
    private final List<String> path;
  }
}

现在你的测试都通过了。

顺便说一句,我不是 Spring 用户,我是从 那里得到灵感的。