🧑 博主簡介:CSDN博客專家,歷代文學網(PC端可以訪問:https://literature.sinhy.com/#/?__c=1000,移動端可微信小程序搜索“歷代文學”)總架構師,
15年
工作經驗,精通Java編程
,高并發設計
,Springboot和微服務
,熟悉Linux
,ESXI虛擬化
以及云原生Docker和K8s
,熱衷于探索科技的邊界,并將理論知識轉化為實際應用。保持對新技術的好奇心,樂于分享所學,希望通過我的實踐經歷和見解,啟發他人的創新思維。在這里,我希望能與志同道合的朋友交流探討,共同進步,一起在技術的世界里不斷學習成長。
技術合作請加本人wx(注明來自csdn):foreast_sea
基于maven-jar-plugin打造一款自動識別主類的maven打包插件
引言
相信每個Java開發者都經歷過這樣的場景:新建一個可執行JAR項目時,總要花幾分鐘在pom.xml里翻找主類路徑,然后小心翼翼地配置到maven-jar-plugin
里。更痛苦的是當項目有多個main
方法時,稍不留神就會打包失敗。這種重復勞動不僅浪費時間,還容易埋下隱患。
在Java項目構建過程中,MANIFEST.MF
文件中的Main-Class
屬性配置是一個關鍵但容易出錯的環節。傳統方式需要在pom.xml中顯式聲明主類路徑,這不僅增加了維護成本,在大型多模塊項目中更可能因配置遺漏導致運行時異常。Spring Boot通過@SpringBootApplication
注解實現自動識別主類的機制廣受好評,但在非Spring Boot
項目中這種能力卻難以直接復用。
本文將深入探討如何通過開發mainclass-finder-maven-plugin
自定義插件,在Maven構建體系中實現智能主類識別與自動注入,既支持常規main
方法識別,也可通過指定注解靈活定位主類,最終無縫集成到標準maven-jar-plugin
打包流程中。這種方案不僅能提升構建效率,更實現了構建配置的智能化演進。
一、插件開發環境準備
插件名:mainclass-finder-maven-plugin
1.1 創建Maven插件項目
首先新建一個標準的Maven項目,pom.xml需要包含以下核心依賴:
<!-- 插件核心依賴 -->
<dependencies><!-- Maven插件開發API --><dependency><groupId>org.apache.maven</groupId><artifactId>maven-plugin-api</artifactId><version>3.8.6</version></dependency><!-- 注解處理器 --><dependency><groupId>org.apache.maven.plugin-tools</groupId><artifactId>maven-plugin-annotations</artifactId><version>3.6.4</version><scope>provided</scope></dependency><!-- 秘密武器:Spring Boot的類掃描工具 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-loader-tools</artifactId><version>2.7.12</version></dependency>
</dependencies><!-- 插件打包配置 -->
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-plugin-plugin</artifactId><version>3.6.4</version></plugin></plugins>
</build>
關鍵依賴說明:
maven-plugin-api
:插件開發的基礎APImaven-plugin-annotations
:簡化開發的注解支持spring-boot-loader-tools
:借用Spring Boot強大的類掃描能力
二、核心代碼實現解析
2.1 Mojo類完整實現
這是我們插件的"大腦",所有魔法都發生在這里:
import java.io.File;
import java.io.IOException;import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.springframework.boot.loader.tools.MainClassFinder;
import com.sinhy.nature.utils.ObjectUtils;/*** 主類發現者插件核心實現* 在PROCESS_CLASSES階段掃描類文件,智能識別主類* * @author lilinhai* @since 2025-04-20 09:49* @version V1.0*/
@Mojo(name = "find", defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class MainClassFinderMojo extends AbstractMojo {// 類文件輸出目錄(默認target/classes)@Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true)private File classesDirectory;// Maven項目上下文@Parameter(defaultValue = "${project}", readonly = true)private MavenProject project;/*** 主類屬性名配置(可自定義)* 示例:存放到mainClassFoundByFinderPlugin屬性*/@Parameter(defaultValue = "mainClassFoundByFinderPlugin")private String mainClassAssignmentAttributeName;/*** 按注解過濾主類(可選配置)* 示例:使用SpringBoot的啟動注解*/@Parameterprivate String findByAnnotationOnMainClass;@Overridepublic void execute() throws MojoExecutionException, MojoFailureException {try {// 核心掃描邏輯String mainClass = detectMainClass();// 屬性注入和配置修改configureJarPlugin(mainClass);} catch (IOException e) {throw new MojoFailureException("主類掃描失敗", e);}}private String detectMainClass() throws IOException, MojoFailureException {String mainClass;// 根據是否配置注解決定掃描策略if (ObjectUtils.isEmpty(findByAnnotationOnMainClass)) {getLog().info("開始掃描標準main方法...");mainClass = MainClassFinder.findSingleMainClass(classesDirectory);} else {getLog().info("按注解[" + findByAnnotationOnMainClass + "]掃描主類...");mainClass = MainClassFinder.findSingleMainClass(classesDirectory, findByAnnotationOnMainClass);}if (mainClass == null) {throw new MojoFailureException("未找到符合條件的主類!檢查:"+ (findByAnnotationOnMainClass != null ? "注解@" + findByAnnotationOnMainClass : "main方法"));}return mainClass;}private void configureJarPlugin(String mainClass) throws MojoExecutionException {// 將主類路徑存入Maven屬性池project.getProperties().setProperty(mainClassAssignmentAttributeName, mainClass);getLog().info("成功識別主類:" + mainClass);// 動態修改maven-jar-plugin配置Plugin jarPlugin = project.getBuild().getPluginsAsMap().get("org.apache.maven.plugins:maven-jar-plugin");if (jarPlugin == null) {throw new MojoExecutionException("請確保已配置maven-jar-plugin!");}// 構建或修改XML配置節點Xpp3Dom config = (Xpp3Dom) jarPlugin.getConfiguration();if (config == null) {config = new Xpp3Dom("configuration");jarPlugin.setConfiguration(config);}// 層級結構:configuration -> archive -> manifest -> mainClassXpp3Dom archiveNode = getOrCreateNode(config, "archive");Xpp3Dom manifestNode = getOrCreateNode(archiveNode, "manifest");Xpp3Dom mainClassNode = getOrCreateNode(manifestNode, "mainClass");// 智能設置值(優先保留用戶配置)if (mainClassNode.getValue() == null || mainClassNode.getValue().contains("${")) {mainClassNode.setValue(mainClass);getLog().info("已自動配置maven-jar-plugin");}}// 輔助方法:獲取或創建XML節點private Xpp3Dom getOrCreateNode(Xpp3Dom parent, String nodeName) {Xpp3Dom node = parent.getChild(nodeName);if (node == null) {node = new Xpp3Dom(nodeName);parent.addChild(node);}return node;}
}
代碼亮點解讀:
- 雙模式掃描:既支持傳統
main
方法,也支持注解標記 - 配置兼容:優先保留用戶自定義配置
- 智能提示:通過
getLog()
輸出構建日志 - 健壯性檢查:對可能缺失的插件進行預校驗
三、插件使用指南
3.1 基礎配置
在需要使用的項目中添加:
<build><plugins><!-- 我們的智能插件 --><plugin><groupId>com.sinhy</groupId><artifactId>mainclass-finder-maven-plugin</artifactId><version>2.1.0</version><executions><execution><phase>process-classes</phase><goals><goal>find</goal></goals></execution></executions></plugin><!-- 標準打包插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifest><!-- 這里使用動態注入的屬性 --><mainClass>${mainClassFoundByFinderPlugin}</mainClass></manifest></archive></configuration></plugin></plugins>
</build>
3.2 高級配置示例
當需要使用注解過濾時:
<plugin><groupId>com.sinhy</groupId><artifactId>mainclass-finder-maven-plugin</artifactId><configuration><findByAnnotationOnMainClass>org.springframework.boot.autoconfigure.SpringBootApplication</findByAnnotationOnMainClass><!-- 可選:自定義屬性名 --><mainClassAssignmentAttributeName>autoDetectedMainClass</mainClassAssignmentAttributeName></configuration><!-- ...其余配置同上... -->
</plugin>
四、工作原理深度剖析
4.1 執行時機把控
我們將插件綁定到process-classes
階段(即編譯生成class文件后),這樣就能:
- 確保掃描到最新編譯的類
- 在打包前完成主類配置
4.2 主類掃描的底層邏輯
借助MainClassFinder
的兩個核心方法:
// 掃描標準main方法
public static String findSingleMainClass(File directory)// 掃描帶指定注解的類
public static String findSingleMainClass(File directory, String annotationClassName)
其內部實現原理是:
- 遍歷目錄下的所有.class文件
- 使用ASM解析字節碼
- 檢查是否包含main方法
- 驗證類/方法修飾符是否符合規范
- 檢查注解標記(如果配置了的話)
4.3 動態配置的奧秘
我們通過讓mainclass-finder-maven-plugin
插件去修改Maven項目的Properties
和Plugin
配置來實現動態注入:
// 設置全局屬性
project.getProperties().setProperty("mainClassFoundByFinderPlugin", "com.example.Main");// mainclass-finder-maven-plugin插件修改maven-jar-plugin的配置后的新XML配置
<configuration><archive><manifest><mainClass>com.example.Main</mainClass></manifest></archive>
</configuration>
五、插件運行測試效果
如下圖所示,是ddns應用項目成功的實踐。圖中顯示已通過mainclass-finder-maven-plugin
插件成功自動獲取到項目的主類,且成功注入到jar包中的MANIFEST.MF
文件中。
總結
經過這個插件的開發實踐,我們不僅解決了具體的工程問題,更重要的是體會到了Maven插件生態的強大之處。當發現重復的配置工作時,不妨停下來想想:能不能通過自動化手段解決?
這個插件現在已經在我的團隊內部使用了半年多,累計節省了數百小時的配置時間。希望它也能給你的項目帶來便利,更期待你能在此基礎上擴展出更強大的功能!