插件中的代码不可能都是手写的,本文将再接再厉,让插件可以依赖自己专属的3方库,当插件与业务代码依赖同一个名称的3方库时不会出现冲突情况。
注:本文将测试名为commons-lang3第3方库分别在插件与业务代码共存不同版本的case
插件目录说明
classes:插件核心代码
lib:插件所依赖的3方库
.
├── alioo-container-plugin-x1-1.0-SNAPSHOT-fat
│ ├── classes
│ │ └── com
│ │ └── lzc
│ │ └── alioo
│ │ └── container
│ │ └── plugin
│ │ └── x1
│ │ ├── HelloOneModel.class
│ │ └── HelloOneService.class
│ └── lib
│ ├── commons-lang3-3.10.jar
│ └── fastjson-1.2.83.jar
为了实现这个插件目录结构,尝试过一些maven自带的插件比如maven-assembly-plugin 和 maven-shade-plugin ,发现第1个需要额外的配置文件,第2个不能满足需求,它是直接打到一个jar中的
故自己实现了一个maven plugin的编写工作
自定义maven plugin
groupId:com.alioo.maven
artifactId:alioo-boot-maven-plugin
将插件绑定到package阶段,当执行mvn package时会自动触发此插件的运行
package com.alioo.maven;
import org.apache.commons.io.FileUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Build;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
@Mojo(name = "run", defaultPhase = LifecyclePhase.PACKAGE)
public class FatJarMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
public void execute() throws MojoExecutionException {
try {
getLog().info("Fat JAR(@alioo-boot-maven-plugin) project:" + project);
getLog().info("Fat JAR(@alioo-boot-maven-plugin) project.getBuild():" + toString(project.getBuild()));
getLog().info("Fat JAR(@alioo-boot-maven-plugin) project.getDependencies():" + project.getDependencies());
String buildDirectory = project.getBuild().getDirectory();
//创建plugin.properties
File classesDir = new File(buildDirectory, "/classes");
File libDir = new File(buildDirectory, "/lib");
if (!libDir.exists()) {
libDir.mkdirs();
}
for (Object artifactObj : project.getDependencyArtifacts()) {
Artifact artifact = (Artifact) artifactObj;
if (artifact.getScope() != null && artifact.getScope().equals("provided")) {
continue;
}
File sourceFile = new File(artifact.getFile().getAbsolutePath());
//如果依赖的jar包是system类型,则直接将依赖的jar包添加到fatJar中的lib目录下
getLog().info("Fat JAR(@alioo-boot-maven-plugin) dependencyArtifactPath =>" + sourceFile.getAbsolutePath());
FileUtils.copyFile(sourceFile, new File(libDir, sourceFile.getName()));
}
// Prepare output jar file name
File fatJar = new File(buildDirectory, project.getBuild().getFinalName() + "-fat.jar");
JarOutputStream jos = new JarOutputStream(new FileOutputStream(fatJar));
// 创建 classes 目录条目
addDir(jos, "classes/");
// 创建 lib 目录条目
addDir(jos, "lib/");
// Add project classes
addClasses(jos, classesDir, "");
for (File dependencyJar : libDir.listFiles()) {
addJar(jos, dependencyJar);
}
jos.close();
} catch (Exception e) {
throw new MojoExecutionException("Error assembling fat jar", e);
}
}
private void addClasses(JarOutputStream jos, File source, String prefix) throws IOException {
if (source.isDirectory()) {
for (File file : FileUtils.listFiles(source, null, true)) {
String entryName = prefix + source.toURI().relativize(file.toURI()).getPath();
if (file.isFile()) {
jos.putNextEntry(new JarEntry("classes/" + entryName));
FileUtils.copyFile(file, jos);
jos.closeEntry();
}
}
}
}
private void addJar(JarOutputStream jos, File jarFile) throws IOException {
JarEntry jarEntry = new JarEntry("lib/" + jarFile.getName());
jos.putNextEntry(jarEntry);
FileUtils.copyFile(jarFile, jos);
jos.closeEntry();
}
private static void addDir(JarOutputStream jos, String dir) throws IOException {
JarEntry libDirEntry = new JarEntry(dir);
jos.putNextEntry(libDirEntry);
jos.closeEntry();
}
public static String toString(Build build) {
//采用json的风格,输出入参对象的各个属性
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"directory\":\"").append(build.getDirectory()).append("\",");
sb.append("\"sourceDirectory\":\"").append(build.getSourceDirectory()).append("\",");
sb.append("\"testSourceDirectory\":\"").append(build.getTestSourceDirectory()).append("\",");
sb.append("\"outputDirectory\":\"").append(build.getOutputDirectory()).append("\",");
sb.append("\"testOutputDirectory\":\"").append(build.getTestOutputDirectory()).append("\",");
sb.append("\"extensions\":\"").append(build.getExtensions()).append("\",");
sb.append("\"defaultGoal\":\"").append(build.getDefaultGoal()).append("\",");
sb.append("\"resources\":\"").append(build.getResources()).append("\",");
sb.append("\"testResources\":\"").append(build.getTestResources()).append("\",");
sb.append("\"finalName\":\"").append(build.getFinalName()).append("\",");
sb.append("\"filters\":\"").append(build.getFilters()).append("\"");
sb.append("}");
return sb.toString();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alioo.maven</groupId>
<artifactId>alioo-boot-maven-plugin</artifactId>
<version>1.1-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<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.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>3.0-alpha-1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<goals>
<goal>descriptor</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
升级业务插件,采用alioo-boot-maven-plugin进行打包
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lzc.alioo.container.plugin</groupId>
<artifactId>alioo-container-plugin-x1</artifactId>
<version>1.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>alioo-container-plugin-x1</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alioo.maven</groupId>
<artifactId>alioo-boot-maven-plugin</artifactId>
<version>1.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.alioo.maven</groupId>
<artifactId>alioo-boot-maven-plugin</artifactId>
<version>1.1-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
升级业务容器
新增JarExtractor,自动完成业务插件解压动作
package com.lzc.alioo.container;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
public class JarExtractor {
public static void main(String[] args) {
// test case
String jarFilePath = "/Users/mac/alioo-plugin/alioo-container-plugin-x1-1.0-SNAPSHOT-fat.jar"; // 替换为你的JAR文件路径
try {
extractJar(jarFilePath);
} catch (IOException e) {
e.printStackTrace();
}
}
public static String extractJar(String jarFilePath) throws IOException {
String destDir = jarFilePath.substring(0, jarFilePath.lastIndexOf("."));
extractJar(jarFilePath, destDir);
return destDir;
}
public static void extractJar(String jarFilePath, String destDir) throws IOException {
File dir = new File(destDir);
if (dir.exists()) {
dir.delete();
}
dir.mkdirs();
try (JarInputStream jarIn = new JarInputStream(new FileInputStream(jarFilePath))) {
JarEntry entry;
while ((entry = jarIn.getNextJarEntry()) != null) {
File file = new File(destDir, entry.getName());
if (entry.isDirectory()) {
file.mkdirs();
} else {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try (FileOutputStream out = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int len;
while ((len = jarIn.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
jarIn.closeEntry();
}
}
}
}
升级PluginClassLoader,支持识别这种新的业务插件结构
package com.lzc.alioo.container;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Slf4j
public class PluginClassLoader extends URLClassLoader {
private static String pluginDir = null;
private PluginClassLoader(String module, URL[] urlPath) {
super(module, urlPath, ClassLoader.getSystemClassLoader());
}
public static PluginClassLoader init(String module, File pluginFile) {
URL[] urlPath = new URL[0];
try {
pluginDir = JarExtractor.extractJar(pluginFile.getAbsolutePath());
urlPath = new URL[]{pluginFile.toURI().toURL()};
} catch (Exception e) {
throw new RuntimeException(e);
}
PluginClassLoader instance = new PluginClassLoader(module, urlPath);
System.out.println("found alioo plugin by pluginPath:" + pluginFile.getAbsolutePath());
return instance;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class clazz = null;
//对于import的类优先使用lzcClassLoader进行统一加载
clazz = loadClassData(name);
if (clazz != null) {
log.info("Loaded By " + this.getName() + "(" + this + ") name: " + name);
}
return clazz;
}
public Class<?> loadClassData(String className) {
Class clazz = findLoadedClass(className);
if (clazz != null) {
return clazz;
}
try {
return super.loadClass(className);
} catch (ClassNotFoundException e) {
}
return null;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
//find class from classes
{
Path classPath = Paths.get(pluginDir, "classes", name.replace('.', '/') + ".class");
try {
if (Files.exists(classPath)) {
byte[] classData = Files.readAllBytes(classPath);
Class<?> cls = defineClass(name, classData, 0, classData.length);
// cache.put(name, cls);
return cls;
}
} catch (IOException e) {
throw new ClassNotFoundException("Class " + name + " not found by " + this.getName());
}
//find class from lib
{
File libDir = new File(pluginDir, "lib");
if (libDir.exists()) {
for (File file : libDir.listFiles(f -> f.getName().endsWith(".jar"))) {
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(file))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(path)) {
byte[] classData = zis.readAllBytes();
Class<?> cls = defineClass(name, classData, 0, classData.length);
// cache.put(name, cls);
return cls;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
throw new ClassNotFoundException("Class " + name + " not found by " + this.getName());
}
public List<String> getExportedClass() {
List<String> list = new ArrayList<>();
String startPathStr = pluginDir + "/classes";
Path startPath = Paths.get(startPathStr);
try {
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".class")) {
list.add(file.toString().substring(startPathStr.length() + 1));
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
}
完整代码库
业务插件
https://github.com/lzc-alioo/alioo-container-plugin-x1
注: alioo-container-plugin-x2 与alioo-container-plugin-x1相同故未上传
业务插件打包工具
https://github.com/lzc-alioo/alioo-boot-maven-plugin
业务容器
https://github.com/lzc-alioo/alioo-container
测试工程
https://github.com/lzc-alioo/classloadertest