springboot项目打包插件为:spring-boot-maven-plugin,使用如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.11.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
</project>
可以看出这里调用的是repackage
的实现(repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original),我们来看实现代码:
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo {
默认执行execute方法:
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
getLog().debug("repackage goal could not be applied to pom project.");
return;
}
//这里设置为true的话,不会进行重新打包,仍然是原始的maven包
if (this.skip) {
getLog().debug("skipping repackaging as per configuration.");
return;
}
repackage();
}
private void repackage() throws MojoExecutionException {
Artifact source = getSourceArtifact();
//最终为可执行的jar 即fat jar
File target = getTargetFile();
//获取重新打包器,将maven生成的jar重新打包成可执行jar
Repackager repackager = getRepackager(source.getFile());
//查找并过滤项目运行时的依赖
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
//将 artifacts 转换成 libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
try {
//获取springboot启动脚本
LaunchScript launchScript = getLaunchScript();
//重新执行打包,生成fat jar
repackager.repackage(target, libraries, launchScript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
//将maven生成的jar更新为.original文件
updateArtifact(source, target, repackager.getBackupFile());
}
Artifact:这里我觉得可以理解为对应的原始的maven打包的jar包的描述性文件,获取如下
private Artifact getSourceArtifact() {
Artifact sourceArtifact = getArtifact(this.classifier);
return (sourceArtifact != null) ? sourceArtifact : this.project.getArtifact();
}
private Artifact getArtifact(String classifier) {
if (classifier != null) {
for (Artifact attachedArtifact : this.project.getAttachedArtifacts()) {
if (classifier.equals(attachedArtifact.getClassifier()) && attachedArtifact.getFile() != null
&& attachedArtifact.getFile().isFile()) {
return attachedArtifact;
}
}
}
return null;
}
关于classifier可以参考:Apache Maven JAR Plugin – How to create an additional attached jar artifact from the project
https://www.baeldung.com/maven-artifact-classifiers,这里我理解为maven打包时候可能会打包出多个jar包,我们根据classifier去多个jar包进行寻找对应的目标包,倘若没找到,则采用maven默认打包的jar包作为原始包进行重新打包处理
private File getTargetFile() {
String classifier = (this.classifier != null) ? this.classifier.trim() : "";
if (!classifier.isEmpty() && !classifier.startsWith("-")) {
classifier = "-" + classifier;
}
if (!this.outputDirectory.exists()) {
this.outputDirectory.mkdirs();
}
return new File(this.outputDirectory,
this.finalName + classifier + "." + this.project.getArtifact().getArtifactHandler().getExtension());
}
这里创建打包之后存放的默认文件夹并且根据classifier创建最终jar包文件的文件句柄,这里指File对象
private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
repackager.setLayout(this.layout.layout());
}
return repackager;
}
private class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener {
@Override
public void handleTimeoutWarning(long duration, String mainMethod) {
getLog().warn("Searching for the main-class is taking some time, "
+ "consider using the mainClass configuration parameter");
}
}
这里主要是进行Repackager类的构建,Repackager存储了重新打包所需的信息,比如打包类型、mainClass等等,内部还有repackage即重新打包的具体实现。这里的layout代表了打包文件的格式,目前支持JAR, WAR, ZIP, DIR类型
protected Set<Artifact> filterDependencies(Set<Artifact> dependencies, FilterArtifacts filters)
throws MojoExecutionException {
try {
Set<Artifact> filtered = new LinkedHashSet<>(dependencies);
filtered.retainAll(filters.filter(dependencies));
return filtered;
}
catch (ArtifactFilterException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
}
protected final FilterArtifacts getFilters(ArtifactsFilter... additionalFilters) {
FilterArtifacts filters = new FilterArtifacts();
for (ArtifactsFilter additionalFilter : additionalFilters) {
filters.addFilter(additionalFilter);
}
filters.addFilter(new MatchingGroupIdFilter(cleanFilterConfig(this.excludeGroupIds)));
if (this.includes != null && !this.includes.isEmpty()) {
filters.addFilter(new IncludeFilter(this.includes));
}
if (this.excludes != null && !this.excludes.isEmpty()) {
filters.addFilter(new ExcludeFilter(this.excludes));
}
return filters;
}
private ArtifactsFilter[] getAdditionalFilters() {
List<ArtifactsFilter> filters = new ArrayList<>();
if (this.excludeDevtools) {
Exclude exclude = new Exclude();
exclude.setGroupId("org.springframework.boot");
exclude.setArtifactId("spring-boot-devtools");
ExcludeFilter filter = new ExcludeFilter(exclude);
filters.add(filter);
}
if (!this.includeSystemScope) {
filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM));
}
return filters.toArray(new ArtifactsFilter[0]);
}
这里主要进行的符合条件的jar的过滤,代码简单不多说
private LaunchScript getLaunchScript() throws IOException {
if (this.executable || this.embeddedLaunchScript != null) {
return new DefaultLaunchScript(this.embeddedLaunchScript, buildLaunchScriptProperties());
}
return null;
}
private Properties buildLaunchScriptProperties() {
Properties properties = new Properties();
if (this.embeddedLaunchScriptProperties != null) {
properties.putAll(this.embeddedLaunchScriptProperties);
}
putIfMissing(properties, "initInfoProvides", this.project.getArtifactId());
putIfMissing(properties, "initInfoShortDescription", this.project.getName(), this.project.getArtifactId());
putIfMissing(properties, "initInfoDescription", removeLineBreaks(this.project.getDescription()),
this.project.getName(), this.project.getArtifactId());
return properties;
}
这里支持执行脚本,可以进行配置,默认会进行initInfoProvides、initInfoShortDescription、initInfoDescription三个属性的设置
//destination代表最终生成的文件 libraries代表项目的依赖包 launchScript需要执行的脚本文件
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
if (destination == null || destination.isDirectory()) {
throw new IllegalArgumentException("Invalid destination");
}
if (libraries == null) {
throw new IllegalArgumentException("Libraries must not be null");
}
if (this.layout == null) {
this.layout = getLayoutFactory().getLayout(this.source);
}
destination = destination.getAbsoluteFile();
File workingSource = this.source;
//如果Manifest文件已经有Spring-Boot-Version属性,说明已经重新打包过了 文件名也一样的话就不用继续了 说明是一个
if (alreadyRepackaged() && this.source.equals(destination)) {
return;
}
//2个文件名相同的话 说明打包过一次了,获取之前的.original即原始jar包删除,将新包重新命名为 原始包名.original
if (this.source.equals(destination)) {
workingSource = getBackupFile();
workingSource.delete();
renameFile(this.source, workingSource);
}
//删除上一次的重打包文件
destination.delete();
try {
try (JarFile jarFileSource = new JarFile(workingSource)) {
//这里执行的是真正的打包过程
repackage(jarFileSource, destination, libraries, launchScript);
}
}
finally {
if (!this.backupSource && !this.source.equals(workingSource)) {
deleteFile(workingSource);
}
}
}
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private boolean alreadyRepackaged() throws IOException {
try (JarFile jarFile = new JarFile(this.source)) {
Manifest manifest = jarFile.getManifest();
return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null);
}
}
public final File getBackupFile() {
return new File(this.source.getParentFile(), this.source.getName() + ".original");
}
这里主要进行的历史文件的处理和重新命名,接下里看真正的打包过程:
//sourceJar:原始jar文件 destination:目标jar文件
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript)
throws IOException {
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
try (JarWriter writer = new JarWriter(destination, launchScript)) {
writer.writeManifest(buildManifest(sourceJar));
writeLoaderClasses(writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar,
new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
writeableLibraries);
}
else {
writer.writeEntries(sourceJar, writeableLibraries);
}
writeableLibraries.write(writer);
}
}
private WritableLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> {
if (isZip(library.getFile())) {
//这里到对应的打包目标中寻找jar包 这里有jar和war2种不同的实现,对应不同的目录
String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(),
library.getScope());
if (libraryDestination != null) {
//这里建立一个索引,方便查找
Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(),
library);
if (existing != null) {
throw new IllegalStateException("Duplicate library " + library.getName());
}
}
}
});
}
public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundException, IOException {
FileOutputStream fileOutputStream = new FileOutputStream(file);
if (launchScript != null) {
fileOutputStream.write(launchScript.toByteArray());
setExecutableFilePermission(file);
}
this.jarOutput = new JarArchiveOutputStream(fileOutputStream);
this.jarOutput.setEncoding("UTF-8");
}
private void setExecutableFilePermission(File file) {
try {
Path path = file.toPath();
//获取文件权限
Set<PosixFilePermission> permissions = new HashSet<>(Files.getPosixFilePermissions(path));
permissions.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(path, permissions);
}
catch (Throwable ex) {
// Ignore and continue creating the jar
}
}
这里先说下manifest文件的构建:
private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
private Manifest buildManifest(JarFile source) throws IOException {
Manifest manifest = source.getManifest();
if (manifest == null) {
manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
}
manifest = new Manifest(manifest);
String startClass = this.mainClass;
if (startClass == null) {
startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
}
if (startClass == null) {
startClass = findMainMethodWithTimeoutWarning(source);
}
String launcherClassName = this.layout.getLauncherClassName();
if (launcherClassName != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName);
if (startClass == null) {
throw new IllegalStateException("Unable to find main class");
}
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);
}
else if (startClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass);
}
String bootVersion = getClass().getPackage().getImplementationVersion();
manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion);
manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout)
? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation());
String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);
if (StringUtils.hasLength(lib)) {
manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);
}
return manifest;
}
private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
long startTime = System.currentTimeMillis();
String mainMethod = findMainMethod(source);
long duration = System.currentTimeMillis() - startTime;
if (duration > FIND_WARNING_TIMEOUT) {
for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
listener.handleTimeoutWarning(duration, mainMethod);
}
}
return mainMethod;
}
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
protected String findMainMethod(JarFile source) throws IOException {
return MainClassFinder.findSingleMainClass(source, this.layout.getClassesLocation(),
SPRING_BOOT_APPLICATION_CLASS_NAME);
}
这里有个查找MainMethod的寻找过程,可以看下findMainMethod:
public static String findSingleMainClass(JarFile jarFile, String classesLocation, String annotationName)
throws IOException {
SingleMainClassCallback callback = new SingleMainClassCallback(annotationName);
MainClassFinder.doWithMainClasses(jarFile, classesLocation, callback);
return callback.getMainClassName();
}
static <T> T doWithMainClasses(JarFile jarFile, String classesLocation, MainClassCallback<T> callback)
throws IOException {
List<JarEntry> classEntries = getClassEntries(jarFile, classesLocation);
classEntries.sort(new ClassEntryComparator());
for (JarEntry entry : classEntries) {
try (InputStream inputStream = new BufferedInputStream(jarFile.getInputStream(entry))) {
ClassDescriptor classDescriptor = createClassDescriptor(inputStream);
if (classDescriptor != null && classDescriptor.isMainMethodFound()) {
String className = convertToClassName(entry.getName(), classesLocation);
T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames()));
if (result != null) {
return result;
}
}
}
}
return null;
}
private static final String DOT_CLASS = ".class";
private static List<JarEntry> getClassEntries(JarFile source, String classesLocation) {
classesLocation = (classesLocation != null) ? classesLocation : "";
Enumeration<JarEntry> sourceEntries = source.entries();
List<JarEntry> classEntries = new ArrayList<>();
while (sourceEntries.hasMoreElements()) {
JarEntry entry = sourceEntries.nextElement();
if (entry.getName().startsWith(classesLocation) && entry.getName().endsWith(DOT_CLASS)) {
classEntries.add(entry);
}
}
return classEntries;
}
private static ClassDescriptor createClassDescriptor(InputStream inputStream) {
try {
ClassReader classReader = new ClassReader(inputStream);
ClassDescriptor classDescriptor = new ClassDescriptor();
//这里用asm读取处理封装成ClassDescriptor对象
classReader.accept(classDescriptor, ClassReader.SKIP_CODE);
return classDescriptor;
}
catch (IOException ex) {
return null;
}
}
private static String convertToClassName(String name, String prefix) {
name = name.replace('/', '.');
name = name.replace('\\', '.');
name = name.substring(0, name.length() - DOT_CLASS.length());
if (prefix != null) {
name = name.substring(prefix.length());
}
return name;
}
private String getMainClassName() {
Set<MainClass> matchingMainClasses = new LinkedHashSet<>();
if (this.annotationName != null) {
for (MainClass mainClass : this.mainClasses) {
if (mainClass.getAnnotationNames().contains(this.annotationName)) {
matchingMainClasses.add(mainClass);
}
}
}
if (matchingMainClasses.isEmpty()) {
matchingMainClasses.addAll(this.mainClasses);
}
if (matchingMainClasses.size() > 1) {
throw new IllegalStateException(
"Unable to find a single main class from the following candidates " + matchingMainClasses);
}
return (matchingMainClasses.isEmpty() ? null : matchingMainClasses.iterator().next().getName());
}
这里读取原始jar包中的文件,用ASM解析class文件,这里查找到包含main方法的类直接返回,而在getMainClassName方法中,有个路径:
- 如果注解不为空,默认注解为@SpringBootApplication,则有对应注解的都是符合条件的matchingMainClasses,如果解析的类中均没有注解且matchingMainClasses为空的,采用ASM解析过程中国存在main方法的对应类
- 如果注解为空,则直接采用ASM解析过程中国存在main方法的对应类
- 存在main方法的类型即matchingMainClasses只能有1个,不然报错
接下来看MANIFEST如何写出到文件:
public void writeManifest(Manifest manifest) throws IOException {
JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
writeEntry(entry, manifest::write);
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)
throws IOException {
String parent = entry.getName();
if (parent.endsWith("/")) {
parent = parent.substring(0, parent.length() - 1);
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
}
else {
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
}
if (parent.lastIndexOf('/') != -1) {
parent = parent.substring(0, parent.lastIndexOf('/') + 1);
if (!parent.isEmpty()) {
writeEntry(new JarArchiveEntry(parent), null, unpackHandler);
}
}
if (this.writtenEntries.add(entry.getName())) {
entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
this.jarOutput.putArchiveEntry(entry);
if (entryWriter != null) {
entryWriter.write(this.jarOutput);
}
this.jarOutput.closeArchiveEntry();
}
}
工具类不多说
writeLoaderClasses这里默认将spring-boot-loader.jar打包到了jar文件中,spring-boot-loader.jar负责spring应用的启动引导
private void writeLoaderClasses(JarWriter writer) throws IOException {
if (this.layout instanceof CustomLoaderLayout) {
((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);
}
else if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
}
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
}
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {
JarEntry entry;
while ((entry = inputStream.getNextJarEntry()) != null) {
if (entry.getName().endsWith(".class")) {
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
}
}
}
}
这里注意spring-boot-loader.jar(该类存在于插件内部)是spring应用的引导类,在writeLoaderClasses方法中,将他打包到了最终的目标文件中,
最后看下writeEntries的实现:
void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException {
this.writeEntries(jarFile, new IdentityEntryTransformer(), unpackHandler);
}
void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
throws IOException {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
setUpEntry(jarFile, entry);
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
if (transformedEntry != null) {
writeEntry(transformedEntry, entryWriter, unpackHandler);
}
}
}
}
工具类写出,不多说