Java使用类加载器解决类冲突
1、案例说明
项目中已经有了一个旧版本的poi库,并且这个库的版本无法修改,现在需要引入新版本的poi库,调用其中的公式方法IFS。之前想采用修改POI包名的方式,但是发现修改后各种报错无奈放弃。经过各种测试,本方法可以实现不同poi版本共存,因本人能力有限,部分代码可能写的不是最优,大家理解理解。
项目中真实包名啥的改成了xxxx,使用时主要改成正确的。
2、打包新版本POI并将要调用的方法封装
2.1、POM文件
xml
<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.xxxx</groupId>
<artifactId>poicustom</artifactId>
<version>2.0</version>
<packaging>jar</packaging>
<name>poicustom</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.2、封装的方法
java
package com.xxxx.poicustom;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class PoiUtil {
/**
* 评分公式计算
* @param formula 公式内容,其中目标值使用A1单元格,实际值使用B1单元格,权重使用C1单元格,结果会存储在D1单元格
* @param target 目标值
* @param actual 实际值
* @param weight 权重
* @return 返回不包含权重的得分
*/
public static double eval(String formula,double target,double actual,double weight) {
Workbook workbook = null;
try {
//创建表格
workbook = new XSSFWorkbook();
//创建sheet页
Sheet sheet = workbook.createSheet();
//创建行
Row row = sheet.createRow(0);
//创建目标值
Cell cellA1 = row.createCell(0, CellType.NUMERIC); // A1
cellA1.setCellValue(target);
//创建实际值
Cell cellB1 = row.createCell(1, CellType.NUMERIC); // B1
cellB1.setCellValue(actual);
//创建权重
Cell cellC1 = row.createCell(2, CellType.NUMERIC); // B1
cellC1.setCellValue(weight);
//创建结果
Cell cellD1 = row.createCell(3, CellType.FORMULA); // C1
cellD1.setCellFormula(formula); // 设置公式字符串
// 评估公式以获取结果
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
CellValue cellValue = evaluator.evaluate(cellD1);
// 输出结果
return cellValue.getNumberValue();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
workbook.close();
} catch (Exception e2) {
}
}
}
}
3、要使用多个POI版本的项目
3.1、打包前面的项目生成一个jar包
打包前面的项目,生成一个jar包,并将jar包放在本项目指定的目录
3.1、POM文件
使用maven-resources-plugin插件将jar前面打包的jar包复制到对应的target目录,注意在本地运行项目前要先执行maven的compile,确保对应的jar包出现在target目录对应的位置
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>xxxx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xxxx</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-test-jar</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/com/xxx/poi</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/java/com/xxx/poi</directory>
<includes>
<include>**/*.jar</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.2、类加载器代码
直接复制过去,不用改
java
package com.xxxx;
import java.net.URL;
import java.net.URLClassLoader;
/**
* jar类加载器
*/
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public static CustomClassLoader createWithJars(ClassLoader parent,URL... jarUrl) throws Exception {
return new CustomClassLoader(jarUrl, parent);
}
}
3.3、Jar加载工具
本类主要实现从一个jar包中加载对应的类,并且此功能会将当前项目父类加载器传给自定义类加载器,确保加载的类和当前项目不冲突。
此功能会自动从多层jar包中解压jar包,并且会自动删除之前解压的jar包,但是项目停止时不会删除最后一次解压的jar包,有需要的可以按照需求修改。
java
package com.xxxx.extandjar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 扩展jar处理
*/
@Slf4j
public class ExtendJarUtil {
//类加载器,每批jar使用一个新的类加载器加载
private CustomClassLoader classLoader;
/**
* 创建扩展jar处理工具
* @param jarPathArray
*/
public ExtendJarUtil(URL... urlArray) {
try {
log.info("------------------------原jar:"+JSONUtil.toJsonStr(urlArray));
urlArray = getNoNestingUrlArray(urlArray);
log.info("------------------------处理后jar:"+JSONUtil.toJsonStr(urlArray));
ClassLoader parent = ExtendJarUtil.class.getClassLoader().getParent();
classLoader = CustomClassLoader.createWithJars(parent,urlArray);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
/**
* 根据类名获取类
* @param name
* @return
* @throws ClassNotFoundException
*/
public Class<?> getClz(String name) throws ClassNotFoundException{
Class<?> clz = classLoader.loadClass(name);
return clz;
}
/**
* 获取一个不包含嵌套jar包的urlArray
* @param urlArray 包含jar包路径的urlArray,jar包路径可能会嵌套
* @return 包含jar包路径的urlArray,jar包路径不会嵌套
* @throws IOException
*/
public static URL[] getNoNestingUrlArray(URL[] urlArray) throws IOException{
//将从urlArray读取到的实际url添加的urlList
List<URL> urlList = new ArrayList<>();
//读取当前目录下extra开头,jar结尾的文件并删除,因为每次会解压出一些临时文件
File[] listFiles = new File(new File("").getAbsolutePath()).listFiles();
if(listFiles!=null && listFiles.length>0) {
for (File file : listFiles) {
if(file.isFile() && file.getName().startsWith("extrajar") && file.getName().endsWith(".jar")) {
FileUtil.del(file);
log.info("删除jar:"+file);
}
}
}
//遍历url,获取最终代表的jar包
for (URL url : urlArray) {
//将url转成字符串
String urlStr = url.toString();
//如果路径中不包含!,则直接将当前url添加到urlList并返回
if(!urlStr.contains("!")) {
urlList.add(url);
continue;
}
//如果路径中有!,则代表要获取的jar包含在某个jar中,这个可能是多层级嵌套,需要将最里面的jar解压到最外面并读取
//如果是jar:开头则去掉这个
if (urlStr.startsWith("jar:")) {
urlStr = urlStr.substring(4, urlStr.length());
}
//如果是file:开头则去掉这个
if (urlStr.startsWith("file:")) {
urlStr = urlStr.substring(5, urlStr.length());
}
File jarFile = getJarFile(urlStr);
urlList.add(jarFile.toURI().toURL());
}
URL[] array = ArrayUtil.toArray(urlList, URL.class);
return array;
}
/**
* 从一个jar文件的url中获取最终的文件,如果有嵌套则解压获取
* @param jarUrlStr jar文件的url
* @return jar文件
* @throws IOException
*/
public static File getJarFile(String jarUrlStr) throws IOException{
//按照!分割,切割后按照顺序分别是每一个层级的文件,需要从第一层文件中获取第二层文件,然后从第二层文件中获取第三层文件
String[] splitArray = jarUrlStr.split("!");
//外层的jar
File outFile = new File(splitArray[0]);
//循环从外层jar解压内部jar
for(int i=1;i<splitArray.length;i++) {
//外层的jar
JarFile outJarFile = new JarFile(outFile);
//内层的jar路径
String innerPath = splitArray[i];
//去掉之前的/,防止相对路径读取文件错误
if (innerPath.startsWith("/")) {
innerPath = innerPath.substring(1, innerPath.length());
}
//获取内部jar的in
InputStream innerIn = outJarFile.getInputStream(new JarEntry(innerPath));
//设置一个临时文件,用来解压内部的jar
File innerFile = new File("extrajar_"+UUID.randomUUID().toString(true)+".jar");
//解压内部jar
FileOutputStream innerOut = new FileOutputStream(innerFile);
IoUtil.copy(innerIn,innerOut);
//关闭资源
try {
innerIn.close();
} catch (Exception e) {
}
try {
innerOut.close();
} catch (Exception e) {
}
try {
outJarFile.close();
} catch (Exception e) {
}
//将解压的文件赋值到外层jar
outFile = innerFile;
}
return outFile;
}
}
3.4、最终调用
在项目中创建一个工具类,里面创建一个静态加载工具对象,并加载对应的jar包,然后写一个方法调用jar包中的方法。实现不同版本的POI隔离。
java
package com.xxxx.poi;
import java.lang.reflect.Method;
import com.xxxx.ExtendJarUtil;
/**
* poi工具
*/
public class PoiUtil {
/**
* 创建一个扩展jar处理
*/
private static final ExtendJarUtil poiJarUtil = new ExtendJarUtil(PoiUtil.class.getResource("poicustom-2.0.jar"));
/**
* 获取公式计算的值
* @param formula 公式
* @param target 目标值
* @param actual 实际值
* @param weight 权重
* @return
*/
public static double eval(String formula,double target,double actual,double weight) {
try {
Class<?> clz = poiJarUtil.getClz("com.xxxx.PoiUtil");
Method method = clz.getMethod("eval",String.class,double.class,double.class,double.class);
return (double)method.invoke(null,formula,target,actual,weight);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}