覆寫實例化的 Java 物件中的方法
1.概述
我們可以透過運行時行為修改來覆寫實例化物件的行為,這通常透過設計模式和框架來實現,而不是在運行時直接改變物件的類別。
讓我們討論一下修改現有物件行為的四種方法。
2. 在計算器中新增日誌記錄
假設我們有一個Calculator
介面和實現,其中定義了兩個基本方法, add
和subtract
:
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
public class SimpleCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int subtract(int a, int b) {
return a - b;
}
}
進一步假設,我們想要追蹤方法呼叫次數或為這些方法新增日誌記錄,但無法或不想修改類別本身。換句話說,當有人呼叫add(3, 5)
或subtract(10, 3)
時,我們也想追蹤方法呼叫次數,或使用 SLF4J Logback 框架記錄一些日誌訊息。
我們可以考慮至少四種方法來做到這一點:
- 子類別
- 裝飾者模式
- JDK動態代理
- Spring 的
ProxyFactory
3. 子類化
子類化是一種簡單的方法。簡單來說,我們可以擴展SimpleCalculator
並重寫其add
和subtract
方法,以包含日誌記錄:
public class LoggingCalculator extends SimpleCalculator {
@Override
public int add(int a, int b) {
log.debug("LOG: Before addition.");
int result = super.add(a, b);
log.debug("LOG: After addition. Result: {}", result);
return result;
}
@Override
public int subtract(int a, int b) {
log.debug("LOG: Before subtraction.");
int result = super.subtract(a, b);
log.debug("LOG: After subtraction. Result: {}", result);
return result;
}
}
現在,如果我們建構一個LoggingCalculator
而不是SimpleCalculator
,我們就能得到想要的日誌記錄行為。然後,我們使用 JUnit 5 測試斷言來驗證我們可以使用這個子類別:
@Test
void givenACalculatorClass_whenSubclassingToAddLogging_thenLoggingCalculatorCanBeUsed() {
Calculator calculator = new LoggingCalculator();
assertEquals(8, calculator.add(5, 3));
assertEquals(2, calculator.add(5, 3));
}
但是,這有一個限制。例如,如果我們在執行時已經有一個SimpleCalculator
實例,該怎麼辦?因此,我們需要找到覆蓋方法行為的執行時間方法,這也是我們接下來要討論的內容。
4.使用裝飾器模式
裝飾器是一種設計模式,它既提供了子類化的優點,也彌補了其限制。它是一種結構化模式,允許將行為添加到單一物件中。此外,我們可以靜態或動態地加入行為。而且,我們可以在不影響類別本身或同一類別中其他物件的行為的情況下添加行為。
讓我們定義一個名為MeteredCalculatorDecorator
的裝飾器類,它實作Calculator
接口,並重寫它的兩個方法來追蹤方法呼叫次數:
public class MeteredCalculatorDecorator implements Calculator {
private final Calculator wrappedCalculator;
private final Map<String, Integer> methodCalls;
public MeteredCalculatorDecorator(Calculator calculator) {
this.wrappedCalculator = calculator;
this.methodCalls = new HashMap<>();
methodCalls.put("add", 0);
methodCalls.put("subtract", 0);
}
@Override
public int add(int a, int b) {
methodCalls.merge("add", 1, Integer::sum);
return wrappedCalculator.add(a, b);
}
@Override
public int subtract(int a, int b) {
methodCalls.merge("subtract", 1, Integer::sum);
return wrappedCalculator.subtract(a, b);
}
public int getCallCount(String methodName) {
return methodCalls.getOrDefault(methodName, 0);
}
}
因此,這個裝飾器包裝了行為,因此,當我們傳遞一個Calculator
物件時,它會包裝其行為以添加方法呼叫計數的追蹤。和之前一樣,我們可以使用測試方法來驗證它確實擴展了方法的行為:
@Test
void givenACalculator_whenUsingMeteredDecorator_thenMethodCallsAreCountedCorrectly() {
Calculator simpleCalc = new SimpleCalculator();
MeteredCalculatorDecorator decoratedCalc = new MeteredCalculatorDecorator(simpleCalc);
decoratedCalc.add(10, 5);
decoratedCalc.add(2, 3);
decoratedCalc.subtract(10, 5);
assertEquals(15, decoratedCalc.add(10, 5), "Core functionality must still work.");
assertEquals(3, decoratedCalc.getCallCount("add"), "The 'add' method should have been called 3 times.");
assertEquals(1, decoratedCalc.getCallCount("subtract"), "The 'subtract' method should have been called 1 time.");
}
5. 使用 JDK 動態代理
或者,我們可以使用 JDK 動態代理。 JDK動態代理在運行時產生代理類別和代理對象,並實作一個或多個介面。此外,它將代理上的方法呼叫重定向到自訂的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 {
log.debug("PROXY LOG: Intercepting method: {}", method.getName());
Object result = method.invoke(target, args);
log.debug("PROXY LOG: Method {} executed.", method.getName());
return result;
}
}
之後,讓我們使用測試方法來驗證我們是否可以產生並使用帶有Calculator
物件的動態代理:
@Test
void givenACalculator_whenUsingJdkDynamicProxy_thenJdkDynamicProxyCanBeUsed() {
Calculator simpleCalc = new SimpleCalculator();
LoggingInvocationHandler handler = new LoggingInvocationHandler(simpleCalc);
Calculator proxyCalc = (Calculator) Proxy.newProxyInstance(
Calculator.class.getClassLoader(),
new Class<?>[] { Calculator.class },
handler
);
assertEquals(30, proxyCalc.add(20, 10));
assertEquals(10, proxyCalc.subtract(20, 10));
}
6.使用 Spring 的ProxyFactory
或者,我們可以使用 Spring 的ProxyFactory
,這是一個複雜的實用程序,它抽象化了代理創建機制。此外,它會自動在 JDK 動態代理(用於介面)和 CGLIB(用於特定類別)之間進行選擇,使我們能夠注入方法攔截器(AOP 建議)。
Spring 的MethodInterceptor與InvocationHandler
類似,但使用 AOP 標準介面。
首先,讓我們將 Spring AOP(面向方面程式設計)的依賴項新增到pom.xml
中:
<dependencies>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.11</version>
</dependency>
</dependencies>
然後,讓我們建立一個與InvocationHandler
等效的 Spring 版本:
public class LoggingMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.debug("SPRING PROXY: Intercepting method: {}", invocation.getMethod().getName());
Object result = invocation.proceed();
log.debug("SPRING PROXY: Method {} completed.", invocation.getMethod().getName());
return result;
}
}
之後,讓我們驗證一下我們是否可以將 Spring 的ProxyFactory
與Calculator
物件一起使用:
@Test
void givenACalculator_whenUsingSpringProxyFactory_thenSpringProxyFactoryCanBeUsed() {
SimpleCalculator simpleCalc = new SimpleCalculator();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(simpleCalc);
factory.addAdvice(new LoggingMethodInterceptor());
Calculator proxyCalc = (Calculator) factory.getProxy();
assertEquals(60, proxyCalc.add(50, 10));
assertEquals(40, proxyCalc.subtract(50, 10));
}
7. 選擇方法
讓我們回顧一下每種方法的用例:
特徵 | 子類化 | 裝飾器模式 | JDK動態代理 | Spring 的ProxyFactory |
---|---|---|---|---|
何時使用 | 當我們想要改變派生類型的所有新實例的行為。 | 當我們需要動態地為單一物件新增行為時,這些修改必須在實例化/組裝時套用。非常適合日誌記錄、快取或驗證等橫切關注點。 | 當我們需要將橫切關注點(日誌記錄、安全性、事務)應用於許多物件而無需修改原始程式碼,也無需編寫特定的裝飾器類別。 | 在 Spring 生態系統中工作時,或者當我們需要一種強大、統一的方式來應用橫切關注點 (AOP) 時,它可以自動處理介面 (JDK 代理) 和類別 (CGLIB)。 |
限制 | 我們無法修改已經實例化的物件的行為。 | 它需要為每一組新行為手動建立一個裝飾器類,這會導致產生大量小型的、單一用途的類別。 | JDK 動態代理只能代理接口,無法直接代理具體類別。性能可能略低於直接方法呼叫。 | 它需要 Spring AOP/Context 相依性。對於一個簡單的獨立應用程式來說,它可能太重了。 |
8. 結論
在本文中,我們探討了 Java 中覆寫方法行為的四種方法。我們也使用了一個比較表來選擇最適合我們用例的方法。
與往常一樣,範例的完整程式碼可在 GitHub 上找到。