相信在实际工作中,大家可能会遇到这种需求,这个jar是外部的,并没有添加到项目依赖中,只能通过类加载器加载并调用相关方法。
这种jar加载,其实也简单,我们通过普通的URLClassLoader就可以加载。代码如下所示:
public static URLClassLoader getClassLoader(String url) {
URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());
try {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
if (!method.isAccessible()) {
method.setAccessible(true);
}
method.invoke(classLoader, new URL("file:" + url));
return classLoader;
} catch (NoSuchMethodException | MalformedURLException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
这里,只是把jar加载到了虚拟机中,要调用,我们需要根据类名与类加载器来 获取类对象。
public static List<Class<?>> getClassListByInterface(String url, ClassLoader classLoader, Class<?> clazz) {
List<Class<?>> classList = new ArrayList<>();
if (!clazz.isInterface()) {
return classList;
}
List<String> classNames = new ArrayList<>();
try (JarFile jarFile = new JarFile(url)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String entryName = jarEntry.getName();
if (entryName != null && entryName.endsWith(".class")) {
entryName = entryName.replace("/", ".").substring(0, entryName.lastIndexOf("."));
classNames.add(entryName);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (classNames.size() > 0) {
for (String className : classNames) {
try {
Class<?> theClass = classLoader.loadClass(className);
if (clazz.isAssignableFrom(theClass) && !theClass.equals(clazz)) {
classList.add(theClass);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
return classList;
}
还需要做一些准备:
1,定义一个接口,外部去实现:
import java.util.Map;
public interface IService {
String name();
Object process(Map<String, Object> params);
}
2、三方实现:
import com.example.service.IService;
import com.example.util.FileUtil;
import java.util.HashMap;
import java.util.Map;
public class BusinessService implements IService {
@Override
public String name() {
return "Test";
}
@Override
public Object process(Map<String, Object> params) {
Map<String, Object> result = new HashMap<>();
result.put("name", name());
result.put("path", FileUtil.getPath("tmp"));
return result;
}
}
我们在这个实现里面,故意调用了另一个三方的依赖FileUtil:
import java.io.File;
public class FileUtil {
public static String getPath(String name) {
return String.join(File.separator, "D:", name);
}
}
打包插件配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<!--告知maven-jar-plugin添加一个classpath到manifest.mf文件,以及在class-path元素中包含所有依赖项-->
<addClasspath>true</addClasspath>
<!--所有依赖应该位于lib包 -->
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/lib</outputDirectory>
<!--是否不包含间接依赖包-->
<excludeTransitive>false</excludeTransitive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
我们会把三方实现打jar包,jar包含了所需的依赖:
实际中使用,我们需要先获取类加载器,然后获取类对象,再实例化,最后调用对象的方法。
public class BusinessTest {
public static void main(String[] args) {
String url = "E:\\workspace\\java\\classloaderexample\\common-test\\target\\common-test-1.0-SNAPSHOT.jar";
URLClassLoader classLoader = ClassLoaderUtil.getClassLoader(url);
// URLClassLoader classLoader = ClassLoaderUtil.loadAllJar(url);
List<Class<?>> classList = ClassLoaderUtil.getClassListByInterface(url, classLoader, IService.class);
System.out.println(classList);
for (Class clazz : classList) {
try {
IService service = (IService) clazz.newInstance();
System.out.println(service.process(null));
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
运行报错:
这里报错信息,提示是NoClassDefFoundError:com/example/util/FileUtil。
但是我们进行打包的时候,明明把FileUtil的jar打到了common-test-1.0-SNAPSHOT.jar中了,为什么找不到呢?
这里其实就是今天要说的重点,我们通过URLClassLoader加载jar,确实能加载到相关的类对象,而且实例化也不会有问题,但是我们在调用实例方法的时候,因为这里面使用了其他三方的jar,但是这个三方jar是包含在common-test本身jar内部的,它不能再像文件系统那样读取自己内部的资源。
解决办法就是把common-test里面lib下的jar资源读出来,写入本地。
public static URLClassLoader loadAllJar(String url) {
URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());
Method method;
try {
method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
if (!method.isAccessible()) {
method.setAccessible(true);
}
method.invoke(classLoader, new URL("file:" + url));
} catch (NoSuchMethodException | MalformedURLException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
// write to local lib
try {
String jarResourceStr = "jar:file:" + url + "!/";
URL libUrl = new URL(jarResourceStr);
URLConnection urlConnection = libUrl.openConnection();
if (urlConnection instanceof JarURLConnection) {
JarURLConnection jarURLConnection = (JarURLConnection) urlConnection;
JarFile jarFile = jarURLConnection.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
String parentPath = Paths.get(url).getParent().toString();
File libDir = Paths.get(parentPath, "lib").toFile();
libDir.deleteOnExit();
boolean isSuccess = libDir.mkdirs();
if (!isSuccess) {
System.out.println("create lib dir fail");
}
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String entryName = jarEntry.getName();
if (entryName.endsWith(".jar")) {
try (InputStream inputStream = classLoader.getResourceAsStream(entryName)) {
File target = new File(parentPath, entryName);
FileUtils.copyInputStreamToFile(inputStream, target);
method.invoke(classLoader, new URL("file:" + target.getPath()));
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return classLoader;
}
这里重新定义了一个类加载器方法,加载jar,获取类加载器之后,把jar包中lib下的jar也写入本地,同样的,也需要把这些jar加载起来。
最后运行调用示例,正确打印:
这里成功调用之后,我们可以看看本地的jar结构:
在common-test-1.0-SNAPSHOT.jar同级目录下,还有一个lib文件夹,这里面就是common-test-1.0-SNAPSHOT.jar依赖的两个jar。
其实本地文件系统有了lib这个目录之后,我们再回过去用getClassLoader()那种方式调用,也是没有问题的。
这是为什么呢?我们可以看看 common-test jar包中的manifest.mf信息:
Manifest-Version: 1.0
Class-Path: lib/common-api-1.0-SNAPSHOT.jar lib/third-party-1.0-SNAPSH
OT.jar
Build-Jdk-Spec: 1.8
Created-By: Maven JAR Plugin 3.2.2
这里指定的Class-Path是lib/comon-api-1.0-SNAPSHOT.jar lib/third-party-1.0-SNAPSHOT.jar,我们通过类加载器加载了common-test jar,但是它内部的lib路径是common-test-1.0-SNAPSHOT.jar!/lib/xxx.jar,通过普通的文件路径lib/xxx.jar是无法加载的。
上面的代码,如果使用loadAllJar()方法,虽然不会有问题,但是,如果我们获取类加载器,得到类对象信息,在实例化之前,我们不小心调用了classloader.close()把类加载器关闭了,同样会执行报错,如下所示:
本文主要讲如何动态调用外部jar,以及调用外部jar过程中可能遇到的问题。我们需要注意,外部jar内部依赖的jar,虽然在jar中,但是因为路径中包含了!/,并不能被访问到,所以需要将内部jar读出来,并写入本地。
本文其实也隐含了一个知识点,就是如何提取jar内部的资源。我们需要借助流读取的方式来将他们写出来。文章来源:https://www.toymoban.com/news/detail-568099.html
完整代码:https://gitee.com/buejee/classloaderexample/文章来源地址https://www.toymoban.com/news/detail-568099.html
到了这里,关于java通过URLClassLoader类加载器加载外部jar的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!