【问题标题】:How to advise (AOP) spring webflux web handlers to catch and transform reactive error如何建议(AOP)spring webflux web 处理程序捕获和转换反应性错误
【发布时间】:2021-11-29 19:43:20
【问题描述】:

[UPDATE 2021-10-11] 添加了 MCVE

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


出于可重用性的考虑,我在服务层运行验证,返回Mono.error( constraintViolationException )...

这样我的 Web 处理程序只需将未编组的域转发到服务层。

到目前为止,非常棒。


但是我如何建议 (AOP) 我的 Web 处理程序,以便它返回带有格式化约束违规的 HTTP 422

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

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

  • 我的网络处理程序返回Mono<DataType>
  • 但我的建议返回ResponseEntity

如果我将我的响应实体(来自建议)包装成一个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("^\\{(.*)\\}$", "$1"))
        ).collect(Collectors.toSet());

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


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

【问题讨论】:

  • 我既不是响应式编程也不是 Spring 专家,只是 AOP 专家。但是,如果您在 GitHub 上有一个 MCVE我可以在最简单的设置中重现您的情况,我可以看看。我需要亲自看看到底发生了什么。
  • 完成。谢谢@kriegaex
  • 我查看了您的 MCVE 并且可以重现该问题。这看起来真的像一个反应式编程问题,我在这里没有深入了解,从来没有学过任何关于反应式编程的东西。但我相信 MCVE 能让一些反应型极客为您找到解决方案。
  • AOP 和反应式 PI 问题都不是……更多的是 Spring Webflux 实现问题。似乎它没有在节点的正确位置处理反应性错误。
  • 嗯,我不能说什么聪明的话。但是如果你解决了你的问题,请写一个答案,以便回馈社区。谢谢。

标签: spring spring-boot spring-webflux project-reactor spring-aop


【解决方案1】:

根据观察(我在文档中没有找到任何证据),无论内容如何,​​响应时的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&lt;Violation&gt; 作为正文只能作为 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

【讨论】:

  • 这确实保持了我在赏金消息中指定的反应精神。使我的快乐最完整。我将它与ObjectMapper 注入和&lt;mono&gt;.onErrorMap(ConstraintViolationException.class, this::handleConstraintViolationException) 一起使用...非常感谢@Nikolas Charalambidis
【解决方案2】:

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

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 用户,我从 this answer 得到了这个想法。

【讨论】:

  • 这很有趣...因此,由于异步错误,WebExchangeBindException 不会被触发(正如我在测试期间观察到的那样),但ConstraintViolationException 会触发。干得好!!!
  • 使用@ExceptionHandler 的有趣方法。最终的响应正文将包含一个验证列表。
  • 如您所见,我将List&lt;Violation&gt; 放入响应正文中,从 OP 的原始方法中复制代码。但实际上,你可以在那里放任何东西。实际上,@ControllerAdvice,顾名思义,在内部也使用类似 AOP、基于代理的拦截,但与尝试使用方面手动拦截控制器方法相比,在这种情况下似乎是更充分、更轻松的方法。实际上,我认为这应该是公认的答案,当然这完全取决于 OP 来决定。
  • 我喜欢您的回答的简单性和实用性。但是,并非所有ConstraintViolationException 都可能希望以这种方式被拦截,需要进一步转换。因此,思考什么答案值得接受是愚蠢的。
  • 谢谢你骂我傻。让 Salathiel 决定哪个答案最能解决问题。他仍然可以使用方面或其他异常处理程序来覆盖其他类型的错误。当然,您定义额外错误属性和异常类的精细方法也可以完成这项工作。
猜你喜欢
  • 2019-10-27
  • 2021-08-06
  • 1970-01-01
  • 2018-08-22
  • 2021-08-04
  • 2018-07-05
  • 2018-06-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多