本章包括涉及 Java 反射 API 的 17 个问题。从经典主题,如检查和实例化 Java 工件(例如,模块、包、类、接口、超类、构造器、方法、注解和数组),到合成和桥接构造或基于嵌套的访问控制(JDK11),本章详细介绍了 Java 反射 API。在本章结束时,Java 反射 API 将不会有任何秘密未被发现,您将准备好向您的同事展示反射可以做什么。
问题
使用以下问题来测试您的 Java 反射 API 编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
- 检查包:编写几个检查 Java 包的示例(例如名称、类列表等)。
- 检查类和超类:写几个检查类和超类的例子(例如,通过类名、修饰符、实现的接口、构造器、方法和字段获取
Class
)。 - 通过反射构造器来实例化:编写通过反射创建实例的程序。
- 获取接收器类型的注解:编写获取接收器类型注解的程序。
- 获得合成和桥接结构:编写一个程序,通过反射获得合成和桥接结构。
- 检查变量个数:编写一个程序,检查一个方法是否获得变量个数。
- 检查默认方法:编写程序检查方法是否为
default
。 - 基于嵌套的反射访问控制:编写一个程序,通过反射提供对基于嵌套的结构的访问。
- 获取器和设置器的反射:写几个例子,通过反射调用获取器和设置器。另外,编写一个程序,通过反射生成获取器和设置器。
- 反射注解:写几个通过反射获取不同种类注解的例子。
- 调用实例方法:编写一个程序,通过反射调用实例方法。
- 获取
static
方法:编写一个程序,对给定类的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().getPackage().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("Type name: " + Pair.Entry.class.getTypeName());
- 对于匿名类,它返回与
getName()
相同的内容:
Thread thread = new Thread() { public void run() { System.out.println("Child Thread"); } }; // modern.challenge.Main$1 System.out.println("Anonymous class type name: " + thread.getClass().getTypeName());
- 对于数组,它返回与
getCanonicalName()
相同的内容:
Pair[] pairs = new Pair[10]; // modern.challenge.Pair[] System.out.println("Array type name: " + pairs.getClass().getTypeName());
获取描述类的字符串
从 JDK8 开始,我们可以通过Class.toGenericString()
方法获得类的快速描述(包含修饰符、名称、类型参数等)。
我们来看几个例子:
// public final class modern.challenge.Pair<L,R> System.out.println("Description of Pair: " + clazz.toGenericString()); // public abstract interface java.lang.Runnable System.out.println("Description of Runnable: " + Runnable.class.toGenericString()); // public abstract interface java.util.Map<K,V> System.out.println("Description of Map: " + Map.class.toGenericString());
获取类的类型描述符字符串
从 JDK12 开始,我们可以通过Class.descriptorString()
方法获取类的类型描述符作为String
对象:
// Lmodern/challenge/Pair; System.out.println("Type descriptor of Pair: " + clazz.descriptorString()); // Ljava/lang/String; System.out.println("Type descriptor of String: " + String.class.descriptorString());
获取数组的组件类型
JDK12 只为数组提供了Class componentType()
方法。此方法返回数组的组件类型,如下两个示例所示:
Pair[] pairs = new Pair[10]; String[] strings = new String[] {"1", "2", "3"}; // class modern.challenge.Pair System.out.println("Component type of Pair[]: " + pairs.getClass().componentType()); // class java.lang.String System.out.println("Component type of String[]: " + strings.getClass().componentType());
为数组类型获取类,其组件类型由Pair
描述
从 JDK12 开始,我们可以得到一个数组类型的Class
,该数组类型的组件类型由给定的类通过Class.arrayType()
来描述:
Class<?> arrayClazz = clazz.arrayType(); // modern.challenge.Pair<L,R>[] System.out.println("Array type: " + arrayClazz.toGenericString());
151 通过反射构造器的实例化
我们可以使用 Java 反射 API 通过Constructor.newInstance()
实例化一个类。
让我们考虑以下类,它有四个构造器:
public class Car { private int id; private String name; private Color color; public Car() {} public Car(int id, String name) { this.id = id; this.name = name; } public Car(int id, Color color) { this.id = id; this.color = color; } public Car(int id, String name, Color color) { this.id = id; this.name = name; this.color = color; } // getters and setters omitted for brevity }
一个Car
实例可以通过这四个构造器中的一个来创建。Constructor
类公开了一个方法,该方法接受构造器的参数类型,并返回反映匹配构造器的Constructor
对象。这种方法称为getConstructor(Class... parameterTypes)
。
让我们调用前面的每个构造器:
Class<Car> clazz = Car.class; Constructor<Car> emptyCnstr = clazz.getConstructor(); Constructor<Car> idNameCnstr = clazz.getConstructor(int.class, String.class); Constructor<Car> idColorCnstr = clazz.getConstructor(int.class, Color.class); Constructor<Car> idNameColorCnstr = clazz.getConstructor(int.class, String.class, Color.class);
此外,Constructor.newInstance(Object... initargs)
可以返回Car
的实例,该实例对应于被调用的构造器:
Car carViaEmptyCnstr = emptyCnstr.newInstance(); Car carViaIdNameCnstr = idNameCnstr.newInstance(1, "Dacia"); Car carViaIdColorCnstr = idColorCnstr .newInstance(1, new Color(0, 0, 0)); Car carViaIdNameColorCnstr = idNameColorCnstr .newInstance(1, "Dacia", new Color(0, 0, 0));
现在,我们来看看如何通过反射实例化一个private
构造器。
通过私有构造器实例化类
Java 反射 API 也可以通过其private
构造器来实例化类。例如,假设我们有一个名为Cars
的工具类。按照最佳实践,我们将此类定义为final
,并使用private
构造器来禁止实例:
public final class Cars { private Cars() {} // static members }
取这个构造器可以通过Class.getDeclaredConstructor()
完成,如下:
Class<Cars> carsClass = Cars.class; Constructor<Cars> emptyCarsCnstr = carsClass.getDeclaredConstructor();
在这个实例中调用newInstance()
会抛出IllegalAccessException
,因为被调用的构造器有private
访问权限。但是,Java 反射允许我们通过标志方法Constructor.setAccessible()
修改访问级别。这一次,实例化按预期工作:
emptyCarsCnstr.setAccessible(true); Cars carsViaEmptyCnstr = emptyCarsCnstr.newInstance();
为了阻止这种方法,建议抛出一个来自private
构造器的错误,如下所示:
public final class Cars { private Cars() { throw new AssertionError("Cannot be instantiated"); } // static members }
这一次,实例化尝试将以AssertionError
失败。
Java 编程问题:七、Java 反射类、接口、构造器、方法和字段2https://developer.aliyun.com/article/1426146