Include type hierarchy in NoSuchMethodError failure analysis

Closes gh-21587
This commit is contained in:
Andy Wilkinson 2020-05-27 10:59:59 +01:00
parent 1975cf498c
commit 744b4d7c26
3 changed files with 101 additions and 20 deletions

View File

@ -100,6 +100,7 @@ dependencies {
testImplementation("org.postgresql:postgresql")
testImplementation("org.springframework:spring-context-support")
testImplementation("org.springframework.data:spring-data-redis")
testImplementation("org.springframework.data:spring-data-r2dbc")
testImplementation("org.xerial:sqlite-jdbc")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

View File

@ -19,6 +19,7 @@ package org.springframework.boot.diagnostics.analyzer;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -58,11 +59,15 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
if (candidates == null) {
return null;
}
URL actual = getActual(className);
if (actual == null) {
Class<?> type = load(className);
if (type == null) {
return null;
}
return new NoSuchMethodDescriptor(message, className, candidates, actual);
List<ClassDescriptor> typeHierarchy = getTypeHierarchy(type);
if (typeHierarchy == null) {
return null;
}
return new NoSuchMethodDescriptor(message, className, candidates, typeHierarchy);
}
private String cleanMessage(String message) {
@ -104,10 +109,24 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
}
}
private URL getActual(String className) {
private Class<?> load(String className) {
try {
return Class.forName(className, false, getClass().getClassLoader()).getProtectionDomain().getCodeSource()
.getLocation();
return Class.forName(className, false, getClass().getClassLoader());
}
catch (Throwable ex) {
return null;
}
}
private List<ClassDescriptor> getTypeHierarchy(Class<?> type) {
try {
List<ClassDescriptor> typeHierarchy = new ArrayList<>();
while (type != null && !type.equals(Object.class)) {
typeHierarchy.add(new ClassDescriptor(type.getCanonicalName(),
type.getProtectionDomain().getCodeSource().getLocation()));
type = type.getSuperclass();
}
return typeHierarchy;
}
catch (Throwable ex) {
return null;
@ -136,10 +155,15 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
writer.println(candidate);
}
writer.println();
writer.println("It was loaded from the following location:");
writer.println("The class hierarchy was loaded from the following locations:");
writer.println();
writer.print(" ");
writer.println(descriptor.getActualLocation());
for (ClassDescriptor type : descriptor.getTypeHierarchy()) {
writer.print(" ");
writer.print(type.getName());
writer.print(": ");
writer.println(type.getLocation());
}
return description.toString();
}
@ -151,14 +175,14 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
private final List<URL> candidateLocations;
private final URL actualLocation;
private final List<ClassDescriptor> typeHierarchy;
public NoSuchMethodDescriptor(String errorMessage, String className, List<URL> candidateLocations,
URL actualLocation) {
List<ClassDescriptor> typeHierarchy) {
this.errorMessage = errorMessage;
this.className = className;
this.candidateLocations = candidateLocations;
this.actualLocation = actualLocation;
this.typeHierarchy = typeHierarchy;
}
public String getErrorMessage() {
@ -173,8 +197,29 @@ class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchMethodEr
return this.candidateLocations;
}
public URL getActualLocation() {
return this.actualLocation;
public List<ClassDescriptor> getTypeHierarchy() {
return this.typeHierarchy;
}
}
protected static class ClassDescriptor {
private final String name;
private final URL location;
public ClassDescriptor(String name, URL location) {
this.name = name;
this.location = location;
}
public String getName() {
return this.name;
}
public URL getLocation() {
return this.location;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -16,14 +16,18 @@
package org.springframework.boot.diagnostics.analyzer;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer.ClassDescriptor;
import org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer.NoSuchMethodDescriptor;
import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@ -34,7 +38,8 @@ import static org.mockito.Mockito.mock;
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
@ClassPathOverrides("javax.servlet:servlet-api:2.5")
@ClassPathOverrides({ "javax.servlet:servlet-api:2.5",
"org.springframework.data:spring-data-relational:1.1.7.RELEASE" })
class NoSuchMethodFailureAnalyzerTests {
@Test
@ -48,7 +53,9 @@ class NoSuchMethodFailureAnalyzerTests {
+ "Ljavax/servlet/ServletRegistration$Dynamic;");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations().size()).isGreaterThan(1);
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
List<ClassDescriptor> typeHierarchy = descriptor.getTypeHierarchy();
assertThat(typeHierarchy).hasSize(1);
assertThat(typeHierarchy.get(0).getLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test
@ -62,7 +69,9 @@ class NoSuchMethodFailureAnalyzerTests {
+ "Ljavax/servlet/ServletRegistration$Dynamic;");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations().size()).isGreaterThan(1);
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
List<ClassDescriptor> typeHierarchy = descriptor.getTypeHierarchy();
assertThat(typeHierarchy).hasSize(1);
assertThat(typeHierarchy.get(0).getLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test
@ -76,11 +85,13 @@ class NoSuchMethodFailureAnalyzerTests {
+ "java.lang.String, javax.servlet.Servlet)'");
assertThat(descriptor.getClassName()).isEqualTo("javax.servlet.ServletContext");
assertThat(descriptor.getCandidateLocations().size()).isGreaterThan(1);
assertThat(descriptor.getActualLocation()).asString().contains("servlet-api-2.5.jar");
List<ClassDescriptor> typeHierarchy = descriptor.getTypeHierarchy();
assertThat(typeHierarchy).hasSize(1);
assertThat(typeHierarchy.get(0).getLocation()).asString().contains("servlet-api-2.5.jar");
}
@Test
void noSuchMethodErrorIsAnalyzed() {
void whenAMethodOnAnInterfaceIsMissingThenNoSuchMethodErrorIsAnalyzed() {
Throwable failure = createFailure();
assertThat(failure).isNotNull();
FailureAnalysis analysis = new NoSuchMethodFailureAnalyzer().analyze(failure);
@ -90,6 +101,20 @@ class NoSuchMethodFailureAnalyzerTests {
.contains("class, javax.servlet.ServletContext,");
}
@Test
void whenAnInheritedMethodIsMissingThenNoSuchMethodErrorIsAnalyzed() {
Throwable failure = createFailureForMissingInheritedMethod();
assertThat(failure).isNotNull();
FailureAnalysis analysis = new NoSuchMethodFailureAnalyzer().analyze(failure);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).contains(R2dbcMappingContext.class.getName() + ".<init>(")
.contains("setForceQuote(Z)V")
.contains("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");
}
private Throwable createFailure() {
try {
ServletContext servletContext = mock(ServletContext.class);
@ -102,4 +127,14 @@ class NoSuchMethodFailureAnalyzerTests {
}
}
private Throwable createFailureForMissingInheritedMethod() {
try {
new R2dbcMappingContext();
return null;
}
catch (Throwable ex) {
return ex;
}
}
}