From f45e25bcd3697b8234a2efa430a411582149f1d1 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Sat, 25 Sep 2021 21:33:03 +0200 Subject: [PATCH] added old executable-jar plugin prototype (excluded from build) --- executable-jar/build.gradle | 10 ++-- .../executable-jar-common/build.gradle | 11 +++++ .../woggioni/executable/jar/Constants.java | 2 + .../executable-jar-launcher/build.gradle | 37 ++++++++++++--- .../executable/jar/JavaAgentLauncher.java | 37 +++++++++++++++ .../net/woggioni/executable/jar/Launcher.java | 41 +++++----------- .../executable/jar/MainClassLoader.java | 13 +++++ .../executable/jar/PathClassLoader.java | 47 +++++++++++++++---- .../src/main/{java => java9}/module-info.java | 2 + .../executable/jar/MainClassLoader.java | 39 +++++++++++++++ .../net/woggioni/executable/jar/FooTest.java | 2 + .../executable/jar/ExecutableJarTask.java | 37 +++++++++++++-- 12 files changed, 225 insertions(+), 53 deletions(-) create mode 100644 executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java create mode 100644 executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java rename executable-jar/executable-jar-launcher/src/main/{java => java9}/module-info.java (57%) create mode 100644 executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java diff --git a/executable-jar/build.gradle b/executable-jar/build.gradle index f814a70..b58967f 100644 --- a/executable-jar/build.gradle +++ b/executable-jar/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'maven-publish' id 'java-gradle-plugin' } @@ -23,6 +22,11 @@ tasks.named("processResources") { inputs.files(copyLauncher) } +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + jar { manifest { attributes "version" : archiveVersion.get() @@ -38,8 +42,8 @@ jar { gradlePlugin { plugins { - create("JlinkPlugin") { - id = "net.woggioni.gradle.executable.jar" + create("ExecutableJarPlugin") { + id = "net.woggioni.gradle.executable-jar" implementationClass = "net.woggioni.gradle.executable.jar.ExecutableJarPlugin" } } diff --git a/executable-jar/executable-jar-common/build.gradle b/executable-jar/executable-jar-common/build.gradle index 620edb7..4270b4d 100644 --- a/executable-jar/executable-jar-common/build.gradle +++ b/executable-jar/executable-jar-common/build.gradle @@ -1,3 +1,14 @@ plugins { id 'java-library' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +jar { + manifest { + attributes "Automatic-Module-Name" : "net.woggioni.executable.jar" + } } \ No newline at end of file diff --git a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java b/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java index 069a622..e770c8d 100644 --- a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java +++ b/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java @@ -11,6 +11,8 @@ public class Constants { public static final String METADATA_FOLDER = "META-INF"; public static final int BUFFER_SIZE = 0x10000; public static final String DEFAULT_LAUNCHER = "net.woggioni.executable.jar.Launcher"; + public static final String AGENT_LAUNCHER = "net.woggioni.executable.jar.JavaAgentLauncher"; + public static final String JAVA_AGENTS_FILE = METADATA_FOLDER + "/javaAgents.properties"; public static class ManifestAttributes { public static final String MAIN_MODULE = "Executable-Jar-Main-Module"; diff --git a/executable-jar/executable-jar-launcher/build.gradle b/executable-jar/executable-jar-launcher/build.gradle index 1ba6d80..3ac9433 100644 --- a/executable-jar/executable-jar-launcher/build.gradle +++ b/executable-jar/executable-jar-launcher/build.gradle @@ -1,8 +1,19 @@ import java.util.jar.Attributes + +buildscript { + dependencies { + classpath project(":multi-release-jar") + } +} + plugins { id 'java-library' } +ext.setProperty("jpms.module.name", "net.woggioni.executable.jar") + +apply plugin: 'net.woggioni.gradle.multi-release-jar' + configurations { embedded compileOnly.extendsFrom(embedded) @@ -10,12 +21,20 @@ configurations { dependencies { embedded project(path: ":executable-jar:executable-jar-common", configuration: 'archives') + embedded group: "net.woggioni", name: "xclassloader", version: getProperty("version.xclassloader") } java { modularity.inferModulePath = true } +tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(16) + } + options.forkOptions.jvmArgs << "--illegal-access=permit" +} + jar { manifest { attributes([ @@ -29,13 +48,17 @@ jar { tasks.register("tar", Tar) { archiveFileName = "${project.name}.tar" - from(sourceSets.main.output) - from { - configurations.named('embedded').map { - it.collect { - it.isDirectory() ? it : zipTree(it) - } + from(project.tasks.named(JavaPlugin.JAR_TASK_NAME) + .flatMap(Jar.&getArchiveFile) + .map(RegularFile.&getAsFile) + .map(project.&zipTree)) + from(configurations.named('embedded').map { + it.collect { + it.isDirectory() ? it : zipTree(it) } + }) { + exclude("**/module-info.class") + exclude("META-INF/MANIFEST.MF") } } @@ -44,4 +67,4 @@ tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { String path = project(":executable-jar:executable-jar-common").extensions.getByType(JavaPluginExtension).sourceSets.named("main").get().output.asPath options.compilerArgs.addAll(["--patch-module", "net.woggioni.executable.jar=$path"]) } -} \ No newline at end of file +} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java new file mode 100644 index 0000000..7460868 --- /dev/null +++ b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java @@ -0,0 +1,37 @@ +package net.woggioni.executable.jar; + +import java.io.InputStream; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; +import lombok.SneakyThrows; + +class JavaAgentLauncher { + + @SneakyThrows + static void premain(String agentArguments, Instrumentation instrumentation) { + ClassLoader cl = JavaAgentLauncher.class.getClassLoader(); + Enumeration it = cl.getResources(Constants.JAVA_AGENTS_FILE); + while (it.hasMoreElements()) { + URL url = it.nextElement(); + Properties properties = new Properties(); + try (InputStream is = url.openStream()) { + properties.load(is); + } + for (Map.Entry entry : properties.entrySet()) { + String agentClassName = (String) entry.getKey(); + String agentArgs = (String) entry.getValue(); + Class agentClass = cl.loadClass(agentClassName); + Method premainMethod = agentClass.getMethod("premain", String.class, Instrumentation.class); + premainMethod.invoke(null, agentArgs, instrumentation); + } + } + } + + static void agentmain(String agentArguments, Instrumentation instrumentation) { + premain(agentArguments, instrumentation); + } +} \ No newline at end of file diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java index 400269e..b77616c 100644 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java +++ b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java @@ -1,8 +1,6 @@ package net.woggioni.executable.jar; import java.io.InputStream; -import java.lang.module.Configuration; -import java.lang.module.ModuleFinder; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; @@ -10,22 +8,23 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; +import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import lombok.SneakyThrows; import lombok.extern.java.Log; + @Log public class Launcher { @@ -50,9 +49,14 @@ public class Launcher { try(InputStream is = manifestResource.openStream()) { mf.read(is); } - try(FileSystem fs = FileSystems.newFileSystem(Path.of(currentJar.getPath()), null)) { + try(FileSystem fs = FileSystems.newFileSystem(Paths.get(currentJar), null)) { Attributes mainAttributes = mf.getMainAttributes(); + Collector, List> immutableListCollector = Collector.of( + ArrayList::new, + List::add, + (l1, l2) -> { l1.addAll(l2); return l1; }, + Collections::unmodifiableList); List jarList = StreamSupport.stream(fs.getRootDirectories().spliterator(), false).flatMap(new Function>() { @Override @SneakyThrows @@ -67,32 +71,11 @@ public class Launcher { public Stream apply(Path path) { return StreamSupport.stream(FileSystems.newFileSystem(path, null).getRootDirectories().spliterator(), false); } - }).collect(Collectors.toUnmodifiableList()); + }).collect(immutableListCollector); -// for (Map.Entry entry : mf.getEntries().entrySet()) { -// String jarEntryName = entry.getKey(); -// Attributes attributes = entry.getValue(); -// if (jarEntryName.startsWith(Constants.LIBRARIES_FOLDER + '/') && attributes.getValue(Constants.ManifestAttributes.ENTRY_HASH) != null) { -// jarList.add(fs.getPath(jarEntryName)); -// } -// } - - Path[] jars = jarList.toArray(new Path[jarList.size()]); - ClassLoader pathClassLoader = new PathClassLoader(jars); String mainClassName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS); String mainModuleName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE); - Class mainClass; - if (mainModuleName == null) { - mainClass = pathClassLoader.loadClass(mainClassName); - } else { - ModuleLayer bootLayer = ModuleLayer.boot(); - Configuration bootConfiguration = ModuleLayer.boot().configuration(); - Configuration cfg = bootConfiguration.resolve(ModuleFinder.of(jars), ModuleFinder.of(), Arrays.asList(mainModuleName)); - ModuleLayer layer = bootLayer.defineModulesWithOneLoader(cfg, pathClassLoader); - Module mainModule = layer.findModule(mainModuleName).orElseThrow( - () -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName))); - mainClass = Class.forName(mainModule, mainClassName); - } + Class mainClass = MainClassLoader.loadMainClass(jarList, mainModuleName, mainClassName); try { Method mainMethod = mainClass.getMethod("main", String[].class); Class returnType = mainMethod.getReturnType(); diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java new file mode 100644 index 0000000..215bc56 --- /dev/null +++ b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java @@ -0,0 +1,13 @@ +package net.woggioni.executable.jar; + +import java.nio.file.Path; +import lombok.SneakyThrows; + + +class MainClassLoader { + @SneakyThrows + static Class loadMainClass(Iterable roots, String mainModuleName, String mainClassName) { + ClassLoader pathClassLoader = new net.woggioni.xclassloader.PathClassLoader(roots); + return pathClassLoader.loadClass(mainClassName); + } +} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java index 036ca91..61282fc 100644 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java +++ b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java @@ -1,19 +1,26 @@ package net.woggioni.executable.jar; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; -import java.nio.file.*; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; /** * A classloader that loads classes from a {@link Path} instance @@ -153,8 +160,30 @@ public final class PathClassLoader extends ClassLoader { @Override @SneakyThrows protected URLConnection openConnection(URL url) { - URI uri = url.toURI(); - Path path = Paths.get(uri); + List stack = new ArrayList<>(); + URL currentURL = url; + while(true) { + String file = currentURL.getFile(); + int exclamationMark = file.lastIndexOf('!'); + if(exclamationMark != -1) { + stack.add(file.substring(exclamationMark + 1)); + currentURL = new URL(file.substring(0, exclamationMark)); + } else { + stack.add(file); + break; + } + } + + Path path; + FileSystem fs = FileSystems.getDefault(); + while(true) { + String pathString = stack.remove(stack.size() - 1); + path = fs.getPath(pathString); + if(stack.isEmpty()) break; + else { + fs = FileSystems.newFileSystem(path, null); + } + } return new PathURLConnection(url, path); } } diff --git a/executable-jar/executable-jar-launcher/src/main/java/module-info.java b/executable-jar/executable-jar-launcher/src/main/java9/module-info.java similarity index 57% rename from executable-jar/executable-jar-launcher/src/main/java/module-info.java rename to executable-jar/executable-jar-launcher/src/main/java9/module-info.java index f0e2139..e39a33f 100644 --- a/executable-jar/executable-jar-launcher/src/main/java/module-info.java +++ b/executable-jar/executable-jar-launcher/src/main/java9/module-info.java @@ -1,4 +1,6 @@ module net.woggioni.executable.jar { requires java.logging; requires static lombok; + requires net.woggioni.xclassloader; + requires java.instrument; } \ No newline at end of file diff --git a/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java new file mode 100644 index 0000000..6b889b9 --- /dev/null +++ b/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java @@ -0,0 +1,39 @@ +package net.woggioni.executable.jar; + +import java.lang.module.Configuration; +import java.lang.module.ModuleFinder; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; + +import lombok.SneakyThrows; + +import net.woggioni.xclassloader.PathClassLoader; +import net.woggioni.xclassloader.PathModuleFinder; + +class MainClassLoader { + @SneakyThrows + static Class loadMainClass(Iterable roots, String mainModuleName, String mainClassName) { + if (mainModuleName == null) { + ClassLoader pathClassLoader = new net.woggioni.xclassloader.PathClassLoader(roots); + return pathClassLoader.loadClass(mainClassName); + } else { + ModuleLayer bootLayer = ModuleLayer.boot(); + Configuration bootConfiguration = bootLayer.configuration(); + Configuration cfg = bootConfiguration.resolve(new PathModuleFinder(roots), ModuleFinder.of(), Collections.singletonList(mainModuleName)); + ClassLoader pathClassLoader = new PathClassLoader(roots, cfg, null); + ModuleLayer.Controller controller = + ModuleLayer.defineModules(cfg, Collections.singletonList(ModuleLayer.boot()), moduleName -> pathClassLoader); + ModuleLayer layer = controller.layer(); + for(Module module : layer.modules()) { + controller.addReads(module, pathClassLoader.getUnnamedModule()); + } + Module mainModule = layer.findModule(mainModuleName).orElseThrow( + () -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName))); + return Optional.ofNullable(mainClassName) + .or(() -> mainModule.getDescriptor().mainClass()) + .map(className -> Class.forName(mainModule, className)) + .orElseThrow(() -> new IllegalStateException(String.format("Unable to determine main class name for module '%s'", mainModule.getName()))); + } + } +} diff --git a/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java b/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java index 8033caa..9b1a4ed 100644 --- a/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java +++ b/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java @@ -22,6 +22,7 @@ import java.util.stream.StreamSupport; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.SneakyThrows; +import org.gradle.internal.impldep.org.junit.Ignore; import org.junit.jupiter.api.Test; public class FooTest { @@ -78,6 +79,7 @@ public class FooTest { } @Test + @Ignore @SneakyThrows void test2() { FileSystem fs = FileSystems.newFileSystem(new URI("jar:file:/home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar"), new HashMap<>()); diff --git a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java b/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java index b863b44..9f3d048 100644 --- a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java +++ b/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java @@ -2,9 +2,18 @@ package net.woggioni.gradle.executable.jar; import java.io.File; import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.security.MessageDigest; +import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Properties; +import java.util.Set; import java.util.function.Supplier; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -27,8 +36,6 @@ import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.BasePluginExtension; import org.gradle.api.plugins.JavaApplication; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -41,7 +48,7 @@ import static java.util.zip.Deflater.BEST_COMPRESSION; import static java.util.zip.Deflater.NO_COMPRESSION; import static net.woggioni.executable.jar.Constants.*; -@SuppressWarnings({ "UnstableApiUsage", "unused" }) +@SuppressWarnings({"unused" }) public class ExecutableJarTask extends AbstractArchiveTask { private static final String MINIMUM_GRADLE_VERSION = "6.0"; @@ -59,6 +66,17 @@ public class ExecutableJarTask extends AbstractArchiveTask { @Getter(onMethod_ = {@Input, @Optional}) private final Property mainModule; + private final Properties javaAgents = new Properties(); + + @Input + public Set> getJavaAgents() { + return Collections.unmodifiableSet(javaAgents.entrySet()); + } + + public void javaAgent(String className, String args) { + javaAgents.put(className, args); + } + public void includeLibraries(Object... files) { into(LIBRARIES_FOLDER, (copySpec) -> copySpec.from(files)); } @@ -69,11 +87,12 @@ public class ExecutableJarTask extends AbstractArchiveTask { setGroup("build"); setDescription("Creates an executable jar file, embedding all of its runtime dependencies"); BasePluginExtension basePluginExtension = getProject().getExtensions().getByType(BasePluginExtension.class); - getDestinationDirectory().set(basePluginExtension.getLibsDirectory()); + getDestinationDirectory().set(basePluginExtension.getDistsDirectory()); getArchiveBaseName().convention(getProject().getName()); getArchiveExtension().convention("jar"); getArchiveVersion().convention(getProject().getVersion().toString()); getArchiveAppendix().convention("executable"); + exclude("**/module-info.class"); mainClass = objects.property(String.class); mainModule = objects.property(String.class); @@ -200,6 +219,10 @@ public class ExecutableJarTask extends AbstractArchiveTask { Attributes mainAttributes = manifest.getMainAttributes(); mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); mainAttributes.put(Attributes.Name.MAIN_CLASS, DEFAULT_LAUNCHER); + mainAttributes.put(Attributes.Name.MULTI_RELEASE, "true"); + mainAttributes.put(new Attributes.Name("Launcher-Agent-Class"), AGENT_LAUNCHER); + mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true"); + mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true"); mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get()); if(mainModule.isPresent()) { mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, mainModule.get()); @@ -228,7 +251,7 @@ public class ExecutableJarTask extends AbstractArchiveTask { } try (ZipOutputStream zipOutputStream = new ZipOutputStream(Common.write(destination, true)); - ZipInputStream zipInputStream = new ZipInputStream(Common.read(temporaryJar, true))) { + ZipInputStream zipInputStream = new ZipInputStream(Common.read(temporaryJar, true))) { zipOutputStream.setLevel(BEST_COMPRESSION); ZipEntry zipEntry = zipEntryFactory.createDirectoryEntry(METADATA_FOLDER); zipOutputStream.putNextEntry(zipEntry); @@ -236,6 +259,10 @@ public class ExecutableJarTask extends AbstractArchiveTask { zipEntry.setMethod(ZipEntry.DEFLATED); zipOutputStream.putNextEntry(zipEntry); manifest.write(zipOutputStream); + zipEntry = zipEntryFactory.createZipEntry(JAVA_AGENTS_FILE); + zipEntry.setMethod(ZipEntry.DEFLATED); + zipOutputStream.putNextEntry(zipEntry); + javaAgents.store(zipOutputStream, null); while (true) { zipEntry = zipInputStream.getNextEntry();