0
# JAR Writing and Entry Management
1
2
Low-level utilities for writing JAR files with proper directory structure, duplicate handling, manifest generation, and nested library support. The JAR writing system provides the foundation for creating executable Spring Boot archives with embedded dependencies.
3
4
## Capabilities
5
6
### High-Level JAR Writing
7
8
Primary JAR writer class for creating executable archives with launch script support and automatic resource management.
9
10
```java { .api }
11
public class JarWriter implements AutoCloseable {
12
/**
13
* Create a JAR writer for the specified file.
14
*
15
* @param file the target JAR file to create
16
* @throws FileNotFoundException if the target file cannot be created
17
* @throws IOException if JAR writing initialization fails
18
*/
19
public JarWriter(File file) throws FileNotFoundException, IOException;
20
21
/**
22
* Create a JAR writer with launch script support.
23
* The launch script is prepended to the JAR to make it directly executable on Unix-like systems.
24
*
25
* @param file the target JAR file to create
26
* @param launchScript the launch script to prepend
27
* @throws FileNotFoundException if the target file cannot be created
28
* @throws IOException if JAR writing initialization fails
29
*/
30
public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundException, IOException;
31
32
/**
33
* Create a JAR writer with launch script and custom modification time.
34
*
35
* @param file the target JAR file to create
36
* @param launchScript the launch script to prepend
37
* @param lastModifiedTime the modification time to set on all entries
38
* @throws FileNotFoundException if the target file cannot be created
39
* @throws IOException if JAR writing initialization fails
40
*/
41
public JarWriter(File file, LaunchScript launchScript, FileTime lastModifiedTime) throws FileNotFoundException, IOException;
42
43
/**
44
* Close the JAR writer and finalize the archive.
45
* This method must be called to ensure the JAR is properly written.
46
*
47
* @throws IOException if closing fails
48
*/
49
@Override
50
public void close() throws IOException;
51
}
52
```
53
54
### Abstract JAR Writer
55
56
Base class providing comprehensive JAR writing functionality with extensibility points for custom implementations.
57
58
```java { .api }
59
public abstract class AbstractJarWriter implements LoaderClassesWriter {
60
/**
61
* Write the manifest file to the JAR.
62
* The manifest defines the main class, classpath, and other metadata.
63
*
64
* @param manifest the manifest to write
65
* @throws IOException if writing fails
66
*/
67
public void writeManifest(Manifest manifest) throws IOException;
68
69
/**
70
* Write an entry from an input stream.
71
*
72
* @param entryName the name/path of the entry within the JAR
73
* @param inputStream the input stream containing entry data
74
* @throws IOException if writing fails
75
*/
76
public void writeEntry(String entryName, InputStream inputStream) throws IOException;
77
78
/**
79
* Write an entry using a custom entry writer.
80
*
81
* @param entryName the name/path of the entry within the JAR
82
* @param entryWriter the writer for entry content
83
* @throws IOException if writing fails
84
*/
85
public void writeEntry(String entryName, EntryWriter entryWriter) throws IOException;
86
87
/**
88
* Write a nested library JAR to the specified location.
89
* Handles compression and ensures proper nested JAR structure.
90
*
91
* @param location the location within the JAR where the library should be placed
92
* @param library the library to write
93
* @throws IOException if writing fails
94
*/
95
public void writeNestedLibrary(String location, Library library) throws IOException;
96
97
/**
98
* Write an index file containing a list of entries.
99
* Used for classpath indexes, layer indexes, and other metadata files.
100
*
101
* @param location the location within the JAR for the index file
102
* @param lines the lines to write to the index file
103
* @throws IOException if writing fails
104
*/
105
public void writeIndexFile(String location, Collection<String> lines) throws IOException;
106
107
/**
108
* Write default Spring Boot loader classes to the JAR.
109
* These classes provide the runtime bootstrap functionality.
110
*
111
* @throws IOException if writing fails
112
*/
113
public void writeLoaderClasses() throws IOException;
114
115
/**
116
* Write specific loader implementation classes.
117
*
118
* @param loaderImplementation the loader implementation to use
119
* @throws IOException if writing fails
120
*/
121
public void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException;
122
123
/**
124
* Write loader classes from a specific JAR resource.
125
*
126
* @param loaderJarResourceName the name of the loader JAR resource
127
* @throws IOException if writing fails
128
*/
129
public void writeLoaderClasses(String loaderJarResourceName) throws IOException;
130
}
131
```
132
133
### Entry Writer Interface
134
135
Interface for writing custom entry content with optional size calculation.
136
137
```java { .api }
138
public interface EntryWriter {
139
/**
140
* Write entry content to the output stream.
141
*
142
* @param outputStream the output stream to write to
143
* @throws IOException if writing fails
144
*/
145
void write(OutputStream outputStream) throws IOException;
146
147
/**
148
* Get the size of the entry content.
149
* Returns -1 if size is unknown or variable.
150
*
151
* @return the entry size in bytes, or -1 if unknown
152
*/
153
default int size() {
154
return -1;
155
}
156
}
157
```
158
159
### Size Calculating Entry Writer
160
161
Entry writer implementation that calculates and tracks the size of written content.
162
163
```java { .api }
164
public class SizeCalculatingEntryWriter implements EntryWriter {
165
// Implementation calculates size during writing
166
// Useful for progress tracking and validation
167
}
168
```
169
170
### Loader Classes Writer
171
172
Interface for writing Spring Boot loader classes that provide runtime bootstrap functionality.
173
174
```java { .api }
175
public interface LoaderClassesWriter {
176
/**
177
* Write default loader classes to the archive.
178
*
179
* @throws IOException if writing fails
180
*/
181
void writeLoaderClasses() throws IOException;
182
183
/**
184
* Write specific loader implementation classes.
185
*
186
* @param loaderImplementation the loader implementation to write
187
* @throws IOException if writing fails
188
*/
189
void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException;
190
191
/**
192
* Write loader classes from a named JAR resource.
193
*
194
* @param loaderJarResourceName the loader JAR resource name
195
* @throws IOException if writing fails
196
*/
197
void writeLoaderClasses(String loaderJarResourceName) throws IOException;
198
199
/**
200
* Write a general entry to the archive.
201
*
202
* @param name the entry name
203
* @param inputStream the entry content
204
* @throws IOException if writing fails
205
*/
206
void writeEntry(String name, InputStream inputStream) throws IOException;
207
}
208
```
209
210
## Usage Examples
211
212
### Basic JAR Creation
213
214
```java
215
import org.springframework.boot.loader.tools.JarWriter;
216
import java.io.File;
217
import java.io.FileInputStream;
218
import java.io.IOException;
219
import java.util.jar.Manifest;
220
221
// Create a simple executable JAR
222
File targetJar = new File("output/myapp.jar");
223
try (JarWriter writer = new JarWriter(targetJar)) {
224
// Write manifest
225
Manifest manifest = new Manifest();
226
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
227
manifest.getMainAttributes().putValue("Main-Class",
228
"org.springframework.boot.loader.launch.JarLauncher");
229
manifest.getMainAttributes().putValue("Start-Class", "com.example.Application");
230
writer.writeManifest(manifest);
231
232
// Write application classes
233
File classesDir = new File("target/classes");
234
writeDirectory(writer, classesDir, "BOOT-INF/classes/");
235
236
// Write loader classes
237
writer.writeLoaderClasses();
238
}
239
240
// Helper method to write directory contents
241
private void writeDirectory(JarWriter writer, File dir, String prefix) throws IOException {
242
File[] files = dir.listFiles();
243
if (files != null) {
244
for (File file : files) {
245
if (file.isDirectory()) {
246
writeDirectory(writer, file, prefix + file.getName() + "/");
247
} else {
248
try (FileInputStream fis = new FileInputStream(file)) {
249
writer.writeEntry(prefix + file.getName(), fis);
250
}
251
}
252
}
253
}
254
}
255
```
256
257
### JAR with Launch Script
258
259
```java
260
import org.springframework.boot.loader.tools.*;
261
import java.io.File;
262
import java.util.Map;
263
264
// Create executable JAR with Unix launch script
265
File targetJar = new File("output/myapp.jar");
266
File scriptTemplate = new File("src/main/scripts/launch.sh");
267
268
// Configure launch script properties
269
Map<String, String> properties = Map.of(
270
"initInfoProvides", "myapp",
271
"initInfoShortDescription", "My Spring Boot Application",
272
"confFolder", "/etc/myapp"
273
);
274
275
LaunchScript launchScript = new DefaultLaunchScript(scriptTemplate, properties);
276
277
try (JarWriter writer = new JarWriter(targetJar, launchScript)) {
278
// Write JAR contents
279
// ... (same as above)
280
}
281
282
// The resulting JAR can be executed directly:
283
// chmod +x myapp.jar
284
// ./myapp.jar
285
```
286
287
### Advanced Entry Writing
288
289
```java
290
import org.springframework.boot.loader.tools.*;
291
import java.io.File;
292
import java.io.ByteArrayOutputStream;
293
import java.io.IOException;
294
import java.nio.charset.StandardCharsets;
295
import java.util.Properties;
296
297
File targetJar = new File("output/myapp.jar");
298
try (JarWriter writer = new JarWriter(targetJar)) {
299
// Write custom properties file
300
Properties props = new Properties();
301
props.setProperty("app.version", "1.0.0");
302
props.setProperty("build.time", "2024-01-15T10:30:00Z");
303
304
writer.writeEntry("BOOT-INF/classes/application.properties", new EntryWriter() {
305
@Override
306
public void write(OutputStream outputStream) throws IOException {
307
props.store(outputStream, "Application Properties");
308
}
309
310
@Override
311
public int size() {
312
try {
313
ByteArrayOutputStream baos = new ByteArrayOutputStream();
314
props.store(baos, null);
315
return baos.size();
316
} catch (IOException e) {
317
return -1;
318
}
319
}
320
});
321
322
// Write text content
323
String configContent = "server.port=8080\nlogging.level.root=INFO";
324
writer.writeEntry("BOOT-INF/classes/config.properties", outputStream -> {
325
outputStream.write(configContent.getBytes(StandardCharsets.UTF_8));
326
});
327
}
328
```
329
330
### Library Integration
331
332
```java
333
import org.springframework.boot.loader.tools.*;
334
import java.io.File;
335
import java.util.List;
336
337
File targetJar = new File("output/myapp.jar");
338
try (JarWriter writer = new JarWriter(targetJar)) {
339
// Write manifest and classes
340
// ... (setup code)
341
342
// Write nested libraries
343
List<Library> libraries = List.of(
344
new Library(new File("lib/spring-boot-3.2.0.jar"), LibraryScope.COMPILE),
345
new Library(new File("lib/logback-classic-1.4.5.jar"), LibraryScope.RUNTIME)
346
);
347
348
Layout layout = new Layouts.Jar();
349
for (Library library : libraries) {
350
String location = layout.getLibraryLocation(library.getName(), library.getScope());
351
writer.writeNestedLibrary(location + library.getName(), library);
352
}
353
354
// Write classpath index
355
List<String> classpathEntries = libraries.stream()
356
.map(lib -> layout.getLibraryLocation(lib.getName(), lib.getScope()) + lib.getName())
357
.collect(Collectors.toList());
358
359
writer.writeIndexFile(layout.getClasspathIndexFileLocation(), classpathEntries);
360
}
361
```
362
363
### Custom AbstractJarWriter Implementation
364
365
```java
366
import org.springframework.boot.loader.tools.*;
367
import java.io.File;
368
import java.io.IOException;
369
import java.util.jar.JarOutputStream;
370
371
// Custom JAR writer with additional functionality
372
public class CustomJarWriter extends AbstractJarWriter {
373
private final JarOutputStream jarOutputStream;
374
375
public CustomJarWriter(File file) throws IOException {
376
// Initialize JAR output stream
377
this.jarOutputStream = new JarOutputStream(new FileOutputStream(file));
378
}
379
380
@Override
381
protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException {
382
// Custom implementation for writing entries
383
jarOutputStream.putNextEntry(entry);
384
if (entryWriter != null) {
385
entryWriter.write(jarOutputStream);
386
}
387
jarOutputStream.closeEntry();
388
}
389
390
// Additional custom methods
391
public void writeCompressedEntry(String name, byte[] data) throws IOException {
392
ZipEntry entry = new ZipEntry(name);
393
entry.setMethod(ZipEntry.DEFLATED);
394
entry.setSize(data.length);
395
396
writeToArchive(entry, outputStream -> outputStream.write(data));
397
}
398
399
public void writeMetadata(String name, Object metadata) throws IOException {
400
// Custom metadata serialization
401
ByteArrayOutputStream baos = new ByteArrayOutputStream();
402
ObjectOutputStream oos = new ObjectOutputStream(baos);
403
oos.writeObject(metadata);
404
oos.close();
405
406
writeEntry(name, outputStream -> outputStream.write(baos.toByteArray()));
407
}
408
}
409
```
410
411
### Progress Tracking
412
413
```java
414
import org.springframework.boot.loader.tools.*;
415
import java.io.File;
416
import java.util.concurrent.atomic.AtomicLong;
417
418
// Track JAR writing progress
419
public class ProgressTrackingJarWriter extends AbstractJarWriter {
420
private final AtomicLong totalBytes = new AtomicLong(0);
421
private final AtomicLong writtenBytes = new AtomicLong(0);
422
private final ProgressCallback callback;
423
424
public ProgressTrackingJarWriter(File file, ProgressCallback callback) throws IOException {
425
super(/* initialization */);
426
this.callback = callback;
427
}
428
429
@Override
430
public void writeEntry(String entryName, EntryWriter entryWriter) throws IOException {
431
long entrySize = entryWriter.size();
432
if (entrySize > 0) {
433
totalBytes.addAndGet(entrySize);
434
}
435
436
// Wrap entry writer to track progress
437
EntryWriter trackingWriter = new EntryWriter() {
438
@Override
439
public void write(OutputStream outputStream) throws IOException {
440
OutputStream trackingStream = new OutputStream() {
441
@Override
442
public void write(int b) throws IOException {
443
outputStream.write(b);
444
writtenBytes.incrementAndGet();
445
updateProgress();
446
}
447
448
@Override
449
public void write(byte[] b, int off, int len) throws IOException {
450
outputStream.write(b, off, len);
451
writtenBytes.addAndGet(len);
452
updateProgress();
453
}
454
};
455
entryWriter.write(trackingStream);
456
}
457
458
@Override
459
public int size() {
460
return entryWriter.size();
461
}
462
};
463
464
super.writeEntry(entryName, trackingWriter);
465
}
466
467
private void updateProgress() {
468
long total = totalBytes.get();
469
long written = writtenBytes.get();
470
if (total > 0) {
471
double percentage = (double) written / total * 100;
472
callback.updateProgress(percentage, written, total);
473
}
474
}
475
476
@FunctionalInterface
477
public interface ProgressCallback {
478
void updateProgress(double percentage, long writtenBytes, long totalBytes);
479
}
480
}
481
```
482
483
### Integration with Repackager
484
485
```java
486
import org.springframework.boot.loader.tools.*;
487
import java.io.File;
488
489
// The repackager uses JAR writers internally
490
File sourceJar = new File("myapp.jar");
491
Repackager repackager = new Repackager(sourceJar);
492
493
// Configure repackager to use custom settings that affect JAR writing
494
repackager.setLayout(new Layouts.Jar());
495
repackager.setLoaderImplementation(LoaderImplementation.DEFAULT);
496
497
// The repackager will create its own JarWriter instance internally
498
repackager.repackage(Libraries.NONE);
499
500
// For more control, you can extend Repackager or use JarWriter directly
501
```
502
503
## JAR Structure
504
505
The JAR writing system creates archives with the following typical structure:
506
507
```
508
myapp.jar
509
├── META-INF/
510
│ └── MANIFEST.MF # JAR manifest with main class
511
├── BOOT-INF/
512
│ ├── classes/ # Application classes
513
│ │ └── com/example/...
514
│ ├── lib/ # Dependency libraries
515
│ │ ├── spring-boot-3.2.0.jar
516
│ │ └── logback-classic-1.4.5.jar
517
│ ├── classpath.idx # Classpath index (optional)
518
│ └── layers.idx # Layer index (optional)
519
└── org/springframework/boot/loader/ # Spring Boot loader classes
520
└── launch/...
521
```
522
523
This structure enables the Spring Boot loader to bootstrap the application and load dependencies at runtime.