Browse Source

Common root cause introspection algorithm in NestedExceptionUtils

Issue: SPR-15510
pull/1418/head
Juergen Hoeller 8 years ago
parent
commit
9d8e9cf243
  1. 10
      spring-core/src/main/java/org/springframework/core/NestedCheckedException.java
  2. 41
      spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java
  3. 10
      spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java
  4. 40
      spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java
  5. 56
      spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java

10
spring-core/src/main/java/org/springframework/core/NestedCheckedException.java

@ -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;
} }
/** /**

41
spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java

@ -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(); return message;
}
StringBuilder sb = new StringBuilder(64);
if (message != null) { if (message != null) {
sb.append(message).append("; "); sb.append(message).append("; ");
} }
sb.append("nested exception is ").append(cause); sb.append("nested exception is ").append(cause);
return sb.toString(); return sb.toString();
} }
else {
return message; /**
* 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);
} }
} }

10
spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java

@ -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;
} }
/** /**

40
spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java

@ -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()));
}
} }

56
spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java

@ -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…
Cancel
Save