Common root cause introspection algorithm in NestedExceptionUtils
Issue: SPR-15510
This commit is contained in:
		
							parent
							
								
									16901b1497
								
							
						
					
					
						commit
						9d8e9cf243
					
				|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2012 the original author or authors. |  * Copyright 2002-2017 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -80,13 +80,7 @@ public abstract class NestedCheckedException extends Exception { | ||||||
| 	 * @return the innermost exception, or {@code null} if none | 	 * @return the innermost exception, or {@code null} if none | ||||||
| 	 */ | 	 */ | ||||||
| 	public Throwable getRootCause() { | 	public Throwable getRootCause() { | ||||||
| 		Throwable rootCause = null; | 		return NestedExceptionUtils.getRootCause(this); | ||||||
| 		Throwable cause = getCause(); |  | ||||||
| 		while (cause != null && cause != rootCause) { |  | ||||||
| 			rootCause = cause; |  | ||||||
| 			cause = cause.getCause(); |  | ||||||
| 		} |  | ||||||
| 		return rootCause; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2008 the original author or authors. |  * Copyright 2002-2017 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -39,17 +39,48 @@ public abstract class NestedExceptionUtils { | ||||||
| 	 * @return the full exception message | 	 * @return the full exception message | ||||||
| 	 */ | 	 */ | ||||||
| 	public static String buildMessage(String message, Throwable cause) { | 	public static String buildMessage(String message, Throwable cause) { | ||||||
| 		if (cause != null) { | 		if (cause == null) { | ||||||
| 			StringBuilder sb = new StringBuilder(); |  | ||||||
| 			if (message != null) { |  | ||||||
| 				sb.append(message).append("; "); |  | ||||||
| 			} |  | ||||||
| 			sb.append("nested exception is ").append(cause); |  | ||||||
| 			return sb.toString(); |  | ||||||
| 		} |  | ||||||
| 		else { |  | ||||||
| 			return message; | 			return message; | ||||||
| 		} | 		} | ||||||
|  | 		StringBuilder sb = new StringBuilder(64); | ||||||
|  | 		if (message != null) { | ||||||
|  | 			sb.append(message).append("; "); | ||||||
|  | 		} | ||||||
|  | 		sb.append("nested exception is ").append(cause); | ||||||
|  | 		return sb.toString(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Retrieve the innermost cause of the given exception, if any. | ||||||
|  | 	 * @param original the original exception to introspect | ||||||
|  | 	 * @return the innermost exception, or {@code null} if none | ||||||
|  | 	 * @since 4.3.9 | ||||||
|  | 	 */ | ||||||
|  | 	public static Throwable getRootCause(Throwable original) { | ||||||
|  | 		if (original == null) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 		Throwable rootCause = null; | ||||||
|  | 		Throwable cause = original.getCause(); | ||||||
|  | 		while (cause != null && cause != rootCause) { | ||||||
|  | 			rootCause = cause; | ||||||
|  | 			cause = cause.getCause(); | ||||||
|  | 		} | ||||||
|  | 		return rootCause; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Retrieve the most specific cause of the given exception, that is, | ||||||
|  | 	 * either the innermost cause (root cause) or the exception itself. | ||||||
|  | 	 * <p>Differs from {@link #getRootCause} in that it falls back | ||||||
|  | 	 * to the original exception if there is no root cause. | ||||||
|  | 	 * @param original the original exception to introspect | ||||||
|  | 	 * @return the most specific cause (never {@code null}) | ||||||
|  | 	 * @since 4.3.9 | ||||||
|  | 	 */ | ||||||
|  | 	public static Throwable getMostSpecificCause(Throwable original) { | ||||||
|  | 		Throwable rootCause = getRootCause(original); | ||||||
|  | 		return (rootCause != null ? rootCause : original); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2012 the original author or authors. |  * Copyright 2002-2017 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -81,13 +81,7 @@ public abstract class NestedRuntimeException extends RuntimeException { | ||||||
| 	 * @since 2.0 | 	 * @since 2.0 | ||||||
| 	 */ | 	 */ | ||||||
| 	public Throwable getRootCause() { | 	public Throwable getRootCause() { | ||||||
| 		Throwable rootCause = null; | 		return NestedExceptionUtils.getRootCause(this); | ||||||
| 		Throwable cause = getCause(); |  | ||||||
| 		while (cause != null && cause != rootCause) { |  | ||||||
| 			rootCause = cause; |  | ||||||
| 			cause = cause.getCause(); |  | ||||||
| 		} |  | ||||||
| 		return rootCause; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ import org.apache.commons.logging.Log; | ||||||
| import org.apache.commons.logging.LogFactory; | import org.apache.commons.logging.LogFactory; | ||||||
| import reactor.core.publisher.Mono; | import reactor.core.publisher.Mono; | ||||||
| 
 | 
 | ||||||
| import org.springframework.core.NestedCheckedException; | import org.springframework.core.NestedExceptionUtils; | ||||||
| import org.springframework.http.HttpStatus; | import org.springframework.http.HttpStatus; | ||||||
| import org.springframework.http.codec.ServerCodecConfigurer; | import org.springframework.http.codec.ServerCodecConfigurer; | ||||||
| import org.springframework.http.server.reactive.HttpHandler; | import org.springframework.http.server.reactive.HttpHandler; | ||||||
|  | @ -33,7 +33,6 @@ import org.springframework.http.server.reactive.ServerHttpResponse; | ||||||
| import org.springframework.util.Assert; | import org.springframework.util.Assert; | ||||||
| import org.springframework.web.server.ServerWebExchange; | import org.springframework.web.server.ServerWebExchange; | ||||||
| import org.springframework.web.server.WebHandler; | import org.springframework.web.server.WebHandler; | ||||||
| import org.springframework.web.server.handler.ExceptionHandlingWebHandler; |  | ||||||
| import org.springframework.web.server.handler.WebHandlerDecorator; | import org.springframework.web.server.handler.WebHandlerDecorator; | ||||||
| import org.springframework.web.server.session.DefaultWebSessionManager; | import org.springframework.web.server.session.DefaultWebSessionManager; | ||||||
| import org.springframework.web.server.session.WebSessionManager; | import org.springframework.web.server.session.WebSessionManager; | ||||||
|  | @ -60,8 +59,17 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa | ||||||
| 	 * or a full stack trace only at TRACE level. | 	 * or a full stack trace only at TRACE level. | ||||||
| 	 */ | 	 */ | ||||||
| 	private static final String DISCONNECTED_CLIENT_LOG_CATEGORY = | 	private static final String DISCONNECTED_CLIENT_LOG_CATEGORY = | ||||||
| 			ExceptionHandlingWebHandler.class.getName() + ".DisconnectedClient"; | 			"org.springframework.web.server.DisconnectedClient"; | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Tomcat: ClientAbortException or EOFException | ||||||
|  | 	 * Jetty: EofException | ||||||
|  | 	 * WildFly, GlassFish: java.io.IOException "Broken pipe" (already covered) | ||||||
|  | 	 * <p>TODO: | ||||||
|  | 	 * This definition is currently duplicated between HttpWebHandlerAdapter | ||||||
|  | 	 * and AbstractSockJsSession. It is a candidate for a common utility class. | ||||||
|  | 	 * @see #indicatesDisconnectedClient(Throwable) | ||||||
|  | 	 */ | ||||||
| 	private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS = | 	private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS = | ||||||
| 			new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException")); | 			new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException")); | ||||||
| 
 | 
 | ||||||
|  | @ -115,7 +123,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa | ||||||
| 	 * Return the configured {@link ServerCodecConfigurer}. | 	 * Return the configured {@link ServerCodecConfigurer}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public ServerCodecConfigurer getCodecConfigurer() { | 	public ServerCodecConfigurer getCodecConfigurer() { | ||||||
| 		return this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create(); | 		return (this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create()); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -125,7 +133,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa | ||||||
| 		return getDelegate().handle(exchange) | 		return getDelegate().handle(exchange) | ||||||
| 				.onErrorResume(ex -> { | 				.onErrorResume(ex -> { | ||||||
| 					response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); | 					response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); | ||||||
| 					logException(ex); | 					logHandleFailure(ex); | ||||||
| 					return Mono.empty(); | 					return Mono.empty(); | ||||||
| 				}) | 				}) | ||||||
| 				.then(Mono.defer(response::setComplete)); | 				.then(Mono.defer(response::setComplete)); | ||||||
|  | @ -135,25 +143,25 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa | ||||||
| 		return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer()); | 		return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer()); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@SuppressWarnings("serial") | 	private void logHandleFailure(Throwable ex) { | ||||||
| 	private void logException(Throwable ex) { | 		if (indicatesDisconnectedClient(ex)) { | ||||||
| 		NestedCheckedException nestedEx = new NestedCheckedException("", ex) {}; |  | ||||||
| 		if ("Broken pipe".equalsIgnoreCase(nestedEx.getMostSpecificCause().getMessage()) || |  | ||||||
| 				DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())) { |  | ||||||
| 
 |  | ||||||
| 			if (disconnectedClientLogger.isTraceEnabled()) { | 			if (disconnectedClientLogger.isTraceEnabled()) { | ||||||
| 				disconnectedClientLogger.trace("Looks like the client has gone away", ex); | 				disconnectedClientLogger.trace("Looks like the client has gone away", ex); | ||||||
| 			} | 			} | ||||||
| 			else if (disconnectedClientLogger.isDebugEnabled()) { | 			else if (disconnectedClientLogger.isDebugEnabled()) { | ||||||
| 				disconnectedClientLogger.debug( | 				disconnectedClientLogger.debug("Looks like the client has gone away: " + ex + | ||||||
| 						"The client has gone away: " + nestedEx.getMessage() + | 						" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY + | ||||||
| 								" (For a full stack trace, set the log category" + | 						"' to TRACE level.)"); | ||||||
| 								"'" + DISCONNECTED_CLIENT_LOG_CATEGORY + "' to TRACE)"); |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
| 			logger.error("Could not complete request", ex); | 			logger.error("Failed to handle request", ex); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	private boolean indicatesDisconnectedClient(Throwable ex)  { | ||||||
|  | 		return ("Broken pipe".equalsIgnoreCase(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()) || | ||||||
|  | 				DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,7 +19,6 @@ package org.springframework.web.socket.sockjs.transport.session; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | @ -31,7 +30,7 @@ import java.util.concurrent.ScheduledFuture; | ||||||
| import org.apache.commons.logging.Log; | import org.apache.commons.logging.Log; | ||||||
| import org.apache.commons.logging.LogFactory; | import org.apache.commons.logging.LogFactory; | ||||||
| 
 | 
 | ||||||
| import org.springframework.core.NestedCheckedException; | import org.springframework.core.NestedExceptionUtils; | ||||||
| import org.springframework.util.Assert; | import org.springframework.util.Assert; | ||||||
| import org.springframework.web.socket.CloseStatus; | import org.springframework.web.socket.CloseStatus; | ||||||
| import org.springframework.web.socket.TextMessage; | import org.springframework.web.socket.TextMessage; | ||||||
|  | @ -70,25 +69,25 @@ public abstract class AbstractSockJsSession implements SockJsSession { | ||||||
| 	public static final String DISCONNECTED_CLIENT_LOG_CATEGORY = | 	public static final String DISCONNECTED_CLIENT_LOG_CATEGORY = | ||||||
| 			"org.springframework.web.socket.sockjs.DisconnectedClient"; | 			"org.springframework.web.socket.sockjs.DisconnectedClient"; | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Tomcat: ClientAbortException or EOFException | ||||||
|  | 	 * Jetty: EofException | ||||||
|  | 	 * WildFly, GlassFish: java.io.IOException "Broken pipe" (already covered) | ||||||
|  | 	 * <p>TODO: | ||||||
|  | 	 * This definition is currently duplicated between HttpWebHandlerAdapter | ||||||
|  | 	 * and AbstractSockJsSession. It is a candidate for a common utility class. | ||||||
|  | 	 * @see #indicatesDisconnectedClient(Throwable) | ||||||
|  | 	 */ | ||||||
|  | 	private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS = | ||||||
|  | 			new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException")); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Separate logger to use on network IO failure after a client has gone away. | 	 * Separate logger to use on network IO failure after a client has gone away. | ||||||
| 	 * @see #DISCONNECTED_CLIENT_LOG_CATEGORY | 	 * @see #DISCONNECTED_CLIENT_LOG_CATEGORY | ||||||
| 	 */ | 	 */ | ||||||
| 	protected static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY); | 	protected static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 	private static final Set<String> disconnectedClientExceptions; |  | ||||||
| 
 |  | ||||||
| 	static { |  | ||||||
| 		Set<String> set = new HashSet<String>(4); |  | ||||||
| 		set.add("ClientAbortException");  // Tomcat |  | ||||||
| 		set.add("EOFException");  // Tomcat |  | ||||||
| 		set.add("EofException");  // Jetty |  | ||||||
| 		// java.io.IOException "Broken pipe" on WildFly, Glassfish (already covered) |  | ||||||
| 		disconnectedClientExceptions = Collections.unmodifiableSet(set); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	protected final Log logger = LogFactory.getLog(getClass()); | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
| 
 | 
 | ||||||
| 	protected final Object responseLock = new Object(); | 	protected final Object responseLock = new Object(); | ||||||
|  | @ -340,28 +339,28 @@ public abstract class AbstractSockJsSession implements SockJsSession { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private void logWriteFrameFailure(Throwable failure) { | 	protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException; | ||||||
| 		@SuppressWarnings("serial") |  | ||||||
| 		NestedCheckedException nestedException = new NestedCheckedException("", failure) {}; |  | ||||||
| 
 |  | ||||||
| 		if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) || |  | ||||||
| 				disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) { |  | ||||||
| 
 | 
 | ||||||
|  | 	private void logWriteFrameFailure(Throwable ex) { | ||||||
|  | 		if (indicatesDisconnectedClient(ex)) { | ||||||
| 			if (disconnectedClientLogger.isTraceEnabled()) { | 			if (disconnectedClientLogger.isTraceEnabled()) { | ||||||
| 				disconnectedClientLogger.trace("Looks like the client has gone away", failure); | 				disconnectedClientLogger.trace("Looks like the client has gone away", ex); | ||||||
| 			} | 			} | ||||||
| 			else if (disconnectedClientLogger.isDebugEnabled()) { | 			else if (disconnectedClientLogger.isDebugEnabled()) { | ||||||
| 				disconnectedClientLogger.debug("Looks like the client has gone away: " + | 				disconnectedClientLogger.debug("Looks like the client has gone away: " + ex + | ||||||
| 						nestedException.getMessage() + " (For full stack trace, set the '" + | 						" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY + | ||||||
| 						DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category to TRACE level)"); | 						"' to TRACE level.)"); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
| 			logger.debug("Terminating connection after failure to send message to client", failure); | 			logger.debug("Terminating connection after failure to send message to client", ex); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException; | 	private boolean indicatesDisconnectedClient(Throwable ex)  { | ||||||
|  | 		return ("Broken pipe".equalsIgnoreCase(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()) || | ||||||
|  | 				DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())); | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	// Delegation methods | 	// Delegation methods | ||||||
|  | @ -421,7 +420,8 @@ public abstract class AbstractSockJsSession implements SockJsSession { | ||||||
| 			delegateError(error); | 			delegateError(error); | ||||||
| 		} | 		} | ||||||
| 		catch (Throwable delegateException) { | 		catch (Throwable delegateException) { | ||||||
| 			// ignore | 			// Ignore | ||||||
|  | 			logger.debug("Exception from error handling delegate", delegateException); | ||||||
| 		} | 		} | ||||||
| 		try { | 		try { | ||||||
| 			close(closeStatus); | 			close(closeStatus); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue