Monkey Patching in Java – Java 中的猴子补丁

最后修改: 2024年 1月 29日

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

1. Introduction

1.导言

In software development, we often need to adapt and enhance our systems’ existing functionalities. Sometimes, modifying the existing codebase may not be possible or may not be the most pragmatic solution. Therefore, a solution to this problem is monkey patching. This technique allows us to modify a class or module runtime without altering its original source code.

在软件开发过程中,我们经常需要调整和增强系统的现有功能。有时,修改现有代码库可能是不可能的,也可能不是最实用的解决方案。因此,解决这一问题的方法就是 打补丁。这种技术允许我们在运行时修改类或模块,而无需修改其原始源代码

In this article, we’ll explore how monkey patching can be used in Java, when to use it, and its drawbacks.

本文将探讨如何在 Java 中使用 “猴子补丁”、何时使用以及其缺点。

2. Monkey Patching

2.猴子修补

The term monkey patching originates from an earlier term, guerilla patch, which refers to changing code sneakily at runtime without any rules. It gained popularity thanks to the flexibility of dynamic programming languages, such as Java, Python, or Ruby.

所谓 “猴子补丁”(monkey patch),源于更早的术语 “游击补丁”(guerilla patch),指的是在运行时不按规则偷偷更改代码。它的流行得益于动态编程语言(如 Java、Python 或 Ruby)的灵活性。

Monkey patching enables us to modify or extend classes or modules at runtime. This allows us to tweak or augment existing code without requiring direct alterations to the source. It is particularly useful when adjustments are imperative, but direct modification is either unfeasible or undesirable due to various constraints.

通过 Monkey patch,我们可以在运行时修改或扩展类或模块。这样,我们就可以调整或扩充现有代码,而无需直接修改源代码。当必须进行调整,但由于各种限制直接修改不可行或不可取时,这种方法尤其有用。

In Java, monkey patching can be achieved through various techniques. These methods include proxies, bytecode instrumentation, aspect-oriented programming, reflection, or decorator patterns. Each method offers its unique approach, suitable for specific scenarios.

在 Java 中,猴子修补可以通过各种技术实现。这些方法包括代理、字节码检测、面向方面编程、反射或装饰器模式。每种方法都提供了适合特定场景的独特方法。

Next, we will create a trivial money converter with a hardcoded exchange rate from EUR to USD to apply monkey patching using different approaches.

接下来,我们将使用硬编码的欧元兑美元汇率创建一个微不足道的货币转换器,并使用不同的方法进行猴子修补。

public interface MoneyConverter {
    double convertEURtoUSD(double amount);
}
public class MoneyConverterImpl implements MoneyConverter {
    private final double conversionRate;

    public MoneyConverterImpl() {
        this.conversionRate = 1.10;
    }

    @Override
    public double convertEURtoUSD(double amount) {
        return amount * conversionRate;
    }
}

3. Dynamic Proxies

3.动态代理

In Java, the use of proxies is a powerful technique for implementing monkey patching. A proxy is a wrapper that passes method invocation through its own facilities. This provides us with an opportunity to modify or enhance the behavior of the original class.

在 Java 中,使用代理是实现猴子补丁的一种强大技术。代理是一个封装器,它通过自己的设施来传递方法调用。这为我们提供了一个修改或增强原始类行为的机会。

Notably, dynamic proxies stand as a fundamental proxy mechanism in Java. Moreover, they are widely used by frameworks like Spring Framework.

值得注意的是,动态代理是 Java 中的一种基本代理机制。此外,它们还被 Spring Framework 等框架广泛使用。</span

A good example is the @Transactional annotation. When applied to a method, the associated class undergoes dynamic proxy wrapping at runtime. Upon invoking the method, Spring redirects the call to the proxy. After that, the proxy initiates a new transaction or joins the existing one. Subsequently, the actual method is called. Note that, to be able to benefit from this transactional behavior, we need to rely on Spring’s dependency injection mechanism because it’s based on dynamic proxies.

@Transactional 注解就是一个很好的例子。当应用到方法时,相关类将在运行时进行动态代理封装。调用方法时,Spring 会将调用重定向到代理。之后,代理会启动一个新事务或加入现有事务。随后,实际方法被调用。请注意,要从这种事务行为中获益,我们需要依赖 Spring 的依赖注入机制,因为它是基于动态代理的。

<br

Let’s use dynamic proxies to wrap our conversion method with some logs for our money converter. First, we must create a subtype of java.lang.reflect.InvocationHandler:

让我们使用动态代理来为货币转换器的转换方法封装一些日志。首先,我们必须创建 java.lang.reflect.InvocationHandler: 的子类型。

public class LoggingInvocationHandler implements InvocationHandler {
    private final Object target;

    public LoggingInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After method: " + method.getName());
        return result;
    }
}

Next, let’s create a test to verify if logs surrounded the conversion method:

接下来,让我们创建一个测试来验证日志是否包围了转换方法:

@Test
public void whenMethodCalled_thenSurroundedByLogs() {
    ByteArrayOutputStream logOutputStream = new ByteArrayOutputStream();
    System.setOut(new PrintStream(logOutputStream));
    MoneyConverter moneyConverter = new MoneyConverterImpl();
    MoneyConverter proxy = (MoneyConverter) Proxy.newProxyInstance(
      MoneyConverter.class.getClassLoader(),
      new Class[]{MoneyConverter.class},
      new LoggingInvocationHandler(moneyConverter)
    );

    double result = proxy.convertEURtoUSD(10);

    Assertions.assertEquals(11, result);
    String logOutput = logOutputStream.toString();
    assertTrue(logOutput.contains("Before method: convertEURtoUSD"));
    assertTrue(logOutput.contains("After method: convertEURtoUSD"));
}

4. Aspect-Oriented Programming

4.面向方面的编程

Aspect-oriented programming (AOP) is a paradigm that addresses the cross-cutting concerns in software development, offering a modular and cohesive approach to separate concerns that would otherwise be scattered throughout the codebase. This is achieved by adding additional behavior to existing code, without modifying the code itself.

面向方面编程(AOP)是一种解决软件开发中交叉问题的范式,它提供了一种模块化和内聚的方法来分离分散在代码库中的问题。实现这一目标的方法是在现有代码中添加额外的行为,而无需修改代码本身。

In Java, we can leverage AOP through frameworks like AspectJ or Spring AOP. While Spring AOP provides a lightweight and Spring-integrated approach, AspectJ offers a more powerful and standalone solution.

在 Java 中,我们可以通过 AspectJSpring AOP 等框架来利用 AOP。Spring AOP 提供了一种轻量级的 Spring 集成方法,而 AspectJ 则提供了一种功能更强大的独立解决方案。

In monkey patching, AOP provides an elegant solution by allowing us to apply changes to multiple classes or methods in a centralized manner. Using aspects, we can address concerns like logging or security policies that need to be applied consistently across various components without altering the core logic.

在猴子补丁中,AOP 提供了一种优雅的解决方案,允许我们以集中的方式对多个类或方法应用更改。使用方面,我们可以在不改变核心逻辑的情况下,解决需要在不同组件中一致应用的日志记录或安全策略等问题。

Let’s try to surround the same method with the same logs. To do so, we will use the AspectJ framework, and we need to add the spring-boot-starter-aop dependency to our project:

让我们尝试用相同的日志来包围相同的方法。为此,我们将使用 AspectJ 框架,并且我们需要将 spring-boot-starter-aop 依赖关系添加到我们的项目:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.2.2</version>
</dependency>

We can find the latest version of the library on Maven Central.

我们可以在 Maven Central 上找到该库的最新版本

In Spring AOP, aspects are typically applied to Spring-managed beans. Therefore, we will define our money converter as a bean for simplicity:

在 Spring AOP 中,方面通常应用于 Spring 管理的 Bean。因此,为了简单起见,我们将把货币转换器定义为一个 Bean:

@Bean
public MoneyConverter moneyConverter() {
    return new MoneyConverterImpl();
}

Now, we need to define our aspect to surround our conversion method with logs:

现在,我们需要定义我们的方面,用日志包围我们的转换方法:

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.baeldung.monkey.patching.converter.MoneyConverter.convertEURtoUSD(..))")
    public void beforeConvertEURtoUSD(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }

    @After("execution(* com.baeldung.monkey.patching.converter.MoneyConverter.convertEURtoUSD(..))")
    public void afterConvertEURtoUSD(JoinPoint joinPoint) {
        System.out.println("After method: " + joinPoint.getSignature().getName());
    }
}

Next, we can create a test to verify if our aspect is applied correctly:

接下来,我们可以创建一个测试来验证我们的方面是否应用正确:

@Test
public void whenMethodCalled_thenSurroundedByLogs() {
    ByteArrayOutputStream logOutputStream = new ByteArrayOutputStream();
    System.setOut(new PrintStream(logOutputStream));

    double result = moneyConverter.convertEURtoUSD(10);

    Assertions.assertEquals(11, result);
    String logOutput = logOutputStream.toString();
    assertTrue(logOutput.contains("Before method: convertEURtoUSD"));
    assertTrue(logOutput.contains("After method: convertEURtoUSD"));
}

5. Decorator Pattern

5.装饰图案

Decorator is a design pattern that allows us to attach behavior to objects by placing them inside wrapper objects. Therefore, we can assume that a decorator provides an enhanced interface to the original object.

Decorator 是一种设计模式,它允许我们通过将对象置于封装对象中来为对象附加行为。 因此,我们可以假设装饰器为原始对象提供了一个增强的接口。

In the context of monkey patching, it provides a flexible solution for enhancing or modifying the behavior of classes without directly modifying their code. We can create decorator classes that implement the same interfaces as the original classes and introduce additional functionality by wrapping instances of the base classes.

在 “猴子修补 “中,它提供了一种灵活的解决方案,可在不直接修改代码的情况下增强或修改类的行为。我们可以创建实现与原始类相同接口的装饰器类,并通过封装基类的实例来引入额外的功能。

This pattern is particularly useful when dealing with a set of related classes that share common interfaces. By employing the Decorator Pattern, modifications can be applied selectively, allowing for a modular and non-intrusive way to adapt or extend the functionality of individual objects.

在处理一组共享共同接口的相关类时,这种模式尤其有用。通过使用 Decorator Pattern,可以有选择地进行修改,从而以模块化和非侵入的方式调整或扩展单个对象的功能。

The Decorator Pattern contrasts with other monkey patching techniques, offering a more structured and explicit approach to augmenting object behavior. Its versatility makes it well-suited for scenarios where a clear separation of concerns and a modular approach to code modification are desired.

装饰器模式与其他猴子修补技术不同,它提供了一种更有条理、更明确的方法来增强对象行为。它的多功能性使其非常适合需要明确分离关注点和模块化代码修改方法的场景。

To implement this pattern, we will create a new class that will implement the MoneyConverter interface. It will have a property of type MoneyConverter, which will process the request. Moreover, the purpose of our decorator is just to add some logs and forward the money conversion request.

为实现这一模式,我们将创建一个新类,该类将实现 MoneyConverter 接口。它将拥有一个类型为 MoneyConverter 的属性,用于处理请求。此外,我们的装饰器只是为了添加一些日志并转发货币转换请求。

public class MoneyConverterDecorator implements MoneyConverter {
    private final MoneyConverter moneyConverter;

    public MoneyConverterDecorator(MoneyConverter moneyConverter) {
        this.moneyConverter = moneyConverter;
    }

    @Override
    public double convertEURtoUSD(double amount) {
        System.out.println("Before method: convertEURtoUSD");
        double result = moneyConverter.convertEURtoUSD(amount);
        System.out.println("After method: convertEURtoUSD");
        return result;
    }
}

Now, let’s create a test to check if the logs were added:

现在,让我们创建一个测试,检查日志是否已添加:

@Test
public void whenMethodCalled_thenSurroundedByLogs() {
    ByteArrayOutputStream logOutputStream = new ByteArrayOutputStream();
    System.setOut(new PrintStream(logOutputStream));
    MoneyConverter moneyConverter = new MoneyConverterDecorator(new MoneyConverterImpl());

    double result = moneyConverter.convertEURtoUSD(10);

    Assertions.assertEquals(11, result);
    String logOutput = logOutputStream.toString();
    assertTrue(logOutput.contains("Before method: convertEURtoUSD"));
    assertTrue(logOutput.contains("After method: convertEURtoUSD"));
}

6. Reflection

6.反思

Reflection is the ability of a program to examine and modify its behavior at runtime. In Java, we can use it with the help of the java.lang.reflect package or the Reflections library. While it provides significant flexibility, it should be used carefully due to its potential impact on code maintainability and performance.

反射是程序在运行时检查和修改其行为的一种能力。lang.reflect 软件包Reflections 库 。虽然它提供了极大的灵活性,但由于其对代码可维护性和性能的潜在影响,应谨慎使用

One common application of reflection for monkey patching involves accessing class metadata, inspecting fields and methods, and even invoking methods at runtime. Therefore, this capability opens the door to making runtime modifications without directly altering the source code.

用于修补猴子的反射的一个常见应用涉及访问类元数据、检查字段和方法,甚至在运行时调用方法。因此,这一功能为运行时修改打开了大门,而无需直接修改源代码。

Let’s suppose that the conversion rate was updated to a new value. We can’t change it because we didn’t create setters for our converter class, and it is hardcoded. Therefore, we can use reflection to break encapsulation and update the conversion rate to the new value:

假设转换率更新为一个新值。我们无法更改它,因为我们没有为转换器类创建设置器,而且它是硬编码的。因此,我们可以使用反射打破封装,将转换率更新为新值:

@Test
public void givenPrivateField_whenUsingReflection_thenBehaviorCanBeChanged() throws IllegalAccessException, NoSuchFieldException {
    MoneyConverter moneyConvertor = new MoneyConverterImpl();

    Field conversionRate = MoneyConverterImpl.class.getDeclaredField("conversionRate");
    conversionRate.setAccessible(true);
    conversionRate.set(moneyConvertor, 1.2);
    double result = moneyConvertor.convertEURtoUSD(10);

    assertEquals(12, result);
}

7. Bytecode Instrumentation

7.字节码工具

Through bytecode instrumentation, we can dynamically modify the bytecode of compiled classes. One popular framework for bytecode instrumentation is the Java Instrumentation API. This API was introduced with the purpose of collecting data for utilization by various tools. As these modifications are exclusively additive, such tools don’t alter the application’s state or behavior. For example, such tools include monitoring agents, profilers, coverage analyzers, and event loggers.

通过字节码工具,我们可以动态修改编译类的字节码。字节码工具的一个流行框架是 Java Instrumentation API。引入该 API 的目的是收集数据供各种工具使用。由于这些修改完全是附加性的,因此此类工具不会改变应用程序的状态或行为。例如,此类工具包括监控代理、剖析器、覆盖率分析器和事件记录器。

However, this approach introduces a more advanced level of complexity, and it’s crucial to handle it with care due to its potential impact on the runtime behavior of our application.

但是,这种方法引入了更高级别的复杂性,由于其对应用程序运行时行为的潜在影响,因此必须谨慎处理。

8. Use Cases of Monkey Patching

8.猴子补丁的使用案例

Monkey patching finds utility in various scenarios where making runtime modifications to code becomes a pragmatic solution. One common use case is fixing urgent bugs in third-party libraries or frameworks without waiting for official updates. It enables us to swiftly address some issues by patching the code temporarily.

在对代码进行运行时修改成为一种实用解决方案的各种应用场景中,我们都能发现 Monkey patch 的实用性。一个常见的使用案例是在不等待官方更新的情况下修复第三方库或框架中的紧急错误。通过临时修补代码,我们可以迅速解决一些问题。

Another scenario is extending or modifying the behavior of existing classes or methods in situations where direct code alterations are challenging or impractical. Also, in testing environments, monkey patching proves beneficial for introducing mock behaviors or altering functionalities temporarily to simulate different scenarios.

另一种情况是,在直接修改代码具有挑战性或不切实际的情况下,扩展或修改现有类或方法的行为。此外,在测试环境中,”猴子修补程序 “有利于引入模拟行为或临时改变功能,以模拟不同的场景。

Furthermore, monkey patching can be employed when rapid prototyping or experimentation is required. This allows us to iterate quickly and explore various implementations without committing to permanent changes.

此外,在需要进行快速原型设计或实验时,可以使用猴子补丁。这使我们能够快速迭代和探索各种实现方法,而无需承诺永久性更改。

9. Risks of Monkey Patching

9.猴子补丁的风险

Despite its utility, monkey patching introduces some risks that should be carefully considered. Potential side effects and conflicts represent one significant risk, as modifications made at runtime might interact unpredictably. Moreover, this lack of predictability can lead to challenging debugging scenarios and increased maintenance overhead.

尽管 “猴子补丁 “非常实用,但它也会带来一些风险,因此应谨慎考虑。潜在的副作用和冲突是一个重大风险,因为在运行时进行的修改可能会产生不可预测的交互。此外,这种缺乏可预测性的情况可能会导致具有挑战性的调试场景,并增加维护开销。

Furthermore, monkey patching can compromise code readability and maintainability. Injecting changes dynamically may obscure the actual behavior of the code, making it challenging for us to understand and maintain, especially in large projects.

此外,monkey patch 会影响代码的可读性和可维护性。动态注入更改可能会掩盖代码的实际行为,使我们难以理解和维护,尤其是在大型项目中。

Security concerns may also arise with monkey patching, as it can introduce vulnerabilities or malicious behavior. Additionally, the reliance on monkey patching may discourage the adoption of standard coding practices and systematic solutions to problems, leading to a less robust and cohesive codebase.

打补丁也可能带来安全问题,因为打补丁可能会引入漏洞或恶意行为。此外,对猴子补丁的依赖可能会阻碍采用标准编码实践和系统化的问题解决方案,从而导致代码库的健壮性和内聚性降低。

10. Conclusion

10.结论

In this article, we learned that monkey patching may prove helpful and powerful in some scenarios. It can also be achieved through various techniques, each with its benefits and drawbacks. However, this approach should be used carefully because it can lead to performance, readability, maintainability, and security issues.

在本文中,我们了解到 在某些情况下,”猴子补丁 “可能会被证明是有用和强大的。它还可以通过各种技术来实现,每种技术都有其优点和缺点。然而,这种方法应谨慎使用,因为它可能导致性能、可读性、可维护性和安全性问题

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

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