Re-organize the Launcher code a bit

pull/59/merge
Dave Syer 11 years ago
parent e9fd7c96b8
commit a70d293c87

@ -0,0 +1,104 @@
/*
* Copyright 2013 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.loader;
import java.io.File;
import java.net.URI;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* Base class for launchers that can start an application with a fully configured
* classpath.
*
* @author Phillip Webb
* @author Dave Syer
*/
public abstract class AbstractLauncher implements ArchiveFilter {
private Logger logger = Logger.getLogger(AbstractLauncher.class.getName());
private LaunchHelper helper = new LaunchHelper();
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
*/
public void launch(String[] args) {
try {
launch(args, getClass().getProtectionDomain());
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
/**
* Launch the application given the protection domain.
* @param args the incoming arguments
* @param protectionDomain the protection domain
* @throws Exception
*/
protected void launch(String[] args, ProtectionDomain protectionDomain)
throws Exception {
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getPath());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
Archive archive = (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
launch(args, archive);
}
/**
* Launch the application given the archive file
* @param args the incoming arguments
* @param archive the underlying (zip/war/jar) archive
* @throws Exception
*/
protected void launch(String[] args, Archive archive) throws Exception {
List<Archive> lib = new ArrayList<Archive>();
lib.addAll(this.helper.findNestedArchives(archive, this));
this.logger.fine("Added " + lib.size() + " entries");
postProcessLib(archive, lib);
String mainClass = this.helper.getMainClass(archive);
this.helper.launch(args, mainClass, lib);
}
/**
* Called to post-process lib entries before they are used. Implementations can add
* and remove entries.
* @param archive the archive
* @param lib the existing lib
* @throws Exception
*/
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
}
}

@ -22,7 +22,7 @@ import java.net.URL;
import java.util.jar.Manifest; import java.util.jar.Manifest;
/** /**
* An archive that can be launched by the {@link Launcher}. * An archive that can be launched by the {@link AbstractLauncher}.
* *
* @author Phillip Webb * @author Phillip Webb
* @see JarFileArchive * @see JarFileArchive

@ -0,0 +1,26 @@
/*
* Copyright 2012-2013 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.loader;
/**
* @author Dave Syer
*/
public interface ArchiveFilter {
public boolean isArchive(Archive.Entry entry);
}

@ -19,15 +19,19 @@ package org.springframework.boot.loader;
import java.util.List; import java.util.List;
/** /**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are * {@link AbstractLauncher} for JAR based archives. This launcher assumes that dependency
* included inside a {@code /lib} directory. * jars are included inside a {@code /lib} directory.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class JarLauncher extends Launcher { public class JarLauncher extends AbstractLauncher {
public static void main(String[] args) {
new JarLauncher().launch(args);
}
@Override @Override
protected boolean isNestedArchive(Archive.Entry entry) { public boolean isArchive(Archive.Entry entry) {
return !entry.isDirectory() && entry.getName().startsWith("lib/"); return !entry.isDirectory() && entry.getName().startsWith("lib/");
} }
@ -36,8 +40,4 @@ public class JarLauncher extends Launcher {
lib.add(0, archive); lib.add(0, archive);
} }
public static void main(String[] args) {
new JarLauncher().launch(args);
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2013 the original author or authors. * Copyright 2012-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,110 +16,71 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import java.io.File;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.jar.JarEntry;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* Base class for launchers that can start an application with a fully configured * Common convenience methods shared by launcher implementations.
* classpath.
* *
* @author Phillip Webb * @author Dave Syer
*/ */
public abstract class Launcher { public class LaunchHelper {
private Logger logger = Logger.getLogger(Launcher.class.getName()); private Logger logger = Logger.getLogger(LaunchHelper.class.getName());
/** /**
* The main runner class. This must be loaded by the created ClassLoader so cannot be * The main runner class. This must be loaded by the created ClassLoader so cannot be
* directly referenced. * directly referenced.
*/ */
private static final String RUNNER_CLASS = Launcher.class.getPackage().getName() private static final String RUNNER_CLASS = AbstractLauncher.class.getPackage()
+ ".MainMethodRunner"; .getName() + ".MainMethodRunner";
/** /**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments * @param args the incoming arguments
*/ * @param mainClass the main class
public void launch(String[] args) { * @param lib a collection of archives (zip/jar/war or directory)
try {
launch(args, getClass().getProtectionDomain());
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
/**
* Launch the application given the protection domain.
* @param args the incoming arguments
* @param protectionDomain the protection domain
* @throws Exception * @throws Exception
*/ */
protected void launch(String[] args, ProtectionDomain protectionDomain) public void launch(String[] args, String mainClass, List<Archive> lib)
throws Exception { throws Exception {
CodeSource codeSource = protectionDomain.getCodeSource(); ClassLoader classLoader = createClassLoader(lib);
URI location = (codeSource == null ? null : codeSource.getLocation().toURI()); launch(args, mainClass, classLoader);
String path = (location == null ? null : location.getPath());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
Archive archive = (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
launch(args, archive);
} }
/** /**
* Launch the application given the archive file * @param archive the archive to search
* @param args the incoming arguments * @return an accumulation of nested archives
* @param archive the underlying (zip/war/jar) archive
* @throws Exception * @throws Exception
*/ */
protected void launch(String[] args, Archive archive) throws Exception { public List<Archive> findNestedArchives(Archive archive, ArchiveFilter filter)
throws Exception {
List<Archive> lib = new ArrayList<Archive>(); List<Archive> lib = new ArrayList<Archive>();
for (Archive.Entry entry : archive.getEntries()) { for (Archive.Entry entry : archive.getEntries()) {
if (isNestedArchive(entry)) { if (filter.isArchive(entry)) {
this.logger.fine("Adding: " + entry.getName()); this.logger.fine("Adding: " + entry.getName());
lib.add(archive.getNestedArchive(entry)); lib.add(archive.getNestedArchive(entry));
} }
} }
return lib;
this.logger.fine("Added " + lib.size() + " entries");
postProcessLib(archive, lib);
ClassLoader classLoader = createClassLoader(lib);
launch(args, archive, classLoader);
} }
/** /**
* Determine if the specified {@link JarEntry} is a nested item that should be added * Obtain the main class that should be used to launch the application. By default
* to the classpath. The method is called once for each entry. * this method uses a {@code Start-Class} manifest entry.
* @param jarEntry the jar entry
* @return {@code true} if the entry is a nested item (jar or folder)
*/
protected abstract boolean isNestedArchive(Archive.Entry jarEntry);
/**
* Called to post-process lib entries before they are used. Implementations can add
* and remove entries.
* @param archive the archive * @param archive the archive
* @param lib the existing lib * @return the main class
* @throws Exception * @throws Exception
*/ */
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception { public String getMainClass(Archive archive) throws Exception {
String mainClass = archive.getManifest().getMainAttributes()
.getValue("Start-Class");
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified");
}
return mainClass;
} }
/** /**
@ -136,26 +97,15 @@ public abstract class Launcher {
return createClassLoader(urls); return createClassLoader(urls);
} }
/**
* Create a classloader for the specified URLs
* @param urls the URLs
* @return the classloader
* @throws Exception
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
/** /**
* Launch the application given the archive file and a fully configured classloader. * Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments * @param args the incoming arguments
* @param archive the archive * @param mainClass the main class to run
* @param classLoader the classloader * @param classLoader the classloader
* @throws Exception * @throws Exception
*/ */
protected void launch(String[] args, Archive archive, ClassLoader classLoader) protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception { throws Exception {
String mainClass = getMainClass(archive);
Runnable runner = createMainMethodRunner(mainClass, args, classLoader); Runnable runner = createMainMethodRunner(mainClass, args, classLoader);
Thread runnerThread = new Thread(runner); Thread runnerThread = new Thread(runner);
runnerThread.setContextClassLoader(classLoader); runnerThread.setContextClassLoader(classLoader);
@ -164,19 +114,13 @@ public abstract class Launcher {
} }
/** /**
* Obtain the main class that should be used to launch the application. By default * Create a classloader for the specified URLs
* this method uses a {@code Start-Class} manifest entry. * @param urls the URLs
* @param archive the archive * @return the classloader
* @return the main class
* @throws Exception * @throws Exception
*/ */
protected String getMainClass(Archive archive) throws Exception { protected ClassLoader createClassLoader(URL[] urls) throws Exception {
String mainClass = archive.getManifest().getMainAttributes() return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
.getValue("Start-Class");
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified");
}
return mainClass;
} }
/** /**

@ -25,7 +25,7 @@ import java.security.PrivilegedExceptionAction;
import org.springframework.boot.loader.jar.RandomAccessJarFile; import org.springframework.boot.loader.jar.RandomAccessJarFile;
/** /**
* {@link ClassLoader} used by the {@link Launcher}. * {@link ClassLoader} used by the {@link AbstractLauncher}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */

@ -19,7 +19,7 @@ package org.springframework.boot.loader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
/** /**
* Utility class that used by {@link Launcher}s to call a main method. This class allows * Utility class that used by {@link AbstractLauncher}s to call a main method. This class allows
* methods to be executed within a thread configured with a specific context classloader. * methods to be executed within a thread configured with a specific context classloader.
* *
* @author Phillip Webb * @author Phillip Webb

@ -23,7 +23,6 @@ import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.security.ProtectionDomain;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -33,8 +32,8 @@ import java.util.logging.Logger;
import org.springframework.boot.loader.util.SystemPropertyUtils; import org.springframework.boot.loader.util.SystemPropertyUtils;
/** /**
* {@link Launcher} for archives with user-configured classpath and main class via a * {@link AbstractLauncher} for archives with user-configured classpath and main class via
* properties file. This model is often more flexible and more amenable to creating * a properties file. This model is often more flexible and more amenable to creating
* well-behaved OS-level services than a model based on executable jars. * well-behaved OS-level services than a model based on executable jars.
* *
* <p> * <p>
@ -49,19 +48,20 @@ import org.springframework.boot.loader.util.SystemPropertyUtils;
* System properties in case the file doesn't exist): * System properties in case the file doesn't exist):
* *
* <ul> * <ul>
* <li><code>loader.path</code>: a comma-separated list of classpath directories * <li><code>loader.path</code>: a comma-separated list of directories to append to the
* (containing file resources and/or archives in *.jar or *.zip). Defaults to * classpath (containing file resources and/or nested archives in *.jar or *.zip).
* <code>lib</code> (i.e. a directory in the current working directory)</li> * Defaults to <code>lib</code> (i.e. a directory in the current working directory)</li>
* <li><code>loader.main</code>: the main method to delegate execution to once the class * <li><code>loader.main</code>: the main method to delegate execution to once the class
* loader is set up. No default, but will fall back to looking in a * loader is set up. No default, but will fall back to looking for a
* <code>MANIFEST.MF</code> if there is one.</li> * <code>Start-Class</code> in a <code>MANIFEST.MF</code>, if there is one in
* <code>${loader.home}/META-INF</code>.</li>
* </ul> * </ul>
* *
* @author Dave Syer * @author Dave Syer
*/ */
public class PropertiesLauncher extends Launcher { public class PropertiesLauncher implements ArchiveFilter {
private Logger logger = Logger.getLogger(Launcher.class.getName()); private Logger logger = Logger.getLogger(AbstractLauncher.class.getName());
/** /**
* Properties key for main class * Properties key for main class
@ -85,10 +85,32 @@ public class PropertiesLauncher extends Launcher {
private Properties properties = new Properties(); private Properties properties = new Properties();
private LaunchHelper helper = new LaunchHelper();
public static void main(String[] args) {
new PropertiesLauncher().launch(args);
}
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
*/
public void launch(String[] args) {
try {
File home = getHomeDirectory();
initialize(home);
this.helper.launch(args, getMainClass(home), getLibrary(home, this.paths));
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
@Override @Override
protected void launch(String[] args, ProtectionDomain protectionDomain) public boolean isArchive(Archive.Entry entry) {
throws Exception { return entry.isDirectory() || isArchive(entry.getName());
launch(args, new ExplodedArchive(getHomeDirectory()));
} }
protected File getHomeDirectory() { protected File getHomeDirectory() {
@ -96,28 +118,62 @@ public class PropertiesLauncher extends Launcher {
"${user.dir}"))); "${user.dir}")));
} }
/** protected String getMainClass(File home) throws Exception {
* Look in various places for a properties file to extract loader settings. Default to if (System.getProperty(MAIN) != null) {
* <code>application.properties</code> either on the current classpath or in the return SystemPropertyUtils.resolvePlaceholders(System.getProperty(MAIN));
* current working directory. }
* @see org.springframework.boot.loader.Launcher#launch(java.lang.String[], if (this.properties.containsKey(MAIN)) {
* org.springframework.boot.loader.Archive) return SystemPropertyUtils.resolvePlaceholders(this.properties
*/ .getProperty(MAIN));
@Override }
protected void launch(String[] args, Archive archive) throws Exception { return this.helper.getMainClass(new ExplodedArchive(home));
initialize();
super.launch(args, archive);
} }
protected void initialize() throws Exception { protected void initialize(File home) throws Exception {
initializeProperties(); initializeProperties(home);
initializePaths(); initializePaths();
} }
private void initializeProperties() throws Exception, IOException { private boolean isArchive(String name) {
return name.endsWith(".jar") || name.endsWith(".zip");
}
/**
* Search the configured paths and look for nested archives.
*
* @param home the home directory for this launch
* @param paths the directory roots for classpath entries
* @return a library of archives that can be used as a classpath
* @throws Exception
*/
private List<Archive> getLibrary(File home, List<String> paths) throws Exception {
List<Archive> lib = new ArrayList<Archive>();
for (String path : paths) {
String root = cleanupPath(stripFileUrlPrefix(path));
File file = new File(root);
if (!root.startsWith("/")) {
file = new File(home, root);
}
if (file.isDirectory()) {
this.logger.info("Adding classpath entries from " + path);
Archive archive = new ExplodedArchive(file);
lib.addAll(this.helper.findNestedArchives(archive, this));
lib.add(0, archive);
}
else {
this.logger.info("No directory found at " + path);
}
}
return lib;
}
private void initializeProperties(File home) throws Exception, IOException {
String config = SystemPropertyUtils.resolvePlaceholders(System.getProperty( String config = SystemPropertyUtils.resolvePlaceholders(System.getProperty(
CONFIG_NAME, "application")) + ".properties"; CONFIG_NAME, "application")) + ".properties";
InputStream resource = getClasspathResource(config); InputStream resource = getClasspathResource(config);
if (resource == null) {
resource = getResource(new File(home, config).getAbsolutePath());
}
if (resource == null) { if (resource == null) {
config = SystemPropertyUtils.resolvePlaceholders(System.getProperty( config = SystemPropertyUtils.resolvePlaceholders(System.getProperty(
CONFIG_LOCATION, config)); CONFIG_LOCATION, config));
@ -262,43 +318,4 @@ public class PropertiesLauncher extends Launcher {
return path; return path;
} }
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
String name = entry.getName();
for (String path : this.paths) {
if (path.length() > 0) {
if ((entry.isDirectory() && name.equals(path))
|| (!entry.isDirectory() && name.startsWith(path) && isArchive(name))) {
return true;
}
}
}
return false;
}
private boolean isArchive(String name) {
return name.endsWith(".jar") || name.endsWith(".zip");
}
@Override
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
lib.add(0, archive);
}
@Override
protected String getMainClass(Archive archive) throws Exception {
if (System.getProperty(MAIN) != null) {
return SystemPropertyUtils.resolvePlaceholders(System.getProperty(MAIN));
}
if (this.properties.containsKey(MAIN)) {
return SystemPropertyUtils.resolvePlaceholders(this.properties
.getProperty(MAIN));
}
return super.getMainClass(archive);
}
public static void main(String[] args) {
new PropertiesLauncher().launch(args);
}
} }

@ -20,16 +20,20 @@ import java.io.IOException;
import java.util.List; import java.util.List;
/** /**
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives. * {@link AbstractLauncher} for WAR based archives. This launcher for standard WAR
* Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided}, * archives. Supports dependencies in {@code WEB-INF/lib} as well as
* classes are loaded from {@code WEB-INF/classes}. * {@code WEB-INF/lib-provided}, classes are loaded from {@code WEB-INF/classes}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class WarLauncher extends Launcher { public class WarLauncher extends AbstractLauncher {
public static void main(String[] args) {
new WarLauncher().launch(args);
}
@Override @Override
protected boolean isNestedArchive(Archive.Entry entry) { public boolean isArchive(Archive.Entry entry) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
return entry.getName().equals("WEB-INF/classes/"); return entry.getName().equals("WEB-INF/classes/");
} }
@ -64,8 +68,4 @@ public class WarLauncher extends Launcher {
}); });
} }
public static void main(String[] args) {
new WarLauncher().launch(args);
}
} }

@ -48,14 +48,14 @@ public class PropertiesLauncherTests {
@Test @Test
public void testUserSpecifiedMain() throws Exception { public void testUserSpecifiedMain() throws Exception {
this.launcher.initialize(); this.launcher.initialize(new File("."));
assertEquals("demo.Application", this.launcher.getMainClass(null)); assertEquals("demo.Application", this.launcher.getMainClass(null));
} }
@Test @Test
public void testUserSpecifiedConfigName() throws Exception { public void testUserSpecifiedConfigName() throws Exception {
System.setProperty("loader.config.name", "foo"); System.setProperty("loader.config.name", "foo");
this.launcher.initialize(); this.launcher.initialize(new File("."));
assertEquals("my.Application", this.launcher.getMainClass(null)); assertEquals("my.Application", this.launcher.getMainClass(null));
assertEquals("[etc/]", ReflectionTestUtils.getField(this.launcher, "paths") assertEquals("[etc/]", ReflectionTestUtils.getField(this.launcher, "paths")
.toString()); .toString());
@ -64,7 +64,7 @@ public class PropertiesLauncherTests {
@Test @Test
public void testSystemPropertySpecifiedMain() throws Exception { public void testSystemPropertySpecifiedMain() throws Exception {
System.setProperty("loader.main", "foo.Bar"); System.setProperty("loader.main", "foo.Bar");
this.launcher.initialize(); this.launcher.initialize(new File("."));
assertEquals("foo.Bar", this.launcher.getMainClass(null)); assertEquals("foo.Bar", this.launcher.getMainClass(null));
} }

Loading…
Cancel
Save