From 37bd286985ffd1a33029cb48e04fc80934c93431 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Aug 2018 12:33:13 +0100 Subject: [PATCH] Perform failure analysis of NoSuchMethodErrors Closes gh-14040 --- .../analyzer/NoSuchMethodFailureAnalyzer.java | 108 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../NoSuchMethodFailureAnalyzerTests.java | 64 +++++++++++ 3 files changed, 173 insertions(+) create mode 100644 spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java new file mode 100644 index 00000000000..3ab1d65511c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzer.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics.analyzer; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.util.ClassUtils; + +/** + * An {@link AbstractFailureAnalyzer} that analyzes {@link NoSuchMethodError + * NoSuchMethodErrors}. + * + * @author Andy Wilkinson + */ +class NoSuchMethodFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, NoSuchMethodError cause) { + String className = extractClassName(cause); + if (className == null) { + return null; + } + List candidates = findCandidates(className); + if (candidates == null) { + return null; + } + URL actual = getActual(className); + if (actual == null) { + return null; + } + StringWriter description = new StringWriter(); + PrintWriter writer = new PrintWriter(description); + writer.print("An attempt was made to call the method "); + writer.print(cause.getMessage()); + writer.print(" but it does not exist. Its class, "); + writer.print(className); + writer.println(", is available from the following locations:"); + writer.println(); + for (URL candidate : candidates) { + writer.print(" "); + writer.println(candidate); + } + writer.println(); + writer.println("It was loaded from the following location:"); + writer.println(); + writer.print(" "); + writer.println(actual); + return new FailureAnalysis(description.toString(), + "Correct the classpath of your application so that it contains a single," + + " compatible version of " + className, + cause); + } + + private String extractClassName(NoSuchMethodError cause) { + int descriptorIndex = cause.getMessage().indexOf('('); + if (descriptorIndex == -1) { + return null; + } + String classAndMethodName = cause.getMessage().substring(0, descriptorIndex); + int methodNameIndex = classAndMethodName.lastIndexOf('.'); + if (methodNameIndex == -1) { + return null; + } + return classAndMethodName.substring(0, methodNameIndex); + } + + private List findCandidates(String className) { + try { + return Collections.list((NoSuchMethodFailureAnalyzer.class.getClassLoader() + .getResources(ClassUtils.convertClassNameToResourcePath(className) + + ".class"))); + } + catch (Throwable ex) { + return null; + } + } + + private URL getActual(String className) { + try { + return getClass().getClassLoader().loadClass(className).getProtectionDomain() + .getCodeSource().getLocation(); + } + catch (Throwable ex) { + return null; + } + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index 2563865d9b1..4334be800ab 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -37,6 +37,7 @@ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnal org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\ +org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java new file mode 100644 index 00000000000..cf687a97456 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoSuchMethodFailureAnalyzerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics.analyzer; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServlet; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.junit.runner.classpath.ClassPathOverrides; +import org.springframework.boot.junit.runner.classpath.ModifiedClassPathRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author awilkinson + */ +@RunWith(ModifiedClassPathRunner.class) +@ClassPathOverrides("javax.servlet:servlet-api:2.5") +public class NoSuchMethodFailureAnalyzerTests { + + @Test + public void noSuchMethodErrorIsAnalyzed() { + Throwable failure = createFailure(); + assertThat(failure).isNotNull(); + FailureAnalysis analysis = new NoSuchMethodFailureAnalyzer().analyze(failure); + assertThat(analysis).isNotNull(); + assertThat(analysis.getDescription()) + .contains("the method javax.servlet.ServletContext.addServlet" + + "(Ljava/lang/String;Ljavax/servlet/Servlet;)" + + "Ljavax/servlet/ServletRegistration$Dynamic;") + .contains("class, javax.servlet.ServletContext,"); + } + + private Throwable createFailure() { + try { + ServletContext servletContext = mock(ServletContext.class); + servletContext.addServlet("example", new HttpServlet() { + }); + return null; + } + catch (Throwable ex) { + return ex; + } + } + +}