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");
|
||||
* 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
|
||||
*/
|
||||
public Throwable getRootCause() {
|
||||
Throwable rootCause = null;
|
||||
Throwable cause = getCause();
|
||||
while (cause != null && cause != rootCause) {
|
||||
rootCause = cause;
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return rootCause;
|
||||
return NestedExceptionUtils.getRootCause(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
* 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
|
||||
*/
|
||||
public static String buildMessage(String message, Throwable cause) {
|
||||
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 {
|
||||
if (cause == null) {
|
||||
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");
|
||||
* 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
|
||||
*/
|
||||
public Throwable getRootCause() {
|
||||
Throwable rootCause = null;
|
||||
Throwable cause = getCause();
|
||||
while (cause != null && cause != rootCause) {
|
||||
rootCause = cause;
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return rootCause;
|
||||
return NestedExceptionUtils.getRootCause(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ import org.apache.commons.logging.Log;
|
|||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.NestedCheckedException;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
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.web.server.ServerWebExchange;
|
||||
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.session.DefaultWebSessionManager;
|
||||
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.
|
||||
*/
|
||||
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 =
|
||||
new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException"));
|
||||
|
||||
|
@ -115,7 +123,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
|
|||
* Return the configured {@link ServerCodecConfigurer}.
|
||||
*/
|
||||
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)
|
||||
.onErrorResume(ex -> {
|
||||
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
logException(ex);
|
||||
logHandleFailure(ex);
|
||||
return Mono.empty();
|
||||
})
|
||||
.then(Mono.defer(response::setComplete));
|
||||
|
@ -135,25 +143,25 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
|
|||
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer());
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private void logException(Throwable ex) {
|
||||
NestedCheckedException nestedEx = new NestedCheckedException("", ex) {};
|
||||
if ("Broken pipe".equalsIgnoreCase(nestedEx.getMostSpecificCause().getMessage()) ||
|
||||
DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())) {
|
||||
|
||||
private void logHandleFailure(Throwable ex) {
|
||||
if (indicatesDisconnectedClient(ex)) {
|
||||
if (disconnectedClientLogger.isTraceEnabled()) {
|
||||
disconnectedClientLogger.trace("Looks like the client has gone away", ex);
|
||||
}
|
||||
else if (disconnectedClientLogger.isDebugEnabled()) {
|
||||
disconnectedClientLogger.debug(
|
||||
"The client has gone away: " + nestedEx.getMessage() +
|
||||
" (For a full stack trace, set the log category" +
|
||||
"'" + DISCONNECTED_CLIENT_LOG_CATEGORY + "' to TRACE)");
|
||||
disconnectedClientLogger.debug("Looks like the client has gone away: " + ex +
|
||||
" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY +
|
||||
"' to TRACE level.)");
|
||||
}
|
||||
}
|
||||
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.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -31,7 +30,7 @@ import java.util.concurrent.ScheduledFuture;
|
|||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.NestedCheckedException;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
|
@ -70,25 +69,25 @@ public abstract class AbstractSockJsSession implements SockJsSession {
|
|||
public static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
|
||||
"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.
|
||||
* @see #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 Object responseLock = new Object();
|
||||
|
@ -340,28 +339,28 @@ public abstract class AbstractSockJsSession implements SockJsSession {
|
|||
}
|
||||
}
|
||||
|
||||
private void logWriteFrameFailure(Throwable failure) {
|
||||
@SuppressWarnings("serial")
|
||||
NestedCheckedException nestedException = new NestedCheckedException("", failure) {};
|
||||
|
||||
if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) ||
|
||||
disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) {
|
||||
protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
|
||||
|
||||
private void logWriteFrameFailure(Throwable ex) {
|
||||
if (indicatesDisconnectedClient(ex)) {
|
||||
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()) {
|
||||
disconnectedClientLogger.debug("Looks like the client has gone away: " +
|
||||
nestedException.getMessage() + " (For full stack trace, set the '" +
|
||||
DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category to TRACE level)");
|
||||
disconnectedClientLogger.debug("Looks like the client has gone away: " + ex +
|
||||
" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY +
|
||||
"' to TRACE level.)");
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -421,7 +420,8 @@ public abstract class AbstractSockJsSession implements SockJsSession {
|
|||
delegateError(error);
|
||||
}
|
||||
catch (Throwable delegateException) {
|
||||
// ignore
|
||||
// Ignore
|
||||
logger.debug("Exception from error handling delegate", delegateException);
|
||||
}
|
||||
try {
|
||||
close(closeStatus);
|
||||
|
|
Loading…
Reference in New Issue