Java反射的入门
首先
“Reflection” refers to the act of contemplating or introspecting.
反思是Java的标准API,用于处理类、构造函数、字段、方法等。
具体来说,有以下几个类别。
java.lang
Class
クラスjava.lang.reflect
Constructor
コンストラクタjava.lang.reflect
Field
フィールドjava.lang.reflect
Method
メソッド了解“反思”的重要性
在Spring等框架中,通过使用反射进行许多内部处理。因此,了解反射有以下优势:
-
- フレームワークのソースコードを読んで理解できるようになる!
-
- ソースコードを読まずとも、フレームワーク内部の処理がイメージできるようになる!
-
- 自分でフレームワークを作れるようになる!
近年のJavaでは既存のOSSフレームワークを使うことが多いので、あんまりやらないかもですが
非常建议了解反思,从初级毕业并晋升为中级Java工程师!
在平常的工作中,不建议在编写业务逻辑等时使用反射,请注意(原因将在后文详述)。
反思的基本原理
班级
Class类是一个表示类的类。它在反射中起着核心的作用。
Class类的定义是带有泛型的Class。T表示指定的类。
每个类都会隐式地创建类似于名为”class”的静态字段的东西。这个类似字段中被赋值为代表该类的Class实例。
换言之,可以想象出这样一个领域。
public class Hoge {
public static final Class<Hoge> class = (Hogeを表すClassインスタンス);
}
“.class”被准确地称为”类文字”,并不是一个字段(参考URL)。然而,在前述的解释中,为了形象化,我们将其描述为“类似于字段的东西”。
除此之外,所有的类都定义了一个getClass()方法(在java.lang.Object类中定义)。这个方法的返回值是与.class相同的“表示该类的Class实例”。
Hoge hoge = new Hoge();
Class<Hoge> clazz = hoge.getClass();
获取类名
在Class类中,提供了一些方法来获取类名。
getName()
完全修飾クラス名(FQCN)を返すgetSimpleName()
単純クラス名を返すClass<?> clazz = Object.class;
String fqcn = clazz.getName();
System.out.println(fqcn);
String name = clazz.getSimpleName();
System.out.println(name);
java.lang.Object
Object
使用反射生成实例。
假设有一个样本类,比如说叫做Sample。
package com.example;
public class Sample {
private int value;
public Sample(int value) {
this.value = value;
}
public int multi(int value2) {
return value * value2;
}
@Override
public String toString() {
return "Sample{" +
"value=" + value +
'}';
}
}
首先,我们可以使用Class.forName()方法来生成表示Sample类的Class实例。在参数中,我们需要指定Sample类的完整限定名。
// Sampleクラスを表すClassインスタンスを生成する
Class<?> sampleClass = Class.forName("com.example.Sample");
接下来,我们将使用getConstructor()方法获取Sample类的构造函数。在参数中,我们需要指定所需构造函数的参数类型。
// Sampleクラスのコンストラクタを取得する
Constructor<?> constructor = sampleClass.getConstructor(int.class);
基本数据类型如int可以使用类字面常量(.class)进行操作。
接下来,我们将使用取得的构造函数的newInstance()方法来创建Sample的实例。您需要在参数中指定传递给构造函数的参数。
// コンストラクタを利用してSampleインスタンスを生成する
Object sampleInstance = constructor.newInstance(100);
我们来确认一下是否真的生成了Sample实例。
// インスタンスの状態を表示
System.out.println(sampleInstance);
在具有设定为100的value字段值的Sample实例中,我们可以确定它的存在。
Sample{value=100}
处理领域
在Class类中有几个用于获取字段的方法。
-
- Field getField(String name)
- Field getDeclaredField(String name)
这是一个用于获取由”name”指定的字段的方法,它们之间的区别如下所示。
getField()
○×○getDeclaredField()
○○×还有一些方法可以返回Field类型的数组,比如getFields()和getDeclaredFields()方法。
由於此次的情況是value字段被設為私有,因此我們將使用後者。
// valueフィールドを取得
Field valueField = sampleClass.getDeclaredField("value");
当将setAccessible()方法设为true时,即使是private字段,也可以获取其值或进行赋值。
// valueフィールドの値を取得
Object value = valueField.get(sampleInstance);
// valueフィールドに値200を設定
valueField.set(sampleInstance, 200);
处理方法
在Class类中,有几个方法可以获取方法。
-
- Method getMethod(String name, Class<?>… parameterTypes)
- Method getDeclaredMethod(String name, Class<?>… parameterTypes)
以下是一种选择性的中文释义:
这是一个用于获取由name指定的方法的方法。区别如下:
getMethod()
○×○getDeclaredMethod()
○○×还有一些方法会返回Method数组,例如getMethods()和getDeclaredMethods()。
这次我们将利用前者。
// multiメソッドの取得
Method multiMethod = sampleClass.getMethod("multi", int.class);
在执行获取的方法时,需要在invoke()方法中指定要执行的方法实例和要传递的参数值。
// multiメソッドの実行
Object ret = multiMethod.invoke(sampleInstance, 2); // 100 * 2 = 200が戻り値となる
与Field类一样,Method类也有setAccessible()方法。
使用标注
自制标注
“Annotation” refers to those starting with “@” such as Java’s standard “@Override” or Spring’s “@RequestMapping”.
注释可以由自己创建。 .)
package com.example;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 実行時にもアノテーション情報を残す
@Retention(RetentionPolicy.RUNTIME)
// このアノテーションを付けられる箇所をクラス・インタフェースに指定する
@Target({ElementType.TYPE})
// このアノテーションを付けたことがJavadocにも掲載される
@Documented
public @interface MyAnnotation {
String name() default "default";
int value();
}
在创建注解时,应该使用@interface而不是class来声明。
标注元素
类似于在MyAnnotation中定义的方法的东西是元素。
常常被称为”属性”,但用英语也可以叫作”element”,所以我认为”要素”是正确的。
这次我们定义了一个元素,它叫做name和value。
在名字要素中使用default关键字来设置默认值。
@保留
@Retention用于指定@MyAnnotation的信息是否在类文件或运行时保留。
RetentionPolicy
説明RUNTIME
アノテーションはクラスファイルに記録され、かつ実行時もJVMによって保持されます。CLASS
アノテーションはクラスファイルに記録される一方、実行時はJVMによって保持されません。SOURCE
アノテーションはクラスファイルに記録されません。如果没有添加@Retention,将被视为与CLASS相同的待遇。
@目标
@Target注解通过使用ElementType枚举类型来指定@MyAnnotation可以添加在方法、字段等位置。
ElementType
説明CONSTRUCTOR
コンストラクタFIELD
フィールドMETHOD
メソッドTYPE
クラス、インタフェースなど型の宣言如果您想查看所有的ElementType,请参阅Javadoc。
如果不加@Target,除了类型参数之外的所有地方都可以添加。
@已记录
只要在类上添加@Documented注解,就会在使用@MyAnnotation注解的类的Javadoc中出现“这个类被@MyAnnotation注解了”的说明。
通过反射获取注释。
本次将在Sample类上添加@MyAnnotation注解。
@MyAnnotation(999)
public class Sample {
}
如果只指定名称为value的元素,则可以写为@MyAnnotation(999)(也可以写成@MyAnnotation(value = 999))。
若要获取添加在类上的注解,请使用Class类的getAnnotation()方法。在参数中指定要获取的注解的类型。
要获取字段和方法上添加的注解,可以使用Field类和Method类的getAnnotation()方法。
Class<?> sampleClass = Class.forName("com.example.Sample");
// クラスに付いたアノテーションを取得
MyAnnotation myAnnotation = sampleClass.getAnnotation(MyAnnotation.class);
获取注释的元素值如下。
// アノテーションの要素値を取得
String name = myAnnotation.name(); // "default"が取得できる
int value = myAnnotation.value(); // 999が取得できる
Proxy: 代理
代理 是指
代理是指在运行时创建接口实现类及其实例的技术,以及指称所创建的实例的术语。
具体来说,Java的java.lang.reflect包中有一个名为Proxy的类。
创建代理服务器
这次,我将尝试创建这样一个接口的代理。
package com.example;
public interface Command {
int execute1(String command);
int execute2(String command);
}
实现这个接口的类和它的实例可以通过Proxy.newProxyInstance()方法来创建。
Command c = (Command) Proxy.newProxyInstance(
Command.class.getClassLoader(),
new Class<?>[]{ Command.class },
new CommandInvocationHandler());
第一个参数是类加载器。类加载器是负责读取类文件的角色。可以使用InterfaceName.class.getClassLoader()来获取。
在第二个参数中,您可以用一个数组来指定代理实现应该包含的接口。
第三个参数是用于描述在代理内部处理的InvocationHandler实现类(如下所述)。
描述在代理服务器内的处理
在处理每个代理方法被调用时,我们需要实现InvocationHandler接口来描述相应的处理过程。
package com.example;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Objects;
public class CommandInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Objects.equals("execute1", method.getName())) {
// execute1()の処理
if ("UP".equals(args[0])) {
return 1;
} else if ("DOWN".equals(args[0])) {
return 0;
} else {
throw new IllegalArgumentException("Arg must be 'UP' or 'DOWN'");
}
}
if (Objects.equals("execute2", method.getName())) {
// execute2()の処理
if ("LEFT".equals(args[0])) {
return 2;
} else if ("RIGHT".equals(args[0])) {
return 3;
} else {
throw new IllegalArgumentException("Arg must be 'LEFT' or 'RIGHT'");
}
}
throw new IllegalArgumentException("Invalid method");
}
}
调用代理
让我们显示并确认一下已创建的代理类的名称是什么。
// クラス名を表示
System.out.println(c.getClass().getName());
如果看到com.sun.proxy.$ProxyN这样的东西,那就是代理已经创建的证据。
com.sun.proxy.$Proxy0
当调用某个方法时,将执行在InvocationHandler实现类中编写的处理过程。
// execute1()の呼び出し
int ret1 = c.execute1("UP");
System.out.println(ret1);
// execute2()の呼び出し
int ret2 = c.execute2("RIGHT");
System.out.println(ret2);
// execute1()の呼び出し(例外発生)
int ret3 = c.execute1("LEFT");
System.out.println(ret3);
1
3
Exception in thread "main" java.lang.IllegalArgumentException: Arg must be 'UP' or 'DOWN'
at com.example.CommandInvocationHandler.invoke(CommandInvocationHandler.java:16)
at com.sun.proxy.$Proxy0.execute1(Unknown Source)
at com.example.ProxySample.main(ProxySample.java:19)
使用反思的时机
如之前所述,反射基本上是用于创建框架和库的技术。
如果在业务逻辑中使用这样做,由于需要将类名以字符串的形式写出来,所以在后期进行维护时会变得非常困难,很难找到该类的使用位置。而且,如果使用setAccessible()方法将值赋给私有字段,那么原本封装起来的内容就会变得毫无意义。
请不要在业务逻辑中使用反射,再次强调一遍。