├── .travis.yml ├── fat-jar-classloader ├── src │ ├── test │ │ ├── resources │ │ │ └── test-fat-jar-class-1.0-SNAPSHOT-fat.jar │ │ └── java │ │ │ └── com │ │ │ └── laomei │ │ │ └── fatjar │ │ │ └── classloader │ │ │ └── test │ │ │ └── FatJarDelegateClassLoaderTest.java │ └── main │ │ └── java │ │ └── com │ │ └── laomei │ │ └── fatjar │ │ └── classloader │ │ ├── FatJarClassLoader.java │ │ └── FatJarDelegateClassLoader.java └── pom.xml ├── fat-jar-test-class ├── test-base-class │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── laomei │ │ │ └── fatjar │ │ │ └── base │ │ │ └── TestClass.java │ └── pom.xml ├── test-fat-jar-class │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── laomei │ │ │ └── fatjar │ │ │ └── clazz │ │ │ └── HelloWorld.java │ └── pom.xml └── pom.xml ├── fat-jar-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── laomei │ │ └── fatjar │ │ └── common │ │ ├── Constant.java │ │ └── boot │ │ ├── tool │ │ ├── DefaultLayoutFactory.java │ │ ├── LayoutFactory.java │ │ ├── RepackagingLayout.java │ │ ├── LibraryCallback.java │ │ ├── Libraries.java │ │ ├── Layout.java │ │ ├── LibraryScope.java │ │ ├── Layouts.java │ │ ├── Library.java │ │ ├── FileUtils.java │ │ └── ArtifactsLibraries.java │ │ ├── jar │ │ ├── CentralDirectoryVisitor.java │ │ ├── JarEntryFilter.java │ │ ├── FileHeader.java │ │ ├── ZipInflaterInputStream.java │ │ ├── Bytes.java │ │ ├── Archive.java │ │ ├── JarEntry.java │ │ ├── CentralDirectoryParser.java │ │ ├── CentralDirectoryEndRecord.java │ │ ├── CentralDirectoryFileHeader.java │ │ ├── AsciiBytes.java │ │ ├── JarFileArchive.java │ │ ├── JarFileEntries.java │ │ ├── Handler.java │ │ ├── JarURLConnection.java │ │ └── JarFile.java │ │ └── data │ │ ├── RandomAccessData.java │ │ └── RandomAccessDataFile.java └── pom.xml ├── .gitignore ├── pom.xml ├── fat-jar-plugin ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── laomei │ └── fatjar │ └── plugin │ ├── FatJarPackagerMojo.java │ ├── Repackager.java │ └── JarWriter.java └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | cache: 3 | directories: 4 | - $HOME/.m2/repository 5 | 6 | sudo: required 7 | 8 | jdk: 9 | - oraclejdk8 10 | 11 | script: 12 | - "mvn clean test" -------------------------------------------------------------------------------- /fat-jar-classloader/src/test/resources/test-fat-jar-class-1.0-SNAPSHOT-fat.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/fat-jar-isolation/master/fat-jar-classloader/src/test/resources/test-fat-jar-class-1.0-SNAPSHOT-fat.jar -------------------------------------------------------------------------------- /fat-jar-test-class/test-base-class/src/main/java/com/laomei/fatjar/base/TestClass.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.base; 2 | 3 | /** 4 | * 对于 fat jar,输出 fat jar class; 否则输出 not fat jar class 5 | * @author laomei on 2019/1/31 16:21 6 | */ 7 | public class TestClass { 8 | 9 | public String hello() { 10 | return "not fat jar class"; 11 | // return "fat jar class"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/Constant.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common; 2 | 3 | /** 4 | * @author laomei on 2019/3/25 11:47 5 | */ 6 | public class Constant { 7 | 8 | public static final String FAT_MDW_PATH = "/opt/fat/mdw"; 9 | 10 | public static final String FAT_JAR_TOOL = "Fat-Jar-Build-Tool"; 11 | 12 | public static final String FAT_JAR_TOOL_VALUE = "Laomei-Fat-Jar-Plugin"; 13 | } 14 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/DefaultLayoutFactory.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Default implementation of {@link LayoutFactory}. 7 | * 8 | * @author Phillip Webb 9 | * @since 1.5.0 10 | */ 11 | public class DefaultLayoutFactory implements LayoutFactory { 12 | 13 | @Override 14 | public Layout getLayout(File source) { 15 | return Layouts.forFile(source); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /fat-jar-test-class/test-fat-jar-class/src/main/java/com/laomei/fatjar/clazz/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.clazz; 2 | 3 | import com.laomei.fatjar.base.TestClass; 4 | 5 | /** 6 | * @author laoemi on 2019/1/31 16:21 7 | */ 8 | public class HelloWorld { 9 | 10 | private final TestClass testClass; 11 | 12 | public HelloWorld(TestClass testClass) { 13 | this.testClass = testClass; 14 | } 15 | 16 | public String hello() { 17 | return testClass.hello(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support com.laomei.fatjar.plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled clazz file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | .idea 27 | *.iml 28 | target 29 | */target 30 | 31 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/LayoutFactory.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Factory interface used to create a {@link Layout}. 7 | * 8 | * @author Dave Syer 9 | * @author Phillip Webb 10 | */ 11 | public interface LayoutFactory { 12 | 13 | /** 14 | * Return a {@link Layout} for the specified source file. 15 | * @param source the source file 16 | * @return the layout to use for the file 17 | */ 18 | Layout getLayout(File source); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/RepackagingLayout.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | /** 4 | * A specialization of {@link Layout} that repackages an existing archive by moving its 5 | * content to a new location. 6 | * 7 | * @author Andy Wilkinson 8 | * @since 1.4.0 9 | */ 10 | public interface RepackagingLayout extends Layout { 11 | 12 | /** 13 | * Returns the location to which classes should be moved. 14 | * @return the repackaged classes location 15 | */ 16 | String getRepackagedClassesLocation(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/LibraryCallback.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Callback interface used to iterate {@link Libraries}. 8 | * 9 | * @author Phillip Webb 10 | */ 11 | public interface LibraryCallback { 12 | 13 | /** 14 | * Callback for a single library backed by a {@link File}. 15 | * @param library the library 16 | * @throws IOException if the operation fails 17 | */ 18 | void library(Library library) throws IOException; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/CentralDirectoryVisitor.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | /** 6 | * Callback visitor triggered by {@link CentralDirectoryParser}. 7 | * 8 | * @author Phillip Webb 9 | */ 10 | interface CentralDirectoryVisitor { 11 | 12 | void visitStart(CentralDirectoryEndRecord endRecord, 13 | RandomAccessData centralDirectoryData); 14 | 15 | void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset); 16 | 17 | void visitEnd(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarEntryFilter.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | /** 4 | * Interface that can be used to filter and optionally rename jar entries. 5 | * 6 | * @author Phillip Webb 7 | */ 8 | interface JarEntryFilter { 9 | 10 | /** 11 | * Apply the jar entry filter. 12 | * @param name the current entry name. This may be different that the original entry 13 | * name if a previous filter has been applied 14 | * @return the new name of the entry or {@code null} if the entry should not be 15 | * included. 16 | */ 17 | AsciiBytes apply(AsciiBytes name); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /fat-jar-test-class/test-base-class/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fat-jar-test-class 7 | com.laomei.github 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | test-base-class 14 | -------------------------------------------------------------------------------- /fat-jar-test-class/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fat-jar-isolation 7 | com.laomei.github 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | fat-jar-test-class 13 | pom 14 | 15 | 16 | test-base-class 17 | test-fat-jar-class 18 | 19 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/Libraries.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Encapsulates information about libraries that may be packed into the archive. 7 | * 8 | * @author Phillip Webb 9 | */ 10 | public interface Libraries { 11 | 12 | /** 13 | * Represents no libraries. 14 | */ 15 | Libraries NONE = new Libraries() { 16 | @Override 17 | public void doWithLibraries(LibraryCallback callback) throws IOException { 18 | } 19 | }; 20 | 21 | /** 22 | * Iterate all relevant libraries. 23 | * @param callback a callback for each relevant library. 24 | * @throws IOException if the operation fails 25 | */ 26 | void doWithLibraries(LibraryCallback callback) throws IOException; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/Layout.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | /** 4 | * Strategy interface used to determine the layout for a particular type of archive. 5 | * Layouts may additionally implement {@link CustomLoaderLayout} if they wish to write 6 | * custom loader classes. 7 | * 8 | * 移除了几个方法方法,事实上我们是需要构建 fat jar , 并且都不需要是可执行的。 9 | * 10 | * @author Phillip Webb 11 | * @see Layouts 12 | * @see RepackagingLayout 13 | */ 14 | public interface Layout { 15 | 16 | /** 17 | * Returns the destination path for a given library. 18 | * @param libraryName the name of the library (excluding any path) 19 | * @param scope the scope of the library 20 | * @return the destination relative to the root of the archive (should end with '/') 21 | * or {@code null} if the library should not be included. 22 | */ 23 | String getLibraryDestination(String libraryName, LibraryScope scope); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /fat-jar-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | fat-jar-isolation 8 | com.laomei.github 9 | 1.0-SNAPSHOT 10 | 11 | 4.0.0 12 | 13 | fat-jar-common 14 | 15 | 16 | 17 | org.apache.maven 18 | maven-artifact 19 | 3.5.2 20 | 21 | 22 | org.apache.maven 23 | maven-model 24 | 3.5.0 25 | 26 | 27 | org.apache.maven 28 | maven-plugin-api 29 | 3.0 30 | 31 | 32 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/FileHeader.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.util.zip.ZipEntry; 4 | 5 | /** 6 | * A file header record that has been loaded from a Jar file. 7 | * 8 | * @author Phillip Webb 9 | * @see JarEntry 10 | * @see CentralDirectoryFileHeader 11 | */ 12 | interface FileHeader { 13 | 14 | /** 15 | * Returns {@code true} if the header has the given name. 16 | * @param name the name to test 17 | * @param suffix an additional suffix (or {@code null}) 18 | * @return {@code true} if the header has the given name 19 | */ 20 | boolean hasName(String name, String suffix); 21 | 22 | /** 23 | * Return the offset of the load file header within the archive data. 24 | * @return the local header offset 25 | */ 26 | long getLocalHeaderOffset(); 27 | 28 | /** 29 | * Return the compressed size of the entry. 30 | * @return the compressed size. 31 | */ 32 | long getCompressedSize(); 33 | 34 | /** 35 | * Return the uncompressed size of the entry. 36 | * @return the uncompressed size. 37 | */ 38 | long getSize(); 39 | 40 | /** 41 | * Return the method used to compress the data. 42 | * @return the zip compression method 43 | * @see ZipEntry#STORED 44 | * @see ZipEntry#DEFLATED 45 | */ 46 | int getMethod(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/LibraryScope.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | /** 4 | * The scope of a library. The common {@link #COMPILE}, {@link #RUNTIME} and 5 | * {@link #PROVIDED} scopes are defined here and supported by the common {@link Layouts}. 6 | * A custom {@link Layout} can handle additional scopes as required. 7 | * 8 | * @author Phillip Webb 9 | */ 10 | public interface LibraryScope { 11 | 12 | @Override 13 | String toString(); 14 | 15 | /** 16 | * The library is used at compile time and runtime. 17 | */ 18 | LibraryScope COMPILE = new LibraryScope() { 19 | 20 | @Override 21 | public String toString() { 22 | return "compile"; 23 | } 24 | 25 | }; 26 | 27 | /** 28 | * The library is used at runtime but not needed for compile. 29 | */ 30 | LibraryScope RUNTIME = new LibraryScope() { 31 | 32 | @Override 33 | public String toString() { 34 | return "runtime"; 35 | } 36 | 37 | }; 38 | 39 | /** 40 | * The library is needed for compile but is usually provided when running. 41 | */ 42 | LibraryScope PROVIDED = new LibraryScope() { 43 | 44 | @Override 45 | public String toString() { 46 | return "provided"; 47 | } 48 | 49 | }; 50 | 51 | /** 52 | * Marker for custom scope when custom configuration is used. 53 | */ 54 | LibraryScope CUSTOM = new LibraryScope() { 55 | 56 | @Override 57 | public String toString() { 58 | return "custom"; 59 | } 60 | 61 | }; 62 | 63 | } 64 | -------------------------------------------------------------------------------- /fat-jar-test-class/test-fat-jar-class/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fat-jar-test-class 7 | com.laomei.github 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | test-fat-jar-class 14 | 15 | 16 | 17 | com.laomei.github 18 | test-base-class 19 | 1.0-SNAPSHOT 20 | 21 | 22 | 23 | 24 | 25 | 26 | com.laomei.github 27 | fat-jar-plugin 28 | 1.0-SNAPSHOT 29 | 30 | 31 | package 32 | 33 | repackage 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.laomei.github 8 | fat-jar-isolation 9 | 1.0-SNAPSHOT 10 | pom 11 | 12 | 13 | fat-jar-classloader 14 | fat-jar-plugin 15 | fat-jar-test-class 16 | fat-jar-common 17 | 18 | 19 | 20 | 21 | 22 | com.laomei.github 23 | fat-jar-common 24 | ${project.version} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-compiler-plugin 34 | 3.6.1 35 | 36 | 1.8 37 | 1.8 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /fat-jar-classloader/src/test/java/com/laomei/fatjar/classloader/test/FatJarDelegateClassLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.classloader.test; 2 | 3 | import com.laomei.fatjar.base.TestClass; 4 | import com.laomei.fatjar.classloader.FatJarDelegateClassLoader; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.BlockJUnit4ClassRunner; 10 | 11 | import java.net.URL; 12 | import java.net.URLClassLoader; 13 | import java.util.Collections; 14 | 15 | /** 16 | * @author laomei on 2019/1/31 15:56 17 | */ 18 | @RunWith(BlockJUnit4ClassRunner.class) 19 | public class FatJarDelegateClassLoaderTest { 20 | 21 | private URLClassLoader classLoader; 22 | 23 | @Before 24 | public void init() { 25 | classLoader = initClassLoader(); 26 | } 27 | 28 | @Test 29 | public void testFatJarDelegateClassLoader() throws ClassNotFoundException { 30 | Class klass = Class.forName("com.laomei.fatjar.base.TestClass", true, classLoader); 31 | Assert.assertEquals(klass.getCanonicalName(), TestClass.class.getCanonicalName()); 32 | Assert.assertNotEquals(klass, TestClass.class); 33 | } 34 | 35 | private static URLClassLoader initClassLoader() { 36 | ClassLoader lastClassLoader = Thread.currentThread().getContextClassLoader(); 37 | URL url = lastClassLoader.getResource("test-fat-jar-class-1.0-SNAPSHOT-fat.jar"); 38 | return new FatJarDelegateClassLoader( 39 | new URL[] { url }, 40 | null, 41 | Collections.singleton("com.laomei.fatjar.base") 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/data/RandomAccessData.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.data; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * Interface that provides read-only random access to some underlying data. 8 | * Implementations must allow concurrent reads in a thread-safe manner. 9 | * 10 | * @author Phillip Webb 11 | */ 12 | public interface RandomAccessData { 13 | 14 | /** 15 | * Returns an {@link InputStream} that can be used to read the underlying data. The 16 | * caller is responsible close the underlying stream. 17 | * @param access hint indicating how the underlying data should be accessed 18 | * @return a new input stream that can be used to read the underlying data. 19 | * @throws IOException if the stream cannot be opened 20 | */ 21 | InputStream getInputStream(ResourceAccess access) throws IOException; 22 | 23 | /** 24 | * Returns a new {@link RandomAccessData} for a specific subsection of this data. 25 | * @param offset the offset of the subsection 26 | * @param length the length of the subsection 27 | * @return the subsection data 28 | */ 29 | RandomAccessData getSubsection(long offset, long length); 30 | 31 | /** 32 | * Returns the size of the data. 33 | * @return the size 34 | */ 35 | long getSize(); 36 | 37 | /** 38 | * Lock modes for accessing the underlying resource. 39 | */ 40 | enum ResourceAccess { 41 | 42 | /** 43 | * Obtain access to the underlying resource once and keep it until the stream is 44 | * closed. 45 | */ 46 | ONCE, 47 | 48 | /** 49 | * Obtain access to the underlying resource on each read, releasing it when done. 50 | */ 51 | PER_READ 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /fat-jar-classloader/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fat-jar-isolation 7 | com.laomei.github 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | fat-jar-classloader 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | org.slf4j 19 | slf4j-api 20 | 1.7.25 21 | true 22 | 23 | 24 | junit 25 | junit 26 | 4.12 27 | test 28 | 29 | 30 | com.laomei.github 31 | test-fat-jar-class 32 | 1.0-SNAPSHOT 33 | test 34 | 35 | 36 | com.laomei.github 37 | fat-jar-common 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | 1.16.20 43 | provided 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/ZipInflaterInputStream.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.zip.Inflater; 7 | import java.util.zip.InflaterInputStream; 8 | 9 | /** 10 | * {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which 11 | * is required with JDK 6) and returns accurate available() results. 12 | * 13 | * @author Phillip Webb 14 | */ 15 | class ZipInflaterInputStream extends InflaterInputStream { 16 | 17 | private boolean extraBytesWritten; 18 | 19 | private int available; 20 | 21 | ZipInflaterInputStream(InputStream inputStream, int size) { 22 | super(inputStream, new Inflater(true), getInflaterBufferSize(size)); 23 | this.available = size; 24 | } 25 | 26 | @Override 27 | public int available() throws IOException { 28 | if (this.available < 0) { 29 | return super.available(); 30 | } 31 | return this.available; 32 | } 33 | 34 | @Override 35 | public int read(byte[] b, int off, int len) throws IOException { 36 | int result = super.read(b, off, len); 37 | if (result != -1) { 38 | this.available -= result; 39 | } 40 | return result; 41 | } 42 | 43 | @Override 44 | protected void fill() throws IOException { 45 | try { 46 | super.fill(); 47 | } 48 | catch (EOFException ex) { 49 | if (this.extraBytesWritten) { 50 | throw ex; 51 | } 52 | this.len = 1; 53 | this.buf[0] = 0x0; 54 | this.extraBytesWritten = true; 55 | this.inf.setInput(this.buf, 0, this.len); 56 | } 57 | } 58 | 59 | private static int getInflaterBufferSize(long size) { 60 | size += 2; // inflater likes some space 61 | size = (size > 65536 ? 8192 : size); 62 | size = (size <= 0 ? 4096 : size); 63 | return (int) size; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/Bytes.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | /** 9 | * Utilities for dealing with bytes from ZIP files. 10 | * 11 | * @author Phillip Webb 12 | */ 13 | final class Bytes { 14 | 15 | private static final byte[] EMPTY_BYTES = new byte[] {}; 16 | 17 | private Bytes() { 18 | } 19 | 20 | public static byte[] get(RandomAccessData data) throws IOException { 21 | InputStream inputStream = data.getInputStream(RandomAccessData.ResourceAccess.ONCE); 22 | try { 23 | return get(inputStream, data.getSize()); 24 | } 25 | finally { 26 | inputStream.close(); 27 | } 28 | } 29 | 30 | public static byte[] get(InputStream inputStream, long length) throws IOException { 31 | if (length == 0) { 32 | return EMPTY_BYTES; 33 | } 34 | byte[] bytes = new byte[(int) length]; 35 | if (!fill(inputStream, bytes)) { 36 | throw new IOException("Unable to read bytes"); 37 | } 38 | return bytes; 39 | } 40 | 41 | public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException { 42 | return fill(inputStream, bytes, 0, bytes.length); 43 | } 44 | 45 | private static boolean fill(InputStream inputStream, byte[] bytes, int offset, 46 | int length) throws IOException { 47 | while (length > 0) { 48 | int read = inputStream.read(bytes, offset, length); 49 | if (read == -1) { 50 | return false; 51 | } 52 | offset += read; 53 | length = -read; 54 | } 55 | return true; 56 | } 57 | 58 | public static long littleEndianValue(byte[] bytes, int offset, int length) { 59 | long value = 0; 60 | for (int i = length - 1; i >= 0; i--) { 61 | value = ((value << 8) | (bytes[offset + i] & 0xFF)); 62 | } 63 | return value; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /fat-jar-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fat-jar-isolation 7 | com.laomei.github 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | maven-plugin 13 | 14 | fat-jar-plugin 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | org.apache.maven 20 | maven-plugin-api 21 | 3.0 22 | 23 | 24 | org.apache.maven.plugin-tools 25 | maven-plugin-annotations 26 | 3.4 27 | 28 | 29 | org.apache.maven 30 | maven-archiver 31 | 2.5 32 | 33 | 34 | org.apache.maven 35 | maven-core 36 | 2.2.1 37 | 38 | 39 | org.apache.maven.shared 40 | maven-common-artifact-filters 41 | 1.4 42 | 43 | 44 | com.laomei.github 45 | fat-jar-common 46 | 47 | 48 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/Archive.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.net.URL; 6 | import java.util.List; 7 | import java.util.jar.Manifest; 8 | 9 | /** 10 | * An archive that can be launched by the {@link Launcher}. 11 | * 12 | * @author Phillip Webb 13 | * @see JarFileArchive 14 | */ 15 | public interface Archive extends Iterable { 16 | 17 | /** 18 | * Returns a URL that can be used to load the archive. 19 | * @return the archive URL 20 | * @throws MalformedURLException if the URL is malformed 21 | */ 22 | URL getUrl() throws MalformedURLException; 23 | 24 | /** 25 | * Returns the manifest of the archive. 26 | * @return the manifest 27 | * @throws IOException if the manifest cannot be read 28 | */ 29 | Manifest getManifest() throws IOException; 30 | 31 | /** 32 | * Returns nested {@link Archive}s for entries that match the specified filter. 33 | * @param filter the filter used to limit entries 34 | * @return nested archives 35 | * @throws IOException if nested archives cannot be read 36 | */ 37 | List getNestedArchives(EntryFilter filter) throws IOException; 38 | 39 | /** 40 | * Represents a single entry in the archive. 41 | */ 42 | interface Entry { 43 | 44 | /** 45 | * Returns {@code true} if the entry represents a directory. 46 | * @return if the entry is a directory 47 | */ 48 | boolean isDirectory(); 49 | 50 | /** 51 | * Returns the name of the entry. 52 | * @return the name of the entry 53 | */ 54 | String getName(); 55 | 56 | } 57 | 58 | /** 59 | * Strategy interface to filter {@link Entry Entries}. 60 | */ 61 | interface EntryFilter { 62 | 63 | /** 64 | * Apply the jar entry filter. 65 | * @param entry the entry to filter 66 | * @return {@code true} if the filter matches 67 | */ 68 | boolean matches(Entry entry); 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/Layouts.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | import java.util.Arrays; 5 | import java.util.HashSet; 6 | import java.util.Locale; 7 | import java.util.Set; 8 | 9 | /** 10 | * Common {@link Layout}s. 11 | * 12 | * @author Phillip Webb 13 | * @author Dave Syer 14 | * @author Andy Wilkinson 15 | */ 16 | public final class Layouts { 17 | 18 | private Layouts() { 19 | } 20 | 21 | /** 22 | * Return a layout for the given source file. 23 | * @param file the source file 24 | * @return a {@link Layout} 25 | */ 26 | public static Layout forFile(File file) { 27 | if (file == null) { 28 | throw new IllegalArgumentException("File must not be null"); 29 | } 30 | if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) { 31 | return new Jar(); 32 | } 33 | throw new IllegalStateException("Unable to deduce layout for '" + file + "'"); 34 | } 35 | 36 | /** 37 | * Executable JAR layout. 38 | */ 39 | public static class Jar implements RepackagingLayout { 40 | 41 | @Override 42 | public String getLibraryDestination(String libraryName, LibraryScope scope) { 43 | return "lib/"; 44 | } 45 | 46 | @Override 47 | public String getRepackagedClassesLocation() { 48 | return ""; 49 | } 50 | } 51 | 52 | /** 53 | * Module layout (designed to be used as a "plug-in"). 54 | * 55 | * @deprecated as of 1.5 in favor of a custom {@link LayoutFactory} 56 | */ 57 | @Deprecated 58 | public static class Module implements Layout { 59 | 60 | private static final Set LIB_DESTINATION_SCOPES = new HashSet( 61 | Arrays.asList(LibraryScope.COMPILE, LibraryScope.RUNTIME, 62 | LibraryScope.CUSTOM)); 63 | 64 | @Override 65 | public String getLibraryDestination(String libraryName, LibraryScope scope) { 66 | if (LIB_DESTINATION_SCOPES.contains(scope)) { 67 | return "lib/"; 68 | } 69 | return null; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sweat123/fat-jar-isolation.svg?branch=master)](https://travis-ci.org/sweat123/fat-jar-isolation) 2 | 3 | # Fat Jar Plugin 4 | 5 | A maven plugin for creating fat jar; 6 | 7 | All of the libraries for the current project will be added into the fat jar including current project jar file; 8 | 9 | ## usage 10 | 11 | ```xml 12 | 13 | com.laomei.github 14 | fat-jar-plugin 15 | 1.0-SNAPSHOT 16 | 17 | 18 | package 19 | 20 | repackage 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 | ## result 28 | 29 | A new jar file will be created which the name will be append `'-fat'` at the end of the name; 30 | 31 | The structure of fat jar 32 | 33 | ```text 34 | . 35 | ├── lib 36 | │   ├── demo-1.0-SNAPSHOT.jar 37 | │   ├── spring-core-4.3.17.RELEASE.jar 38 | └── META-INF 39 | └── MANIFEST.MF 40 | ``` 41 | 42 | - `demo-1.0-SNAPSHOT.jar` jar file of the current project 43 | - `spring-core-4.3.17.RELEASE.jar` jar file that declared in the pom.xml 44 | 45 | # Fat Jar ClassLoader 46 | 47 | The classloader that can load class from fat jar; 48 | 49 | One fat jar corresponds to one `FatJarClassLoader`; The project which contains multiple fat jar will has multiple `FatJarClassLoader`; 50 | 51 | In fact users should use `FatJarDelegateClassLoader` which will manage all `FatJarClassLoader`; 52 | 53 | `FatJarDelegateClassLoader` expected 3 args in constructors: 54 | 55 | 1. urls 56 | 2. parent classloader 57 | 3. the prefix name for class which will be load by `FatJarDelegateClassLoader` 58 | 59 | >FatJarClassLoader was write with spring-boot-loader; 60 | 61 | ## How to use 62 | 63 | `xxx.jar` is a project jar file witch contains some fat jars; 64 | We only need to give the `xxx.jar` url to `FatJarDelegateClassLoader`; `FatJarDelegateClassLoader` will search all fat jar files in the giving urls and creating multiple `FatJarClassLoader`; The class which name is begin with `com.xxx` will be load by `FatJarDelegateClassLoader`; 65 | 66 | 67 | ```java 68 | URL url = lastClassLoader.getResource("xxx.jar"); 69 | new FatJarDelegateClassLoader( 70 | new URL[] { url }, 71 | null, 72 | Collections.singleton("com.xxx") 73 | ); 74 | ``` 75 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/Library.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Encapsulates information about a single library that may be packed into the archive. 7 | * 8 | * @author Phillip Webb 9 | * @since 1.1.2 10 | * @see Libraries 11 | */ 12 | public class Library { 13 | 14 | private final String name; 15 | 16 | private final File file; 17 | 18 | private final LibraryScope scope; 19 | 20 | private final boolean unpackRequired; 21 | 22 | /** 23 | * Create a new {@link Library}. 24 | * @param file the source file 25 | * @param scope the scope of the library 26 | */ 27 | public Library(File file, LibraryScope scope) { 28 | this(file, scope, false); 29 | } 30 | 31 | /** 32 | * Create a new {@link Library}. 33 | * @param file the source file 34 | * @param scope the scope of the library 35 | * @param unpackRequired if the library needs to be unpacked before it can be used 36 | */ 37 | public Library(File file, LibraryScope scope, boolean unpackRequired) { 38 | this(null, file, scope, unpackRequired); 39 | } 40 | 41 | /** 42 | * Create a new {@link Library}. 43 | * @param name the name of the library as it should be written or {@code null} to use 44 | * the file name 45 | * @param file the source file 46 | * @param scope the scope of the library 47 | * @param unpackRequired if the library needs to be unpacked before it can be used 48 | */ 49 | public Library(String name, File file, LibraryScope scope, boolean unpackRequired) { 50 | this.name = (name != null ? name : file.getName()); 51 | this.file = file; 52 | this.scope = scope; 53 | this.unpackRequired = unpackRequired; 54 | } 55 | 56 | /** 57 | * Return the name of file as it should be written. 58 | * @return the name 59 | */ 60 | public String getName() { 61 | return this.name; 62 | } 63 | 64 | /** 65 | * Return the library file. 66 | * @return the file 67 | */ 68 | public File getFile() { 69 | return this.file; 70 | } 71 | 72 | /** 73 | * Return the scope of the library. 74 | * @return the scope 75 | */ 76 | public LibraryScope getScope() { 77 | return this.scope; 78 | } 79 | 80 | /** 81 | * Return if the file cannot be used directly as a nested jar and needs to be 82 | * unpacked. 83 | * @return if unpack is required 84 | */ 85 | public boolean isUnpackRequired() { 86 | return this.unpackRequired; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.security.DigestInputStream; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | /** 11 | * Utilities for manipulating files and directories in Spring Boot tooling. 12 | * 13 | * @author Dave Syer 14 | * @author Phillip Webb 15 | */ 16 | public abstract class FileUtils { 17 | 18 | /** 19 | * Utility to remove duplicate files from an "output" directory if they already exist 20 | * in an "origin". Recursively scans the origin directory looking for files (not 21 | * directories) that exist in both places and deleting the copy. 22 | * @param outputDirectory the output directory 23 | * @param originDirectory the origin directory 24 | */ 25 | public static void removeDuplicatesFromOutputDirectory(File outputDirectory, 26 | File originDirectory) { 27 | if (originDirectory.isDirectory()) { 28 | for (String name : originDirectory.list()) { 29 | File targetFile = new File(outputDirectory, name); 30 | if (targetFile.exists() && targetFile.canWrite()) { 31 | if (!targetFile.isDirectory()) { 32 | targetFile.delete(); 33 | } 34 | else { 35 | FileUtils.removeDuplicatesFromOutputDirectory(targetFile, 36 | new File(originDirectory, name)); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Generate a SHA.1 Hash for a given file. 45 | * @param file the file to hash 46 | * @return the hash value as a String 47 | * @throws IOException if the file cannot be read 48 | */ 49 | public static String sha1Hash(File file) throws IOException { 50 | try { 51 | DigestInputStream inputStream = new DigestInputStream( 52 | new FileInputStream(file), MessageDigest.getInstance("SHA-1")); 53 | try { 54 | byte[] buffer = new byte[4098]; 55 | while (inputStream.read(buffer) != -1) { 56 | // Read the entire stream 57 | } 58 | return bytesToHex(inputStream.getMessageDigest().digest()); 59 | } 60 | finally { 61 | inputStream.close(); 62 | } 63 | } 64 | catch (NoSuchAlgorithmException ex) { 65 | throw new IllegalStateException(ex); 66 | } 67 | } 68 | 69 | private static String bytesToHex(byte[] bytes) { 70 | StringBuilder hex = new StringBuilder(); 71 | for (byte b : bytes) { 72 | hex.append(String.format("%02x", b)); 73 | } 74 | return hex.toString(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarEntry.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.net.URL; 6 | import java.security.CodeSigner; 7 | import java.security.cert.Certificate; 8 | import java.util.jar.Attributes; 9 | import java.util.jar.Manifest; 10 | 11 | /** 12 | * Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s. 13 | * 14 | * @author Phillip Webb 15 | */ 16 | class JarEntry extends java.util.jar.JarEntry implements FileHeader { 17 | 18 | private Certificate[] certificates; 19 | 20 | private CodeSigner[] codeSigners; 21 | 22 | private final JarFile jarFile; 23 | 24 | private long localHeaderOffset; 25 | 26 | JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) { 27 | super(header.getName().toString()); 28 | this.jarFile = jarFile; 29 | this.localHeaderOffset = header.getLocalHeaderOffset(); 30 | setCompressedSize(header.getCompressedSize()); 31 | setMethod(header.getMethod()); 32 | setCrc(header.getCrc()); 33 | setSize(header.getSize()); 34 | setExtra(header.getExtra()); 35 | setComment(header.getComment().toString()); 36 | setSize(header.getSize()); 37 | setTime(header.getTime()); 38 | } 39 | 40 | @Override 41 | public boolean hasName(String name, String suffix) { 42 | return getName().length() == name.length() + suffix.length() 43 | && getName().startsWith(name) && getName().endsWith(suffix); 44 | } 45 | 46 | /** 47 | * Return a {@link URL} for this {@link JarEntry}. 48 | * @return the URL for the entry 49 | * @throws MalformedURLException if the URL is not valid 50 | */ 51 | URL getUrl() throws MalformedURLException { 52 | return new URL(this.jarFile.getUrl(), getName()); 53 | } 54 | 55 | @Override 56 | public Attributes getAttributes() throws IOException { 57 | Manifest manifest = this.jarFile.getManifest(); 58 | return (manifest != null ? manifest.getAttributes(getName()) : null); 59 | } 60 | 61 | @Override 62 | public Certificate[] getCertificates() { 63 | if (this.jarFile.isSigned() && this.certificates == null) { 64 | this.jarFile.setupEntryCertificates(this); 65 | } 66 | return this.certificates; 67 | } 68 | 69 | @Override 70 | public CodeSigner[] getCodeSigners() { 71 | if (this.jarFile.isSigned() && this.codeSigners == null) { 72 | this.jarFile.setupEntryCertificates(this); 73 | } 74 | return this.codeSigners; 75 | } 76 | 77 | void setCertificates(java.util.jar.JarEntry entry) { 78 | this.certificates = entry.getCertificates(); 79 | this.codeSigners = entry.getCodeSigners(); 80 | } 81 | 82 | @Override 83 | public long getLocalHeaderOffset() { 84 | return this.localHeaderOffset; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /fat-jar-plugin/src/main/java/com/laomei/fatjar/plugin/FatJarPackagerMojo.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.plugin; 2 | 3 | import com.laomei.fatjar.common.boot.tool.ArtifactsLibraries; 4 | import com.laomei.fatjar.common.boot.tool.Libraries; 5 | import org.apache.maven.artifact.Artifact; 6 | import org.apache.maven.plugin.AbstractMojo; 7 | import org.apache.maven.plugin.MojoExecutionException; 8 | import org.apache.maven.plugin.MojoFailureException; 9 | import org.apache.maven.plugin.logging.Log; 10 | import org.apache.maven.plugins.annotations.Component; 11 | import org.apache.maven.plugins.annotations.LifecyclePhase; 12 | import org.apache.maven.plugins.annotations.Mojo; 13 | import org.apache.maven.plugins.annotations.Parameter; 14 | import org.apache.maven.plugins.annotations.ResolutionScope; 15 | import org.apache.maven.project.MavenProject; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.Collections; 20 | import java.util.Set; 21 | 22 | /** 23 | * @author laomei on 2019/1/7 14:24 24 | */ 25 | @Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME) 26 | public class FatJarPackagerMojo extends AbstractMojo { 27 | 28 | @Component 29 | private MavenProject project; 30 | 31 | @Parameter(defaultValue = "${project.build.directory}", required = true) 32 | private File outputDirectory; 33 | 34 | @Parameter 35 | private String classifier; 36 | 37 | @Parameter(defaultValue = "${project.build.finalName}", required = true) 38 | private String finalName; 39 | 40 | private Log logger = getLog(); 41 | 42 | @Override 43 | public void execute() throws MojoExecutionException, MojoFailureException { 44 | logger.info("============> begin to repackage jar as fat jar"); 45 | repackage(); 46 | logger.info("============> create fat jar successfully"); 47 | } 48 | 49 | private void repackage() throws MojoExecutionException, MojoFailureException { 50 | 51 | File sourceFile = project.getArtifact().getFile(); 52 | Repackager repackager = new Repackager(sourceFile); 53 | File target = getTargetFile(); 54 | Set artifacts = project.getArtifacts(); 55 | Libraries libraries = new ArtifactsLibraries(artifacts, Collections.emptyList(), getLog()); 56 | try { 57 | repackager.repackage(target, libraries); 58 | } 59 | catch (IOException ex) { 60 | throw new MojoExecutionException(ex.getMessage(), ex); 61 | } 62 | } 63 | 64 | private File getTargetFile() { 65 | String classifier = (this.classifier != null ? this.classifier.trim() : ""); 66 | if (classifier.length() > 0 && !classifier.startsWith("-")) { 67 | classifier = "-" + classifier; 68 | } 69 | if (!this.outputDirectory.exists()) { 70 | this.outputDirectory.mkdirs(); 71 | } 72 | String name = this.finalName + classifier + "-fat." + this.project.getArtifact().getArtifactHandler().getExtension(); 73 | return new File(this.outputDirectory, name); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/CentralDirectoryParser.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * Parses the central directory from a JAR file. 11 | * 12 | * @author Phillip Webb 13 | * @see CentralDirectoryVisitor 14 | */ 15 | class CentralDirectoryParser { 16 | 17 | private int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46; 18 | 19 | private final List visitors = new ArrayList(); 20 | 21 | public T addVisitor(T visitor) { 22 | this.visitors.add(visitor); 23 | return visitor; 24 | } 25 | 26 | /** 27 | * Parse the source data, triggering {@link CentralDirectoryVisitor visitors}. 28 | * @param data the source data 29 | * @param skipPrefixBytes if prefix bytes should be skipped 30 | * @return The actual archive data without any prefix bytes 31 | * @throws IOException on error 32 | */ 33 | public RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) 34 | throws IOException { 35 | CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data); 36 | if (skipPrefixBytes) { 37 | data = getArchiveData(endRecord, data); 38 | } 39 | RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data); 40 | visitStart(endRecord, centralDirectoryData); 41 | parseEntries(endRecord, centralDirectoryData); 42 | visitEnd(); 43 | return data; 44 | } 45 | 46 | private void parseEntries(CentralDirectoryEndRecord endRecord, 47 | RandomAccessData centralDirectoryData) throws IOException { 48 | byte[] bytes = Bytes.get(centralDirectoryData); 49 | CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); 50 | int dataOffset = 0; 51 | for (int i = 0; i < endRecord.getNumberOfRecords(); i++) { 52 | fileHeader.load(bytes, dataOffset, null, 0, null); 53 | visitFileHeader(dataOffset, fileHeader); 54 | dataOffset += this.CENTRAL_DIRECTORY_HEADER_BASE_SIZE 55 | + fileHeader.getName().length() + fileHeader.getComment().length() 56 | + fileHeader.getExtra().length; 57 | } 58 | } 59 | 60 | private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, 61 | RandomAccessData data) { 62 | long offset = endRecord.getStartOfArchive(data); 63 | if (offset == 0) { 64 | return data; 65 | } 66 | return data.getSubsection(offset, data.getSize() - offset); 67 | } 68 | 69 | private void visitStart(CentralDirectoryEndRecord endRecord, 70 | RandomAccessData centralDirectoryData) { 71 | for (CentralDirectoryVisitor visitor : this.visitors) { 72 | visitor.visitStart(endRecord, centralDirectoryData); 73 | } 74 | } 75 | 76 | private void visitFileHeader(int dataOffset, CentralDirectoryFileHeader fileHeader) { 77 | for (CentralDirectoryVisitor visitor : this.visitors) { 78 | visitor.visitFileHeader(fileHeader, dataOffset); 79 | } 80 | } 81 | 82 | private void visitEnd() { 83 | for (CentralDirectoryVisitor visitor : this.visitors) { 84 | visitor.visitEnd(); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/tool/ArtifactsLibraries.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.tool; 2 | 3 | import org.apache.maven.artifact.Artifact; 4 | import org.apache.maven.model.Dependency; 5 | import org.apache.maven.plugin.logging.Log; 6 | 7 | import java.io.IOException; 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.Map; 13 | import java.util.Set; 14 | 15 | /** 16 | * {@link Libraries} backed by Maven {@link Artifact}s. 17 | * 18 | * @author Phillip Webb 19 | * @author Andy Wilkinson 20 | * @author Stephane Nicoll 21 | */ 22 | public class ArtifactsLibraries implements Libraries { 23 | 24 | private static final Map SCOPES; 25 | 26 | static { 27 | Map libraryScopes = new HashMap(); 28 | libraryScopes.put(Artifact.SCOPE_COMPILE, LibraryScope.COMPILE); 29 | libraryScopes.put(Artifact.SCOPE_RUNTIME, LibraryScope.RUNTIME); 30 | libraryScopes.put(Artifact.SCOPE_PROVIDED, LibraryScope.PROVIDED); 31 | libraryScopes.put(Artifact.SCOPE_SYSTEM, LibraryScope.PROVIDED); 32 | SCOPES = Collections.unmodifiableMap(libraryScopes); 33 | } 34 | 35 | private final Set artifacts; 36 | 37 | private final Collection unpacks; 38 | 39 | private final Log log; 40 | 41 | public ArtifactsLibraries(Set artifacts, Collection unpacks, 42 | Log log) { 43 | this.artifacts = artifacts; 44 | this.unpacks = unpacks; 45 | this.log = log; 46 | } 47 | 48 | @Override 49 | public void doWithLibraries(LibraryCallback callback) throws IOException { 50 | Set duplicates = getDuplicates(this.artifacts); 51 | for (Artifact artifact : this.artifacts) { 52 | LibraryScope scope = SCOPES.get(artifact.getScope()); 53 | if (scope != null && artifact.getFile() != null) { 54 | String name = getFileName(artifact); 55 | if (duplicates.contains(name)) { 56 | this.log.debug("Duplicate found: " + name); 57 | name = artifact.getGroupId() + "-" + name; 58 | this.log.debug("Renamed to: " + name); 59 | } 60 | callback.library(new Library(name, artifact.getFile(), scope, 61 | isUnpackRequired(artifact))); 62 | } 63 | } 64 | } 65 | 66 | private Set getDuplicates(Set artifacts) { 67 | Set duplicates = new HashSet(); 68 | Set seen = new HashSet(); 69 | for (Artifact artifact : artifacts) { 70 | String fileName = getFileName(artifact); 71 | if (artifact.getFile() != null && !seen.add(fileName)) { 72 | duplicates.add(fileName); 73 | } 74 | } 75 | return duplicates; 76 | } 77 | 78 | private boolean isUnpackRequired(Artifact artifact) { 79 | if (this.unpacks != null) { 80 | for (Dependency unpack : this.unpacks) { 81 | if (artifact.getGroupId().equals(unpack.getGroupId()) 82 | && artifact.getArtifactId().equals(unpack.getArtifactId())) { 83 | return true; 84 | } 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | private String getFileName(Artifact artifact) { 91 | StringBuilder sb = new StringBuilder(); 92 | sb.append(artifact.getArtifactId()).append("-").append(artifact.getBaseVersion()); 93 | String classifier = artifact.getClassifier(); 94 | if (classifier != null) { 95 | sb.append("-").append(classifier); 96 | } 97 | sb.append(".").append(artifact.getArtifactHandler().getExtension()); 98 | return sb.toString(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/CentralDirectoryEndRecord.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * A ZIP File "End of central directory record" (EOCD). 9 | * 10 | * @author Phillip Webb 11 | * @author Andy Wilkinson 12 | * @see Zip File Format 13 | */ 14 | class CentralDirectoryEndRecord { 15 | 16 | private static final int MINIMUM_SIZE = 22; 17 | 18 | private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; 19 | 20 | private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; 21 | 22 | private static final int SIGNATURE = 0x06054b50; 23 | 24 | private static final int COMMENT_LENGTH_OFFSET = 20; 25 | 26 | private static final int READ_BLOCK_SIZE = 256; 27 | 28 | private byte[] block; 29 | 30 | private int offset; 31 | 32 | private int size; 33 | 34 | /** 35 | * Create a new {@link CentralDirectoryEndRecord} instance from the specified 36 | * {@link RandomAccessData}, searching backwards from the end until a valid block is 37 | * located. 38 | * @param data the source data 39 | * @throws IOException in case of I/O errors 40 | */ 41 | CentralDirectoryEndRecord(RandomAccessData data) throws IOException { 42 | this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE); 43 | this.size = MINIMUM_SIZE; 44 | this.offset = this.block.length - this.size; 45 | while (!isValid()) { 46 | this.size++; 47 | if (this.size > this.block.length) { 48 | if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) { 49 | throw new IOException("Unable to find ZIP central directory " 50 | + "records after reading " + this.size + " bytes"); 51 | } 52 | this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE); 53 | } 54 | this.offset = this.block.length - this.size; 55 | } 56 | } 57 | 58 | private byte[] createBlockFromEndOfData(RandomAccessData data, int size) 59 | throws IOException { 60 | int length = (int) Math.min(data.getSize(), size); 61 | return Bytes.get(data.getSubsection(data.getSize() - length, length)); 62 | } 63 | 64 | private boolean isValid() { 65 | if (this.block.length < MINIMUM_SIZE 66 | || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) { 67 | return false; 68 | } 69 | // Total size must be the structure size + comment 70 | long commentLength = Bytes.littleEndianValue(this.block, 71 | this.offset + COMMENT_LENGTH_OFFSET, 2); 72 | return this.size == MINIMUM_SIZE + commentLength; 73 | } 74 | 75 | /** 76 | * Returns the location in the data that the archive actually starts. For most files 77 | * the archive data will start at 0, however, it is possible to have prefixed bytes 78 | * (often used for startup scripts) at the beginning of the data. 79 | * @param data the source data 80 | * @return the offset within the data where the archive begins 81 | */ 82 | public long getStartOfArchive(RandomAccessData data) { 83 | long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); 84 | long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); 85 | long actualOffset = data.getSize() - this.size - length; 86 | return actualOffset - specifiedOffset; 87 | } 88 | 89 | /** 90 | * Return the bytes of the "Central directory" based on the offset indicated in this 91 | * record. 92 | * @param data the source data 93 | * @return the central directory data 94 | */ 95 | public RandomAccessData getCentralDirectory(RandomAccessData data) { 96 | long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); 97 | long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); 98 | return data.getSubsection(offset, length); 99 | } 100 | 101 | /** 102 | * Return the number of ZIP entries in the file. 103 | * @return the number of records in the zip 104 | */ 105 | public int getNumberOfRecords() { 106 | long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); 107 | if (numberOfRecords == 0xFFFF) { 108 | throw new IllegalStateException("Zip64 archives are not supported"); 109 | } 110 | return (int) numberOfRecords; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/CentralDirectoryFileHeader.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.IOException; 6 | import java.util.Calendar; 7 | import java.util.GregorianCalendar; 8 | 9 | /** 10 | * A ZIP File "Central directory file header record" (CDFH). 11 | * 12 | * @author Phillip Webb 13 | * @author Andy Wilkinson 14 | * @see Zip File Format 15 | */ 16 | 17 | final class CentralDirectoryFileHeader implements FileHeader { 18 | 19 | private static final AsciiBytes SLASH = new AsciiBytes("/"); 20 | 21 | private static final byte[] NO_EXTRA = {}; 22 | 23 | private static final AsciiBytes NO_COMMENT = new AsciiBytes(""); 24 | 25 | private byte[] header; 26 | 27 | private int headerOffset; 28 | 29 | private AsciiBytes name; 30 | 31 | private byte[] extra; 32 | 33 | private AsciiBytes comment; 34 | 35 | private long localHeaderOffset; 36 | 37 | CentralDirectoryFileHeader() { 38 | } 39 | 40 | CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, 41 | byte[] extra, AsciiBytes comment, long localHeaderOffset) { 42 | super(); 43 | this.header = header; 44 | this.headerOffset = headerOffset; 45 | this.name = name; 46 | this.extra = extra; 47 | this.comment = comment; 48 | this.localHeaderOffset = localHeaderOffset; 49 | } 50 | 51 | void load(byte[] data, int dataOffset, RandomAccessData variableData, 52 | int variableOffset, JarEntryFilter filter) throws IOException { 53 | // Load fixed part 54 | this.header = data; 55 | this.headerOffset = dataOffset; 56 | long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2); 57 | long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2); 58 | long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2); 59 | this.localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); 60 | // Load variable part 61 | dataOffset += 46; 62 | if (variableData != null) { 63 | data = Bytes.get(variableData.getSubsection(variableOffset + 46, 64 | nameLength + extraLength + commentLength)); 65 | dataOffset = 0; 66 | } 67 | this.name = new AsciiBytes(data, dataOffset, (int) nameLength); 68 | if (filter != null) { 69 | this.name = filter.apply(this.name); 70 | } 71 | this.extra = NO_EXTRA; 72 | this.comment = NO_COMMENT; 73 | if (extraLength > 0) { 74 | this.extra = new byte[(int) extraLength]; 75 | System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, 76 | this.extra.length); 77 | } 78 | if (commentLength > 0) { 79 | this.comment = new AsciiBytes(data, 80 | (int) (dataOffset + nameLength + extraLength), (int) commentLength); 81 | } 82 | } 83 | 84 | public AsciiBytes getName() { 85 | return this.name; 86 | } 87 | 88 | @Override 89 | public boolean hasName(String name, String suffix) { 90 | return this.name.equals(new AsciiBytes(suffix != null ? name + suffix : name)); 91 | } 92 | 93 | public boolean isDirectory() { 94 | return this.name.endsWith(SLASH); 95 | } 96 | 97 | @Override 98 | public int getMethod() { 99 | return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2); 100 | } 101 | 102 | public long getTime() { 103 | long date = Bytes.littleEndianValue(this.header, this.headerOffset + 14, 2); 104 | long time = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 2); 105 | return decodeMsDosFormatDateTime(date, time).getTimeInMillis(); 106 | } 107 | 108 | /** 109 | * Decode MS-DOS Date Time details. See 110 | * mindprod.com/jgloss/zip.html for 111 | * more details of the format. 112 | * @param date the date part 113 | * @param time the time part 114 | * @return a {@link Calendar} containing the decoded date. 115 | */ 116 | private Calendar decodeMsDosFormatDateTime(long date, long time) { 117 | int year = (int) ((date >> 9) & 0x7F) + 1980; 118 | int month = (int) ((date >> 5) & 0xF) - 1; 119 | int day = (int) (date & 0x1F); 120 | int hours = (int) ((time >> 11) & 0x1F); 121 | int minutes = (int) ((time >> 5) & 0x3F); 122 | int seconds = (int) ((time << 1) & 0x3E); 123 | return new GregorianCalendar(year, month, day, hours, minutes, seconds); 124 | } 125 | 126 | public long getCrc() { 127 | return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4); 128 | } 129 | 130 | @Override 131 | public long getCompressedSize() { 132 | return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4); 133 | } 134 | 135 | @Override 136 | public long getSize() { 137 | return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4); 138 | } 139 | 140 | public byte[] getExtra() { 141 | return this.extra; 142 | } 143 | 144 | public AsciiBytes getComment() { 145 | return this.comment; 146 | } 147 | 148 | @Override 149 | public long getLocalHeaderOffset() { 150 | return this.localHeaderOffset; 151 | } 152 | 153 | @Override 154 | public CentralDirectoryFileHeader clone() { 155 | byte[] header = new byte[46]; 156 | System.arraycopy(this.header, this.headerOffset, header, 0, header.length); 157 | return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, 158 | this.localHeaderOffset); 159 | } 160 | 161 | public static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, 162 | int offset, JarEntryFilter filter) throws IOException { 163 | CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); 164 | byte[] bytes = Bytes.get(data.getSubsection(offset, 46)); 165 | fileHeader.load(bytes, 0, data, offset, filter); 166 | return fileHeader; 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/AsciiBytes.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.nio.charset.Charset; 4 | 5 | /** 6 | * Simple wrapper around a byte array that represents an ASCII. Used for performance 7 | * reasons to save constructing Strings for ZIP data. 8 | * 9 | * @author Phillip Webb 10 | * @author Andy Wilkinson 11 | */ 12 | final class AsciiBytes { 13 | 14 | private static final Charset UTF_8 = Charset.forName("UTF-8"); 15 | 16 | private final byte[] bytes; 17 | 18 | private final int offset; 19 | 20 | private final int length; 21 | 22 | private String string; 23 | 24 | private int hash; 25 | 26 | /** 27 | * Create a new {@link AsciiBytes} from the specified String. 28 | * @param string the source string 29 | */ 30 | AsciiBytes(String string) { 31 | this(string.getBytes(UTF_8)); 32 | this.string = string; 33 | } 34 | 35 | /** 36 | * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes 37 | * are not expected to change. 38 | * @param bytes the source bytes 39 | */ 40 | AsciiBytes(byte[] bytes) { 41 | this(bytes, 0, bytes.length); 42 | } 43 | 44 | /** 45 | * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes 46 | * are not expected to change. 47 | * @param bytes the source bytes 48 | * @param offset the offset 49 | * @param length the length 50 | */ 51 | AsciiBytes(byte[] bytes, int offset, int length) { 52 | if (offset < 0 || length < 0 || (offset + length) > bytes.length) { 53 | throw new IndexOutOfBoundsException(); 54 | } 55 | this.bytes = bytes; 56 | this.offset = offset; 57 | this.length = length; 58 | } 59 | 60 | public int length() { 61 | return this.length; 62 | } 63 | 64 | public boolean startsWith(AsciiBytes prefix) { 65 | if (this == prefix) { 66 | return true; 67 | } 68 | if (prefix.length > this.length) { 69 | return false; 70 | } 71 | for (int i = 0; i < prefix.length; i++) { 72 | if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) { 73 | return false; 74 | } 75 | } 76 | return true; 77 | } 78 | 79 | public boolean endsWith(AsciiBytes postfix) { 80 | if (this == postfix) { 81 | return true; 82 | } 83 | if (postfix.length > this.length) { 84 | return false; 85 | } 86 | for (int i = 0; i < postfix.length; i++) { 87 | if (this.bytes[this.offset + (this.length - 1) 88 | - i] != postfix.bytes[postfix.offset + (postfix.length - 1) - i]) { 89 | return false; 90 | } 91 | } 92 | return true; 93 | } 94 | 95 | public AsciiBytes substring(int beginIndex) { 96 | return substring(beginIndex, this.length); 97 | } 98 | 99 | public AsciiBytes substring(int beginIndex, int endIndex) { 100 | int length = endIndex - beginIndex; 101 | if (this.offset + length > this.bytes.length) { 102 | throw new IndexOutOfBoundsException(); 103 | } 104 | return new AsciiBytes(this.bytes, this.offset + beginIndex, length); 105 | } 106 | 107 | public AsciiBytes append(String string) { 108 | if (string == null || string.isEmpty()) { 109 | return this; 110 | } 111 | return append(string.getBytes(UTF_8)); 112 | } 113 | 114 | public AsciiBytes append(AsciiBytes asciiBytes) { 115 | if (asciiBytes == null || asciiBytes.length() == 0) { 116 | return this; 117 | } 118 | return append(asciiBytes.bytes); 119 | } 120 | 121 | public AsciiBytes append(byte[] bytes) { 122 | if (bytes == null || bytes.length == 0) { 123 | return this; 124 | } 125 | byte[] combined = new byte[this.length + bytes.length]; 126 | System.arraycopy(this.bytes, this.offset, combined, 0, this.length); 127 | System.arraycopy(bytes, 0, combined, this.length, bytes.length); 128 | return new AsciiBytes(combined); 129 | } 130 | 131 | @Override 132 | public String toString() { 133 | if (this.string == null) { 134 | this.string = new String(this.bytes, this.offset, this.length, UTF_8); 135 | } 136 | return this.string; 137 | } 138 | 139 | @Override 140 | public int hashCode() { 141 | int hash = this.hash; 142 | if (hash == 0 && this.bytes.length > 0) { 143 | for (int i = this.offset; i < this.offset + this.length; i++) { 144 | int b = this.bytes[i]; 145 | if (b < 0) { 146 | b = b & 0x7F; 147 | int limit; 148 | int excess = 0x80; 149 | if (b < 96) { 150 | limit = 1; 151 | excess += 0x40 << 6; 152 | } 153 | else if (b < 112) { 154 | limit = 2; 155 | excess += (0x60 << 12) + (0x80 << 6); 156 | } 157 | else { 158 | limit = 3; 159 | excess += (0x70 << 18) + (0x80 << 12) + (0x80 << 6); 160 | } 161 | for (int j = 0; j < limit; j++) { 162 | b = (b << 6) + (this.bytes[++i] & 0xFF); 163 | } 164 | b -= excess; 165 | } 166 | if (b <= 0xFFFF) { 167 | hash = 31 * hash + b; 168 | } 169 | else { 170 | hash = 31 * hash + ((b >> 0xA) + 0xD7C0); 171 | hash = 31 * hash + ((b & 0x3FF) + 0xDC00); 172 | } 173 | } 174 | this.hash = hash; 175 | } 176 | return hash; 177 | } 178 | 179 | @Override 180 | public boolean equals(Object obj) { 181 | if (obj == null) { 182 | return false; 183 | } 184 | if (this == obj) { 185 | return true; 186 | } 187 | if (obj.getClass().equals(AsciiBytes.class)) { 188 | AsciiBytes other = (AsciiBytes) obj; 189 | if (this.length == other.length) { 190 | for (int i = 0; i < this.length; i++) { 191 | if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) { 192 | return false; 193 | } 194 | } 195 | return true; 196 | } 197 | } 198 | return false; 199 | } 200 | 201 | static String toString(byte[] bytes) { 202 | return new String(bytes, UTF_8); 203 | } 204 | 205 | public static int hashCode(String string) { 206 | // We're compatible with String's hashCode(). 207 | return string.hashCode(); 208 | } 209 | 210 | public static int hashCode(int hash, String string) { 211 | for (int i = 0; i < string.length(); i++) { 212 | hash = 31 * hash + string.charAt(i); 213 | } 214 | return hash; 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarFileArchive.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData.ResourceAccess; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.Enumeration; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | import java.util.UUID; 18 | import java.util.jar.JarEntry; 19 | import java.util.jar.Manifest; 20 | 21 | /** 22 | * {@link Archive} implementation backed by a {@link JarFile}. 23 | * 24 | * @author Phillip Webb 25 | * @author Andy Wilkinson 26 | */ 27 | public class JarFileArchive implements Archive { 28 | 29 | private static final String UNPACK_MARKER = "UNPACK:"; 30 | 31 | private static final int BUFFER_SIZE = 32 * 1024; 32 | 33 | private final JarFile jarFile; 34 | 35 | private URL url; 36 | 37 | private File tempUnpackFolder; 38 | 39 | public JarFileArchive(File file) throws IOException { 40 | this(file, null); 41 | } 42 | 43 | public JarFileArchive(File file, URL url) throws IOException { 44 | this(new JarFile(file)); 45 | this.url = url; 46 | } 47 | 48 | public JarFileArchive(JarFile jarFile) { 49 | this.jarFile = jarFile; 50 | } 51 | 52 | @Override 53 | public URL getUrl() throws MalformedURLException { 54 | if (this.url != null) { 55 | return this.url; 56 | } 57 | return this.jarFile.getUrl(); 58 | } 59 | 60 | @Override 61 | public Manifest getManifest() throws IOException { 62 | return this.jarFile.getManifest(); 63 | } 64 | 65 | @Override 66 | public List getNestedArchives(EntryFilter filter) throws IOException { 67 | List nestedArchives = new ArrayList(); 68 | for (Entry entry : this) { 69 | if (filter.matches(entry)) { 70 | nestedArchives.add(getNestedArchive(entry)); 71 | } 72 | } 73 | return Collections.unmodifiableList(nestedArchives); 74 | } 75 | 76 | @Override 77 | public Iterator iterator() { 78 | return new EntryIterator(this.jarFile.entries()); 79 | } 80 | 81 | protected Archive getNestedArchive(Entry entry) throws IOException { 82 | JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); 83 | if (jarEntry.getComment().startsWith(UNPACK_MARKER)) { 84 | return getUnpackedNestedArchive(jarEntry); 85 | } 86 | try { 87 | JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); 88 | return new JarFileArchive(jarFile); 89 | } 90 | catch (Exception ex) { 91 | throw new IllegalStateException( 92 | "Failed to get nested archive for entry " + entry.getName(), ex); 93 | } 94 | } 95 | 96 | private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { 97 | String name = jarEntry.getName(); 98 | if (name.lastIndexOf("/") != -1) { 99 | name = name.substring(name.lastIndexOf("/") + 1); 100 | } 101 | File file = new File(getTempUnpackFolder(), name); 102 | if (!file.exists() || file.length() != jarEntry.getSize()) { 103 | unpack(jarEntry, file); 104 | } 105 | return new JarFileArchive(file, file.toURI().toURL()); 106 | } 107 | 108 | private File getTempUnpackFolder() { 109 | if (this.tempUnpackFolder == null) { 110 | File tempFolder = new File(System.getProperty("java.io.tmpdir")); 111 | this.tempUnpackFolder = createUnpackFolder(tempFolder); 112 | } 113 | return this.tempUnpackFolder; 114 | } 115 | 116 | private File createUnpackFolder(File parent) { 117 | int attempts = 0; 118 | while (attempts++ < 1000) { 119 | String fileName = new File(this.jarFile.getName()).getName(); 120 | File unpackFolder = new File(parent, 121 | fileName + "-spring-boot-libs-" + UUID.randomUUID()); 122 | if (unpackFolder.mkdirs()) { 123 | return unpackFolder; 124 | } 125 | } 126 | throw new IllegalStateException( 127 | "Failed to create unpack folder in directory '" + parent + "'"); 128 | } 129 | 130 | private void unpack(JarEntry entry, File file) throws IOException { 131 | InputStream inputStream = this.jarFile.getInputStream(entry, ResourceAccess.ONCE); 132 | try { 133 | OutputStream outputStream = new FileOutputStream(file); 134 | try { 135 | byte[] buffer = new byte[BUFFER_SIZE]; 136 | int bytesRead; 137 | while ((bytesRead = inputStream.read(buffer)) != -1) { 138 | outputStream.write(buffer, 0, bytesRead); 139 | } 140 | outputStream.flush(); 141 | } 142 | finally { 143 | outputStream.close(); 144 | } 145 | } 146 | finally { 147 | inputStream.close(); 148 | } 149 | } 150 | 151 | @Override 152 | public String toString() { 153 | try { 154 | return getUrl().toString(); 155 | } 156 | catch (Exception ex) { 157 | return "jar archive"; 158 | } 159 | } 160 | 161 | /** 162 | * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. 163 | */ 164 | private static class EntryIterator implements Iterator { 165 | 166 | private final Enumeration enumeration; 167 | 168 | EntryIterator(Enumeration enumeration) { 169 | this.enumeration = enumeration; 170 | } 171 | 172 | @Override 173 | public boolean hasNext() { 174 | return this.enumeration.hasMoreElements(); 175 | } 176 | 177 | @Override 178 | public Entry next() { 179 | return new JarFileEntry(this.enumeration.nextElement()); 180 | } 181 | 182 | @Override 183 | public void remove() { 184 | throw new UnsupportedOperationException("remove"); 185 | } 186 | 187 | } 188 | 189 | /** 190 | * {@link Archive.Entry} implementation backed by a {@link JarEntry}. 191 | */ 192 | private static class JarFileEntry implements Entry { 193 | 194 | private final JarEntry jarEntry; 195 | 196 | JarFileEntry(JarEntry jarEntry) { 197 | this.jarEntry = jarEntry; 198 | } 199 | 200 | public JarEntry getJarEntry() { 201 | return this.jarEntry; 202 | } 203 | 204 | @Override 205 | public boolean isDirectory() { 206 | return this.jarEntry.isDirectory(); 207 | } 208 | 209 | @Override 210 | public String getName() { 211 | return this.jarEntry.getName(); 212 | } 213 | 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /fat-jar-classloader/src/main/java/com/laomei/fatjar/classloader/FatJarClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.classloader; 2 | 3 | import com.laomei.fatjar.common.boot.jar.Handler; 4 | 5 | import java.io.IOException; 6 | import java.net.JarURLConnection; 7 | import java.net.URL; 8 | import java.net.URLClassLoader; 9 | import java.net.URLConnection; 10 | import java.security.AccessController; 11 | import java.security.PrivilegedExceptionAction; 12 | import java.util.Enumeration; 13 | import java.util.jar.JarFile; 14 | 15 | /** 16 | * 拿 Spring Boot ClassLoader 抄的。一个中间件模块对应一个 Fat Jar ClassLoader。 17 | * Spring Boot ClassLoader 会解析 fat jar 路径。如果是 fat jar,我们必须定义 package,保证能够找到对应的类。 18 | * @see #definePackageIfNecessary(String) 19 | * 20 | * @author Phillip Webb 21 | * @author Dave Syer 22 | * @author Andy Wilkinson 23 | * @author laomei on 2019/1/9 17:32 24 | */ 25 | public class FatJarClassLoader extends URLClassLoader { 26 | 27 | public FatJarClassLoader(final URL[] urls, ClassLoader parent) { 28 | super(urls, parent); 29 | } 30 | 31 | @Override 32 | public URL findResource(String name) { 33 | Handler.setUseFastConnectionExceptions(true); 34 | try { 35 | return super.findResource(name); 36 | } 37 | finally { 38 | Handler.setUseFastConnectionExceptions(false); 39 | } 40 | } 41 | 42 | @Override 43 | public Enumeration findResources(String name) throws IOException { 44 | Handler.setUseFastConnectionExceptions(true); 45 | try { 46 | return super.findResources(name); 47 | } 48 | finally { 49 | Handler.setUseFastConnectionExceptions(false); 50 | } 51 | } 52 | 53 | @Override 54 | protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { 55 | Handler.setUseFastConnectionExceptions(true); 56 | try { 57 | try { 58 | definePackageIfNecessary(name); 59 | } 60 | catch (IllegalArgumentException ex) { 61 | // Tolerate race condition due to being parallel capable 62 | if (getPackage(name) == null) { 63 | // This should never happen as the IllegalArgumentException indicates 64 | // that the package has already been defined and, therefore, 65 | // getPackage(name) should not return null. 66 | throw new AssertionError("Package " + name + " has already been " 67 | + "defined but it could not be found"); 68 | } 69 | } 70 | return super.loadClass(name, resolve); 71 | } 72 | finally { 73 | Handler.setUseFastConnectionExceptions(false); 74 | } 75 | } 76 | 77 | /** 78 | * Define a package before a {@code findClass} call is made. This is necessary to 79 | * ensure that the appropriate manifest for nested JARs is associated with the 80 | * package. 81 | * @param className the class name being found 82 | */ 83 | private void definePackageIfNecessary(String className) { 84 | int lastDot = className.lastIndexOf('.'); 85 | if (lastDot >= 0) { 86 | String packageName = className.substring(0, lastDot); 87 | if (getPackage(packageName) == null) { 88 | try { 89 | definePackage(className, packageName); 90 | } 91 | catch (IllegalArgumentException ex) { 92 | // Tolerate race condition due to being parallel capable 93 | if (getPackage(packageName) == null) { 94 | // This should never happen as the IllegalArgumentException 95 | // indicates that the package has already been defined and, 96 | // therefore, getPackage(name) should not have returned null. 97 | throw new AssertionError( 98 | "Package " + packageName + " has already been defined " 99 | + "but it could not be found"); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | private void definePackage(final String className, final String packageName) { 107 | try { 108 | AccessController.doPrivileged(new PrivilegedExceptionAction() { 109 | @Override 110 | public Object run() throws ClassNotFoundException { 111 | String packageEntryName = packageName.replace('.', '/') + "/"; 112 | String classEntryName = className.replace('.', '/') + ".class"; 113 | for (URL url : getURLs()) { 114 | try { 115 | URLConnection connection = url.openConnection(); 116 | if (connection instanceof JarURLConnection) { 117 | JarFile jarFile = ((JarURLConnection) connection) 118 | .getJarFile(); 119 | if (jarFile.getEntry(classEntryName) != null 120 | && jarFile.getEntry(packageEntryName) != null 121 | && jarFile.getManifest() != null) { 122 | definePackage(packageName, jarFile.getManifest(), 123 | url); 124 | return null; 125 | } 126 | } 127 | } 128 | catch (IOException ex) { 129 | // Ignore 130 | } 131 | } 132 | return null; 133 | } 134 | }, AccessController.getContext()); 135 | } 136 | catch (java.security.PrivilegedActionException ex) { 137 | // Ignore 138 | } 139 | } 140 | 141 | /** 142 | * Clear URL caches. 143 | */ 144 | public void clearCache() { 145 | for (URL url : getURLs()) { 146 | try { 147 | URLConnection connection = url.openConnection(); 148 | if (connection instanceof JarURLConnection) { 149 | clearCache(connection); 150 | } 151 | } 152 | catch (IOException ex) { 153 | // Ignore 154 | } 155 | } 156 | 157 | } 158 | 159 | private void clearCache(URLConnection connection) throws IOException { 160 | Object jarFile = ((JarURLConnection) connection).getJarFile(); 161 | if (jarFile instanceof com.laomei.fatjar.common.boot.jar.JarFile) { 162 | ((com.laomei.fatjar.common.boot.jar.JarFile) jarFile).clearCache(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/data/RandomAccessDataFile.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.data; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.RandomAccessFile; 7 | import java.util.Queue; 8 | import java.util.concurrent.ConcurrentLinkedQueue; 9 | import java.util.concurrent.Semaphore; 10 | 11 | /** 12 | * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. 13 | * 14 | * @author Phillip Webb 15 | */ 16 | public class RandomAccessDataFile implements RandomAccessData { 17 | 18 | private static final int DEFAULT_CONCURRENT_READS = 4; 19 | 20 | private final File file; 21 | 22 | private final FilePool filePool; 23 | 24 | private final long offset; 25 | 26 | private final long length; 27 | 28 | /** 29 | * Create a new {@link RandomAccessDataFile} backed by the specified file. 30 | * @param file the underlying file 31 | * @throws IllegalArgumentException if the file is null or does not exist 32 | * @see #RandomAccessDataFile(File, int) 33 | */ 34 | public RandomAccessDataFile(File file) { 35 | this(file, DEFAULT_CONCURRENT_READS); 36 | } 37 | 38 | /** 39 | * Create a new {@link RandomAccessDataFile} backed by the specified file. 40 | * @param file the underlying file 41 | * @param concurrentReads the maximum number of concurrent reads allowed on the 42 | * underlying file before blocking 43 | * @throws IllegalArgumentException if the file is null or does not exist 44 | * @see #RandomAccessDataFile(File) 45 | */ 46 | public RandomAccessDataFile(File file, int concurrentReads) { 47 | if (file == null) { 48 | throw new IllegalArgumentException("File must not be null"); 49 | } 50 | if (!file.exists()) { 51 | throw new IllegalArgumentException( 52 | String.format("File %s must exist", file.getAbsolutePath())); 53 | } 54 | this.file = file; 55 | this.filePool = new FilePool(file, concurrentReads); 56 | this.offset = 0L; 57 | this.length = file.length(); 58 | } 59 | 60 | /** 61 | * Private constructor used to create a {@link #getSubsection(long, long) subsection}. 62 | * @param file the underlying file 63 | * @param pool the underlying pool 64 | * @param offset the offset of the section 65 | * @param length the length of the section 66 | */ 67 | private RandomAccessDataFile(File file, FilePool pool, long offset, long length) { 68 | this.file = file; 69 | this.filePool = pool; 70 | this.offset = offset; 71 | this.length = length; 72 | } 73 | 74 | /** 75 | * Returns the underlying File. 76 | * @return the underlying file 77 | */ 78 | public File getFile() { 79 | return this.file; 80 | } 81 | 82 | @Override 83 | public InputStream getInputStream(ResourceAccess access) throws IOException { 84 | return new DataInputStream(access); 85 | } 86 | 87 | @Override 88 | public RandomAccessData getSubsection(long offset, long length) { 89 | if (offset < 0 || length < 0 || offset + length > this.length) { 90 | throw new IndexOutOfBoundsException(); 91 | } 92 | return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset, 93 | length); 94 | } 95 | 96 | @Override 97 | public long getSize() { 98 | return this.length; 99 | } 100 | 101 | public void close() throws IOException { 102 | this.filePool.close(); 103 | } 104 | 105 | /** 106 | * {@link RandomAccessDataInputStream} implementation for the 107 | * {@link RandomAccessDataFile}. 108 | */ 109 | private class DataInputStream extends InputStream { 110 | 111 | private RandomAccessFile file; 112 | 113 | private int position; 114 | 115 | DataInputStream(ResourceAccess access) throws IOException { 116 | if (access == ResourceAccess.ONCE) { 117 | this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r"); 118 | this.file.seek(RandomAccessDataFile.this.offset); 119 | } 120 | } 121 | 122 | @Override 123 | public int read() throws IOException { 124 | return doRead(null, 0, 1); 125 | } 126 | 127 | @Override 128 | public int read(byte[] b) throws IOException { 129 | return read(b, 0, b != null ? b.length : 0); 130 | } 131 | 132 | @Override 133 | public int read(byte[] b, int off, int len) throws IOException { 134 | if (b == null) { 135 | throw new NullPointerException("Bytes must not be null"); 136 | } 137 | return doRead(b, off, len); 138 | } 139 | 140 | /** 141 | * Perform the actual read. 142 | * @param b the bytes to read or {@code null} when reading a single byte 143 | * @param off the offset of the byte array 144 | * @param len the length of data to read 145 | * @return the number of bytes read into {@code b} or the actual read byte if 146 | * {@code b} is {@code null}. Returns -1 when the end of the stream is reached 147 | * @throws IOException in case of I/O errors 148 | */ 149 | public int doRead(byte[] b, int off, int len) throws IOException { 150 | if (len == 0) { 151 | return 0; 152 | } 153 | int cappedLen = cap(len); 154 | if (cappedLen <= 0) { 155 | return -1; 156 | } 157 | RandomAccessFile file = this.file; 158 | try { 159 | if (file == null) { 160 | file = RandomAccessDataFile.this.filePool.acquire(); 161 | file.seek(RandomAccessDataFile.this.offset + this.position); 162 | } 163 | if (b == null) { 164 | int rtn = file.read(); 165 | moveOn(rtn != -1 ? 1 : 0); 166 | return rtn; 167 | } 168 | else { 169 | return (int) moveOn(file.read(b, off, cappedLen)); 170 | } 171 | } 172 | finally { 173 | if (this.file == null && file != null) { 174 | RandomAccessDataFile.this.filePool.release(file); 175 | } 176 | } 177 | } 178 | 179 | @Override 180 | public long skip(long n) throws IOException { 181 | return (n <= 0 ? 0 : moveOn(cap(n))); 182 | } 183 | 184 | @Override 185 | public void close() throws IOException { 186 | if (this.file != null) { 187 | this.file.close(); 188 | } 189 | } 190 | 191 | /** 192 | * Cap the specified value such that it cannot exceed the number of bytes 193 | * remaining. 194 | * @param n the value to cap 195 | * @return the capped value 196 | */ 197 | private int cap(long n) { 198 | return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); 199 | } 200 | 201 | /** 202 | * Move the stream position forwards the specified amount. 203 | * @param amount the amount to move 204 | * @return the amount moved 205 | */ 206 | private long moveOn(int amount) { 207 | this.position += amount; 208 | return amount; 209 | } 210 | 211 | } 212 | 213 | /** 214 | * Manage a pool that can be used to perform concurrent reads on the underlying 215 | * {@link RandomAccessFile}. 216 | */ 217 | static class FilePool { 218 | 219 | private final File file; 220 | 221 | private final int size; 222 | 223 | private final Semaphore available; 224 | 225 | private final Queue files; 226 | 227 | FilePool(File file, int size) { 228 | this.file = file; 229 | this.size = size; 230 | this.available = new Semaphore(size); 231 | this.files = new ConcurrentLinkedQueue(); 232 | } 233 | 234 | public RandomAccessFile acquire() throws IOException { 235 | this.available.acquireUninterruptibly(); 236 | RandomAccessFile file = this.files.poll(); 237 | if (file != null) { 238 | return file; 239 | } 240 | return new RandomAccessFile(this.file, "r"); 241 | } 242 | 243 | public void release(RandomAccessFile file) { 244 | this.files.add(file); 245 | this.available.release(); 246 | } 247 | 248 | public void close() throws IOException { 249 | this.available.acquireUninterruptibly(this.size); 250 | try { 251 | RandomAccessFile pooledFile = this.files.poll(); 252 | while (pooledFile != null) { 253 | pooledFile.close(); 254 | pooledFile = this.files.poll(); 255 | } 256 | } 257 | finally { 258 | this.available.release(this.size); 259 | } 260 | } 261 | 262 | } 263 | 264 | } 265 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarFileEntries.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.Iterator; 10 | import java.util.LinkedHashMap; 11 | import java.util.Map; 12 | import java.util.NoSuchElementException; 13 | import java.util.zip.ZipEntry; 14 | 15 | /** 16 | * Provides access to entries from a {@link JarFile}. In order to reduce memory 17 | * consumption entry details are stored using int arrays. The {@code hashCodes} array 18 | * stores the hash code of the entry name, the {@code centralDirectoryOffsets} provides 19 | * the offset to the central directory record and {@code positions} provides the original 20 | * order position of the entry. The arrays are stored in hashCode order so that a binary 21 | * search can be used to find a name. 22 | *

23 | * A typical Spring Boot application will have somewhere in the region of 10,500 entries 24 | * which should consume about 122K. 25 | * 26 | * @author Phillip Webb 27 | */ 28 | class JarFileEntries implements CentralDirectoryVisitor, Iterable { 29 | 30 | private static final long LOCAL_FILE_HEADER_SIZE = 30; 31 | 32 | private static final String SLASH = "/"; 33 | 34 | private static final String NO_SUFFIX = ""; 35 | 36 | protected static final int ENTRY_CACHE_SIZE = 25; 37 | 38 | private final JarFile jarFile; 39 | 40 | private final JarEntryFilter filter; 41 | 42 | private RandomAccessData centralDirectoryData; 43 | 44 | private int size; 45 | 46 | private int[] hashCodes; 47 | 48 | private int[] centralDirectoryOffsets; 49 | 50 | private int[] positions; 51 | 52 | private final Map entriesCache = Collections 53 | .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { 54 | 55 | @Override 56 | protected boolean removeEldestEntry( 57 | Map.Entry eldest) { 58 | if (JarFileEntries.this.jarFile.isSigned()) { 59 | return false; 60 | } 61 | return size() >= ENTRY_CACHE_SIZE; 62 | } 63 | 64 | }); 65 | 66 | JarFileEntries(JarFile jarFile, JarEntryFilter filter) { 67 | this.jarFile = jarFile; 68 | this.filter = filter; 69 | } 70 | 71 | @Override 72 | public void visitStart(CentralDirectoryEndRecord endRecord, 73 | RandomAccessData centralDirectoryData) { 74 | int maxSize = endRecord.getNumberOfRecords(); 75 | this.centralDirectoryData = centralDirectoryData; 76 | this.hashCodes = new int[maxSize]; 77 | this.centralDirectoryOffsets = new int[maxSize]; 78 | this.positions = new int[maxSize]; 79 | } 80 | 81 | @Override 82 | public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) { 83 | AsciiBytes name = applyFilter(fileHeader.getName()); 84 | if (name != null) { 85 | add(name, fileHeader, dataOffset); 86 | } 87 | } 88 | 89 | private void add(AsciiBytes name, CentralDirectoryFileHeader fileHeader, 90 | int dataOffset) { 91 | this.hashCodes[this.size] = name.hashCode(); 92 | this.centralDirectoryOffsets[this.size] = dataOffset; 93 | this.positions[this.size] = this.size; 94 | this.size++; 95 | } 96 | 97 | @Override 98 | public void visitEnd() { 99 | sort(0, this.size - 1); 100 | int[] positions = this.positions; 101 | this.positions = new int[positions.length]; 102 | for (int i = 0; i < this.size; i++) { 103 | this.positions[positions[i]] = i; 104 | } 105 | } 106 | 107 | int getSize() { 108 | return this.size; 109 | } 110 | 111 | private void sort(int left, int right) { 112 | // Quick sort algorithm, uses hashCodes as the source but sorts all arrays 113 | if (left < right) { 114 | int pivot = this.hashCodes[left + (right - left) / 2]; 115 | int i = left; 116 | int j = right; 117 | while (i <= j) { 118 | while (this.hashCodes[i] < pivot) { 119 | i++; 120 | } 121 | while (this.hashCodes[j] > pivot) { 122 | j--; 123 | } 124 | if (i <= j) { 125 | swap(i, j); 126 | i++; 127 | j--; 128 | } 129 | } 130 | if (left < j) { 131 | sort(left, j); 132 | } 133 | if (right > i) { 134 | sort(i, right); 135 | } 136 | } 137 | } 138 | 139 | private void swap(int i, int j) { 140 | swap(this.hashCodes, i, j); 141 | swap(this.centralDirectoryOffsets, i, j); 142 | swap(this.positions, i, j); 143 | } 144 | 145 | private void swap(int[] array, int i, int j) { 146 | int temp = array[i]; 147 | array[i] = array[j]; 148 | array[j] = temp; 149 | } 150 | 151 | @Override 152 | public Iterator iterator() { 153 | return new EntryIterator(); 154 | } 155 | 156 | public boolean containsEntry(String name) { 157 | return getEntry(name, FileHeader.class, true) != null; 158 | } 159 | 160 | public JarEntry getEntry(String name) { 161 | return getEntry(name, JarEntry.class, true); 162 | } 163 | 164 | public InputStream getInputStream(String name, RandomAccessData.ResourceAccess access) 165 | throws IOException { 166 | FileHeader entry = getEntry(name, FileHeader.class, false); 167 | return getInputStream(entry, access); 168 | } 169 | 170 | public InputStream getInputStream(FileHeader entry, RandomAccessData.ResourceAccess access) 171 | throws IOException { 172 | if (entry == null) { 173 | return null; 174 | } 175 | InputStream inputStream = getEntryData(entry).getInputStream(access); 176 | if (entry.getMethod() == ZipEntry.DEFLATED) { 177 | inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize()); 178 | } 179 | return inputStream; 180 | } 181 | 182 | public RandomAccessData getEntryData(String name) throws IOException { 183 | FileHeader entry = getEntry(name, FileHeader.class, false); 184 | if (entry == null) { 185 | return null; 186 | } 187 | return getEntryData(entry); 188 | } 189 | 190 | private RandomAccessData getEntryData(FileHeader entry) throws IOException { 191 | // aspectjrt-1.7.4.jar has a different ext bytes length in the 192 | // local directory to the central directory. We need to re-read 193 | // here to skip them 194 | RandomAccessData data = this.jarFile.getData(); 195 | byte[] localHeader = Bytes.get( 196 | data.getSubsection(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE)); 197 | long nameLength = Bytes.littleEndianValue(localHeader, 26, 2); 198 | long extraLength = Bytes.littleEndianValue(localHeader, 28, 2); 199 | return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE 200 | + nameLength + extraLength, entry.getCompressedSize()); 201 | } 202 | 203 | private T getEntry(String name, Class type, 204 | boolean cacheEntry) { 205 | int hashCode = AsciiBytes.hashCode(name); 206 | T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry); 207 | if (entry == null) { 208 | hashCode = AsciiBytes.hashCode(hashCode, SLASH); 209 | entry = getEntry(hashCode, name, SLASH, type, cacheEntry); 210 | } 211 | return entry; 212 | } 213 | 214 | private T getEntry(int hashCode, String name, String suffix, 215 | Class type, boolean cacheEntry) { 216 | int index = getFirstIndex(hashCode); 217 | while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { 218 | T entry = getEntry(index, type, cacheEntry); 219 | if (entry.hasName(name, suffix)) { 220 | return entry; 221 | } 222 | index++; 223 | } 224 | return null; 225 | } 226 | 227 | @SuppressWarnings("unchecked") 228 | private T getEntry(int index, Class type, 229 | boolean cacheEntry) { 230 | try { 231 | FileHeader cached = this.entriesCache.get(index); 232 | FileHeader entry = (cached != null ? cached 233 | : CentralDirectoryFileHeader.fromRandomAccessData( 234 | this.centralDirectoryData, 235 | this.centralDirectoryOffsets[index], this.filter)); 236 | if (CentralDirectoryFileHeader.class.equals(entry.getClass()) 237 | && type.equals(JarEntry.class)) { 238 | entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry); 239 | } 240 | if (cacheEntry && cached != entry) { 241 | this.entriesCache.put(index, entry); 242 | } 243 | return (T) entry; 244 | } 245 | catch (IOException ex) { 246 | throw new IllegalStateException(ex); 247 | } 248 | } 249 | 250 | private int getFirstIndex(int hashCode) { 251 | int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode); 252 | if (index < 0) { 253 | return -1; 254 | } 255 | while (index > 0 && this.hashCodes[index - 1] == hashCode) { 256 | index--; 257 | } 258 | return index; 259 | } 260 | 261 | public void clearCache() { 262 | this.entriesCache.clear(); 263 | } 264 | 265 | private AsciiBytes applyFilter(AsciiBytes name) { 266 | return (this.filter != null ? this.filter.apply(name) : name); 267 | } 268 | 269 | /** 270 | * Iterator for contained entries. 271 | */ 272 | private class EntryIterator implements Iterator { 273 | 274 | private int index = 0; 275 | 276 | @Override 277 | public boolean hasNext() { 278 | return this.index < JarFileEntries.this.size; 279 | } 280 | 281 | @Override 282 | public JarEntry next() { 283 | if (!hasNext()) { 284 | throw new NoSuchElementException(); 285 | } 286 | int entryIndex = JarFileEntries.this.positions[this.index]; 287 | this.index++; 288 | return getEntry(entryIndex, JarEntry.class, false); 289 | } 290 | 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /fat-jar-plugin/src/main/java/com/laomei/fatjar/plugin/Repackager.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.plugin; 2 | 3 | import com.laomei.fatjar.common.boot.tool.DefaultLayoutFactory; 4 | import com.laomei.fatjar.common.boot.tool.Layout; 5 | import com.laomei.fatjar.common.boot.tool.LayoutFactory; 6 | import com.laomei.fatjar.common.boot.tool.Libraries; 7 | import com.laomei.fatjar.common.boot.tool.Library; 8 | import com.laomei.fatjar.common.boot.tool.LibraryCallback; 9 | import com.laomei.fatjar.common.boot.tool.RepackagingLayout; 10 | 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.ArrayList; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Set; 19 | import java.util.jar.JarEntry; 20 | import java.util.jar.JarFile; 21 | import java.util.jar.Manifest; 22 | 23 | import static com.laomei.fatjar.common.Constant.FAT_JAR_TOOL; 24 | import static com.laomei.fatjar.common.Constant.FAT_JAR_TOOL_VALUE; 25 | 26 | /** 27 | * Utility class that can be used to repackage an archive so that it can be executed using 28 | * '{@literal java -jar}'. 29 | * 30 | * @author Phillip Webb 31 | * @author Andy Wilkinson 32 | * @author Stephane Nicoll 33 | * @author laomei 34 | */ 35 | public class Repackager { 36 | 37 | private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; 38 | 39 | private final File source; 40 | 41 | private Layout layout; 42 | 43 | public Repackager(File source) { 44 | if (source == null) { 45 | throw new IllegalArgumentException("Source file must be provided"); 46 | } 47 | if (!source.exists() || !source.isFile()) { 48 | throw new IllegalArgumentException("Source must refer to an existing file, " 49 | + "got " + source.getAbsolutePath()); 50 | } 51 | this.source = source.getAbsoluteFile(); 52 | } 53 | 54 | /** 55 | * Repackage to the given destination so that it can be launched using ' 56 | * {@literal java -jar}'. 57 | * @param destination the destination file (may be the same as the source) 58 | * @param libraries the libraries required to run the archive 59 | * @throws IOException if the file cannot be repackaged 60 | */ 61 | public void repackage(File destination, Libraries libraries) throws IOException { 62 | if (destination == null || destination.isDirectory()) { 63 | throw new IllegalArgumentException("Invalid destination"); 64 | } 65 | if (libraries == null) { 66 | throw new IllegalArgumentException("Libraries must not be null"); 67 | } 68 | if (this.layout == null) { 69 | this.layout = getLayoutFactory().getLayout(this.source); 70 | } 71 | if (alreadyRepackaged()) { 72 | return; 73 | } 74 | destination = destination.getAbsoluteFile(); 75 | File workingSource = this.source; 76 | if (this.source.equals(destination)) { 77 | workingSource = getBackupFile(); 78 | workingSource.delete(); 79 | renameFile(this.source, workingSource); 80 | } 81 | destination.delete(); 82 | JarFile jarFileSource = new JarFile(workingSource); 83 | try { 84 | repackage(jarFileSource, destination, libraries); 85 | } finally { 86 | jarFileSource.close(); 87 | } 88 | } 89 | 90 | private LayoutFactory getLayoutFactory() { 91 | return new DefaultLayoutFactory(); 92 | } 93 | 94 | /** 95 | * Return the {@link File} to use to backup the original source. 96 | * @return the file to use to backup the original source 97 | */ 98 | public final File getBackupFile() { 99 | return new File(this.source.getParentFile(), this.source.getName() + ".original"); 100 | } 101 | 102 | private boolean alreadyRepackaged() throws IOException { 103 | JarFile jarFile = new JarFile(this.source); 104 | try { 105 | Manifest manifest = jarFile.getManifest(); 106 | return (manifest != null && manifest.getMainAttributes() 107 | .getValue(FAT_JAR_TOOL_VALUE) != null); 108 | } 109 | finally { 110 | jarFile.close(); 111 | } 112 | } 113 | 114 | private void repackage(JarFile sourceJar, File destination, Libraries libraries) throws IOException { 115 | JarWriter writer = new JarWriter(destination); 116 | try { 117 | final List unpackLibraries = new ArrayList(); 118 | final List standardLibraries = new ArrayList(); 119 | libraries.doWithLibraries(new LibraryCallback() { 120 | 121 | @Override 122 | public void library(Library library) throws IOException { 123 | File file = library.getFile(); 124 | if (isZip(file)) { 125 | if (library.isUnpackRequired()) { 126 | unpackLibraries.add(library); 127 | } 128 | else { 129 | standardLibraries.add(library); 130 | } 131 | } 132 | } 133 | 134 | }); 135 | repackage(sourceJar, writer, unpackLibraries, standardLibraries); 136 | } 137 | finally { 138 | try { 139 | writer.close(); 140 | } 141 | catch (Exception ex) { 142 | // Ignore 143 | } 144 | } 145 | } 146 | 147 | private void repackage(JarFile sourceJar, JarWriter writer, 148 | final List unpackLibraries, final List standardLibraries) 149 | throws IOException { 150 | writer.writeManifest(buildManifest(sourceJar)); 151 | Set seen = new HashSet(); 152 | // 对于我们的场景,下面的这个方法其实好像没什么用。 153 | writeNestedLibraries(unpackLibraries, seen, writer); 154 | if (this.layout instanceof RepackagingLayout) { 155 | writer.writeEntries(sourceJar, new RenamingEntryTransformer( 156 | ((RepackagingLayout) this.layout).getRepackagedClassesLocation())); 157 | } 158 | else { 159 | writer.writeEntries(sourceJar); 160 | } 161 | // 这个就是写如 jar 包了,修改一下 jar 目录即可 162 | writeNestedLibraries(standardLibraries, seen, writer); 163 | } 164 | 165 | private void writeNestedLibraries(List libraries, Set alreadySeen, 166 | JarWriter writer) throws IOException { 167 | for (Library library : libraries) { 168 | String destination = Repackager.this.layout 169 | .getLibraryDestination(library.getName(), library.getScope()); 170 | if (destination != null) { 171 | if (!alreadySeen.add(destination + library.getName())) { 172 | throw new IllegalStateException( 173 | "Duplicate library " + library.getName()); 174 | } 175 | writer.writeNestedLibrary(destination, library); 176 | } 177 | } 178 | } 179 | 180 | private boolean isZip(File file) { 181 | try { 182 | FileInputStream fileInputStream = new FileInputStream(file); 183 | try { 184 | return isZip(fileInputStream); 185 | } 186 | finally { 187 | fileInputStream.close(); 188 | } 189 | } 190 | catch (IOException ex) { 191 | return false; 192 | } 193 | } 194 | 195 | private boolean isZip(InputStream inputStream) throws IOException { 196 | for (int i = 0; i < ZIP_FILE_HEADER.length; i++) { 197 | if (inputStream.read() != ZIP_FILE_HEADER[i]) { 198 | return false; 199 | } 200 | } 201 | return true; 202 | } 203 | 204 | private Manifest buildManifest(JarFile source) throws IOException { 205 | Manifest manifest = source.getManifest(); 206 | if (manifest == null) { 207 | manifest = new Manifest(); 208 | } 209 | manifest = new Manifest(manifest); 210 | manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); 211 | manifest.getMainAttributes().putValue(FAT_JAR_TOOL, FAT_JAR_TOOL_VALUE); 212 | return manifest; 213 | } 214 | 215 | private void renameFile(File file, File dest) { 216 | if (!file.renameTo(dest)) { 217 | throw new IllegalStateException( 218 | "Unable to rename '" + file + "' to '" + dest + "'"); 219 | } 220 | } 221 | 222 | private void deleteFile(File file) { 223 | if (!file.delete()) { 224 | throw new IllegalStateException("Unable to delete '" + file + "'"); 225 | } 226 | } 227 | 228 | /** 229 | * An {@code EntryTransformer} that renames entries by applying a prefix. 230 | */ 231 | private static final class RenamingEntryTransformer implements JarWriter.EntryTransformer { 232 | 233 | private final String namePrefix; 234 | 235 | private RenamingEntryTransformer(String namePrefix) { 236 | this.namePrefix = namePrefix; 237 | } 238 | 239 | @Override 240 | public JarEntry transform(JarEntry entry) { 241 | if (entry.getName().equals("META-INF/INDEX.LIST")) { 242 | return null; 243 | } 244 | if ((entry.getName().startsWith("META-INF/") 245 | && !entry.getName().equals("META-INF/aop.xml")) 246 | || entry.getName().startsWith("BOOT-INF/")) { 247 | return entry; 248 | } 249 | JarEntry renamedEntry = new JarEntry(this.namePrefix + entry.getName()); 250 | renamedEntry.setTime(entry.getTime()); 251 | renamedEntry.setSize(entry.getSize()); 252 | renamedEntry.setMethod(entry.getMethod()); 253 | if (entry.getComment() != null) { 254 | renamedEntry.setComment(entry.getComment()); 255 | } 256 | renamedEntry.setCompressedSize(entry.getCompressedSize()); 257 | renamedEntry.setCrc(entry.getCrc()); 258 | setCreationTimeIfPossible(entry, renamedEntry); 259 | if (entry.getExtra() != null) { 260 | renamedEntry.setExtra(entry.getExtra()); 261 | } 262 | setLastAccessTimeIfPossible(entry, renamedEntry); 263 | setLastModifiedTimeIfPossible(entry, renamedEntry); 264 | return renamedEntry; 265 | } 266 | 267 | private void setCreationTimeIfPossible(JarEntry source, JarEntry target) { 268 | try { 269 | if (source.getCreationTime() != null) { 270 | target.setCreationTime(source.getCreationTime()); 271 | } 272 | } 273 | catch (NoSuchMethodError ex) { 274 | // Not running on Java 8. Continue. 275 | } 276 | } 277 | 278 | private void setLastAccessTimeIfPossible(JarEntry source, JarEntry target) { 279 | try { 280 | if (source.getLastAccessTime() != null) { 281 | target.setLastAccessTime(source.getLastAccessTime()); 282 | } 283 | } 284 | catch (NoSuchMethodError ex) { 285 | // Not running on Java 8. Continue. 286 | } 287 | } 288 | 289 | private void setLastModifiedTimeIfPossible(JarEntry source, JarEntry target) { 290 | try { 291 | if (source.getLastModifiedTime() != null) { 292 | target.setLastModifiedTime(source.getLastModifiedTime()); 293 | } 294 | } 295 | catch (NoSuchMethodError ex) { 296 | // Not running on Java 8. Continue. 297 | } 298 | } 299 | 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /fat-jar-plugin/src/main/java/com/laomei/fatjar/plugin/JarWriter.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.plugin; 2 | 3 | import com.laomei.fatjar.common.boot.tool.FileUtils; 4 | import com.laomei.fatjar.common.boot.tool.Library; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.FileNotFoundException; 10 | import java.io.FileOutputStream; 11 | import java.io.FilterInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | import java.util.Arrays; 16 | import java.util.Enumeration; 17 | import java.util.HashSet; 18 | import java.util.Set; 19 | import java.util.jar.JarEntry; 20 | import java.util.jar.JarFile; 21 | import java.util.jar.JarOutputStream; 22 | import java.util.jar.Manifest; 23 | import java.util.zip.CRC32; 24 | import java.util.zip.ZipEntry; 25 | 26 | /** 27 | * Writes JAR content, ensuring valid directory entries are always create and duplicate 28 | * items are ignored. 29 | * 30 | * @author Phillip Webb 31 | * @author Andy Wilkinson 32 | */ 33 | public class JarWriter { 34 | 35 | private static final int BUFFER_SIZE = 32 * 1024; 36 | 37 | private final JarOutputStream jarOutput; 38 | 39 | private final Set writtenEntries = new HashSet(); 40 | 41 | /** 42 | * Create a new {@link JarWriter} instance. 43 | * @param file the file to write 44 | * @throws IOException if the file cannot be opened 45 | * @throws FileNotFoundException if the file cannot be found 46 | */ 47 | public JarWriter(File file) throws FileNotFoundException, IOException { 48 | FileOutputStream fileOutputStream = new FileOutputStream(file); 49 | this.jarOutput = new JarOutputStream(fileOutputStream); 50 | } 51 | 52 | /** 53 | * Write the specified manifest. 54 | * @param manifest the manifest to write 55 | * @throws IOException of the manifest cannot be written 56 | */ 57 | public void writeManifest(final Manifest manifest) throws IOException { 58 | JarEntry entry = new JarEntry("META-INF/MANIFEST.MF"); 59 | writeEntry(entry, new EntryWriter() { 60 | @Override 61 | public void write(OutputStream outputStream) throws IOException { 62 | manifest.write(outputStream); 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * Write all entries from the specified jar file. 69 | * @param jarFile the source jar file 70 | * @throws IOException if the entries cannot be written 71 | */ 72 | public void writeEntries(JarFile jarFile) throws IOException { 73 | this.writeEntries(jarFile, new IdentityEntryTransformer()); 74 | } 75 | 76 | void writeEntries(JarFile jarFile, EntryTransformer entryTransformer) 77 | throws IOException { 78 | Enumeration entries = jarFile.entries(); 79 | while (entries.hasMoreElements()) { 80 | JarEntry entry = entries.nextElement(); 81 | ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream( 82 | jarFile.getInputStream(entry)); 83 | try { 84 | if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) { 85 | new CrcAndSize(inputStream).setupStoredEntry(entry); 86 | inputStream.close(); 87 | inputStream = new ZipHeaderPeekInputStream( 88 | jarFile.getInputStream(entry)); 89 | } 90 | EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true); 91 | JarEntry transformedEntry = entryTransformer.transform(entry); 92 | if (transformedEntry != null) { 93 | writeEntry(transformedEntry, entryWriter); 94 | } 95 | } 96 | finally { 97 | inputStream.close(); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Write a nested library. 104 | * @param destination the destination of the library 105 | * @param library the library 106 | * @throws IOException if the write fails 107 | */ 108 | public void writeNestedLibrary(String destination, Library library) 109 | throws IOException { 110 | File file = library.getFile(); 111 | JarEntry entry = new JarEntry(destination + library.getName()); 112 | entry.setTime(getNestedLibraryTime(file)); 113 | if (library.isUnpackRequired()) { 114 | entry.setComment("UNPACK:" + FileUtils.sha1Hash(file)); 115 | } 116 | new CrcAndSize(file).setupStoredEntry(entry); 117 | writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true)); 118 | } 119 | 120 | private long getNestedLibraryTime(File file) { 121 | try { 122 | JarFile jarFile = new JarFile(file); 123 | try { 124 | Enumeration entries = jarFile.entries(); 125 | while (entries.hasMoreElements()) { 126 | JarEntry entry = entries.nextElement(); 127 | if (!entry.isDirectory()) { 128 | return entry.getTime(); 129 | } 130 | } 131 | } 132 | finally { 133 | jarFile.close(); 134 | } 135 | } 136 | catch (Exception ex) { 137 | // Ignore and just use the source file timestamp 138 | } 139 | return file.lastModified(); 140 | } 141 | 142 | /** 143 | * Close the writer. 144 | * @throws IOException if the file cannot be closed 145 | */ 146 | public void close() throws IOException { 147 | this.jarOutput.close(); 148 | } 149 | 150 | /** 151 | * Perform the actual write of a {@link JarEntry}. All other {@code write} method 152 | * delegate to this one. 153 | * @param entry the entry to write 154 | * @param entryWriter the entry writer or {@code null} if there is no content 155 | * @throws IOException in case of I/O errors 156 | */ 157 | private void writeEntry(JarEntry entry, EntryWriter entryWriter) throws IOException { 158 | String parent = entry.getName(); 159 | if (parent.endsWith("/")) { 160 | parent = parent.substring(0, parent.length() - 1); 161 | } 162 | if (parent.lastIndexOf("/") != -1) { 163 | parent = parent.substring(0, parent.lastIndexOf("/") + 1); 164 | if (parent.length() > 0) { 165 | writeEntry(new JarEntry(parent), null); 166 | } 167 | } 168 | 169 | if (this.writtenEntries.add(entry.getName())) { 170 | this.jarOutput.putNextEntry(entry); 171 | if (entryWriter != null) { 172 | entryWriter.write(this.jarOutput); 173 | } 174 | this.jarOutput.closeEntry(); 175 | } 176 | } 177 | 178 | /** 179 | * Interface used to write jar entry date. 180 | */ 181 | private interface EntryWriter { 182 | 183 | /** 184 | * Write entry data to the specified output stream. 185 | * @param outputStream the destination for the data 186 | * @throws IOException in case of I/O errors 187 | */ 188 | void write(OutputStream outputStream) throws IOException; 189 | 190 | } 191 | 192 | /** 193 | * {@link EntryWriter} that writes content from an {@link InputStream}. 194 | */ 195 | private static class InputStreamEntryWriter implements EntryWriter { 196 | 197 | private final InputStream inputStream; 198 | 199 | private final boolean close; 200 | 201 | InputStreamEntryWriter(InputStream inputStream, boolean close) { 202 | this.inputStream = inputStream; 203 | this.close = close; 204 | } 205 | 206 | @Override 207 | public void write(OutputStream outputStream) throws IOException { 208 | byte[] buffer = new byte[BUFFER_SIZE]; 209 | int bytesRead; 210 | while ((bytesRead = this.inputStream.read(buffer)) != -1) { 211 | outputStream.write(buffer, 0, bytesRead); 212 | } 213 | outputStream.flush(); 214 | if (this.close) { 215 | this.inputStream.close(); 216 | } 217 | } 218 | 219 | } 220 | 221 | /** 222 | * {@link InputStream} that can peek ahead at zip header bytes. 223 | */ 224 | private static class ZipHeaderPeekInputStream extends FilterInputStream { 225 | 226 | private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 }; 227 | 228 | private final byte[] header; 229 | 230 | private ByteArrayInputStream headerStream; 231 | 232 | protected ZipHeaderPeekInputStream(InputStream in) throws IOException { 233 | super(in); 234 | this.header = new byte[4]; 235 | int len = in.read(this.header); 236 | this.headerStream = new ByteArrayInputStream(this.header, 0, len); 237 | } 238 | 239 | @Override 240 | public int read() throws IOException { 241 | int read = (this.headerStream != null ? this.headerStream.read() : -1); 242 | if (read != -1) { 243 | this.headerStream = null; 244 | return read; 245 | } 246 | return super.read(); 247 | } 248 | 249 | @Override 250 | public int read(byte[] b) throws IOException { 251 | return read(b, 0, b.length); 252 | } 253 | 254 | @Override 255 | public int read(byte[] b, int off, int len) throws IOException { 256 | int read = (this.headerStream != null ? this.headerStream.read(b, off, len) 257 | : -1); 258 | if (read != -1) { 259 | this.headerStream = null; 260 | return read; 261 | } 262 | return super.read(b, off, len); 263 | } 264 | 265 | public boolean hasZipHeader() { 266 | return Arrays.equals(this.header, ZIP_HEADER); 267 | } 268 | 269 | } 270 | 271 | /** 272 | * Data holder for CRC and Size. 273 | */ 274 | private static class CrcAndSize { 275 | 276 | private final CRC32 crc = new CRC32(); 277 | 278 | private long size; 279 | 280 | CrcAndSize(File file) throws IOException { 281 | FileInputStream inputStream = new FileInputStream(file); 282 | try { 283 | load(inputStream); 284 | } 285 | finally { 286 | inputStream.close(); 287 | } 288 | } 289 | 290 | CrcAndSize(InputStream inputStream) throws IOException { 291 | load(inputStream); 292 | } 293 | 294 | private void load(InputStream inputStream) throws IOException { 295 | byte[] buffer = new byte[BUFFER_SIZE]; 296 | int bytesRead; 297 | while ((bytesRead = inputStream.read(buffer)) != -1) { 298 | this.crc.update(buffer, 0, bytesRead); 299 | this.size += bytesRead; 300 | } 301 | } 302 | 303 | public void setupStoredEntry(JarEntry entry) { 304 | entry.setSize(this.size); 305 | entry.setCompressedSize(this.size); 306 | entry.setCrc(this.crc.getValue()); 307 | entry.setMethod(ZipEntry.STORED); 308 | } 309 | 310 | } 311 | 312 | /** 313 | * An {@code EntryTransformer} enables the transformation of {@link JarEntry jar 314 | * entries} during the writing process. 315 | */ 316 | interface EntryTransformer { 317 | 318 | JarEntry transform(JarEntry jarEntry); 319 | 320 | } 321 | 322 | /** 323 | * An {@code EntryTransformer} that returns the entry unchanged. 324 | */ 325 | private static final class IdentityEntryTransformer implements EntryTransformer { 326 | 327 | @Override 328 | public JarEntry transform(JarEntry jarEntry) { 329 | return jarEntry; 330 | } 331 | 332 | } 333 | 334 | } 335 | -------------------------------------------------------------------------------- /fat-jar-classloader/src/main/java/com/laomei/fatjar/classloader/FatJarDelegateClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.classloader; 2 | 3 | import com.laomei.fatjar.common.boot.jar.Archive; 4 | import com.laomei.fatjar.common.boot.jar.JarFileArchive; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.net.MalformedURLException; 10 | import java.net.URL; 11 | import java.net.URLClassLoader; 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.Collections; 15 | import java.util.Enumeration; 16 | import java.util.LinkedList; 17 | import java.util.List; 18 | import java.util.jar.JarFile; 19 | import java.util.jar.Manifest; 20 | 21 | import static com.laomei.fatjar.common.Constant.FAT_JAR_TOOL; 22 | import static com.laomei.fatjar.common.Constant.FAT_JAR_TOOL_VALUE; 23 | import static com.laomei.fatjar.common.Constant.FAT_MDW_PATH; 24 | 25 | /** 26 | * 管理多个 Far jar ClassLoader。 默认读取中间件jar包的路径 27 | * /opt 28 | * |___fat 29 | * |___mdw 30 | * |___mdw1 31 | * |___mdw2 32 | * |___mdw3 33 | * . 34 | * . 35 | * . 36 | * 37 | * 由于传入 FatJarDelegateClassLoader 的 url 都是 fat jar url,即使 FatJarDelegateClassLoader url 里含有这些 jar url, 38 | * 但是它无法成功加载这些 fat jar (默认的 URLClassLoader 没有解析 fat jar 能力)。所以不需要担心 FatJarDelegateClassLoader 39 | * 本身会加载一些类,影响应用类加载顺序。 40 | * 41 | * FatJarDelegateClassLoader 实现使用了大量的 Spring Boot 的代码。 42 | * 43 | * @author laomei on 2019/1/9 16:01 44 | */ 45 | @Slf4j 46 | public class FatJarDelegateClassLoader extends URLClassLoader { 47 | 48 | /** 49 | * 这是一个留的后门,用来指定中间件根文件夹,如果配置了fatDir,需要调用 getInstance(ClassLoader parentClassloader, Collection resourcePrefixes) 50 | */ 51 | public static String fatDir = null; 52 | 53 | public static FatJarDelegateClassLoader getInstance(ClassLoader parentClassloader, Collection resourcePrefixes) { 54 | File rt = null; 55 | if (fatDir != null) { 56 | rt = new File(fatDir); 57 | } else { 58 | rt = new File(FAT_MDW_PATH); 59 | } 60 | return getInstance(parentClassloader, rt, resourcePrefixes); 61 | } 62 | 63 | public static FatJarDelegateClassLoader getInstance(ClassLoader parentClassloader, File rootDir, Collection resourcePrefixes) { 64 | log.info("load middleware jar files from {}", rootDir); 65 | if (!rootDir.exists()) { 66 | throw new IllegalStateException("根目录'" + rootDir + "'不存在"); 67 | } 68 | File[] files = rootDir.listFiles(); 69 | if (files == null || files.length == 0) { 70 | throw new IllegalStateException("根目录'" + rootDir + "'内容为空"); 71 | } 72 | final List urls = new LinkedList<>(); 73 | for (final File file : files) { 74 | if (!file.isDirectory()) { 75 | log.debug("{} is not a directory, we will ignore this file", file); 76 | } 77 | List jarUrls = getJarUrls(file); 78 | urls.addAll(jarUrls); 79 | } 80 | log.debug("add {} into custom classloader", urls); 81 | return new FatJarDelegateClassLoader(urls.toArray(new URL[0]), parentClassloader, resourcePrefixes); 82 | } 83 | 84 | /** 85 | * 获取 dir 下的所有 .jar 结尾的文件 86 | */ 87 | private static List getJarUrls(File dir) { 88 | File[] files = dir.listFiles(); 89 | if (files == null || files.length == 0) { 90 | return Collections.emptyList(); 91 | } 92 | final List urls = new ArrayList<>(files.length); 93 | for (final File file : files) { 94 | if (!file.getAbsolutePath().endsWith(".jar")) { 95 | log.debug("{} is not a jar file, ignore this file", file); 96 | } 97 | URL url = null; 98 | try { 99 | url = new URL(file.toURI() + ""); 100 | } catch (MalformedURLException ignore) { 101 | } 102 | if (url != null) { 103 | urls.add(url); 104 | } 105 | } 106 | return urls; 107 | } 108 | 109 | /** 110 | * 中间件 classloader 111 | */ 112 | private final List fatJarClassLoaders; 113 | 114 | /** 115 | * 允许访问的资源前缀名 116 | */ 117 | private final Collection resourcePrefixes; 118 | 119 | public FatJarDelegateClassLoader(final URL[] urls, final ClassLoader parent, 120 | final Collection resourcePrefixes) { 121 | super(urls, parent); 122 | this.resourcePrefixes = resourcePrefixes; 123 | this.fatJarClassLoaders = new ArrayList<>(1 << 2); 124 | init(); 125 | } 126 | 127 | @Override 128 | public Enumeration findResources(final String name) throws IOException { 129 | LinkedList urlLinkedList = new LinkedList<>(); 130 | if (containsResources(name)) { 131 | for (FatJarClassLoader fatJarClassLoader : fatJarClassLoaders) { 132 | Enumeration enumeration = fatJarClassLoader.getResources(name); 133 | while (enumeration.hasMoreElements()) { 134 | urlLinkedList.add(enumeration.nextElement()); 135 | } 136 | } 137 | } 138 | return Collections.enumeration(urlLinkedList); 139 | } 140 | 141 | @Override 142 | public URL findResource(final String name) { 143 | if (!containsResources(name)) { 144 | return null; 145 | } 146 | for (FatJarClassLoader fatJarClassLoader : fatJarClassLoaders) { 147 | URL url = fatJarClassLoader.getResource(name); 148 | if (url != null) { 149 | return url; 150 | } 151 | } 152 | return null; 153 | } 154 | 155 | @Override 156 | protected Class findClass(final String name) throws ClassNotFoundException { 157 | Class clazz = null; 158 | if (!containsResources(name)) { 159 | return null; 160 | } 161 | for (FatJarClassLoader fatJarClassLoader : fatJarClassLoaders) { 162 | try { 163 | clazz = fatJarClassLoader.loadClass(name); 164 | if (clazz != null) { 165 | return clazz; 166 | } 167 | } catch (ClassNotFoundException ignore) { 168 | } 169 | } 170 | return null; 171 | } 172 | 173 | private boolean containsResources(String name) { 174 | for (String prefix : resourcePrefixes) { 175 | if (name.startsWith(prefix)) { 176 | return true; 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | /** 183 | * 初始化,获取传入的 url 里,所有的由 fat jar plugin 打包的 fat jar,对每个 fat jar 构建一个 FatJarClassLoader 184 | */ 185 | private void init() { 186 | final List unFatJarUrls = new LinkedList<>(); 187 | URL[] urls = getURLs(); 188 | for (URL url : urls) { 189 | initWithUrl(url, unFatJarUrls); 190 | } 191 | if (!unFatJarUrls.isEmpty()) { 192 | initJarClassLoader(unFatJarUrls); 193 | } 194 | } 195 | 196 | private void initJarClassLoader(List unFarJarUrls) { 197 | fatJarClassLoaders.add(new FatJarClassLoader(unFarJarUrls.toArray(new URL[0]), null)); 198 | } 199 | 200 | private void initWithUrl(URL url, List unFatJarUrls) { 201 | final List fatJarFiles = getFatJarFiles(url); 202 | if (!fatJarFiles.isEmpty()) { 203 | initFatJarClassLoaders(fatJarFiles); 204 | } else { 205 | unFatJarUrls.add(url); 206 | } 207 | } 208 | 209 | /** 210 | * 获取当前目录下所有的 fat jar 文件 211 | */ 212 | private List getFatJarFiles(URL url) { 213 | final List jarFiles = new ArrayList<>(); 214 | File file0 = new File(url.getFile()); 215 | listAllJarFiles(jarFiles, file0); 216 | return filterFatJarFiles(jarFiles); 217 | } 218 | 219 | /** 220 | * 初始化 fat jar classloader 221 | */ 222 | private void initFatJarClassLoaders(final List fatJarFiles) { 223 | for (File file : fatJarFiles) { 224 | try { 225 | List urlList = getUrlsForFatJar(file); 226 | String urlStr = "jar:" + file.toURI() + "!/"; 227 | URL currentJarPath = new URL(urlStr); 228 | urlList.add(currentJarPath); 229 | FatJarClassLoader fatJarClassLoader = new FatJarClassLoader(urlList.toArray(new URL[0]), null); 230 | fatJarClassLoaders.add(fatJarClassLoader); 231 | } catch (IOException e) { 232 | throw new IllegalStateException("create jarFile failed", e); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * 获取 fat jar 的含有的所有 jar urls 239 | */ 240 | private List getUrlsForFatJar(File file) throws IOException { 241 | JarFileArchive jarFileArchive = new JarFileArchive(file); 242 | List archives = jarFileArchive.getNestedArchives(this::isNestedArchive); 243 | List urlList = new LinkedList<>(); 244 | for (Archive archive : archives) { 245 | urlList.add(archive.getUrl()); 246 | } 247 | return urlList; 248 | } 249 | 250 | /** 251 | * 过滤出由 fat jar plugin 构建的 fat jar 252 | */ 253 | private List filterFatJarFiles(final List jarFiles) { 254 | final List fatJarFiles = new ArrayList<>(4); 255 | for (File file : jarFiles) { 256 | try (JarFile jarFile = new JarFile(file)) { 257 | Manifest manifest = jarFile.getManifest(); 258 | String value = manifest.getMainAttributes().getValue(FAT_JAR_TOOL); 259 | if (FAT_JAR_TOOL_VALUE.equals(value)) { 260 | fatJarFiles.add(file); 261 | } 262 | } catch (IOException e) { 263 | throw new IllegalStateException("create jarFile failed", e); 264 | } 265 | } 266 | return fatJarFiles; 267 | } 268 | 269 | private void listAllJarFiles(List jarFiles, File file0) { 270 | if (!file0.canRead() || !file0.exists()) { 271 | return; 272 | } 273 | if (file0.isDirectory()) { 274 | if (file0.getName().startsWith(".")) { 275 | //ignore 276 | return; 277 | } 278 | File[] files = file0.listFiles(); 279 | if (files != null) { 280 | for (File file : files) { 281 | listAllJarFiles(jarFiles, file); 282 | } 283 | } 284 | } else { 285 | if (file0.getName().endsWith(".jar")) { 286 | jarFiles.add(file0); 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * fat jar plugin 打成的 fat jar,所有的内置 jar 包都在 lib内。 293 | */ 294 | private boolean isNestedArchive(Archive.Entry entry) { 295 | return entry.getName().startsWith("lib/"); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/Handler.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.lang.ref.SoftReference; 6 | import java.lang.reflect.Method; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.net.URLConnection; 10 | import java.net.URLDecoder; 11 | import java.net.URLStreamHandler; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. 20 | * 21 | * @author Phillip Webb 22 | * @author Andy Wilkinson 23 | * @see JarFile#registerUrlProtocolHandler() 24 | */ 25 | public class Handler extends URLStreamHandler { 26 | 27 | // NOTE: in order to be found as a URL protocol handler, this class must be public, 28 | // must be named Handler and must be in a package ending '.jar' 29 | 30 | private static final String JAR_PROTOCOL = "jar:"; 31 | 32 | private static final String FILE_PROTOCOL = "file:"; 33 | 34 | private static final String SEPARATOR = "!/"; 35 | 36 | private static final String CURRENT_DIR = "/./"; 37 | 38 | private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR); 39 | 40 | private static final String PARENT_DIR = "/../"; 41 | 42 | private static final String[] FALLBACK_HANDLERS = { 43 | "sun.net.www.protocol.jar.Handler" }; 44 | 45 | private static final Method OPEN_CONNECTION_METHOD; 46 | 47 | static { 48 | Method method = null; 49 | try { 50 | method = URLStreamHandler.class.getDeclaredMethod("openConnection", 51 | URL.class); 52 | } 53 | catch (Exception ex) { 54 | // Swallow and ignore 55 | } 56 | OPEN_CONNECTION_METHOD = method; 57 | } 58 | 59 | private static SoftReference> rootFileCache; 60 | 61 | static { 62 | rootFileCache = new SoftReference>(null); 63 | } 64 | 65 | private final JarFile jarFile; 66 | 67 | private URLStreamHandler fallbackHandler; 68 | 69 | public Handler() { 70 | this(null); 71 | } 72 | 73 | public Handler(JarFile jarFile) { 74 | this.jarFile = jarFile; 75 | } 76 | 77 | @Override 78 | protected URLConnection openConnection(URL url) throws IOException { 79 | if (this.jarFile != null 80 | && url.toString().startsWith(this.jarFile.getUrl().toString())) { 81 | return JarURLConnection.get(url, this.jarFile); 82 | } 83 | try { 84 | return JarURLConnection.get(url, getRootJarFileFromUrl(url)); 85 | } 86 | catch (Exception ex) { 87 | return openFallbackConnection(url, ex); 88 | } 89 | } 90 | 91 | private URLConnection openFallbackConnection(URL url, Exception reason) 92 | throws IOException { 93 | try { 94 | return openConnection(getFallbackHandler(), url); 95 | } 96 | catch (Exception ex) { 97 | if (reason instanceof IOException) { 98 | log(false, "Unable to open fallback handler", ex); 99 | throw (IOException) reason; 100 | } 101 | log(true, "Unable to open fallback handler", ex); 102 | if (reason instanceof RuntimeException) { 103 | throw (RuntimeException) reason; 104 | } 105 | throw new IllegalStateException(reason); 106 | } 107 | } 108 | 109 | private void log(boolean warning, String message, Exception cause) { 110 | try { 111 | Logger.getLogger(getClass().getName()) 112 | .log((warning ? Level.WARNING : Level.FINEST), message, cause); 113 | } 114 | catch (Exception ex) { 115 | if (warning) { 116 | System.err.println("WARNING: " + message); 117 | } 118 | } 119 | } 120 | 121 | private URLStreamHandler getFallbackHandler() { 122 | if (this.fallbackHandler != null) { 123 | return this.fallbackHandler; 124 | } 125 | for (String handlerClassName : FALLBACK_HANDLERS) { 126 | try { 127 | Class handlerClass = Class.forName(handlerClassName); 128 | this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance(); 129 | return this.fallbackHandler; 130 | } 131 | catch (Exception ex) { 132 | // Ignore 133 | } 134 | } 135 | throw new IllegalStateException("Unable to find fallback handler"); 136 | } 137 | 138 | private URLConnection openConnection(URLStreamHandler handler, URL url) 139 | throws Exception { 140 | if (OPEN_CONNECTION_METHOD == null) { 141 | throw new IllegalStateException( 142 | "Unable to invoke fallback open connection method"); 143 | } 144 | OPEN_CONNECTION_METHOD.setAccessible(true); 145 | return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url); 146 | } 147 | 148 | @Override 149 | protected void parseURL(URL context, String spec, int start, int limit) { 150 | if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) { 151 | setFile(context, getFileFromSpec(spec.substring(start, limit))); 152 | } 153 | else { 154 | setFile(context, getFileFromContext(context, spec.substring(start, limit))); 155 | } 156 | } 157 | 158 | private String getFileFromSpec(String spec) { 159 | int separatorIndex = spec.lastIndexOf("!/"); 160 | if (separatorIndex == -1) { 161 | throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); 162 | } 163 | try { 164 | new URL(spec.substring(0, separatorIndex)); 165 | return spec; 166 | } 167 | catch (MalformedURLException ex) { 168 | throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); 169 | } 170 | } 171 | 172 | private String getFileFromContext(URL context, String spec) { 173 | String file = context.getFile(); 174 | if (spec.startsWith("/")) { 175 | return trimToJarRoot(file) + SEPARATOR + spec.substring(1); 176 | } 177 | if (file.endsWith("/")) { 178 | return file + spec; 179 | } 180 | int lastSlashIndex = file.lastIndexOf('/'); 181 | if (lastSlashIndex == -1) { 182 | throw new IllegalArgumentException( 183 | "No / found in context URL's file '" + file + "'"); 184 | } 185 | return file.substring(0, lastSlashIndex + 1) + spec; 186 | } 187 | 188 | private String trimToJarRoot(String file) { 189 | int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); 190 | if (lastSeparatorIndex == -1) { 191 | throw new IllegalArgumentException( 192 | "No !/ found in context URL's file '" + file + "'"); 193 | } 194 | return file.substring(0, lastSeparatorIndex); 195 | } 196 | 197 | private void setFile(URL context, String file) { 198 | setURL(context, JAR_PROTOCOL, null, -1, null, null, normalize(file), null, null); 199 | } 200 | 201 | private String normalize(String file) { 202 | if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) { 203 | return file; 204 | } 205 | int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); 206 | String afterSeparator = file.substring(afterLastSeparatorIndex); 207 | afterSeparator = replaceParentDir(afterSeparator); 208 | afterSeparator = replaceCurrentDir(afterSeparator); 209 | return file.substring(0, afterLastSeparatorIndex) + afterSeparator; 210 | } 211 | 212 | private String replaceParentDir(String file) { 213 | int parentDirIndex; 214 | while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) { 215 | int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); 216 | if (precedingSlashIndex >= 0) { 217 | file = file.substring(0, precedingSlashIndex) 218 | + file.substring(parentDirIndex + 3); 219 | } 220 | else { 221 | file = file.substring(parentDirIndex + 4); 222 | } 223 | } 224 | return file; 225 | } 226 | 227 | private String replaceCurrentDir(String file) { 228 | return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/"); 229 | } 230 | 231 | @Override 232 | protected int hashCode(URL u) { 233 | return hashCode(u.getProtocol(), u.getFile()); 234 | } 235 | 236 | private int hashCode(String protocol, String file) { 237 | int result = (protocol != null ? protocol.hashCode() : 0); 238 | int separatorIndex = file.indexOf(SEPARATOR); 239 | if (separatorIndex == -1) { 240 | return result + file.hashCode(); 241 | } 242 | String source = file.substring(0, separatorIndex); 243 | String entry = canonicalize(file.substring(separatorIndex + 2)); 244 | try { 245 | result += new URL(source).hashCode(); 246 | } 247 | catch (MalformedURLException ex) { 248 | result += source.hashCode(); 249 | } 250 | result += entry.hashCode(); 251 | return result; 252 | } 253 | 254 | @Override 255 | protected boolean sameFile(URL u1, URL u2) { 256 | if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { 257 | return false; 258 | } 259 | int separator1 = u1.getFile().indexOf(SEPARATOR); 260 | int separator2 = u2.getFile().indexOf(SEPARATOR); 261 | if (separator1 == -1 || separator2 == -1) { 262 | return super.sameFile(u1, u2); 263 | } 264 | String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); 265 | String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); 266 | if (!nested1.equals(nested2)) { 267 | String canonical1 = canonicalize(nested1); 268 | String canonical2 = canonicalize(nested2); 269 | if (!canonical1.equals(canonical2)) { 270 | return false; 271 | } 272 | } 273 | String root1 = u1.getFile().substring(0, separator1); 274 | String root2 = u2.getFile().substring(0, separator2); 275 | try { 276 | return super.sameFile(new URL(root1), new URL(root2)); 277 | } 278 | catch (MalformedURLException ex) { 279 | // Continue 280 | } 281 | return super.sameFile(u1, u2); 282 | } 283 | 284 | private String canonicalize(String path) { 285 | return path.replace(SEPARATOR, "/"); 286 | } 287 | 288 | public JarFile getRootJarFileFromUrl(URL url) throws IOException { 289 | String spec = url.getFile(); 290 | int separatorIndex = spec.indexOf(SEPARATOR); 291 | if (separatorIndex == -1) { 292 | throw new MalformedURLException("Jar URL does not contain !/ separator"); 293 | } 294 | String name = spec.substring(0, separatorIndex); 295 | return getRootJarFile(name); 296 | } 297 | 298 | private JarFile getRootJarFile(String name) throws IOException { 299 | try { 300 | if (!name.startsWith(FILE_PROTOCOL)) { 301 | throw new IllegalStateException("Not a file URL"); 302 | } 303 | String path = name.substring(FILE_PROTOCOL.length()); 304 | File file = new File(URLDecoder.decode(path, "UTF-8")); 305 | Map cache = rootFileCache.get(); 306 | JarFile result = (cache != null ? cache.get(file) : null); 307 | if (result == null) { 308 | result = new JarFile(file); 309 | addToRootFileCache(file, result); 310 | } 311 | return result; 312 | } 313 | catch (Exception ex) { 314 | throw new IOException("Unable to open root Jar file '" + name + "'", ex); 315 | } 316 | } 317 | 318 | /** 319 | * Add the given {@link JarFile} to the root file cache. 320 | * @param sourceFile the source file to add 321 | * @param jarFile the jar file. 322 | */ 323 | static void addToRootFileCache(File sourceFile, JarFile jarFile) { 324 | Map cache = rootFileCache.get(); 325 | if (cache == null) { 326 | cache = new ConcurrentHashMap(); 327 | rootFileCache = new SoftReference>(cache); 328 | } 329 | cache.put(sourceFile, jarFile); 330 | } 331 | 332 | /** 333 | * Set if a generic static exception can be thrown when a URL cannot be connected. 334 | * This optimization is used during class loading to save creating lots of exceptions 335 | * which are then swallowed. 336 | * @param useFastConnectionExceptions if fast connection exceptions can be used. 337 | */ 338 | public static void setUseFastConnectionExceptions( 339 | boolean useFastConnectionExceptions) { 340 | JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarURLConnection.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.FileNotFoundException; 7 | import java.io.FilePermission; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.UnsupportedEncodingException; 11 | import java.net.MalformedURLException; 12 | import java.net.URL; 13 | import java.net.URLConnection; 14 | import java.net.URLEncoder; 15 | import java.net.URLStreamHandler; 16 | import java.security.Permission; 17 | 18 | /** 19 | * {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}. 20 | * 21 | * @author Phillip Webb 22 | * @author Andy Wilkinson 23 | * @author Rostyslav Dudka 24 | */ 25 | final class JarURLConnection extends java.net.JarURLConnection { 26 | 27 | private static ThreadLocal useFastExceptions = new ThreadLocal(); 28 | 29 | private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException( 30 | "Jar file or entry not found"); 31 | 32 | private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException( 33 | FILE_NOT_FOUND_EXCEPTION); 34 | 35 | private static final String SEPARATOR = "!/"; 36 | 37 | private static final URL EMPTY_JAR_URL; 38 | 39 | static { 40 | try { 41 | EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() { 42 | @Override 43 | protected URLConnection openConnection(URL u) throws IOException { 44 | // Stub URLStreamHandler to prevent the wrong JAR Handler from being 45 | // Instantiated and cached. 46 | return null; 47 | } 48 | }); 49 | } 50 | catch (MalformedURLException ex) { 51 | throw new IllegalStateException(ex); 52 | } 53 | } 54 | 55 | private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(""); 56 | 57 | private static final String READ_ACTION = "read"; 58 | 59 | private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection 60 | .notFound(); 61 | 62 | private final JarFile jarFile; 63 | 64 | private Permission permission; 65 | 66 | private URL jarFileUrl; 67 | 68 | private final JarEntryName jarEntryName; 69 | 70 | private JarEntry jarEntry; 71 | 72 | private JarURLConnection(URL url, JarFile jarFile, JarEntryName jarEntryName) 73 | throws IOException { 74 | // What we pass to super is ultimately ignored 75 | super(EMPTY_JAR_URL); 76 | this.url = url; 77 | this.jarFile = jarFile; 78 | this.jarEntryName = jarEntryName; 79 | } 80 | 81 | @Override 82 | public void connect() throws IOException { 83 | if (this.jarFile == null) { 84 | throw FILE_NOT_FOUND_EXCEPTION; 85 | } 86 | if (!this.jarEntryName.isEmpty() && this.jarEntry == null) { 87 | this.jarEntry = this.jarFile.getJarEntry(getEntryName()); 88 | if (this.jarEntry == null) { 89 | throwFileNotFound(this.jarEntryName, this.jarFile); 90 | } 91 | } 92 | this.connected = true; 93 | } 94 | 95 | @Override 96 | public JarFile getJarFile() throws IOException { 97 | connect(); 98 | return this.jarFile; 99 | } 100 | 101 | @Override 102 | public URL getJarFileURL() { 103 | if (this.jarFile == null) { 104 | throw NOT_FOUND_CONNECTION_EXCEPTION; 105 | } 106 | if (this.jarFileUrl == null) { 107 | this.jarFileUrl = buildJarFileUrl(); 108 | } 109 | return this.jarFileUrl; 110 | } 111 | 112 | private URL buildJarFileUrl() { 113 | try { 114 | String spec = this.jarFile.getUrl().getFile(); 115 | if (spec.endsWith(SEPARATOR)) { 116 | spec = spec.substring(0, spec.length() - SEPARATOR.length()); 117 | } 118 | if (spec.indexOf(SEPARATOR) == -1) { 119 | return new URL(spec); 120 | } 121 | return new URL("jar:" + spec); 122 | } 123 | catch (MalformedURLException ex) { 124 | throw new IllegalStateException(ex); 125 | } 126 | } 127 | 128 | @Override 129 | public JarEntry getJarEntry() throws IOException { 130 | if (this.jarEntryName == null || this.jarEntryName.isEmpty()) { 131 | return null; 132 | } 133 | connect(); 134 | return this.jarEntry; 135 | } 136 | 137 | @Override 138 | public String getEntryName() { 139 | if (this.jarFile == null) { 140 | throw NOT_FOUND_CONNECTION_EXCEPTION; 141 | } 142 | return this.jarEntryName.toString(); 143 | } 144 | 145 | @Override 146 | public InputStream getInputStream() throws IOException { 147 | if (this.jarFile == null) { 148 | throw FILE_NOT_FOUND_EXCEPTION; 149 | } 150 | if (this.jarEntryName.isEmpty() 151 | && this.jarFile.getType() == JarFile.JarFileType.DIRECT) { 152 | throw new IOException("no entry name specified"); 153 | } 154 | connect(); 155 | InputStream inputStream = (this.jarEntryName.isEmpty() 156 | ? this.jarFile.getData().getInputStream(RandomAccessData.ResourceAccess.ONCE) 157 | : this.jarFile.getInputStream(this.jarEntry)); 158 | if (inputStream == null) { 159 | throwFileNotFound(this.jarEntryName, this.jarFile); 160 | } 161 | return inputStream; 162 | } 163 | 164 | private void throwFileNotFound(Object entry, JarFile jarFile) 165 | throws FileNotFoundException { 166 | if (Boolean.TRUE.equals(useFastExceptions.get())) { 167 | throw FILE_NOT_FOUND_EXCEPTION; 168 | } 169 | throw new FileNotFoundException( 170 | "JAR entry " + entry + " not found in " + jarFile.getName()); 171 | } 172 | 173 | @Override 174 | public int getContentLength() { 175 | long length = getContentLengthLong(); 176 | if (length > Integer.MAX_VALUE) { 177 | return -1; 178 | } 179 | return (int) length; 180 | } 181 | 182 | @Override 183 | public long getContentLengthLong() { 184 | if (this.jarFile == null) { 185 | return -1; 186 | } 187 | try { 188 | if (this.jarEntryName.isEmpty()) { 189 | return this.jarFile.size(); 190 | } 191 | JarEntry entry = getJarEntry(); 192 | return (entry != null ? (int) entry.getSize() : -1); 193 | } 194 | catch (IOException ex) { 195 | return -1; 196 | } 197 | } 198 | 199 | @Override 200 | public Object getContent() throws IOException { 201 | connect(); 202 | return (this.jarEntryName.isEmpty() ? this.jarFile : super.getContent()); 203 | } 204 | 205 | @Override 206 | public String getContentType() { 207 | return (this.jarEntryName != null ? this.jarEntryName.getContentType() : null); 208 | } 209 | 210 | @Override 211 | public Permission getPermission() throws IOException { 212 | if (this.jarFile == null) { 213 | throw FILE_NOT_FOUND_EXCEPTION; 214 | } 215 | if (this.permission == null) { 216 | this.permission = new FilePermission( 217 | this.jarFile.getRootJarFile().getFile().getPath(), READ_ACTION); 218 | } 219 | return this.permission; 220 | } 221 | 222 | @Override 223 | public long getLastModified() { 224 | if (this.jarFile == null || this.jarEntryName.isEmpty()) { 225 | return 0; 226 | } 227 | try { 228 | JarEntry entry = getJarEntry(); 229 | return (entry != null ? entry.getTime() : 0); 230 | } 231 | catch (IOException ex) { 232 | return 0; 233 | } 234 | } 235 | 236 | static void setUseFastExceptions(boolean useFastExceptions) { 237 | JarURLConnection.useFastExceptions.set(useFastExceptions); 238 | } 239 | 240 | static JarURLConnection get(URL url, JarFile jarFile) throws IOException { 241 | String spec = extractFullSpec(url, jarFile.getPathFromRoot()); 242 | int separator; 243 | int index = 0; 244 | while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { 245 | JarEntryName entryName = JarEntryName.get(spec.substring(index, separator)); 246 | JarEntry jarEntry = jarFile.getJarEntry(entryName.toString()); 247 | if (jarEntry == null) { 248 | return JarURLConnection.notFound(jarFile, entryName); 249 | } 250 | jarFile = jarFile.getNestedJarFile(jarEntry); 251 | index += separator + SEPARATOR.length(); 252 | } 253 | JarEntryName jarEntryName = JarEntryName.get(spec, index); 254 | if (Boolean.TRUE.equals(useFastExceptions.get())) { 255 | if (!jarEntryName.isEmpty() 256 | && !jarFile.containsEntry(jarEntryName.toString())) { 257 | return NOT_FOUND_CONNECTION; 258 | } 259 | } 260 | return new JarURLConnection(url, jarFile, jarEntryName); 261 | } 262 | 263 | private static String extractFullSpec(URL url, String pathFromRoot) { 264 | String file = url.getFile(); 265 | int separatorIndex = file.indexOf(SEPARATOR); 266 | if (separatorIndex < 0) { 267 | return ""; 268 | } 269 | int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length(); 270 | return file.substring(specIndex); 271 | } 272 | 273 | private static JarURLConnection notFound() { 274 | try { 275 | return notFound(null, null); 276 | } 277 | catch (IOException ex) { 278 | throw new IllegalStateException(ex); 279 | } 280 | } 281 | 282 | private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) 283 | throws IOException { 284 | if (Boolean.TRUE.equals(useFastExceptions.get())) { 285 | return NOT_FOUND_CONNECTION; 286 | } 287 | return new JarURLConnection(null, jarFile, jarEntryName); 288 | } 289 | 290 | /** 291 | * A JarEntryName parsed from a URL String. 292 | */ 293 | static class JarEntryName { 294 | 295 | private final String name; 296 | 297 | private String contentType; 298 | 299 | JarEntryName(String spec) { 300 | this.name = decode(spec); 301 | } 302 | 303 | private String decode(String source) { 304 | if (source.isEmpty() || (source.indexOf('%') < 0)) { 305 | return source; 306 | } 307 | ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); 308 | write(source, bos); 309 | // AsciiBytes is what is used to store the JarEntries so make it symmetric 310 | return AsciiBytes.toString(bos.toByteArray()); 311 | } 312 | 313 | private void write(String source, ByteArrayOutputStream outputStream) { 314 | int length = source.length(); 315 | for (int i = 0; i < length; i++) { 316 | int c = source.charAt(i); 317 | if (c > 127) { 318 | try { 319 | String encoded = URLEncoder.encode(String.valueOf((char) c), 320 | "UTF-8"); 321 | write(encoded, outputStream); 322 | } 323 | catch (UnsupportedEncodingException ex) { 324 | throw new IllegalStateException(ex); 325 | } 326 | } 327 | else { 328 | if (c == '%') { 329 | if ((i + 2) >= length) { 330 | throw new IllegalArgumentException( 331 | "Invalid encoded sequence \"" + source.substring(i) 332 | + "\""); 333 | } 334 | c = decodeEscapeSequence(source, i); 335 | i += 2; 336 | } 337 | outputStream.write(c); 338 | } 339 | } 340 | } 341 | 342 | private char decodeEscapeSequence(String source, int i) { 343 | int hi = Character.digit(source.charAt(i + 1), 16); 344 | int lo = Character.digit(source.charAt(i + 2), 16); 345 | if (hi == -1 || lo == -1) { 346 | throw new IllegalArgumentException( 347 | "Invalid encoded sequence \"" + source.substring(i) + "\""); 348 | } 349 | return ((char) ((hi << 4) + lo)); 350 | } 351 | 352 | @Override 353 | public String toString() { 354 | return this.name; 355 | } 356 | 357 | public boolean isEmpty() { 358 | return this.name.isEmpty(); 359 | } 360 | 361 | public String getContentType() { 362 | if (this.contentType == null) { 363 | this.contentType = deduceContentType(); 364 | } 365 | return this.contentType; 366 | } 367 | 368 | private String deduceContentType() { 369 | // Guess the content type, don't bother with streams as mark is not supported 370 | String type = (isEmpty() ? "x-java/jar" : null); 371 | type = (type != null ? type : guessContentTypeFromName(toString())); 372 | type = (type != null ? type : "content/unknown"); 373 | return type; 374 | } 375 | 376 | public static JarEntryName get(String spec) { 377 | return get(spec, 0); 378 | } 379 | 380 | public static JarEntryName get(String spec, int beginIndex) { 381 | if (spec.length() <= beginIndex) { 382 | return EMPTY_JAR_ENTRY_NAME; 383 | } 384 | return new JarEntryName(spec.substring(beginIndex)); 385 | } 386 | 387 | } 388 | 389 | } 390 | -------------------------------------------------------------------------------- /fat-jar-common/src/main/java/com/laomei/fatjar/common/boot/jar/JarFile.java: -------------------------------------------------------------------------------- 1 | package com.laomei.fatjar.common.boot.jar; 2 | 3 | import com.laomei.fatjar.common.boot.data.RandomAccessData; 4 | import com.laomei.fatjar.common.boot.data.RandomAccessDataFile; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.lang.ref.SoftReference; 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.net.URLStreamHandler; 13 | import java.net.URLStreamHandlerFactory; 14 | import java.util.Enumeration; 15 | import java.util.Iterator; 16 | import java.util.jar.JarInputStream; 17 | import java.util.jar.Manifest; 18 | import java.util.zip.ZipEntry; 19 | 20 | /** 21 | * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but 22 | * offers the following additional functionality. 23 | *

    24 | *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based 25 | * on any directory entry.
  • 26 | *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for 27 | * embedded JAR files (as long as their entry is not compressed).
  • 28 | *
29 | * 30 | * @author Phillip Webb 31 | */ 32 | public class JarFile extends java.util.jar.JarFile { 33 | 34 | private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; 35 | 36 | private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; 37 | 38 | private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; 39 | 40 | private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); 41 | 42 | private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); 43 | 44 | private final RandomAccessDataFile rootFile; 45 | 46 | private final String pathFromRoot; 47 | 48 | private final RandomAccessData data; 49 | 50 | private final JarFileType type; 51 | 52 | private URL url; 53 | 54 | private JarFileEntries entries; 55 | 56 | private SoftReference manifest; 57 | 58 | private boolean signed; 59 | 60 | /** 61 | * Create a new {@link JarFile} backed by the specified file. 62 | * @param file the root jar file 63 | * @throws IOException if the file cannot be read 64 | */ 65 | public JarFile(File file) throws IOException { 66 | this(new RandomAccessDataFile(file)); 67 | } 68 | 69 | /** 70 | * Create a new {@link JarFile} backed by the specified file. 71 | * @param file the root jar file 72 | * @throws IOException if the file cannot be read 73 | */ 74 | JarFile(RandomAccessDataFile file) throws IOException { 75 | this(file, "", file, JarFileType.DIRECT); 76 | } 77 | 78 | /** 79 | * Private constructor used to create a new {@link JarFile} either directly or from a 80 | * nested entry. 81 | * @param rootFile the root jar file 82 | * @param pathFromRoot the name of this file 83 | * @param data the underlying data 84 | * @param type the type of the jar file 85 | * @throws IOException if the file cannot be read 86 | */ 87 | private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, 88 | RandomAccessData data, JarFileType type) throws IOException { 89 | this(rootFile, pathFromRoot, data, null, type); 90 | } 91 | 92 | private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, 93 | RandomAccessData data, JarEntryFilter filter, JarFileType type) 94 | throws IOException { 95 | super(rootFile.getFile()); 96 | this.rootFile = rootFile; 97 | this.pathFromRoot = pathFromRoot; 98 | CentralDirectoryParser parser = new CentralDirectoryParser(); 99 | this.entries = parser.addVisitor(new JarFileEntries(this, filter)); 100 | parser.addVisitor(centralDirectoryVisitor()); 101 | this.data = parser.parse(data, filter == null); 102 | this.type = type; 103 | } 104 | 105 | private CentralDirectoryVisitor centralDirectoryVisitor() { 106 | return new CentralDirectoryVisitor() { 107 | 108 | @Override 109 | public void visitStart(CentralDirectoryEndRecord endRecord, 110 | RandomAccessData centralDirectoryData) { 111 | } 112 | 113 | @Override 114 | public void visitFileHeader(CentralDirectoryFileHeader fileHeader, 115 | int dataOffset) { 116 | AsciiBytes name = fileHeader.getName(); 117 | if (name.startsWith(META_INF) 118 | && name.endsWith(SIGNATURE_FILE_EXTENSION)) { 119 | JarFile.this.signed = true; 120 | } 121 | } 122 | 123 | @Override 124 | public void visitEnd() { 125 | } 126 | 127 | }; 128 | } 129 | 130 | protected final RandomAccessDataFile getRootJarFile() { 131 | return this.rootFile; 132 | } 133 | 134 | RandomAccessData getData() { 135 | return this.data; 136 | } 137 | 138 | @Override 139 | public Manifest getManifest() throws IOException { 140 | Manifest manifest = (this.manifest != null ? this.manifest.get() : null); 141 | if (manifest == null) { 142 | if (this.type == JarFileType.NESTED_DIRECTORY) { 143 | manifest = new JarFile(this.getRootJarFile()).getManifest(); 144 | } 145 | else { 146 | InputStream inputStream = getInputStream(MANIFEST_NAME, 147 | RandomAccessData.ResourceAccess.ONCE); 148 | if (inputStream == null) { 149 | return null; 150 | } 151 | try { 152 | manifest = new Manifest(inputStream); 153 | } 154 | finally { 155 | inputStream.close(); 156 | } 157 | } 158 | this.manifest = new SoftReference(manifest); 159 | } 160 | return manifest; 161 | } 162 | 163 | @Override 164 | public Enumeration entries() { 165 | final Iterator iterator = this.entries.iterator(); 166 | return new Enumeration() { 167 | 168 | @Override 169 | public boolean hasMoreElements() { 170 | return iterator.hasNext(); 171 | } 172 | 173 | @Override 174 | public java.util.jar.JarEntry nextElement() { 175 | return iterator.next(); 176 | } 177 | 178 | }; 179 | } 180 | 181 | @Override 182 | public JarEntry getJarEntry(String name) { 183 | return (JarEntry) getEntry(name); 184 | } 185 | 186 | public boolean containsEntry(String name) { 187 | return this.entries.containsEntry(name); 188 | } 189 | 190 | @Override 191 | public ZipEntry getEntry(String name) { 192 | return this.entries.getEntry(name); 193 | } 194 | 195 | @Override 196 | public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { 197 | return getInputStream(ze, RandomAccessData.ResourceAccess.PER_READ); 198 | } 199 | 200 | public InputStream getInputStream(ZipEntry ze, RandomAccessData.ResourceAccess access) 201 | throws IOException { 202 | if (ze instanceof JarEntry) { 203 | return this.entries.getInputStream((JarEntry) ze, access); 204 | } 205 | return getInputStream((ze != null ? ze.getName() : null), access); 206 | } 207 | 208 | InputStream getInputStream(String name, RandomAccessData.ResourceAccess access) throws IOException { 209 | return this.entries.getInputStream(name, access); 210 | } 211 | 212 | /** 213 | * Return a nested {@link JarFile} loaded from the specified entry. 214 | * @param entry the zip entry 215 | * @return a {@link JarFile} for the entry 216 | * @throws IOException if the nested jar file cannot be read 217 | */ 218 | public synchronized JarFile getNestedJarFile(final ZipEntry entry) 219 | throws IOException { 220 | return getNestedJarFile((JarEntry) entry); 221 | } 222 | 223 | /** 224 | * Return a nested {@link JarFile} loaded from the specified entry. 225 | * @param entry the zip entry 226 | * @return a {@link JarFile} for the entry 227 | * @throws IOException if the nested jar file cannot be read 228 | */ 229 | public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException { 230 | try { 231 | return createJarFileFromEntry(entry); 232 | } 233 | catch (Exception ex) { 234 | throw new IOException( 235 | "Unable to open nested jar file '" + entry.getName() + "'", ex); 236 | } 237 | } 238 | 239 | private JarFile createJarFileFromEntry(JarEntry entry) throws IOException { 240 | if (entry.isDirectory()) { 241 | return createJarFileFromDirectoryEntry(entry); 242 | } 243 | return createJarFileFromFileEntry(entry); 244 | } 245 | 246 | private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { 247 | final AsciiBytes sourceName = new AsciiBytes(entry.getName()); 248 | JarEntryFilter filter = new JarEntryFilter() { 249 | 250 | @Override 251 | public AsciiBytes apply(AsciiBytes name) { 252 | if (name.startsWith(sourceName) && !name.equals(sourceName)) { 253 | return name.substring(sourceName.length()); 254 | } 255 | return null; 256 | } 257 | 258 | }; 259 | return new JarFile(this.rootFile, 260 | this.pathFromRoot + "!/" 261 | + entry.getName().substring(0, sourceName.length() - 1), 262 | this.data, filter, JarFileType.NESTED_DIRECTORY); 263 | } 264 | 265 | private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { 266 | if (entry.getMethod() != ZipEntry.STORED) { 267 | throw new IllegalStateException("Unable to open nested entry '" 268 | + entry.getName() + "'. It has been compressed and nested " 269 | + "jar files must be stored without compression. Please check the " 270 | + "mechanism used to create your executable jar file"); 271 | } 272 | RandomAccessData entryData = this.entries.getEntryData(entry.getName()); 273 | return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), 274 | entryData, JarFileType.NESTED_JAR); 275 | } 276 | 277 | @Override 278 | public int size() { 279 | return this.entries.getSize(); 280 | } 281 | 282 | @Override 283 | public void close() throws IOException { 284 | super.close(); 285 | this.rootFile.close(); 286 | } 287 | 288 | /** 289 | * Return a URL that can be used to access this JAR file. NOTE: the specified URL 290 | * cannot be serialized and or cloned. 291 | * @return the URL 292 | * @throws MalformedURLException if the URL is malformed 293 | */ 294 | public URL getUrl() throws MalformedURLException { 295 | if (this.url == null) { 296 | Handler handler = new Handler(this); 297 | String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; 298 | file = file.replace("file:////", "file://"); // Fix UNC paths 299 | this.url = new URL("jar", "", -1, file, handler); 300 | } 301 | return this.url; 302 | } 303 | 304 | @Override 305 | public String toString() { 306 | return getName(); 307 | } 308 | 309 | @Override 310 | public String getName() { 311 | return this.rootFile.getFile() + this.pathFromRoot; 312 | } 313 | 314 | boolean isSigned() { 315 | return this.signed; 316 | } 317 | 318 | void setupEntryCertificates(JarEntry entry) { 319 | // Fallback to JarInputStream to obtain certificates, not fast but hopefully not 320 | // happening that often. 321 | try { 322 | JarInputStream inputStream = new JarInputStream( 323 | getData().getInputStream(RandomAccessData.ResourceAccess.ONCE)); 324 | try { 325 | java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry(); 326 | while (certEntry != null) { 327 | inputStream.closeEntry(); 328 | if (entry.getName().equals(certEntry.getName())) { 329 | setCertificates(entry, certEntry); 330 | } 331 | setCertificates(getJarEntry(certEntry.getName()), certEntry); 332 | certEntry = inputStream.getNextJarEntry(); 333 | } 334 | } 335 | finally { 336 | inputStream.close(); 337 | } 338 | } 339 | catch (IOException ex) { 340 | throw new IllegalStateException(ex); 341 | } 342 | } 343 | 344 | private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) { 345 | if (entry != null) { 346 | entry.setCertificates(certEntry); 347 | } 348 | } 349 | 350 | public void clearCache() { 351 | this.entries.clearCache(); 352 | } 353 | 354 | protected String getPathFromRoot() { 355 | return this.pathFromRoot; 356 | } 357 | 358 | JarFileType getType() { 359 | return this.type; 360 | } 361 | 362 | /** 363 | * Register a {@literal 'java.protocol.handler.pkgs'} property so that a 364 | * {@link URLStreamHandler} will be located to deal with jar URLs. 365 | */ 366 | public static void registerUrlProtocolHandler() { 367 | String handlers = System.getProperty(PROTOCOL_HANDLER, ""); 368 | System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE 369 | : handlers + "|" + HANDLERS_PACKAGE)); 370 | resetCachedUrlHandlers(); 371 | } 372 | 373 | /** 374 | * Reset any cached handlers just in case a jar protocol has already been used. We 375 | * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which 376 | * should have no effect other than clearing the handlers cache. 377 | */ 378 | private static void resetCachedUrlHandlers() { 379 | try { 380 | URL.setURLStreamHandlerFactory(null); 381 | } 382 | catch (Error ex) { 383 | // Ignore 384 | } 385 | } 386 | 387 | /** 388 | * The type of a {@link JarFile}. 389 | */ 390 | enum JarFileType { 391 | 392 | DIRECT, NESTED_DIRECTORY, NESTED_JAR 393 | 394 | } 395 | 396 | } 397 | --------------------------------------------------------------------------------