Commit 38a42a86 authored by Stephane Nicoll's avatar Stephane Nicoll

Improve NoSuchMethodError message parsing

Closes gh-17544
parent 26ee9150
...@@ -37,6 +37,18 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr ...@@ -37,6 +37,18 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
@Override @Override
protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) { protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) {
NoSuchMethodDescriptor descriptor = getNoSuchMethodDescriptor(cause.getMessage());
if (descriptor == null) {
return null;
}
String description = getDescription(cause, descriptor);
return new FailureAnalysis(description,
"Correct the classpath of your application so that it contains a single, compatible version of "
+ descriptor.getClassName(),
cause);
}
protected NoSuchMethodDescriptor getNoSuchMethodDescriptor(String cause) {
String message = cleanMessage(cause); String message = cleanMessage(cause);
String className = extractClassName(message); String className = extractClassName(message);
if (className == null) { if (className == null) {
...@@ -50,22 +62,25 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr ...@@ -50,22 +62,25 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
if (actual == null) { if (actual == null) {
return null; return null;
} }
String description = getDescription(cause, message, className, candidates, actual); return new NoSuchMethodDescriptor(message, className, candidates, actual);
return new FailureAnalysis(description,
"Correct the classpath of your application so that it contains a single, compatible version of "
+ className,
cause);
} }
private String cleanMessage(NoSuchMethodError error) { private String cleanMessage(String message) {
int loadedFromIndex = error.getMessage().indexOf(" (loaded from"); int loadedFromIndex = message.indexOf(" (loaded from");
if (loadedFromIndex == -1) { if (loadedFromIndex == -1) {
return error.getMessage(); return message;
} }
return error.getMessage().substring(0, loadedFromIndex); return message.substring(0, loadedFromIndex);
} }
private String extractClassName(String message) { private String extractClassName(String message) {
if (message.startsWith("'") && message.endsWith("'")) {
int splitIndex = message.indexOf(' ');
if (splitIndex == -1) {
return null;
}
message = message.substring(splitIndex + 1);
}
int descriptorIndex = message.indexOf('('); int descriptorIndex = message.indexOf('(');
if (descriptorIndex == -1) { if (descriptorIndex == -1) {
return null; return null;
...@@ -98,8 +113,7 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr ...@@ -98,8 +113,7 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
} }
} }
private String getDescription(NoSuchMethodError cause, String message, String className, List<URL> candidates, private String getDescription(NoSuchMethodError cause, NoSuchMethodDescriptor descriptor) {
URL actual) {
StringWriter description = new StringWriter(); StringWriter description = new StringWriter();
PrintWriter writer = new PrintWriter(description); PrintWriter writer = new PrintWriter(description);
writer.println("An attempt was made to call a method that does not" writer.println("An attempt was made to call a method that does not"
...@@ -111,11 +125,12 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr ...@@ -111,11 +125,12 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
writer.println("The following method did not exist:"); writer.println("The following method did not exist:");
writer.println(); writer.println();
writer.print(" "); writer.print(" ");
writer.println(message); writer.println(descriptor.getErrorMessage());
writer.println(); writer.println();
writer.println("The method's class, " + className + ", is available from the following locations:"); writer.println(
"The method's class, " + descriptor.getClassName() + ", is available from the following locations:");
writer.println(); writer.println();
for (URL candidate : candidates) { for (URL candidate : descriptor.getCandidateLocations()) {
writer.print(" "); writer.print(" ");
writer.println(candidate); writer.println(candidate);
} }
...@@ -123,8 +138,44 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr ...@@ -123,8 +138,44 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
writer.println("It was loaded from the following location:"); writer.println("It was loaded from the following location:");
writer.println(); writer.println();
writer.print(" "); writer.print(" ");
writer.println(actual); writer.println(descriptor.getActualLocation());
return description.toString(); return description.toString();
} }
protected static class NoSuchMethodDescriptor {
private final String errorMessage;
private final String className;
private final List<URL> candidateLocations;
private final URL actualLocation;
public NoSuchMethodDescriptor(String errorMessage, String className, List<URL> candidateLocations,
URL actualLocation) {
this.errorMessage = errorMessage;
this.className = className;
this.candidateLocations = candidateLocations;
this.actualLocation = actualLocation;
}
public String getErrorMessage() {
return this.errorMessage;
}
public String getClassName() {
return this.className;
}
public List<URL> getCandidateLocations() {
return this.candidateLocations;
}
public URL getActualLocation() {
return this.actualLocation;
}
}
} }
...@@ -25,6 +25,7 @@ import org.junit.jupiter.api.condition.EnabledOnJre; ...@@ -25,6 +25,7 @@ import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.condition.JRE;
import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer.NoSuchMethodDescriptor;
import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -34,10 +35,53 @@ import static org.mockito.Mockito.mock; ...@@ -34,10 +35,53 @@ import static org.mockito.Mockito.mock;
* Tests for {@link NoSuchMethodFailureAnalyzer}. * Tests for {@link NoSuchMethodFailureAnalyzer}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll
*/ */
@ClassPathOverrides("javax.servlet:servlet-api:2.5") @ClassPathOverrides("javax.servlet:servlet-api:2.5")
class NoSuchMethodFailureAnalyzerTests { class NoSuchMethodFailureAnalyzerTests {
@Test
void parseJava8ErrorMessage() {
NoSuchMethodDescriptor descriptor = new NoSuchMethodFailureAnalyzer().getNoSuchMethodDescriptor(
"javax.servlet.ServletContext.addServlet(Ljava/lang/String;Ljavax/servlet/Servlet;)"
+ "Ljavax/servlet/ServletRegistration$Dynamic;");
assertThat(descriptor).isNotNull();
assertThat(descriptor.getErrorMessage())
.isEqualTo("javax.servlet.ServletContext.addServlet(Ljava/lang/String;Ljavax/servlet/Servlet;)"
+ "Ljavax/servlet/ServletRegistration$Dynamic;");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations()).isNotEmpty();
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test
void parseJava13OpenJ9ErrorMessage() {
NoSuchMethodDescriptor descriptor = new NoSuchMethodFailureAnalyzer().getNoSuchMethodDescriptor(
"javax/servlet/ServletContext.addServlet(Ljava/lang/String;Ljavax/servlet/Servlet;)"
+ "Ljavax/servlet/ServletRegistration$Dynamic; (loaded from file...");
assertThat(descriptor).isNotNull();
assertThat(descriptor.getErrorMessage())
.isEqualTo("javax/servlet/ServletContext.addServlet(Ljava/lang/String;Ljavax/servlet/Servlet;)"
+ "Ljavax/servlet/ServletRegistration$Dynamic;");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations()).isNotEmpty();
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test
void parseJava13HotspotErrorMessage() {
NoSuchMethodDescriptor descriptor = new NoSuchMethodFailureAnalyzer().getNoSuchMethodDescriptor(
"'javax.servlet.ServletRegistration$Dynamic javax.servlet.ServletContext.addServlet("
+ "java.lang.String, javax.servlet.Servlet)'");
assertThat(descriptor).isNotNull();
assertThat(descriptor.getErrorMessage())
.isEqualTo("'javax.servlet.ServletRegistration$Dynamic javax.servlet.ServletContext.addServlet("
+ "java.lang.String, javax.servlet.Servlet)'");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations()).isNotEmpty();
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test @Test
@EnabledOnJre({ JRE.JAVA_8, JRE.JAVA_11, JRE.JAVA_12 }) @EnabledOnJre({ JRE.JAVA_8, JRE.JAVA_11, JRE.JAVA_12 })
void noSuchMethodErrorIsAnalyzedJava8To12() { void noSuchMethodErrorIsAnalyzedJava8To12() {
...@@ -50,8 +94,8 @@ class NoSuchMethodFailureAnalyzerTests { ...@@ -50,8 +94,8 @@ class NoSuchMethodFailureAnalyzerTests {
@DisabledOnJre({ JRE.JAVA_8, JRE.JAVA_11, JRE.JAVA_12 }) @DisabledOnJre({ JRE.JAVA_8, JRE.JAVA_11, JRE.JAVA_12 })
void noSuchMethodErrorIsAnalyzedJava13AndLater() { void noSuchMethodErrorIsAnalyzedJava13AndLater() {
testNoSuchMethodErrorFailureAnalysis( testNoSuchMethodErrorFailureAnalysis(
"javax/servlet/ServletContext.addServlet(Ljava/lang/String;Ljavax/servlet/Servlet;)" "'javax.servlet.ServletRegistration$Dynamic javax.servlet.ServletContext.addServlet("
+ "Ljavax/servlet/ServletRegistration$Dynamic;"); + "java.lang.String, javax.servlet.Servlet)'");
} }
private void testNoSuchMethodErrorFailureAnalysis(String expectedMethodRepresentation) { private void testNoSuchMethodErrorFailureAnalysis(String expectedMethodRepresentation) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment