diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/task/Repackage.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/task/Repackage.java index 0b59bcbdd2..69c37ba013 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/task/Repackage.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/task/Repackage.java @@ -1,5 +1,5 @@ /* - * 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()); diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java index 0fe363d86d..8e3c7a9909 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java @@ -1,5 +1,5 @@ /* - * 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() { + @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 doWithMainClasses(File rootFolder, ClassNameCallback 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 stack = new ArrayDeque(); - 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() { + @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 doWithMainClasses(JarFile jarFile, String classesLocation, + ClassNameCallback callback) throws IOException { List 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 { + + /** + * 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); + + } } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 3cdcf63b02..54341ea3ea 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -1,5 +1,5 @@ /* - * 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 { + + private final List classNames = new ArrayList(); + + @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); + } + + } } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java index 80dde5db22..33d82eda36 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java @@ -1,5 +1,5 @@ /* - * 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 { + + private final List classNames = new ArrayList(); + + @Override + public Object doWith(String className) { + this.classNames.add(className); + return null; + } + + public List getClassNames() { + return this.classNames; + } + + } + } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index ee632ecabe..b80e12e4d5 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -1,5 +1,5 @@ /* - * 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); diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 447df033d0..de66230cf5 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -1,5 +1,5 @@ /* - * 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);