如何打包發(fā)布一個springboot項目腰懂?
SpringBoot 提供了 Maven 插件 spring-boot-maven-plugin,將 Spring Boot 項目打成 jar 包或者 war 包茫多。
只需要在pom.xml文件中加入下面這個插件配置,再通過mvn clean package獲取jar包即可喂江。
<project ...>
...
<build>
...
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
打包后 通過下面的命令即可啟動一個服務(wù)胆敞。
java -jar **.jar
Jar包如何運行并啟動SpringBoot項目?
springboot jar 的目錄結(jié)構(gòu)
可以看到,主要有三個大目錄META-INF,BOOT-INF以及org携悯,
META-INF
比較重要的是MAINIFEST.MF文件:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: jarlearn
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.jsvc.jarlearn.JarlearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
該文件聲明了Main-Class 配置項:可以理解為jar包的啟動類祭芦,這里設(shè)置為 spring-boot-loader 項目的 JarLauncher類,進行 Spring Boot 應(yīng)用的啟動憔鬼。
還有一個Start-Class 配置項:配置的內(nèi)容是我們springboot項目的主啟動類龟劲。
BOOT-INF
classes文件中保存了 Java 類所編譯的 .class文件以及配置文件等。
lib目錄中保存了我們項目所依賴的jar包轴或。
org
該文件中即springboot為我們提供的jar包啟動類昌跌,亦即JarLauncher.class
當(dāng)使用 java -jar filename.jar
命令啟動時,會執(zhí)行封裝在 JAR 文件中的程序照雁。JAR 文件需包含 manifest蚕愤,其中一行格式為 Main-Class:classname,指定了一個包含 public static void main(String[] args) 方法的類,作為該程序的啟動點萍诱。
為什么不可以直接Main-Class 放置為springboot的啟動類呢悬嗓?
對應(yīng)在示例的這個項目,問題可以翻譯為為什么不可以直接使用com.jsvc.jarlearn.JarlearnApplication類作為啟動類砂沛?
主要是因為烫扼,Java 沒有提供任何加載嵌套 jar 文件的標準方法(即加載本身包含在 jar 中的 jar 文件)。當(dāng)需要分發(fā)一個可以從命令行運行而不需要解壓縮的自包含應(yīng)用程序時 , 會出現(xiàn)問題碍庵。
同時映企,我試了下,直接運行application類的話静浴,是找不到主類的:
java -classpath /Users/sensu/jarl/jarlearn-0.0.1-SNAPSHOT.jar com.jsvc.jarlearn.JarlearnApplication
錯誤: 找不到或無法加載主類 com.jsvc.jarlearn.JarlearnApplication
因為在文件目錄中堰氓,JarlearnApplication實際上是在META-INF/maven/... 中的,所以會找不到苹享。
所以双絮,springboot以org.springframework.boot.loader.JarLauncher
為啟動類,
又自定義了LaunchedURLClassLoader
用來加載BOOT-INF中的class文件以及BOOT-INF/lib中的嵌套jar包得问。
關(guān)于JarLaunch
我這邊通過引入spring-boot-loader
模塊來看下JarLaunch的源碼:
//JarLauncher
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
可以看到main方法中囤攀,執(zhí)行了launch方法,改方法由JarLaunch的父類Launcher提供:
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
launch方法主要分為三步:
- 在launch方法中,首先調(diào)用了registerUrlProtocolHandler方法注冊URLStreamHandler類用于jar包的解析.
- 調(diào)用了createClassLoader方法宫纬,創(chuàng)建自定義的ClassLoader,用于從jar中加載類焚挠。
- 執(zhí)行Application啟動類。
registerUrlProtocolHandler
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
Handler.captureJarContextUrl();
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
基本思路就是將org.springframework.boot.loader
包路徑添加到java.protocol.handler.pkgs
環(huán)境變量中漓骚,從而使用自定義的 URLStreamHandler
實現(xiàn)類 Handler處理 jar:
協(xié)議的 URL蝌衔。
關(guān)于handler 可以自行百度下。
createClassLoader
這里有兩個主要方法:
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
也就是 getClassPathArchivesIterator
以及createClassLoader
首先是 getClassPathArchivesIterator
:
// ExecutableArchiveLauncher
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
首先是isSearchCandidate蝌蹂,在JarLaunch中實現(xiàn):
//JarLaunch
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
可以看出是只處理BOOT-INF/文件夾下的內(nèi)容噩斟。
然后會通過getNestedArchives
獲取到嵌套的Archive,其中的isNestedArchive
方法也由JarLaunch實現(xiàn):
//JarLaunch
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
基本就是獲取 BOOT-INF/classes/
下的目錄以及BOOT-INF/lib/
下的jar文件剃允,最終通過getNestedArchives
將其封裝為對應(yīng)的Archive并返回。
然后就是createClassLoader
方法:
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
* @since 2.3.0
*/
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
基本上就是通過archives獲取到所有的URL齐鲤,然后創(chuàng)建處理這些URL的ClassLoader硅急。
執(zhí)行Application啟動類方法
主要就是通過getMainClass
方法獲取到manifest文件中配置的Start-Class
:
// ExecutableArchiveLauncher
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
然后通過另一個launch方法,開始執(zhí)行:
// Launcher
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param launchClass the launch class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();
}
這里createMainMethodRunner創(chuàng)建出來的是什么呢佳遂?
// MainMethodRunner
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
最終調(diào)用的其實就是MainMethodRunner的run方法了营袜,其實也就是通過反射調(diào)用Application的main方法了。