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");
* 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 Stephane Nicoll
* @author Scott Frederick
*/
class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodError> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) {
NoSuchMethodDescriptor descriptor = getNoSuchMethodDescriptor(cause.getMessage());
if (descriptor == null) {
NoSuchMethodDescriptor callerDescriptor = getCallerMethodDescriptor(cause);
if (callerDescriptor == 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);
NoSuchMethodDescriptor calledDescriptor = getNoSuchMethodDescriptor(cause.getMessage());
if (calledDescriptor == null) {
return null;
}
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) {
String message = cleanMessage(cause);
String className = extractClassName(message);
return getDescriptorForClass(message, className);
}
private NoSuchMethodDescriptor getDescriptorForClass(String message, String className) {
if (className == 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();
PrintWriter writer = new PrintWriter(description);
writer.println("An attempt was made to call a method that does not"
+ " exist. The attempt was made from the following location:");
writer.println();
writer.print(" ");
writer.println(cause.getStackTrace()[0]);
writer.printf(" %s%n", callerDescriptor.getErrorMessage());
writer.println();
writer.println("The following method did not exist:");
writer.println();
writer.print(" ");
writer.println(descriptor.getErrorMessage());
writer.printf(" %s%n", calledDescriptor.getErrorMessage());
writer.println();
writer.println(
"The method's class, " + descriptor.getClassName() + ", is available from the following locations:");
writer.println();
for (URL candidate : descriptor.getCandidateLocations()) {
writer.print(" ");
writer.println(candidate);
if (callerDescriptor.getCandidateLocations().size() > 1) {
writer.printf("The calling method's class, %s, is available from the following locations:%n",
callerDescriptor.getClassName());
writer.println();
for (URL candidate : callerDescriptor.getCandidateLocations()) {
writer.printf(" %s%n", candidate);
}
writer.println();
writer.println("The calling method's class was loaded from the following location:");
writer.println();
writer.printf(" %s%n", callerDescriptor.getTypeHierarchy().get(0).getLocation());
}
else {
writer.printf("The calling method's class, %s, was loaded from the following location:%n",
callerDescriptor.getClassName());
writer.println();
writer.printf(" %s%n", callerDescriptor.getCandidateLocations().get(0));
}
writer.println();
writer.println("The class hierarchy was loaded from the following locations:");
writer.printf("The called method's class, %s, is available from the following locations:%n",
calledDescriptor.getClassName());
writer.println();
for (ClassDescriptor type : descriptor.getTypeHierarchy()) {
writer.print(" ");
writer.print(type.getName());
writer.print(": ");
writer.println(type.getLocation());
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();
}
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 {
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");
* 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 Stephane Nicoll
* @author Scott Frederick
*/
@ClassPathOverrides({ "javax.servlet:servlet-api:2.5",
"org.springframework.data:spring-data-relational:1.1.7.RELEASE" })
@ -98,7 +99,10 @@ class NoSuchMethodFailureAnalyzerTests {
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription())
.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
@ -109,10 +113,12 @@ class NoSuchMethodFailureAnalyzerTests {
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).contains(R2dbcMappingContext.class.getName() + ".<init>(")
.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.relational.core.mapping.RelationalMappingContext")
.contains(" org.springframework.data.mapping.context.AbstractMappingContext");
assertThat(analysis.getAction()).contains("org.springframework.data.r2dbc.mapping.R2dbcMappingContext");
}
private Throwable createFailure() {