From fffe2e4f979a96cf2c8512caecdce60949808952 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 17 Jun 2021 13:37:33 -0500 Subject: [PATCH] Include caller info in NoSuchMethodError failure analysis Fixes gh-26895 --- .../analyzer/NoSuchMethodFailureAnalyzer.java | 88 +++++++++++++------ .../NoSuchMethodFailureAnalyzerTests.java | 12 ++- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java index ad73fc21ff8..1b0214beba9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java @@ -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 { @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 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; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java index 8fe48d0e206..a185391c431 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java @@ -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() + ".(") .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() {