();
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