Java使用类加载器解决类冲突,多版本jar共存

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);
        }
	}
}
相关推荐
阿伟*rui1 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei3 小时前
java的类加载机制的学习
java·学习
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~5 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616885 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7895 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust