Commit c41a3fd5 authored by Phillip Webb's avatar Phillip Webb

Fail builds if multiple main classes are found

Update the maven and gradle plugins to fail the build if a single
unique main class cannot be found. Additionally plugins will warn
if the search is taking too long.

Fixes gh-210
parent 312535bc
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -18,6 +18,7 @@ package org.springframework.boot.gradle.task;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
......@@ -29,11 +30,13 @@ import org.springframework.boot.loader.tools.Repackager;
/**
* Repackage task.
*
*
* @author Phillip Webb
*/
public class Repackage extends DefaultTask {
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private String customConfiguration;
private Object withJarTask;
......@@ -74,7 +77,22 @@ public class Repackage extends DefaultTask {
if ("".equals(archive.getClassifier())) {
File file = archive.getArchivePath();
if (file.exists()) {
Repackager repackager = new Repackager(file);
Repackager repackager = new Repackager(file) {
protected String findMainMethod(java.util.jar.JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
try {
return super.findMainMethod(source);
}
finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
getLogger().warn(
"Searching for the main-class is taking some time, "
+ "consider using setting 'springBoot.mainClass'");
}
}
};
};
repackager.setMainClass(extension.getMainClass());
if (extension.convertLayout() != null) {
repackager.setLayout(extension.convertLayout());
......
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -77,27 +77,42 @@ public abstract class MainClassFinder {
* @throws IOException
*/
public static String findMainClass(File rootFolder) throws IOException {
return doWithMainClasses(rootFolder, new ClassNameCallback<String>() {
@Override
public String doWith(String className) {
return className;
}
});
}
/**
* Perform the given callback operation on all main classes from the given root
* folder.
* @param rootFolder the root folder
* @param callback the callback
* @return the first callback result or {@code null}
* @throws IOException
*/
public static <T> T doWithMainClasses(File rootFolder, ClassNameCallback<T> callback)
throws IOException {
if (!rootFolder.isDirectory()) {
throw new IllegalArgumentException("Inavlid root folder '" + rootFolder + "'");
}
File mainClassFile = findMainClassFile(rootFolder);
if (mainClassFile == null) {
return null;
}
String mainClass = mainClassFile.getAbsolutePath();
return convertToClassName(mainClass, rootFolder.getAbsolutePath() + "/");
}
private static File findMainClassFile(File root) throws IOException {
String prefix = rootFolder.getAbsolutePath() + "/";
Deque<File> stack = new ArrayDeque<File>();
stack.push(root);
stack.push(rootFolder);
while (!stack.isEmpty()) {
File file = stack.pop();
if (file.isFile()) {
InputStream inputStream = new FileInputStream(file);
try {
if (isMainClass(inputStream)) {
return file;
String className = convertToClassName(file.getAbsolutePath(),
prefix);
T result = callback.doWith(className);
if (result != null) {
return result;
}
}
}
finally {
......@@ -133,6 +148,24 @@ public abstract class MainClassFinder {
*/
public static String findMainClass(JarFile jarFile, String classesLocation)
throws IOException {
return doWithMainClasses(jarFile, classesLocation,
new ClassNameCallback<String>() {
@Override
public String doWith(String className) {
return className;
}
});
}
/**
* Perform the given callback operation on all main classes from the given jar.
* @param jarFile the jar file to search
* @param classesLocation the location within the jar containing classes
* @return the first callback result or {@code null}
* @throws IOException
*/
public static <T> T doWithMainClasses(JarFile jarFile, String classesLocation,
ClassNameCallback<T> callback) throws IOException {
List<JarEntry> classEntries = getClassEntries(jarFile, classesLocation);
Collections.sort(classEntries, new ClassEntryComparator());
for (JarEntry entry : classEntries) {
......@@ -140,9 +173,12 @@ public abstract class MainClassFinder {
jarFile.getInputStream(entry));
try {
if (isMainClass(inputStream)) {
String name = entry.getName();
name = convertToClassName(name, classesLocation);
return name;
String className = convertToClassName(entry.getName(),
classesLocation);
T result = callback.doWith(className);
if (result != null) {
return result;
}
}
}
finally {
......@@ -238,6 +274,20 @@ public abstract class MainClassFinder {
public boolean isFound() {
return this.found;
}
}
/**
* Callback interface used to receive class names.
*/
public static interface ClassNameCallback<T> {
/**
* Handle the specified class name
* @param className the class name
* @return a non-null value if processing should end or {@code null} to continue
*/
T doWith(String className);
}
}
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -18,9 +18,13 @@ package org.springframework.boot.loader.tools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.springframework.boot.loader.tools.MainClassFinder.ClassNameCallback;
/**
* Utility class that can be used to repackage an archive so that it can be executed using
* '{@literal java -jar}'.
......@@ -171,8 +175,7 @@ public class Repackager {
startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
}
if (startClass == null) {
startClass = MainClassFinder.findMainClass(source,
this.layout.getClassesLocation());
startClass = findMainMethod(source);
}
String launcherClassName = this.layout.getLauncherClassName();
if (launcherClassName != null) {
......@@ -193,6 +196,13 @@ public class Repackager {
return manifest;
}
protected String findMainMethod(JarFile source) throws IOException {
MainClassesCallback callback = new MainClassesCallback();
MainClassFinder.doWithMainClasses(source, this.layout.getClassesLocation(),
callback);
return callback.getMainClass();
}
private void renameFile(File file, File dest) {
if (!file.renameTo(dest)) {
throw new IllegalStateException("Unable to rename '" + file + "' to '" + dest
......@@ -206,4 +216,24 @@ public class Repackager {
}
}
private static class MainClassesCallback implements ClassNameCallback<Object> {
private final List<String> classNames = new ArrayList<String>();
@Override
public Object doWith(String className) {
this.classNames.add(className);
return null;
}
public String getMainClass() {
if (this.classNames.size() > 1) {
throw new IllegalStateException(
"Unable to find a single main class from the following candidates "
+ this.classNames);
}
return this.classNames.isEmpty() ? null : this.classNames.get(0);
}
}
}
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -17,12 +17,14 @@
package org.springframework.boot.loader.tools;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.tools.MainClassFinder;
import org.springframework.boot.loader.tools.MainClassFinder.ClassNameCallback;
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
......@@ -106,4 +108,42 @@ public class MainClassFinderTests {
assertThat(actual, equalTo("a.B"));
}
@Test
public void doWithFolderMainMethods() throws Exception {
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
this.testJarFile.addClass("a/b/G.class", ClassWithMainMethod.class);
ClassNameCollector callback = new ClassNameCollector();
MainClassFinder.doWithMainClasses(this.testJarFile.getJarSource(), callback);
assertThat(callback.getClassNames().toString(), equalTo("[a.b.G, a.b.c.D]"));
}
@Test
public void doWithJarMainMethods() throws Exception {
this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
this.testJarFile.addClass("a/b/G.class", ClassWithMainMethod.class);
ClassNameCollector callback = new ClassNameCollector();
MainClassFinder.doWithMainClasses(this.testJarFile.getJarFile(), "", callback);
assertThat(callback.getClassNames().toString(), equalTo("[a.b.G, a.b.c.D]"));
}
private static class ClassNameCollector implements ClassNameCallback<Object> {
private final List<String> classNames = new ArrayList<String>();
@Override
public Object doWith(String className) {
this.classNames.add(className);
return null;
}
public List<String> getClassNames() {
return this.classNames;
}
}
}
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -130,6 +130,18 @@ public class RepackagerTests {
assertThat(hasLauncherClasses(file), equalTo(true));
}
@Test
public void multipleMainClassFound() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class);
File file = this.testJarFile.getFile();
Repackager repackager = new Repackager(file);
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Unable to find a single main class "
+ "from the following candidates [a.b.C, a.b.D]");
repackager.repackage(NO_LIBRARIES);
}
@Test
public void noMainClass() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class);
......
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
......@@ -18,6 +18,8 @@ package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
......@@ -46,6 +48,8 @@ import org.springframework.boot.loader.tools.Repackager;
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractMojo {
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
/**
* The Maven project.
*/
......@@ -95,7 +99,24 @@ public class RepackageMojo extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
File source = this.project.getArtifact().getFile();
File target = getTargetFile();
Repackager repackager = new Repackager(source);
Repackager repackager = new Repackager(source) {
@Override
protected String findMainMethod(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
try {
return super.findMainMethod(source);
}
finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
getLog().warn(
"Searching for the main-class is taking some time, "
+ "consider using the mainClass configuration "
+ "parameter");
}
}
}
};
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment