自定义Maven打包插件

前言

  Maven大家都很熟悉,插件也非常丰富。比如它的打包插件maven-assembly-plugin可以根据模板配置自己想要的打包内容,但当它的模板配置无法满足自己定制化打包需求时,此时就需要我们将maven研究的更深入些,利用自定义maven插件去实现我们自己的打包逻辑。

自定义Maven打包插件实战

打包业务描述:

  打包时,根据需要打包的模块.json配置文件,动态打入自己需要的Controller,排除掉不需要的模块Controller类。

打包插件设计思路:

自定义Maven打包插件_第1张图片

插件使用如下:

自定义Maven打包插件_第2张图片

一 、先创建Maven项目,编写pom.xml



    4.0.0

    org.example
    pre-maven-plugin
    maven-plugin
    1.0-SNAPSHOT

    
        8
        8
        3.5.0
        3.5.2
        0.9.10
        1.2.58
        1.18.16
        2.6
    

    
        
            org.apache.maven
            maven-plugin-api
            ${maven.plugin.api}
        
        
            org.apache.maven.plugin-tools
            maven-plugin-annotations
            ${maven-plugin-annotations}
            provided
        
        
            org.reflections
            reflections
            ${reflections.version}
        
        
            com.alibaba
            fastjson
            ${fastjson.version}
        
        
            org.projectlombok
            lombok
            ${lombok.version}
            true
        
        
            commons-io
            commons-io
            ${commons-io.version}
        
    

    
        
            
                org.apache.maven.plugins
                maven-plugin-plugin
                3.6.0
            
        
    



注意:maven插件的 packaging 必须是maven-plugin,其次是需要依赖的jar包maven-plugin-api、maven-plugin-annotations。

二、编写插件实现类

package mojo;

import com.alibaba.fastjson.JSON;
import dto.Module;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
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 java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.util.*;
import util.*;

/**
 * @author zkc
 * @version 1.0.0
 * @since 2020-12-21
 */
@Mojo(name = "jwfRepackage", defaultPhase = LifecyclePhase.PACKAGE)
@Data
public class ZkcRepackageMojo extends AbstractMojo {
    /**
     * 应用的模块json配置文件,支持“,”逗号分隔多个应用
     */
    @Parameter(required = true)
    private String applicationModuleJson;
    /**
     * 配置应用模块json所在文件夹名称,默认application
     */
    @Parameter
    private String applicationFolderName = "application";
    /**
     * maven编译时classes路径,只读,不需要手动配置
     */
    @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
    private List compilePath;
    /**
     * maven项目中版本号,只读,不需要手动配置
     */
    @Parameter(defaultValue = "${project.version}", readonly = true, required = true)
    private String projectVersion;
    /**
     * maven项目中artifactId,只读,不需要手动配置
     */
    @Parameter(defaultValue = "${project.artifactId}", readonly = true, required = true)
    private String artifactId;


    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {

        long startTime = System.currentTimeMillis();
        System.out.println(MessageFormat.format("自定义分模块打包插件开始执行:[{0}]", startTime));

        // 解析应用模块json配置文件
        List moduleList = analysisApplicationModuleJson(StringUtils.split(applicationModuleJson, ","));
        if (CommonUtil.isCollectionEmpty(moduleList)) {
            System.out.println(MessageFormat.format("应用模块json解析为空:maven编译地址[{0}],应用模块json地址[{1}]", String.join(",", compilePath), applicationModuleJson));
            return;
        }

        // 自定义类加载器,加载原始jar包,过滤出非应用的模块Controller类
        String moduleJarPath = PathUtil.buildJarPath(compilePath) + PathUtil.buildJarName(artifactId, projectVersion);
        Set excludeClasses = getExcludeClasses(moduleJarPath, moduleList);
        if (CommonUtil.isCollectionEmpty(excludeClasses)) {
            long endTime = System.currentTimeMillis();
            System.out.println(MessageFormat.format("自定义分模块打包插件结束执行,没有需要排除的模块Class,耗时:[{0}]毫秒", endTime - startTime));
            return;
        }

        // 删除无用的应用模块Controller类,重新打包
        try {
            JarUtil.delete(moduleJarPath, excludeClasses);
        } catch (Exception e) {
            System.out.println(MessageFormat.format("删除无用应用模块Controller类,重新打包失败:[{0}],[{1}]", moduleJarPath, excludeClasses));
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println(MessageFormat.format("自定义分模块打包插件结束执行,供排除[{0}]个非应用模块Class,耗时:[{1}]毫秒", excludeClasses.size(), endTime - startTime));

    }

    /**
     * 解析应用模块json配置文件
     *
     * @param moduleJsons 应用模块json配置地址
     * @return 返回解析后的模块集合
     */
    private List analysisApplicationModuleJson(String[] moduleJsons) {
        if (CommonUtil.isArrayEmpty(moduleJsons) || CommonUtil.isCollectionEmpty(compilePath)) {
            return null;
        }
        List moduleList = new ArrayList<>();
        Arrays.stream(moduleJsons).forEach(moduleJson -> {
            String moduleJsonPath = PathUtil.buildModuleJsonPath(compilePath, moduleJson, applicationFolderName);
            File jsonFile = new File(moduleJsonPath);
            if (!jsonFile.exists()) {
                System.out.println(MessageFormat.format("应用模块json文件未找到:[{0}]", moduleJsonPath));
            }
            try {
                List moduleListTemp = JSON.parseArray(new String(Files.readAllBytes(jsonFile.toPath())),
                        Module.class);
                moduleList.addAll(moduleListTemp);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        return moduleList;
    }

    /**
     * 自定义类加载器,加载原始jar包,过滤出非应用的模块Controller类
     *
     * @param moduleJarPath 原始jar包路径
     * @param moduleList    应用模块类集合
     * @return 返回需要过滤的模块类
     */
    @SuppressWarnings("unchecked")
    private Set getExcludeClasses(String moduleJarPath, List moduleList) {
        Set excludeClasses = new HashSet<>();
        ModuleClassLoader moduleClassLoader = new ModuleClassLoader(moduleJarPath);
        ModuleClassLoader.getModuleClassMap().forEach((className, bytes) -> {
            // 解析@ModuleController注解
            try {
                Class clazz = moduleClassLoader.loadClass(className);
                Annotation[] annotations = clazz.getAnnotationsByType(moduleClassLoader.loadClass(PathUtil.MODULE_CLASS));
                for (Annotation annotation : annotations) {
                    Class aClass = annotation.getClass();
                    try {
                        Method method = aClass.getMethod(PathUtil.MODULE_METHOD);
                        String value = (String) method.invoke(annotation);
                        boolean moduleClassFlag = moduleList.stream().anyMatch(module -> Objects.equals(module.getModuleId(), value));
                        if (!moduleClassFlag) {
                            excludeClasses.add(className);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } catch (ClassNotFoundException e) {
                System.out.println(MessageFormat.format("加载[{0}]类报错,因未加载对应jar包,可忽略", className));
            }
        });
        return excludeClasses;
    }

}

注意:maven插件支持注解方式和Javadoc 方式开发,本实战中使用注解方式@Mojo指定插件的目标类和插件的运行阶段。

三、自定义类加载

package util;

import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author zkc
 * @version 1.0.0
 * @since 2020-12-21
 */
public class ModuleClassLoader extends ClassLoader {

    private final static Map MODULE_CLASS_MAP = new ConcurrentHashMap<>();

    public ModuleClassLoader(String moduleJarPath) {
        try {
            readJar(moduleJarPath);
        } catch (IOException e) {
            System.out.println(MessageFormat.format("读取jar包异常:地址[{0}]", moduleJarPath));
            e.printStackTrace();
        }
    }

    public static void addClass(String className, byte[] byteCode) {
        if (!MODULE_CLASS_MAP.containsKey(className)) {
            MODULE_CLASS_MAP.put(className, byteCode);
        }
    }

    /**
     * 遵守双亲委托规则
     *
     * @param name 类全路径
     * @return 返回类字节码对象
     */
    @Override
    protected Class findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result != null) {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        return MODULE_CLASS_MAP.getOrDefault(className, null);
    }

    private void readJar(String moduleJarPath) throws IOException {
        readJarFile(moduleJarPath);
    }

    private void readJarFile(String moduleJarPath) throws IOException {
        File file = new File(moduleJarPath);
        if (!file.exists()) {
            throw new FileNotFoundException();
        }
        JarFile jarFile = new JarFile(file);
        Enumeration enumeration = jarFile.entries();
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = enumeration.nextElement();
            String name = jarEntry.getName().replace("BOOT-INF/classes/", "").replace("\\", ".")
                    .replace("/", ".");
            if (isRead(name)) {
                String className = name.replace(".class", "");
                try (InputStream input = jarFile.getInputStream(jarEntry); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private boolean isRead(String name) {
        return StringUtils.isNotBlank(name) && name.endsWith(PathUtil.CLASS)
                && (name.startsWith(PathUtil.COMMON_CONTROLLER) || name.startsWith(PathUtil.COMMON_ANNOTATION));
    }

    public static Map getModuleClassMap() {
        return MODULE_CLASS_MAP;
    }
}


注意:maven插件运行时,插件的运行环境中类加载器肯定只会加载插件本身的class信息。如果你想操作应用的类,需要自定义类加载器,通过一定的寻址逻辑找到你想加载的jar包,自行加入到插件的运行环境中。

四、备份原始springboot的jar包,生成新jar包

package util;

import org.apache.commons.io.FileUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
 * @author zkc
 * @version 1.0.0
 * @since 2020-12-21
 */
public class JarUtil {

    public static void delete(String jarName, Set deletes) throws Exception {
        // 先备份原始jar包
        File oriFile = new File(jarName);
        if (!oriFile.exists()) {
            System.out.println(MessageFormat.format("排除类时,原始jar包不存在[{0}]", jarName));
            return;
        }
        // 以时间戳重命名备份文件名
        String bakJarName = jarName.substring(0, jarName.length() - 3) + System.currentTimeMillis() + PathUtil.JAR;
        File bakFile = new File(bakJarName);
        // 备份原始jar包
        FileUtils.copyFile(oriFile, bakFile);
        // 排除deletes中的class,重新打包新jar包文件
        JarFile bakJarFile = new JarFile(bakJarName);
        JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarName));
        Enumeration entries = bakJarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String name = entry.getName().replace("BOOT-INF/classes/", "").replace("\\", ".").replace("/", ".").replace(".class", "");
            if (!deletes.contains(name)) {
                InputStream inputStream = bakJarFile.getInputStream(entry);
                jos.putNextEntry(entry);
                byte[] bytes = readStream(inputStream);
                jos.write(bytes, 0, bytes.length);
            } else {
                System.out.println(MessageFormat.format("排除非应用模块类:[{0}]", entry.getName()));
            }
        }
        // 关闭流
        jos.flush();
        jos.finish();
        jos.close();
        bakJarFile.close();
    }

    private static byte[] readStream(InputStream inStream) throws Exception {
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = -1;
        while ((len = inStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, len);
        }
        outSteam.close();
        inStream.close();
        return outSteam.toByteArray();
    }

}

五、自定义模块注解类

package com.zkc.web.annotation;

import java.lang.annotation.*;

/**
 * @author zkc
 * @version 1.0.0
 * @since 2020-12-21
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Module {

    String moduleId();

    String moduleName();

}

插件运行效果

[INFO] 
[INFO] --- pre-maven-plugin:1.0-SNAPSHOT:jwfRepackage (default) @ web ---
自定义分模块打包插件开始执行:[1,608,560,919,883]
排除非应用模块类:[BOOT-INF/classes/com/zkc/web/controller/ProductController.class]
排除非应用模块类:[BOOT-INF/classes/com/zkc/web/controller/RoleController.class]
自定义分模块打包插件结束执行,供排除[2]个非应用模块Class,耗时:[153]毫秒
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

完整代码以上传码云

码云地址: git地址


我的专栏

  1. 设计模式
  2. 认证授权框架实战
  3. java进阶知识
  4. maven进阶知
  5. spring进阶知识

你可能感兴趣的:(maven进阶知识,maven,java)