基于maven-jar-plugin打造一款自动识别主类的maven打包插件

🧑 博主简介:CSDN博客专家历代文学网 (PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索"历代文学 ")总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生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需要包含以下核心依赖:

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:插件开发的基础API
  • maven-plugin-annotations:简化开发的注解支持
  • spring-boot-loader-tools:借用Spring Boot强大的类扫描能力

二、核心代码实现解析

2.1 Mojo类完整实现

这是我们插件的"大脑",所有魔法都发生在这里:

java 复制代码
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的启动注解
     */
    @Parameter
    private String findByAnnotationOnMainClass;

    @Override
    public 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 -> mainClass
        Xpp3Dom 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;
    }
}

代码亮点解读:

  1. 双模式扫描 :既支持传统main方法,也支持注解标记
  2. 配置兼容:优先保留用户自定义配置
  3. 智能提示 :通过getLog()输出构建日志
  4. 健壮性检查:对可能缺失的插件进行预校验

三、插件使用指南

3.1 基础配置

在需要使用的项目中添加:

xml 复制代码
<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 高级配置示例

当需要使用注解过滤时:

xml 复制代码
<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文件后),这样就能:

  1. 确保扫描到最新编译的类
  2. 在打包前完成主类配置

4.2 主类扫描的底层逻辑

借助MainClassFinder的两个核心方法:

java 复制代码
// 扫描标准main方法
public static String findSingleMainClass(File directory)

// 扫描带指定注解的类
public static String findSingleMainClass(File directory, 
                                      String annotationClassName)

其内部实现原理是:

  1. 遍历目录下的所有.class文件
  2. 使用ASM解析字节码
  3. 检查是否包含main方法
  4. 验证类/方法修饰符是否符合规范
  5. 检查注解标记(如果配置了的话)

4.3 动态配置的奥秘

我们通过让mainclass-finder-maven-plugin插件去修改Maven项目的PropertiesPlugin配置来实现动态注入:

java 复制代码
// 设置全局属性
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插件生态的强大之处。当发现重复的配置工作时,不妨停下来想想:能不能通过自动化手段解决?

这个插件现在已经在我的团队内部使用了半年多,累计节省了数百小时的配置时间。希望它也能给你的项目带来便利,更期待你能在此基础上扩展出更强大的功能!

相关推荐
异常君7 分钟前
分布式锁隐患解析:当业务执行时间超过锁过期时间的完整对策
java·redis·后端
V功夫兔8 分钟前
Spring_MVC 快速入门指南
java·笔记·spring·springmvc
掘金詹姆斯10 分钟前
在项目中如何进行分库分表?
java·mysql
旅行的狮子11 分钟前
二、在springboot 中使用 AIService
java·spring boot·langchain4j
扎瓦13 分钟前
Java 动态代理
java·后端·面试
码农小灰19 分钟前
Java 自动装箱与拆箱:基本数据类型与包装类的转换
java
Ares-Wang19 分钟前
kubernetes》》k8s》》Endpoint
java·容器·kubernetes
CatShitK37 分钟前
【Android】 如何将 APK 内置为系统应用(适用于编辑设置属性)
android·java·linux
努力努力再努力wz38 分钟前
【C++深入系列】:模版详解(上)
java·c语言·开发语言·c++
颇有几分姿色1 小时前
深入理解路由器、IP地址及网络配置
java·网络·计算机网络