本文主要研究一下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

@FunctionalInterfacepublic 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