Java反射的入门

首先

“Reflection” refers to the act of contemplating or introspecting.

反思是Java的标准API,用于处理类、构造函数、字段、方法等。

具体来说,有以下几个类别。

パッケージクラス名表すものjava.langClassクラスjava.lang.reflectConstructorコンストラクタjava.lang.reflectFieldフィールドjava.lang.reflectMethodメソッド

了解“反思”的重要性

在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”指定的字段的方法,它们之间的区别如下所示。

メソッド名publicフィールドの取得public以外のフィールドの取得スーパークラスで定義されたフィールドの取得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指定的方法的方法。区别如下:

メソッド名publicメソッドの取得public以外のメソッドの取得スーパークラスで定義されたメソッドの取得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()方法将值赋给私有字段,那么原本封装起来的内容就会变得毫无意义。

请不要在业务逻辑中使用反射,再次强调一遍。

广告
将在 10 秒后关闭
bannerAds