Java Reflection - 动态类加载和重加载,Java 反射 - 模块
您可以在运行时加载和重新加载 Java 中的类,尽管这并不像人们希望的那么简单。这是通过 Java 平台的内置类加载器系统完成的。本文将解释何时以及如何在 Java 中加载和重新加载类。
您可以争论 Java 的动态类加载功能是否真的是 Java Reflection 的一部分,还是核心 Java 平台的一部分。不管怎样,这篇文章已经被放在 Java Reflection 路径中,但缺乏更好的地方来放置它。
类加载器
Java 应用程序中的所有类都是使用 java.lang.ClassLoader
的某个子类加载的。因此,动态加载类也必须使用 java.lang.ClassLoader
子类来完成。
当加载一个类时,它引用的所有类也会被加载。这种类加载模式会递归发生,直到加载了所有需要的类。这可能不是应用程序中的所有类。未引用的类只有在被引用时才会加载。
类加载器层次结构
Java 中的类加载器被组织成层次结构。当您创建新的标准 Java ClassLoader
时,您必须为其提供父级 ClassLoader
。如果 ClassLoader
被要求加载一个类,它会要求它的父类加载器加载它。如果父类加载器找不到该类,则子类加载器会尝试自行加载该类。
类加载
给定的类加载器在加载类时使用的步骤是:
- 检查类是否已经加载。
- 如果没有加载,则要求父类加载器加载该类。
- 如果父类加载器无法加载类,请尝试在该类加载器中加载它。
当您实现能够重新加载类的类加载器时,您将需要稍微偏离此顺序。要重新加载的类不应请求父类加载器加载。稍后会详细介绍。
动态类加载
动态加载类很容易。您需要做的就是获取 ClassLoader
并调用其 loadClass()
方法。这是一个例子:
public class MainClass {
public static void main(String[] args){
ClassLoader classLoader = MainClass.class.getClassLoader();
try {
Class aClass = classLoader.loadClass("com.jenkov.MyClass");
System.out.println("aClass.getName() = " + aClass.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
动态类重载
动态类重新加载更具挑战性。 Java 的内置类加载器在加载类之前总是检查类是否已经加载。因此,使用 Java 的内置类加载器重新加载该类是不可能的。
要重新加载类,您必须实现自己的 ClassLoader
子类。
即使使用 ClassLoader
的自定义子类,您也会面临挑战。每个加载的类都需要链接。这是使用 ClassLoader.resolve()
方法完成的。此方法是最终方法,因此不能在您的 ClassLoader
子类中重写。 resolve()
方法不允许任何给定的 ClassLoader
实例链接同一个类两次。因此,每次您想要重新加载类时,您都必须使用 ClassLoader
子类的新实例。这并非不可能,但在设计类重新加载时有必要了解这一点。
设计用于类重新加载的代码
如前所述,您无法使用已经加载过该类一次的 ClassLoader
重新加载该类。因此,您必须使用不同的 ClassLoader
实例重新加载该类。但这带来了一些新的挑战。
Java 应用程序中加载的每个类都由其完全限定名称(包名称 + 类名称)以及加载它的 ClassLoader
实例来标识。这意味着,类加载器 A 加载的类 MyObject
与类加载器 B 加载的 MyObject
类不是同一个类。请看以下代码:
MyObject object = (MyObject)
myClassReloadingFactory.newInstance("com.jenkov.MyObject");
请注意如何在代码中引用 MyObject
类作为 object
变量的类型。这会导致 MyObject
类由加载此代码所在类的同一个类加载器加载。
如果 myClassReloadingFactory
对象工厂使用与上述代码所在的类不同的类加载器重新加载 MyObject
类,则无法转换重新加载的 MyObject
的实例 class 为 object
变量的 MyObject
类型。由于两个 MyObject
类是使用不同的类加载器加载的,因此即使它们具有相同的完全限定类名,它们也会被视为不同的类。尝试将一个类的对象转换为另一个类的引用将导致 ClassCastException
。
可以解决此限制,但您必须通过以下两种方式之一更改代码:
- 使用接口作为变量类型,只需重新加载实现类即可。
- 使用超类作为变量类型,然后重新加载子类。
下面是两个相应的代码示例:
MyObjectInterface object = (MyObjectInterface)
myClassReloadingFactory.newInstance("com.jenkov.MyObject");
MyObjectSuperclass object = (MyObjectSuperclass)
myClassReloadingFactory.newInstance("com.jenkov.MyObject");
如果在重新加载实现类或子类时未重新加载变量(接口或超类)的类型,则这两种方法都将起作用。
为了完成这项工作,您当然需要实现类加载器,以让接口或超类由其父类加载。当类加载器被要求加载 MyObject
类时,它也会被要求加载 MyObjectInterface
类或 MyObjectSuperclass
类,因为它们是被引用的来自 MyObject
类中。您的类加载器必须将这些类的加载委托给加载包含接口或超类类型变量的类的同一类加载器。
类加载器加载 / 重新加载示例
上面的文字已经包含了很多谈话。让我们看一个简单的例子。下面是一个简单的 ClassLoader
子类的示例。请注意它如何将类加载委托给其父级,但它打算能够重新加载的一个类除外。如果该类的加载委托给父类加载器,则以后无法重新加载。
请记住,同一个 ClassLoader
实例只能加载一个类一次。
如前所述,这只是一个示例,旨在向您展示 ClassLoader
行为的基础知识。它不是您自己的类加载器的生产就绪模板。您自己的类加载器可能不应该限于单个类,而是您知道需要重新加载的类的集合。
此外,您可能也不应该对类路径进行硬编码。
public class MyClassLoader extends ClassLoader{
public MyClassLoader(ClassLoader parent) {
super(parent);
}
public Class loadClass(String name) throws ClassNotFoundException {
if(!"reflection.MyObject".equals(name))
return super.loadClass(name);
try {
String url = "file:C:/data/projects/tutorials/web/WEB-INF/" +
"classes/reflection/MyObject.class";
URL myUrl = new URL(url);
URLConnection connection = myUrl.openConnection();
InputStream input = connection.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int data = input.read();
while(data != -1){
buffer.write(data);
data = input.read();
}
input.close();
byte[] classData = buffer.toByteArray();
return defineClass("reflection.MyObject",
classData, 0, classData.length);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
下面是 MyClassLoader
的使用示例。
public static void main(String[] args) throws
ClassNotFoundException,
IllegalAccessException,
InstantiationException {
ClassLoader parentClassLoader = MyClassLoader.class.getClassLoader();
MyClassLoader classLoader = new MyClassLoader(parentClassLoader);
Class myObjectClass = classLoader.loadClass("reflection.MyObject");
AnInterface2 object1 =
(AnInterface2) myObjectClass.newInstance();
MyObjectSuperClass object2 =
(MyObjectSuperClass) myObjectClass.newInstance();
//create new class loader so classes can be reloaded.
classLoader = new MyClassLoader(parentClassLoader);
myObjectClass = classLoader.loadClass("reflection.MyObject");
object1 = (AnInterface2) myObjectClass.newInstance();
object2 = (MyObjectSuperClass) myObjectClass.newInstance();
}
这是使用类加载器加载的 reflection.MyObject
类。请注意它如何扩展超类并实现接口。这只是为了举例。在您自己的代码中,您只需执行两者之一 - 扩展或实现。
public class MyObject extends MyObjectSuperClass implements AnInterface2{
//... body of class ... override superclass methods
// or implement interface methods
}
Java Reflection - 动态类加载和重加载 --- Java Reflection - Dynamic Class Loading and Reloading
本 Java 模块反射教程将解释如何通过 Java 反射访问 Java 类所属的 Java 模块。
Java 模块的概念是通过 Java 平台模块系统添加到 Java 9 中的。 Java 模块是一组 Java 包。因此,每个 Java 类都属于一个包,而包又属于一个模块。
Java 模块由 Java 模块 java.base
中的 Java 反射类 java.lang.Module
表示。通过此类,您可以与 Java 平台模块系统交互以获取有关给定模块的信息,或修改模块。本教程将介绍您可以通过 Java 反射对 Module
实例执行的一些操作。
获取模块实例
您可以通过 Class
实例获取 Module
类的实例,如下所示:
Module myClassModule = MyClass.class.getModule();
是命名模块吗?
您可以通过调用 Module
isNamed()
方法来检查 Module
实例 a 是否代表命名模块。这是一个例子:
boolean isNamed = myClassModule.isNamed();
是开放模块吗?
您可以通过 Module
isOpen()
方法检查 Module
是否是命名模块。这是一个例子:
boolean isOpen = myClassModule.isOpen();
获取模块描述符
一旦您有权访问 Module
实例,您就可以通过 getDescriptor()
方法访问其 ModuleDescriptor
。下面是通过 getDescriptor()
访问 Java Module
的 ModuleDescriptor
的示例:
ModuleDescriptor descriptor = myClassModule.getDescriptor();
从 ModuleDescriptor
中,您可以读取模块的模块描述符中的信息。本 Java 模块反射教程将介绍您可以从以下部分中的模块描述符获取的一些信息。
模块名称
您可以通过 ModuleDescriptor
name()
方法从模块描述符中获取命名模块的名称。下面是通过反射读取 Java 模块名称的示例:
String moduleName = descriptor.name();
导出的包
您可以通过 Java 反射,通过 ModuleDescriptor
exports()
方法读取 Java 模块导出的包列表。下面是从 Java 模块获取导出包集的示例:
Set<ModuleDescriptor.Exports> exports = descriptor.exports();
是自动模块吗?
您可以通过 ModuleDescriptor
isAutomatic()
方法检查 Java 模块是否是自动模块。下面是检查 Java 模块是否自动的示例:
boolean isAutomatic = descriptor.isAutomatic();
是开放模块吗?
您可以通过 ModuleDescriptor
isOpen()
方法检查 Java 模块是否是开放模块。下面是检查 Java 模块是否打开的示例:
boolean isOpen = descriptor.isOpen();
模块中的包
您可以通过 Java 反射获取给定 Java 模块中的包名称列表。您可以通过 ModuleDescriptor
packages()
方法执行此操作。以下是通过反射获取模块的包名称列表的示例:
Set packages = descriptor.packages();
使用的服务
您也可以通过 Java 反射了解给定 Java 模块使用哪些服务。模块使用的服务也称为模块的服务依赖项。您可以通过 ModuleDescriptor
uses()
方法读取模块服务依赖项。下面是如何通过反射读取 Java 模块的服务依赖关系的示例:
Set<String> uses = descriptor.uses();