Is Java Reflection Bad Practice? – Java 反射是糟糕的做法吗?

最后修改: 2023年 10月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The use of the Reflection API has sparked extensive debates within the Java community over time and is sometimes seen as a bad practice. While it is widely used by popular Java frameworks and libraries, its potential drawbacks have discouraged its frequent use in regular server-side applications.

随着时间的推移,Reflection API 的使用在 Java 社区中引发了广泛的争论,有时甚至被视为一种糟糕的做法。虽然它被流行的 Java 框架和库广泛使用,但其潜在的缺点却阻碍了它在常规服务器端应用程序中的频繁使用。

In this tutorial, we’ll delve into the benefits and drawbacks that reflection may introduce into our codebases. Additionally, we’ll explore when it is appropriate or inappropriate to use reflection, ultimately helping us determine whether it qualifies as a bad practice.

在本教程中,我们将深入探讨反射可能引入代码库的优点和缺点。此外,我们还将探讨何时适合或不适合使用反射,最终帮助我们确定反射是否属于不良实践。

2. Understanding Java Reflection

2.了解 Java 反射

In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its structure and behavior. When a programming language fully supports reflection, it permits the inspection and modification of the structure and behavior of classes and objects in the codebase at runtime, allowing the source code to rewrite aspects of itself.

在计算机科学中,反思编程或反思是指进程检查、反省和修改其结构和行为的能力。当编程语言完全支持反思时,它允许在运行时检查和修改代码库中类和对象的结构和行为,允许源代码重写自身的某些方面。

According to this definition, Java offers full support for reflection. Apart from Java, other common programming languages that offer support for reflective programming are C#, Python, and JavaScript.

根据这一定义,Java 完全支持反射。除 Java 外,C#、Python 和 JavaScript 也是支持反射编程的常见编程语言。

Many popular Java frameworks, like Spring and Hibernate, rely on it to provide advanced features like dependency injection, aspect-oriented programming, and database mapping. Apart from using reflection indirectly through frameworks or libraries, we can use it directly with the help of the java.lang.reflect package, or the Reflections library.

许多流行的 Java 框架(如SpringHibernate)都依赖反射来提供依赖注入、面向方面编程和数据库映射等高级功能。除了通过框架或库间接使用反射外,我们还可以借助java.lang.reflect反射库直接使用反射。

3. The Pros of Java Reflection

3.Java 反射的优点

Java Reflection can be a powerful and versatile feature if used carefully. In this section, we’ll explore some of the key advantages of reflection and how we can use it effectively in certain scenarios.

如果谨慎使用,Java 反射将是一项强大而多用途的功能。在本节中,我们将探讨反射的一些主要优势,以及如何在某些应用场景中有效使用反射。

3.1. Dynamic Configuration

3.1.动态配置

Reflection API empowers dynamic programming, enhancing an application’s flexibility and adaptability. This aspect proves valuable when we encounter scenarios where required classes or modules are unknown until runtime.

反射 API 增强了动态编程的能力,提高了应用程序的灵活性和适应性。当我们遇到在运行前未知所需类或(模块)的情况时,这一点就显得非常重要。

Moreover, by making use of reflection’s dynamic capabilities, developers can build systems that can be reconfigured effortlessly in real-time, without the need for extensive code changes.

此外,通过利用反射的动态功能,开发人员可以构建能够毫不费力地实时重新配置的系统,而无需大量修改代码。

For example, the Spring framework uses reflection for creating and configuring beans. It scans classpath components and dynamically instantiates and configures beans based on annotations and XML configurations, allowing developers to add or modify beans without changing the source code.

例如,Spring 框架使用反射来创建和配置 Bean。它扫描类路径组件,并根据注解和 XML 配置动态地实例化和配置 Bean,允许开发人员在不更改源代码的情况下添加或修改 Bean。

3.2. Extensibility

3.2.可扩展性

Another significant advantage of using reflection is extensibility. This enables us to incorporate new functionalities or modules at runtime, without changing the application’s core code.

使用反射的另一个显著优势是可扩展性。这使我们能够在运行时加入新的功能或模块,而无需更改应用程序的核心代码。

To illustrate that, let’s suppose we are using a third-party library that defines a base class and incorporates multiple sub-types for polymorphic deserialization. We would like to extend the functionality by introducing our own custom sub-types that extend the same base class. The Reflection API comes in handy for this particular use case because we can utilize it to dynamically register these custom sub-types at runtime and effortlessly integrate them with the third-party library. Thus, we can adapt a library to our specific requirements without altering its codebase.

为了说明这一点,假设我们正在使用一个第三方库,该库定义了一个基类,并为多态反序列化集成了多个子类。我们希望通过引入扩展相同基类的自定义子类型来扩展该功能。Reflection API 在此特定用例中非常有用,因为我们可以利用它在运行时动态注册这些自定义子类型,并毫不费力地将它们与第三方库集成。因此,我们可以根据自己的特定需求调整库,而无需更改其代码库。

3.3. Code Analysis

3.3.代码分析

Another use case of reflection is code analysis, which allows us to inspect code dynamically. This is particularly useful because it can lead to enhanced quality of software development.

反射的另一个用例是代码分析,它允许我们动态检查代码。这一点特别有用,因为它可以提高软件开发的质量。

For example, ArchUnit, a Java library for architectural unit testing, makes use of reflection and bytecode analysis. Information that the library cannot obtain through the Reflection API is obtained at the bytecode level. This way, the library analyzes the code dynamically, and we’re able to enforce architectural rules and constraints, ensuring the integrity and high quality of our software projects.

例如,用于架构单元测试的 Java 库 ArchUnit 使用了反射和字节码分析。该库无法通过反射 API 获取的信息可在字节码级别获取。这样,该库就能动态分析代码,我们也就能执行架构规则和约束,确保软件项目的完整性和高质量。

4. The Cons of Java Reflection

4.Java 反射的缺点

As we saw in the previous section, reflection is a powerful feature with various applications. However, it comes with a set of drawbacks that we need to consider before we decide to use it in our projects. In this section, we’ll delve into some of the major cons of this feature.

正如我们在上一节中所看到的,反射是一项功能强大的特性,具有多种应用。然而,在我们决定在项目中使用它之前,我们需要考虑它的一系列缺点。在本节中,我们将深入探讨该功能的一些主要缺点。

4.1. Performance Overhead

4.1.性能开销

Java reflection dynamically resolves types and may limit certain JVM optimizations. Consequently, reflective operations have slower performance than their non-reflective counterparts. So, when dealing with performance-sensitive applications, we should consider avoiding reflection in parts of the code that are called frequently.

Java 反射动态解析类型,并可能限制某些 JVM 优化。因此,反射操作的性能比非反射操作低。因此,在处理对性能敏感的应用程序时,我们应考虑避免在代码中频繁调用反射。

To demonstrate this, we’re going to create a very simple Person class and perform some reflective and non-reflective operations on it:

为了演示这一点,我们将创建一个非常简单的 Person 类,并对其执行一些反射和非反射操作:

public class Person {

    private String firstName;
    private String lastName;
    private Integer age;

    public Person(String firstName, String lastName, Integer age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    // standard getters and setters
}

Now, we can create a benchmark in order to see the time difference in calling the getters of our class:

现在,我们可以创建一个基准,以查看调用类的 getters 时的时间差:

public class MethodInvocationBenchmark {

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public void directCall(Blackhole blackhole) {

        Person person = new Person("John", "Doe", 50);

        blackhole.consume(person.getFirstName());
        blackhole.consume(person.getLastName());
        blackhole.consume(person.getAge());
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public void reflectiveCall(Blackhole blackhole) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {

        Person person = new Person("John", "Doe", 50);

        Method getFirstNameMethod = Person.class.getMethod("getFirstName");
        blackhole.consume(getFirstNameMethod.invoke(person));

        Method getLastNameMethod = Person.class.getMethod("getLastName");
        blackhole.consume(getLastNameMethod.invoke(person));

        Method getAgeMethod = Person.class.getMethod("getAge");
        blackhole.consume(getAgeMethod.invoke(person));
    }
}

Let’s check the results of running our method invocation benchmark:

让我们检查一下方法调用基准的运行结果:

Benchmark                                 Mode  Cnt    Score   Error  Units
MethodInvocationBenchmark.directCall      avgt    5    8.428 ± 0.365  ns/op
MethodInvocationBenchmark.reflectiveCall  avgt    5  102.785 ± 2.493  ns/op

Now, let’s create another benchmark to test the performance of reflective initialization compared to directly calling the constructor:

现在,让我们创建另一个基准,测试反射式初始化与直接调用构造函数相比的性能:

public class InitializationBenchmark {

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public void directInit(Blackhole blackhole) {

        blackhole.consume(new Person("John", "Doe", 50));
    }

    @Benchmark
    @Fork(value = 1, warmups = 1)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public void reflectiveInit(Blackhole blackhole) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        Constructor<Person> constructor = Person.class.getDeclaredConstructor(String.class, String.class, Integer.class);
        blackhole.consume(constructor.newInstance("John", "Doe", 50));
    }
}

Let’s check our results for constructor invocation:

让我们检查一下构造函数调用的结果:

Benchmark                                 Mode  Cnt    Score   Error  Units
InitializationBenchmark.directInit        avgt    5    5.290 ± 0.395  ns/op
InitializationBenchmark.reflectiveInit    avgt    5   23.331 ± 0.141  ns/op

After reviewing the results of both benchmarks, we can reasonably infer that using reflection in Java can be considerably slower for use cases like invoking methods or initializing objects.

回顾这两项基准测试的结果后,我们可以合理地推断出,在 Java 中使用反射会大大降低调用方法或初始化对象等用例的速度。

Our article Microbenchmarking with Java has more information about what we’ve used for comparing the execution times.

我们的文章Microbenchmarking with Java提供了更多有关我们用于比较执行时间的信息。

4.2. Exposure of Internals

4.2.暴露内部结构

Reflection permits operations that might be restricted in non-reflective code. A good example is the ability to access and manipulate private fields and methods of classes. By doing so, we violate encapsulation, a fundamental principle of object-oriented programming.

反射允许进行在非反射代码中可能受到限制的操作。访问和操作类的私有字段和方法就是一个很好的例子。这样做违反了面向对象编程的基本原则–封装。

As an example, let’s create a dummy class with only one private field, without creating any getters or setters:

例如,让我们创建一个只有一个私有字段的虚拟类,不创建任何 getterssetters

public class MyClass {
    
    private String veryPrivateField;
    
    public MyClass() {
        this.veryPrivateField = "Secret Information";
    }
}

Now, let’s try to access this private field in a unit test:

现在,让我们尝试在单元测试中访问这个私有字段:

@Test
public void givenPrivateField_whenUsingReflection_thenIsAccessible()
  throws IllegalAccessException, NoSuchFieldException {
      MyClass myClassInstance = new MyClass();

      Field privateField = MyClass.class.getDeclaredField("veryPrivateField");
      privateField.setAccessible(true);

      String accessedField = privateField.get(myClassInstance).toString();
      assertEquals(accessedField, "Secret Information");
}

4.3. Loss of Compile-Time Safety

4.3.编译时安全性的丧失

Another drawback of reflection is the loss of compile-time safety. In typical Java development, the compiler performs strict type checks and ensures that we’re using classes, methods, and fields correctly. However, reflection bypasses these checks, and as a result, some errors aren’t discoverable until runtime. Therefore, this can lead to hard-to-detect bugs and may compromise the reliability of our codebase.

反射的另一个缺点是丧失了编译时安全性。在典型的 Java 开发中,编译器会执行严格的类型检查,确保我们正确使用类、方法和字段。但是,反射绕过了这些检查,因此有些错误直到运行时才会被发现。因此,这会导致难以检测的错误,并可能影响代码库的可靠性。

4.4. Decreased Maintainability of Code

4.4.代码可维护性降低

Using reflection can significantly decrease code maintainability. Code that relies heavily on reflection tends to be less readable than non-reflective code. Reduced readability can lead to maintenance difficulties because it’s harder for developers to understand the code’s intent and functionality.

使用反射会大大降低代码的可维护性。与非反射代码相比,严重依赖反射的代码往往可读性较差。可读性降低会导致维护困难,因为开发人员更难理解代码的意图和功能。

Another challenge is represented by limited tool support. Reflection isn’t fully supported by all development tools and IDEs. As a result, this can slow down development and make it more error-prone, as developers must rely on manual inspections to catch issues.

另一个挑战是有限的工具支持。并非所有开发工具和集成开发环境都完全支持反射。因此,这会减慢开发速度并增加出错率,因为开发人员必须依靠手动检查才能发现问题。

4.5. Security Concerns

4.5.安全问题

Java reflection involves accessing and manipulating internal elements of the program, which can cause security concerns. In a restrictive environment, allowing reflective access can be risky because malicious code might attempt to exploit reflection to gain unauthorized access to sensitive resources or perform actions that violate security policies.

Java 反射涉及访问和操作程序的内部元素,这可能会引起安全问题。在限制性环境中,允许反射访问可能会带来风险,因为恶意代码可能会试图利用反射未经授权访问敏感资源或执行违反安全策略的操作

5. Impact of Java 9 on Reflection

5.Java 9 对反射的影响

The introduction of modules in Java 9 brought significant changes to the way modules encapsulate their code. Before Java 9, encapsulation could have been broken easily using reflection.

在 Java 9 中引入模块给模块封装代码的方式带来了重大变化。在 Java 9 之前,使用反射可以轻松地破坏封装。

Modules no longer expose their internals by default. However, Java 9 provided some mechanisms to selectively grant permissions for reflective access between modules. This allows us to open specific packages when necessary, ensuring compatibility with legacy code or third-party libraries.

默认情况下,模块不再公开其内部结构。然而,Java 9 提供了一些机制 来有选择性地授予模块之间的反射访问权限。这使我们能够在必要时打开特定的包,确保与遗留代码或第三方库的兼容性。

6. When Should We Use Java Reflection?

6.何时使用 Java 反射?

Having explored the advantages and disadvantages of reflection, we can identify some use cases when it would be appropriate or not to use this powerful feature.

在探究了反射的优缺点之后,我们可以确定在哪些使用情况下适合或不适合使用这一强大的功能。

Using the Reflection API proves valuable where dynamic behavior is essential. As we’ve already seen, many well-known frameworks and libraries, such as Spring and Hibernate, rely on it for key features. In these cases, reflection enables these frameworks to offer flexibility and customization to developers. Additionally, when we’re creating a library or framework ourselves, reflection can empower other developers to extend and customize their interactions with our code, making it a suitable choice.

正如我们已经看到的,许多著名的框架和库(如 Spring 和 Hibernate)的关键功能都依赖于反射 API。在这些情况下,反射使这些框架能够为开发人员提供灵活性和自定义功能。此外,当我们自己创建一个库或框架时,反射可以让其他开发人员扩展和定制他们与我们代码的交互,因此是一个合适的选择。

Furthermore, reflection can be an option for extending code we can’t modify. Therefore, it can be a powerful tool when we’re working with third-party libraries or legacy code and need to integrate new functionalities or adapt existing ones without altering the original codebase. It allows us to access and manipulate elements that would otherwise be inaccessible, making it a practical choice for such scenarios.

此外,反射也是扩展我们无法修改的代码的一种选择。因此,当我们使用第三方库或遗留代码,需要集成新功能或调整现有功能而又不改变原始代码库时,反射可以成为一种强大的工具。它允许我们访问和操作原本无法访问的元素,使其成为此类场景中的实用选择。

However, it’s important to exercise caution when considering using reflection. In applications with strong security requirements, using reflective code should be approached carefully. Reflection allows access to internal elements of a program, which can potentially be exploited by malicious code. Additionally, when dealing with performance-critical applications, particularly in sections of code that are frequently called, reflection’s performance overhead can become a concern. Moreover, if compile-time type checking is crucial for our project, we should consider avoiding using reflective code because of its lack of compile-time safety.

然而,在考虑使用反射时,必须谨慎行事。在具有较高安全性要求的应用程序中,应谨慎使用反射代码。反射允许访问程序的内部元素,而恶意代码有可能利用这些元素。此外,在处理对性能要求较高的应用程序时,尤其是在频繁调用的代码段中,反射的性能开销可能会成为一个问题。此外,如果编译时类型检查对我们的项目至关重要,我们就应该考虑避免使用反射代码,因为它缺乏编译时安全性。

7. Conclusion

7.结论

As we’ve learned throughout this article, reflection in Java should be viewed as a powerful tool that demands careful use, rather than being labeled as a bad practice. Similar to any feature, excessive use of reflection can indeed be considered a bad practice. However, when applied carefully and only when genuinely necessary, reflection can be a valuable asset.

正如我们在本文中所了解到的,Java 中的反射应被视为一种需要谨慎使用的强大工具,而不是被贴上不良实践的标签。与任何功能类似,过度使用反射确实会被视为一种糟糕的做法。但是,如果谨慎使用,并且只在真正必要时才使用,那么反射就会成为一种宝贵的资产。

As always, the source code is available over on GitHub.

与往常一样,源代码可在 GitHub 上获取