原文:Java Coding Problems
协议:CC BY-NC-SA 4.0
贡献者:飞龙
本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。
本章包括涉及 Java 反射 API 的 17 问题。从经典主题,如检查和实例化 Java 工件(如模块、包、类、接口、超类、结构、方法、注释和数组)合成和桥接基于嵌套的结构或访问控制(JDK11)本章详细介绍 Java 反射 API。本章结束时,Java 反射 API 没有秘密会被发现,你会准备好向同事展示反射能做什么。
问题
用以下问题来测试你 Java 反射 API 编程能力。在使用解决方案和下载示例程序之前,我强烈建议您尝试每个问题:
-
:写几个检查 Java 包的示例(如名称、类列表等。
-
:写几个检查类和超类的例子(例如,通过类名、修饰符、实现接口、结构、方法和字段获取
Class
)。 -
:编写通过反射创建实例的程序。
-
:编写获取接收器类型注释的程序。
-
:通过反射编写程序获得合成和桥接结构。
-
:编写程序,检查方法是否获得变量数。
-
:编程检查方法是否准备?
default
。 -
:编写一个基于嵌套结构的访问程序。
-
:写几个例子,通过反射调用获取器和设置器。此外,通过反射编写程序生成获取器和设置器。
-
:写几个通过反射获得不同类型注释的例子。
-
:通过反射调用实例法编写程序。
-
:为给定类编写程序
static
该方法分组,并通过反射调用其中一种方法。 -
:编写程序,通过反射获得给定的方法、字段和异常的泛型类型。
-
:编写一个程序,通过反射获得给定类
public
和private
字段。 -
:通过反射射使用数组的例子。
-
:通过反射检查写几个 Java9 模块的例子。
-
:编写依赖动态代理统计给定接口的调用次数。
解决方案
以下部分介绍了上述问题的解决方案。请记住,通常没有正确的方法来解决特定的问题。此外,请记住,这里显示的解释只包括解决问题所需的最有趣和最重要的细节。您可以从此页面下载示例解决方案,查看更多详细信息并尝试程序。
149 检查包
当我们需要获取特定包的信息时,java.lang.Package
类别是我们的主要焦点。使用这个类别,我们可以找到包的名称,实现包的供应商,它的标题,包的版本等等。
这通常用于查找包含特定类别的包的名称。Integer
类的包名可以容易地获得如下:
Class clazz = Class.forName("java.lang.Integer"); Package packageOfClazz = clazz.getPackage(); // java.lang String packageNameOfClazz = packageOfClazz.getName();
现在,我们来看看File
类的包名:
File file = new File("."); Package packageOfFile = file.getClass().getPackage(); // java.io String packageNameOfFile = packageOfFile.getName();
如果我们试图找到当前类别的包名,我们可以依靠它this.getClass().getPckage().getName()
。这在非静态环境中工作。
但是如果我们只想快速列出当前类装入器的所有包,那么我们可以依赖getPackages()
方法,如下所示:
Package[] packages = Package.getPackages();
基于getPackages()
方法,我们可以列出调用者的类装入器定义的所有包,以及以给定前缀开头的祖先包,如下所示:
public static List<String> fetchPackagesByPrefix(String prefix) {
return Arrays.stream(Package.getPackages())
.map(Package::getName)
.filter(n -> n.startsWith(prefix))
.collect(Collectors.toList());
}
如果这个方法存在于一个名为Packages
的实用类中,那么我们可以如下调用它:
List<String> packagesSamePrefix
= Packages.fetchPackagesByPrefix("java.util");
您将看到类似于以下内容的输出:
java.util.function, java.util.jar, java.util.concurrent.locks,
java.util.spi, java.util.logging, ...
有时,我们只想在系统类加载器中列出一个包的所有类。让我们看看怎么做。
获取包的类
例如,我们可能希望列出当前应用的一个包中的类(例如,modern.challenge
包)或编译时库中的一个包中的类(例如,commons-lang-2.4.jar
。
类被包装在可以在 Jar 中存档的包中,尽管它们不必这样。为了涵盖这两种情况,我们需要发现给定的包是否存在于 JAR 中。我们可以通过ClassLoader.getSystemClassLoader().getResource(package_path)
加载资源并检查返回的资源 URL 来完成。如果包不在 JAR 中,那么资源将是以file:
方案开始的 URL,如下面的示例(我们使用的是modern.challenge
):
file:/D:/Java%20Modern%20Challenge/Code/Chapter%207/Inspect%20packages/build/classes/modern/challenge
但是如果包在 JAR 中(例如,org.apache.commons.lang3.builder
,那么 URL 将以jar:
方案开始,如下例所示:
jar:file:/D:/.../commons-lang3-3.9.jar!/org/apache/commons/lang3/builder
如果我们考虑到来自 JAR 的包的资源以jar:
前缀开头,那么我们可以编写一个方法来区分它们,如下所示:
private static final String JAR_PREFIX = "jar:";
public static List<Class<?>> fetchClassesFromPackage(
String packageName) throws URISyntaxException, IOException {
List<Class<?>> classes = new ArrayList<>();
String packagePath = packageName.replace('.', '/');
URL resource = ClassLoader
.getSystemClassLoader().getResource(packagePath);
if (resource != null) {
if (resource.toString().startsWith(JAR_PREFIX)) {
classes.addAll(fetchClassesFromJar(resource, packageName));
} else {
File file = new File(resource.toURI());
classes.addAll(fetchClassesFromDirectory(file, packageName));
}
} else {
throw new RuntimeException("Resource not found for package: "
+ packageName);
}
return classes;
}
因此,如果给定的包在 JAR 中,那么我们调用另一个辅助方法fetchClassesFromJar()
;否则,我们调用这个辅助方法fetchClassesFromDirectory()
。顾名思义,这些助手知道如何从 JAR 或目录中提取给定包的类。
主要来说,这两种方法只是一些用来识别具有.class
扩展名的文件的意大利面代码片段。每个类都通过Class.forName()
来确保返回的是Class
,而不是String
。这两种方法在本书附带的代码中都可用。
如何列出不在系统类加载器中的包中的类,例如,外部 JAR 中的包?实现这一点的便捷方法依赖于URLClassLoader
。此类用于从引用 JAR 文件和目录的 URL 搜索路径加载类和资源。我们将只处理 Jar,但对目录也这样做非常简单。
因此,根据给定的路径,我们需要获取所有 Jar 并将它们返回为URL[]
(这个数组需要定义URLClassLoader
。例如,我们可以依赖于Files.find()
方法遍历给定的路径并提取所有 Jar,如下所示:
public static URL[] fetchJarsUrlsFromClasspath(Path classpath)
throws IOException {
List<URL> urlsOfJars = new ArrayList<>();
List<File> jarFiles = Files.find(
classpath,
Integer.MAX_VALUE,
(path, attr) -> !attr.isDirectory() &&
path.toString().toLowerCase().endsWith(JAR_EXTENSION))
.map(Path::toFile)
.collect(Collectors.toList());
for (File jarFile: jarFiles) {
try {
urlsOfJars.add(jarFile.toURI().toURL());
} catch (MalformedURLException e) {
logger.log(Level.SEVERE, "Bad URL for{0} {1}",
new Object[] {
jarFile, e
});
}
}
return urlsOfJars.toArray(URL[]::new);
}
注意,我们正在扫描所有子目录,从给定的路径开始。当然,这是一个设计决策,很容易参数化搜索深度。现在,让我们从tomcat8/lib
文件夹中获取 Jar(不需要为此安装 Tomcat;只需使用 Jar 的任何其他本地目录并进行适当的修改):
URL[] urls = Packages.fetchJarsUrlsFromClasspath(
Path.of("D:/tomcat8/lib"));
现在,我们可以实例化URLClassLoader
:
URLClassLoader urlClassLoader = new URLClassLoader(
urls, Thread.currentThread().getContextClassLoader());
这将为给定的 URL 构造一个新的URLClassLoader
对象,并使用当前的类加载器进行委托(第二个参数也可以是null
)。我们的URL[]
只指向 JAR,但根据经验,假设任何jar:
方案 URL 都引用 JAR 文件,而任何以/
结尾的file:
方案 URL 都引用目录。
tomcat8/lib
文件夹中的一个 Jar 称为tomcat-jdbc.jar
。在这个 JAR 中,有一个名为org.apache.tomcat.jdbc.pool
的包。让我们列出这个包的类:
List<Class<?>> classes = Packages.fetchClassesFromPackage(
"org.apache.tomcat.jdbc.pool", urlClassLoader);
fetchClassesFromPackage()
方法是一个助手,它只扫描URLClassLoader
的URL[]
数组并获取给定包中的类。它的源代码与本书附带的代码一起提供。
检查模块内的包
如果我们使用 Java9 模块化,那么我们的包将生活在模块中。例如,如果我们在一个名为org.tournament
的模块中的一个名为com.management
的包中有一个名为Manager
的类,那么我们可以这样获取该模块的所有包:
Manager mgt = new Manager();
Set<String> packages = mgt.getClass().getModule().getPackages();
另外,如果我们想创建一个类,那么我们需要以下的Class.forName()
风格:
Class<?> clazz = Class.forName(mgt.getClass()
.getModule(), "com.management.Manager");
请记住,每个模块在磁盘上都表示为具有相同名称的目录。例如,org.tournament
模块在磁盘上有一个同名文件夹。此外,每个模块被映射为一个具有此名称的单独 JAR(例如,org.tournament.jar
)。通过记住这些坐标,很容易修改本节中的代码,从而列出给定模块的给定包的所有类。
150 检查类
通过使用 Java 反射 API,我们可以检查类的详细信息,对象的类名、修饰符、构造器、方法、字段、实现接口等。
假设我们有以下Pair
类:
public final class Pair<L, R> extends Tuple implements Comparable {
final L left;
final R right;
public Pair(L left, R right) {
this.left = left;
this.right = right;
}
public class Entry<L, R> {
}
...
}
我们还假设有一个实例:
Pair pair = new Pair(1, 1);
现在,让我们使用反射来获取Pair
类的名称。
通过实例获取Pair
类的名称
通过拥有Pair
的实例(对象),我们可以通过调用getClass()
方法,以及Class.getName()
、getSimpleName()
、getCanonicalName()
找到其类的名称,如下例所示:
Class<?> clazz = pair.getClass();
// modern.challenge.Pair
System.out.println("Name: " + clazz.getName());
// Pair
System.out.println("Simple name: " + clazz.getSimpleName());
// modern.challenge.Pair
System.out.println("Canonical name: " + clazz.getCanonicalName());
匿名类没有简单的和规范的名称。
注意,getSimpleName()
返回非限定类名。或者,我们可以获得如下类:
Class<Pair> clazz = Pair.class;
Class<?> clazz = Class.forName("modern.challenge.Pair");
获取Pair
类修饰符
为了得到类的修饰符(public
、protected
、private
、final
、static
、abstract
、interface
,我们可以调用Class.getModifiers()
方法。此方法返回一个int
值,该值将每个修饰符表示为标志位。为了解码结果,我们依赖于Modifier
类,如下所示:
int modifiers = clazz.getModifiers();
System.out.println("Is public? "
+ Modifier.isPublic(modifiers)); // true
System.out.println("Is final? "
+ Modifier.isFinal(modifiers)); // true
System.out.println("Is abstract? "
+ Modifier.isAbstract(modifiers)); // false
获取Pair
类实现的接口
为了获得由类或对象表示的接口直接实现的接口,我们只需调用Class.getInterfaces()
。此方法返回一个数组。因为Pair
类实现了一个接口(Comparable
,所以返回的数组将包含一个元素:
Class<?>[] interfaces = clazz.getInterfaces();
// interface java.lang.Comparable
System.out.println("Interfaces: " + Arrays.toString(interfaces));
// Comparable
System.out.println("Interface simple name: "
+ interfaces[0].getSimpleName());
获取Pair
类构造器
类的public
构造器可以通过Class.getConstructors()
类获得。返回结果为Constructor<?>[]
:
Constructor<?>[] constructors = clazz.getConstructors();
// public modern.challenge.Pair(java.lang.Object,java.lang.Object)
System.out.println("Constructors: " + Arrays.toString(constructors));
要获取所有声明的构造器(例如,private
和protected
构造器),请调用getDeclaredConstructors()
。搜索某个构造器时,调用getConstructor(Class<?>... parameterTypes)
或getDeclaredConstructor(Class<?>... parameterTypes)
。
获取Pair
类字段
类的所有字段都可以通过Class.getDeclaredFields()
方法访问。此方法返回一个数组Field
:
Field[] fields = clazz.getDeclaredFields();
// final java.lang.Object modern.challenge.Pair.left
// final java.lang.Object modern.challenge.Pair.right
System.out.println("Fields: " + Arrays.toString(fields));
为了获取字段的实际名称,我们可以很容易地提供一个辅助方法:
public static List<String> getFieldNames(Field[] fields) {
return Arrays.stream(fields)
.map(Field::getName)
.collect(Collectors.toList());
}
现在,我们只收到字段的名称:
List<String> fieldsName = getFieldNames(fields);
// left, right
System.out.println("Fields names: " + fieldsName);
获取字段的值可以通过一个名为Object get(Object obj)
的通用方法和一组getFoo()
方法来完成(有关详细信息,请参阅文档)。obj
表示static
或实例字段。例如,假设ProcedureOutputs
类有一个名为callableStatement
的private
字段,其类型为CallableStatement
。让我们用Field.get()
方法访问此字段,检查CallableStatement
是否关闭:
ProcedureOutputs procedureOutputs
= storedProcedure.unwrap(ProcedureOutputs.class);
Field csField = procedureOutputs.getClass()
.getDeclaredField("callableStatement");
csField.setAccessible(true);
CallableStatement cs
= (CallableStatement) csField.get(procedureOutputs);
System.out.println("Is closed? " + cs.isClosed());
如果只获取public
字段,请调用getFields()
。要搜索某个字段,请调用getField(String fieldName)
或getDeclaredField(String name)
。
获取Pair
类方法
类的public
方法可以通过Class.getMethods()
方法访问。此方法返回一个数组Method
:
Method[] methods = clazz.getMethods();
// public boolean modern.challenge.Pair.equals(java.lang.Object)
// public int modern.challenge.Pair.hashCode()
// public int modern.challenge.Pair.compareTo(java.lang.Object)
// ...
System.out.println("Methods: " + Arrays.toString(methods));
为了获取方法的实际名称,我们可以快速提供一个辅助方法:
public static List<String> getMethodNames(Method[] methods) {
return Arrays.stream(methods)
.map(Method::getName)
.collect(Collectors.toList());
}
现在,我们只检索方法的名称:
List<String> methodsName = getMethodNames(methods);
// equals, hashCode, compareTo, wait, wait,
// wait, toString, getClass, notify, notifyAll
System.out.println("Methods names: " + methodsName);
获取所有声明的方法(例如,private
、protected
),调用getDeclaredMethods()
。要搜索某个方法,请调用getMethod(String name, Class<?>... parameterTypes)
或getDeclaredMethod(String name, Class<?>... parameterTypes)
。
获取Pair
类模块
如果我们使用 JDK9 模块化,那么我们的类将生活在模块中。Pair
类不在模块中,但是我们可以通过 JDK9 的Class.getModule()
方法很容易得到类的模块(如果类不在模块中,那么这个方法返回null
):
// null, since Pair is not in a Module
Module module = clazz.getModule();
获取Pair
类超类
Pair
类扩展了Tuple
类,因此Tuple
类是Pair
的超类。我们可以通过Class.getSuperclass()
方法得到,如下所示:
Class<?> superClass = clazz.getSuperclass();
// modern.challenge.Tuple
System.out.println("Superclass: " + superClass.getName());
获取某个类型的名称
从 JDK8 开始,我们可以获得特定类型名称的信息字符串。
此方法返回与getName()
、getSimpleName()
或getCanonicalName()
中的一个或多个相同的字符串:
- 对于原始类型,它会为所有三个方法返回相同的结果:
System.out.println("Type: " + int.class.getTypeName()); // int
- 对于
Pair
,返回与getName()
、getCanonicalName()
相同的东西:
// modern.challenge.Pair
System.out.println("Type name: " + clazz.getTypeName());
- 对于内部类(比如
Entry
代表Pair
,它返回与getName()
相同的东西:
// modern.challenge.Pair$Entry System.out.println(