Update packaged JARs to use standard JarLauncher

Change CLI generated JARs to use the standard `JarLauncher` instead of
a custom `JarRunner`. The `PackagedSpringApplicationLauncher` is used
as the `Start-Class` which in turn calls `SpringApplication.run()`.
This commit is contained in:
Phillip Webb 2014-01-29 14:38:36 -08:00
parent 5cf2387e58
commit c852fb5a79
8 changed files with 137 additions and 209 deletions

View File

@ -28,13 +28,10 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import joptsimple.OptionSet;
@ -58,8 +55,10 @@ import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.loader.ArchiveResolver;
import org.springframework.boot.cli.jar.PackagedSpringApplicationLauncher;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.util.StringUtils;
/**
@ -69,6 +68,8 @@ import org.springframework.util.StringUtils;
*/
public class JarCommand extends OptionParsingCommand {
private static final Layout LAYOUT = new Layouts.Jar();
public JarCommand() {
super(
"jar",
@ -146,8 +147,11 @@ public class JarCommand extends OptionParsingCommand {
addDependencies(jarWriter, dependencyUrls);
addClasspathEntries(jarWriter, classpathEntries);
addApplicationClasses(jarWriter, compiledClasses);
String runnerClassName = getClassFile(PackagedSpringApplicationLauncher.class
.getName());
jarWriter.writeEntry(runnerClassName,
getClass().getResourceAsStream("/" + runnerClassName));
jarWriter.writeLoaderClasses();
addJarRunner(jarWriter);
}
finally {
jarWriter.close();
@ -206,9 +210,10 @@ public class JarCommand extends OptionParsingCommand {
"Application-Classes",
StringUtils.collectionToCommaDelimitedString(compiledClasses
.keySet()));
manifest.getMainAttributes()
.putValue("Main-Class", JarRunner.class.getName());
manifest.getMainAttributes().putValue("Main-Class",
LAYOUT.getLauncherClassName());
manifest.getMainAttributes().putValue("Start-Class",
PackagedSpringApplicationLauncher.class.getName());
return manifest;
}
@ -243,27 +248,14 @@ public class JarCommand extends OptionParsingCommand {
final Map<String, byte[]> compiledClasses) throws IOException {
for (Entry<String, byte[]> entry : compiledClasses.entrySet()) {
String className = entry.getKey().replace(".", "/") + ".class";
jarWriter.writeEntry(className,
jarWriter.writeEntry(getClassFile(entry.getKey()),
new ByteArrayInputStream(entry.getValue()));
}
}
private void addJarRunner(JarWriter jar) throws IOException, URISyntaxException {
JarFile jarFile = getJarFile();
String namePrefix = JarRunner.class.getName().replace(".", "/");
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith(namePrefix)) {
jar.writeEntry(entry.getName(), jarFile.getInputStream(entry));
}
}
private String getClassFile(String className) {
return className.replace(".", "/") + ".class";
}
private JarFile getJarFile() throws URISyntaxException, IOException {
return new JarFile(
new ArchiveResolver().resolveArchiveLocation(JarCommand.class));
}
}
}

View File

@ -1,98 +0,0 @@
/*
* 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.
* 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.cli.command.jar;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.ArchiveResolver;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
/**
* A runner for a CLI application that has been compiled and packaged as a jar file
*
* @author Andy Wilkinson
*/
public class JarRunner {
private static final AsciiBytes LIB = new AsciiBytes("lib/");
public static void main(String[] args) throws URISyntaxException, IOException,
ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Archive archive = new ArchiveResolver().resolveArchive(JarRunner.class);
ClassLoader classLoader = createClassLoader(archive);
Class<?>[] classes = loadApplicationClasses(archive, classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
// Use reflection to load and call Spring
Class<?> application = classLoader
.loadClass("org.springframework.boot.SpringApplication");
Method method = application.getMethod("run", Object[].class, String[].class);
method.invoke(null, classes, args);
}
private static ClassLoader createClassLoader(Archive archive) throws IOException,
MalformedURLException {
List<Archive> nestedArchives = archive.getNestedArchives(new EntryFilter() {
@Override
public boolean matches(Entry entry) {
return entry.getName().startsWith(LIB);
}
});
List<URL> urls = new ArrayList<URL>();
urls.add(archive.getUrl());
for (Archive nestedArchive : nestedArchives) {
urls.add(nestedArchive.getUrl());
}
ClassLoader classLoader = new LaunchedURLClassLoader(urls.toArray(new URL[urls
.size()]), JarRunner.class.getClassLoader());
return classLoader;
}
private static Class<?>[] loadApplicationClasses(Archive archive,
ClassLoader classLoader) throws ClassNotFoundException, IOException {
String[] classNames = archive.getManifest().getMainAttributes()
.getValue("Application-Classes").split(",");
Class<?>[] classes = new Class<?>[classNames.length];
for (int i = 0; i < classNames.length; i++) {
Class<?> applicationClass = classLoader.loadClass(classNames[i]);
classes[i] = applicationClass;
}
return classes;
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.
* 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.cli.jar;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.Manifest;
/**
* A launcher for a CLI application that has been compiled and packaged as a jar file.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class PackagedSpringApplicationLauncher {
private static final String SPRING_APPLICATION_CLASS = "org.springframework.boot.SpringApplication";
private void run(String[] args) throws Exception {
URLClassLoader classLoader = (URLClassLoader) Thread.currentThread()
.getContextClassLoader();
Class<?> application = classLoader.loadClass(SPRING_APPLICATION_CLASS);
Method method = application.getMethod("run", Object[].class, String[].class);
method.invoke(null, getSources(classLoader), args);
}
private Object[] getSources(URLClassLoader classLoader) throws Exception {
URL url = classLoader.findResource("META-INF/MANIFEST.MF");
Manifest manifest = new Manifest(url.openStream());
String attribute = manifest.getMainAttributes().getValue("Application-Classes");
return loadClasses(classLoader, attribute.split(","));
}
private Class<?>[] loadClasses(ClassLoader classLoader, String[] names)
throws ClassNotFoundException {
Class<?>[] classes = new Class<?>[names.length];
for (int i = 0; i < names.length; i++) {
classes[i] = classLoader.loadClass(names[i]);
}
return classes;
}
public static void main(String[] args) throws Exception {
new PackagedSpringApplicationLauncher().run(args);
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.
* 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.
*/
/**
* Class that are packaged as part of CLI generated JARs.
* @see org.springframework.boot.cli.command.jar.JarCommand
*/
package org.springframework.boot.cli.jar;

View File

@ -44,6 +44,7 @@ import java.util.zip.ZipEntry;
* items are ignored.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarWriter {
@ -109,7 +110,6 @@ public class JarWriter {
/**
* Writes an entry. The {@code inputStream} is closed once the entry has been written
*
* @param entryName The name of the entry
* @param inputStream The stream from which the entry's data can be read
* @throws IOException if the write fails
@ -123,7 +123,7 @@ public class JarWriter {
* Write a nested library.
* @param destination the destination of the library
* @param file the library file
* @throws IOException
* @throws IOException if the write fails
*/
public void writeNestedLibrary(String destination, File file) throws IOException {
JarEntry entry = new JarEntry(destination + file.getName());

View File

@ -1,83 +0,0 @@
/*
* 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.
* 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.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
/**
* Resolves the {@link Archive} from which a {@link Class} was loaded.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class ArchiveResolver {
/**
* Resolves the {@link Archive} that contains the given {@code clazz}.
* @param clazz The class whose containing archive is to be resolved
*
* @return The class's containing archive
* @throws IOException if an error occurs when resolving the containing archive
*/
public Archive resolveArchive(Class<?> clazz) throws IOException {
File root = resolveArchiveLocation(clazz);
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
/**
* Resolves the location of the archive that contains the given {@code clazz}.
* @param clazz The class for which the location of the containing archive is to be
* resolved
*
* @return The location of the class's containing archive
* @throws IOException if an error occurs when resolving the containing archive's
* location
*/
public File resolveArchiveLocation(Class<?> clazz) throws IOException {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
if (codeSource != null) {
File root;
URL location = codeSource.getLocation();
URLConnection connection = location.openConnection();
if (connection instanceof JarURLConnection) {
root = new File(((JarURLConnection) connection).getJarFile().getName());
}
else {
root = new File(location.getPath());
}
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return root;
}
throw new IllegalStateException("Unable to determine code source archive");
}
}

View File

@ -16,6 +16,10 @@
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.jar.JarEntry;
@ -23,6 +27,8 @@ import java.util.jar.JarEntry;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
/**
* Base class for executable archive {@link Launcher}s.
@ -43,7 +49,19 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
}
private Archive createArchive() throws Exception {
return new ArchiveResolver().resolveArchive(getClass());
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
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);
}
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
protected final Archive getArchive() {

View File

@ -21,10 +21,13 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -494,7 +497,19 @@ public class PropertiesLauncher extends Launcher {
}
private Archive createArchive() throws Exception {
return new ArchiveResolver().resolveArchive(getClass());
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
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);
}
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
private void addParentClassLoaderEntries(List<Archive> lib) throws IOException,