對 Java 來說,只要類別定義完成,就語法上來說,沒有方式可以動態地增加或修補行為,對類別的實例也是如此,有些語言支援 Mixin、物件個體化(Object individualization)、開放類別等,然而,Java 要達到類似功能,會是麻煩許多。
舉例來說,若有個類別的方法,原先在定義時並沒有檢查傳入的引數是否為 null
,而且你拿不到它的原始碼,在這樣的情況下,又想為它增加 enable
、disable
的行為,以決定要不要在每個方法執行前,檢查傳入引數是否為 null
。
一個簡單的想法是先定義介面,例如:
package cc.openhome.proxy;
public interface Nullable {
void enable();
void disable();
boolean isEnabled();
}
如果想令〈動態代理〉中,AccountDAO
的實現,可以有檢查 null
的新行為,可以實作一個包裹器(Wrapper):
public class NullableAccountDAOProxy implements Nullable, AccountDAO {
private boolean enabled;
private AccountDAO accountDAO;
public NullableProxy(AccountDAO accountDAO) {
this.accountDAO = accountDAO;
}
public void enable() {
enabled = true;
}
public void disable() {
enabled = false;
}
public boolean isEnabled() {
return enabled;
}
public void accountByEmail(String email) {
if(!isEnabled() && email == null) {
throw new IllegalArgumentException("argument of accountByEmail cannot be null");
}
accountDAO.accountByEmail(email);
}
...略
}
NullableAccountDAOProxy
同時實現了 Nullable
與 AccountDAO
,在每個 AccountDAO
的方法實作中進行傳入引數的 null
檢查,使用 AccountDAO
的客戶端,注入的實例會像是 new NullableAccountDAOProxy(new AccountDAOJdbcImpl(dataSource))
,因此可以如下:
...
((Nullable) accountDAO).enable(); // 必須 enable() 才允許傳入 null
accountDAO.accountByEmail(null);
...
當然,如上的 NullableAccountDAOProxy
專用於 AccountDAO
,而且逐一實現介面定義的每個方法也是個麻煩;類似地,可以試著使用動態代理的方式來解決這個需求:
package cc.openhome.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
public class NullableProxy implements Nullable, InvocationHandler {
private boolean enabled;
private Object target;
public NullableProxy(Object target) {
this.target = target;
}
@Override
public void enable() {
enabled = true;
}
@Override
public void disable() {
enabled = false;
}
public boolean isEnabled() {
return enabled;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if(methodName.equals("enable") || methodName.equals("disable")) {
return method.invoke(this, args);
}
if(!isEnabled() && Arrays.stream(args).anyMatch(arg -> arg == null)) {
throw new IllegalArgumentException(
String.format("argument(s) of %s.%s cannot be null",
target.getClass().getName(),
method.getName()
)
);
}
return method.invoke(target, args);
}
}
只有在 enabled
為 true
的情況下,才會允許呼叫目標物件,類似地,可以透過 Proxy.newProxyInstance
來建立實作指定介面的實例:
package cc.openhome;
...略
@Configuration
@ComponentScan
public class AppConfig {
...略
@Bean
public AccountDAO accountDAO() {
List<Class<?>> interfaces = new ArrayList<>(
Arrays.asList(accountDAO.getClass().getInterfaces())
);
interfaces.add(Nullable.class);
return (AccountDAO) Proxy.newProxyInstance(
accountDAO.getClass().getClassLoader(),
interfaces.toArray(new Class[interfaces.size()]),
new NullableProxy(accountDAO)
);
}
...略
}
藉由測試來驗證一下想法是否行得通:
package test.cc.openhome;
...略
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class MainTest {
@Autowired
private AccountDAO accountDAO;
@Test(expected = IllegalArgumentException.class)
public void testNotNullable() {
accountDAO.accountByEmail(null);
}
@Test public void testNullable() {
((Nullable) accountDAO).enable();
assertTrue(accountDAO.accountByEmail(null).isEmpty());
}
}
當然,想實現這樣的過程稍微複雜了一些,也容易模糊掉關切的要點,Spring AOP 實際上也提供這類的功能,之後再來看看是否可以省點功夫。
你可以在 BeforeAOP3 找到以上的範例專案。