Maven大家都很熟悉,插件也非常丰富。比如它的打包插件maven-assembly-plugin可以根据模板配置自己想要的打包内容,但当它的模板配置无法满足自己定制化打包需求时,此时就需要我们将maven研究的更深入些,利用自定义maven插件去实现我们自己的打包逻辑。
打包时,根据需要打包的模块.json配置文件,动态打入自己需要的Controller,排除掉不需要的模块Controller类。
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包,自行加入到插件的运行环境中。
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