如何建议 (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 :
- 我的网络处理程序 return
Mono<DataType>
- 但我的建议return一个
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("^\{(.*)\}$", ""))
).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 用户,我是从 那里得到灵感的。
[更新 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 :
- 我的网络处理程序 return
Mono<DataType>
- 但我的建议return一个
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("^\{(.*)\}$", ""))
).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 用户,我是从