聊聊spring boot的ErrorWebFluxAutoConfiguration

634 阅读5分钟

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