Include caller info in NoSuchMethodError failure analysis

Fixes gh-26895
This commit is contained in:
Scott Frederick 2021-06-17 13:37:33 -05:00
parent b1c04e91d7
commit fffe2e4f97
2 changed files with 72 additions and 28 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 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.
@ -33,25 +33,39 @@ import org.springframework.util.ClassUtils;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Scott Frederick
*/ */
class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodError> { class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodError> {
@Override @Override
protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) { protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) {
NoSuchMethodDescriptor descriptor = getNoSuchMethodDescriptor(cause.getMessage()); NoSuchMethodDescriptor callerDescriptor = getCallerMethodDescriptor(cause);
if (descriptor == null) { if (callerDescriptor == null) {
return null; return null;
} }
String description = getDescription(cause, descriptor); NoSuchMethodDescriptor calledDescriptor = getNoSuchMethodDescriptor(cause.getMessage());
return new FailureAnalysis(description, if (calledDescriptor == null) {
"Correct the classpath of your application so that it contains a single, compatible version of " return null;
+ descriptor.getClassName(), }
cause); String description = getDescription(callerDescriptor, calledDescriptor);
String action = getAction(callerDescriptor, calledDescriptor);
return new FailureAnalysis(description, action, cause);
}
private NoSuchMethodDescriptor getCallerMethodDescriptor(NoSuchMethodError cause) {
StackTraceElement firstStackTraceElement = cause.getStackTrace()[0];
String message = firstStackTraceElement.toString();
String className = firstStackTraceElement.getClassName();
return getDescriptorForClass(message, className);
} }
protected NoSuchMethodDescriptor getNoSuchMethodDescriptor(String cause) { protected NoSuchMethodDescriptor getNoSuchMethodDescriptor(String cause) {
String message = cleanMessage(cause); String message = cleanMessage(cause);
String className = extractClassName(message); String className = extractClassName(message);
return getDescriptorForClass(message, className);
}
private NoSuchMethodDescriptor getDescriptorForClass(String message, String className) {
if (className == null) { if (className == null) {
return null; return null;
} }
@ -133,40 +147,64 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
} }
} }
private String getDescription(NoSuchMethodError cause, NoSuchMethodDescriptor descriptor) { private String getDescription(NoSuchMethodDescriptor callerDescriptor, NoSuchMethodDescriptor calledDescriptor) {
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"
+ " exist. The attempt was made from the following location:"); + " exist. The attempt was made from the following location:");
writer.println(); writer.println();
writer.print(" "); writer.printf(" %s%n", callerDescriptor.getErrorMessage());
writer.println(cause.getStackTrace()[0]);
writer.println(); writer.println();
writer.println("The following method did not exist:"); writer.println("The following method did not exist:");
writer.println(); writer.println();
writer.print(" "); writer.printf(" %s%n", calledDescriptor.getErrorMessage());
writer.println(descriptor.getErrorMessage());
writer.println(); writer.println();
writer.println( if (callerDescriptor.getCandidateLocations().size() > 1) {
"The method's class, " + descriptor.getClassName() + ", is available from the following locations:"); writer.printf("The calling method's class, %s, is available from the following locations:%n",
callerDescriptor.getClassName());
writer.println(); writer.println();
for (URL candidate : descriptor.getCandidateLocations()) { for (URL candidate : callerDescriptor.getCandidateLocations()) {
writer.print(" "); writer.printf(" %s%n", candidate);
writer.println(candidate);
} }
writer.println(); writer.println();
writer.println("The class hierarchy was loaded from the following locations:"); writer.println("The calling method's class was loaded from the following location:");
writer.println(); writer.println();
for (ClassDescriptor type : descriptor.getTypeHierarchy()) { writer.printf(" %s%n", callerDescriptor.getTypeHierarchy().get(0).getLocation());
writer.print(" "); }
writer.print(type.getName()); else {
writer.print(": "); writer.printf("The calling method's class, %s, was loaded from the following location:%n",
writer.println(type.getLocation()); callerDescriptor.getClassName());
writer.println();
writer.printf(" %s%n", callerDescriptor.getCandidateLocations().get(0));
}
writer.println();
writer.printf("The called method's class, %s, is available from the following locations:%n",
calledDescriptor.getClassName());
writer.println();
for (URL candidate : calledDescriptor.getCandidateLocations()) {
writer.printf(" %s%n", candidate);
}
writer.println();
writer.println("The called method's class hierarchy was loaded from the following locations:");
writer.println();
for (ClassDescriptor type : calledDescriptor.getTypeHierarchy()) {
writer.printf(" %s: %s%n", type.getName(), type.getLocation());
} }
return description.toString(); return description.toString();
} }
private String getAction(NoSuchMethodDescriptor callerDescriptor, NoSuchMethodDescriptor calledDescriptor) {
if (callerDescriptor.getClassName().equals(calledDescriptor.getClassName())) {
return "Correct the classpath of your application so that it contains a single, compatible version of "
+ calledDescriptor.getClassName();
}
else {
return "Correct the classpath of your application so that it contains compatible versions of the classes "
+ callerDescriptor.getClassName() + " and " + calledDescriptor.getClassName();
}
}
protected static class NoSuchMethodDescriptor { protected static class NoSuchMethodDescriptor {
private final String errorMessage; private final String errorMessage;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 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.
@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Scott Frederick
*/ */
@ClassPathOverrides({ "javax.servlet:servlet-api:2.5", @ClassPathOverrides({ "javax.servlet:servlet-api:2.5",
"org.springframework.data:spring-data-relational:1.1.7.RELEASE" }) "org.springframework.data:spring-data-relational:1.1.7.RELEASE" })
@ -98,7 +99,10 @@ class NoSuchMethodFailureAnalyzerTests {
assertThat(analysis).isNotNull(); assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()) assertThat(analysis.getDescription())
.contains(NoSuchMethodFailureAnalyzerTests.class.getName() + ".createFailure(").contains("addServlet(") .contains(NoSuchMethodFailureAnalyzerTests.class.getName() + ".createFailure(").contains("addServlet(")
.contains("class, javax.servlet.ServletContext,"); .contains("calling method's class, " + NoSuchMethodFailureAnalyzerTests.class.getName() + ",")
.contains("called method's class, javax.servlet.ServletContext,");
assertThat(analysis.getAction()).contains(NoSuchMethodFailureAnalyzerTests.class.getName())
.contains("javax.servlet.ServletContext");
} }
@Test @Test
@ -109,10 +113,12 @@ class NoSuchMethodFailureAnalyzerTests {
assertThat(analysis).isNotNull(); assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).contains(R2dbcMappingContext.class.getName() + ".<init>(") assertThat(analysis.getDescription()).contains(R2dbcMappingContext.class.getName() + ".<init>(")
.contains(R2dbcMappingContext.class.getName() + ".setForceQuote(") .contains(R2dbcMappingContext.class.getName() + ".setForceQuote(")
.contains("class, org.springframework.data.r2dbc.mapping.R2dbcMappingContext,") .contains("calling method's class, org.springframework.data.r2dbc.mapping.R2dbcMappingContext,")
.contains("called method's class, org.springframework.data.r2dbc.mapping.R2dbcMappingContext,")
.contains(" org.springframework.data.r2dbc.mapping.R2dbcMappingContext") .contains(" org.springframework.data.r2dbc.mapping.R2dbcMappingContext")
.contains(" org.springframework.data.relational.core.mapping.RelationalMappingContext") .contains(" org.springframework.data.relational.core.mapping.RelationalMappingContext")
.contains(" org.springframework.data.mapping.context.AbstractMappingContext"); .contains(" org.springframework.data.mapping.context.AbstractMappingContext");
assertThat(analysis.getAction()).contains("org.springframework.data.r2dbc.mapping.R2dbcMappingContext");
} }
private Throwable createFailure() { private Throwable createFailure() {