共计 16138 个字符,预计需要花费 41 分钟才能阅读完成。
序
本文主要研究一下 spring boot 的 ErrorWebFluxAutoConfiguration
ErrorWebFluxAutoConfiguration
spring-boot-autoconfigure-2.1.5.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@AutoConfigureBefore(WebFluxAutoConfiguration.class)
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorWebFluxAutoConfiguration {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties,
ResourceProperties resourceProperties,
ObjectProvider<ViewResolver> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.orderedStream()
.collect(Collectors.toList());
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class,
search = SearchStrategy.CURRENT)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(
errorAttributes, this.resourceProperties,
this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class,
search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
}
- ErrorWebFluxAutoConfiguration 注册了 DefaultErrorAttributes、ErrorWebExceptionHandler
ErrorAttributes
spring-boot-2.1.5.RELEASE-sources.jar!/org/springframework/boot/web/reactive/error/ErrorAttributes.java
public interface ErrorAttributes {
/**
* Return a {@link Map} of the error attributes. The map can be used as the model of
* an error page, or returned as a {@link ServerResponse} body.
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @return a map of error attributes
*/
Map<String, Object> getErrorAttributes(ServerRequest request,
boolean includeStackTrace);
/**
* Return the underlying cause of the error or {@code null} if the error cannot be
* extracted.
* @param request the source ServerRequest
* @return the {@link Exception} that caused the error or {@code null}
*/
Throwable getError(ServerRequest request);
/**
* Store the given error information in the current {@link ServerWebExchange}.
* @param error the {@link Exception} that caused the error
* @param exchange the source exchange
*/
void storeErrorInformation(Throwable error, ServerWebExchange exchange);
}
- ErrorAttributes 接口定义了 getErrorAttributes、getError、storeErrorInformation 三个方法
DefaultErrorAttributes
spring-boot-2.1.5.RELEASE-sources.jar!/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java
public class DefaultErrorAttributes implements ErrorAttributes {private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName()
+ ".ERROR";
private final boolean includeException;
/**
* Create a new {@link DefaultErrorAttributes} instance that does not include the
* "exception" attribute.
*/
public DefaultErrorAttributes() {this(false);
}
/**
* Create a new {@link DefaultErrorAttributes} instance.
* @param includeException whether to include the "exception" attribute
*/
public DefaultErrorAttributes(boolean includeException) {this.includeException = includeException;}
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("path", request.path());
Throwable error = getError(request);
HttpStatus errorStatus = determineHttpStatus(error);
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("error", errorStatus.getReasonPhrase());
errorAttributes.put("message", determineMessage(error));
handleException(errorAttributes, determineException(error), includeStackTrace);
return errorAttributes;
}
private HttpStatus determineHttpStatus(Throwable error) {if (error instanceof ResponseStatusException) {return ((ResponseStatusException) error).getStatus();}
ResponseStatus responseStatus = AnnotatedElementUtils
.findMergedAnnotation(error.getClass(), ResponseStatus.class);
if (responseStatus != null) {return responseStatus.code();
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}
private String determineMessage(Throwable error) {if (error instanceof WebExchangeBindException) {return error.getMessage();
}
if (error instanceof ResponseStatusException) {return ((ResponseStatusException) error).getReason();}
ResponseStatus responseStatus = AnnotatedElementUtils
.findMergedAnnotation(error.getClass(), ResponseStatus.class);
if (responseStatus != null) {return responseStatus.reason();
}
return error.getMessage();}
private Throwable determineException(Throwable error) {if (error instanceof ResponseStatusException) {return (error.getCause() != null) ? error.getCause() : error;}
return error;
}
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
errorAttributes.put("trace", stackTrace.toString());
}
private void handleException(Map<String, Object> errorAttributes, Throwable error,
boolean includeStackTrace) {if (this.includeException) {errorAttributes.put("exception", error.getClass().getName());
}
if (includeStackTrace) {addStackTrace(errorAttributes, error);
}
if (error instanceof BindingResult) {BindingResult result = (BindingResult) error;
if (result.hasErrors()) {errorAttributes.put("errors", result.getAllErrors());
}
}
}
@Override
public Throwable getError(ServerRequest request) {return (Throwable) request.attribute(ERROR_ATTRIBUTE)
.orElseThrow(() -> new IllegalStateException("Missing exception attribute in ServerWebExchange"));
}
@Override
public void storeErrorInformation(Throwable error, ServerWebExchange exchange) {exchange.getAttributes().putIfAbsent(ERROR_ATTRIBUTE, error);
}
}
- DefaultErrorAttributes 实现了 ErrorAttributes 接口,它的 getErrorAttributes 方法会返回 timestamp、path、status、error、message、exception(
includeException
)、trace(includeStackTrace
) 等信息;getError 方法会从 ServerRequest 的 ERROR_ATTRIBUTE 中获取 Throwable;storeErrorInformation 则是把 Throwable 存放到 ServerWebExchange 的 attributes 中
WebExceptionHandler
spring-web-5.1.7.RELEASE-sources.jar!/org/springframework/web/server/WebExceptionHandler.java
public interface WebExceptionHandler {
/**
* Handle the given exception. A completion signal through the return value
* indicates error handling is complete while an error signal indicates the
* exception is still not handled.
* @param exchange the current exchange
* @param ex the exception to handle
* @return {@code Mono<Void>} to indicate when exception handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}
- WebExceptionHandler 定义了 handle 方法
ErrorWebExceptionHandler
spring-boot-2.1.5.RELEASE-sources.jar!/org/springframework/boot/web/reactive/error/ErrorWebExceptionHandler.java
@FunctionalInterface
public interface ErrorWebExceptionHandler extends WebExceptionHandler {}
- ErrorWebExceptionHandler 继承了 WebExceptionHandler 接口,仅仅是通过类名来标识它用来 render errors
AbstractErrorWebExceptionHandler
public abstract class AbstractErrorWebExceptionHandler
implements ErrorWebExceptionHandler, InitializingBean {
/**
* Currently duplicated from Spring WebFlux HttpWebHandlerAdapter.
*/
private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS;
static {Set<String> exceptions = new HashSet<>();
exceptions.add("AbortedException");
exceptions.add("ClientAbortException");
exceptions.add("EOFException");
exceptions.add("EofException");
DISCONNECTED_CLIENT_EXCEPTIONS = Collections.unmodifiableSet(exceptions);
}
private static final Log logger = HttpLogging
.forLogName(AbstractErrorWebExceptionHandler.class);
private final ApplicationContext applicationContext;
private final ErrorAttributes errorAttributes;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
private List<ViewResolver> viewResolvers = Collections.emptyList();
public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties,
ApplicationContext applicationContext) {Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
Assert.notNull(applicationContext, "ApplicationContext must not be null");
this.errorAttributes = errorAttributes;
this.resourceProperties = resourceProperties;
this.applicationContext = applicationContext;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
//......
@Override
public void afterPropertiesSet() throws Exception {if (CollectionUtils.isEmpty(this.messageWriters)) {throw new IllegalArgumentException("Property'messageWriters'is required");
}
}
/**
* Create a {@link RouterFunction} that can route and handle errors as JSON responses
* or HTML views.
* <p>
* If the returned {@link RouterFunction} doesn't route to a {@code HandlerFunction},
* the original exception is propagated in the pipeline and can be processed by other
* {@link org.springframework.web.server.WebExceptionHandler}s.
* @param errorAttributes the {@code ErrorAttributes} instance to use to extract error
* information
* @return a {@link RouterFunction} that routes and handles errors
*/
protected abstract RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {if (exchange.getResponse().isCommitted()
|| isDisconnectedClientError(throwable)) {return Mono.error(throwable);
}
this.errorAttributes.storeErrorInformation(throwable, exchange);
ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
return getRoutingFunction(this.errorAttributes).route(request)
.switchIfEmpty(Mono.error(throwable))
.flatMap((handler) -> handler.handle(request))
.doOnNext((response) -> logError(request, response, throwable))
.flatMap((response) -> write(exchange, response));
}
//......
}
- AbstractErrorWebExceptionHandler 声明实现 ErrorWebExceptionHandler 以及 InitializingBean 接口;其 handle 方法首先把 throwable 存储到 errorAttributes 汇总,然后通过 getRoutingFunction 进行 route;afterPropertiesSet 主要是确保 messageWriters 不为空;它定义了 getRoutingFunction 要子类去实现
DefaultErrorWebExceptionHandler
spring-boot-autoconfigure-2.1.5.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
private static final Map<HttpStatus.Series, String> SERIES_VIEWS;
static {Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
views.put(HttpStatus.Series.CLIENT_ERROR, "4xx");
views.put(HttpStatus.Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private final ErrorProperties errorProperties;
/**
* Create a new {@code DefaultErrorWebExceptionHandler} instance.
* @param errorAttributes the error attributes
* @param resourceProperties the resources configuration properties
* @param errorProperties the error configuration properties
* @param applicationContext the current application context
*/
public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties, ErrorProperties errorProperties,
ApplicationContext applicationContext) {super(errorAttributes, resourceProperties, applicationContext);
this.errorProperties = errorProperties;
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(),
this::renderErrorResponse);
}
/**
* Render the error information as an HTML view.
* @param request the current request
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {boolean includeStackTrace = isIncludeStackTrace(request, MediaType.TEXT_HTML);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
HttpStatus errorStatus = getHttpStatus(error);
ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus)
.contentType(MediaType.TEXT_HTML);
return Flux
.just("error/" + errorStatus.value(),
"error/" + SERIES_VIEWS.get(errorStatus.series()), "error/error")
.flatMap((viewName) -> renderErrorView(viewName, responseBody, error))
.switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled()
? renderDefaultErrorView(responseBody, error)
: Mono.error(getError(request)))
.next();}
/**
* Render the error information as a JSON payload.
* @param request the current request
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
return ServerResponse.status(getHttpStatus(error))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(error));
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(ServerRequest request, MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.errorProperties
.getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {return true;}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {return isTraceEnabled(request);
}
return false;
}
/**
* Get the HTTP error status information from the error map.
* @param errorAttributes the current error information
* @return the error HTTP status
*/
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {int statusCode = (int) errorAttributes.get("status");
return HttpStatus.valueOf(statusCode);
}
/**
* Predicate that checks whether the current request explicitly support
* {@code "text/html"} media type.
* <p>
* The "match-all" media type is not considered here.
* @return the request predicate
*/
protected RequestPredicate acceptsTextHtml() {return (serverRequest) -> {
try {List<MediaType> acceptedMediaTypes = serverRequest.headers().accept();
acceptedMediaTypes.remove(MediaType.ALL);
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
return acceptedMediaTypes.stream()
.anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
}
catch (InvalidMediaTypeException ex) {return false;}
};
}
}
- DefaultErrorWebExceptionHandler 继承了 AbstractErrorWebExceptionHandler;其 getRoutingFunction 方法会对 acceptsTextHtml 的 renderErrorView,其他的通过 renderErrorResponse 来返回 json 格式的错误信息
ExceptionHandlingWebHandler
spring-web-5.1.7.RELEASE-sources.jar!/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java
public class ExceptionHandlingWebHandler extends WebHandlerDecorator {
private final List<WebExceptionHandler> exceptionHandlers;
public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {super(delegate);
this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
}
/**
* Return a read-only list of the configured exception handlers.
*/
public List<WebExceptionHandler> getExceptionHandlers() {return this.exceptionHandlers;}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Mono<Void> completion;
try {completion = super.handle(exchange);
}
catch (Throwable ex) {completion = Mono.error(ex);
}
for (WebExceptionHandler handler : this.exceptionHandlers) {completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
}
return completion;
}
}
- ExceptionHandlingWebHandler 继承了 WebHandlerDecorator,它会挨个调用 WebExceptionHandler 的 handle 方法
小结
- ErrorWebFluxAutoConfiguration 注册了 DefaultErrorAttributes、ErrorWebExceptionHandler
- DefaultErrorAttributes 实现了 ErrorAttributes 接口,它的 getErrorAttributes 方法会返回 timestamp、path、status、error、message、exception(
includeException
)、trace(includeStackTrace
) 等信息;getError 方法会从 ServerRequest 的 ERROR_ATTRIBUTE 中获取 Throwable;storeErrorInformation 则是把 Throwable 存放到 ServerWebExchange 的 attributes 中 - DefaultErrorWebExceptionHandler 继承了 AbstractErrorWebExceptionHandler;其 getRoutingFunction 方法会对 acceptsTextHtml 的 renderErrorView,其他的通过 renderErrorResponse 来返回 json 格式的错误信息
doc
- ErrorWebFluxAutoConfiguration