動態代理


在〈在 AOP 之前〉中,雖然可以對 AccountDAOJdbcImpl 的每個方法呼叫進行日誌,而又不用修改 AccountDAOJdbcImpl,然而,AccountDAOLoggingProxy 只能服務 AccountDAO 型態,如果能有個方式能為不同型態建立代理物件的話就好了。

Reflection API 中提供動態代理相關類別,可讓你不必為特定介面實作特定代理物件,使用動態代理機制,可使用一個處理器代理多個介面的實作物件。

處理者類別必須實作 java.lang.reflect.InvocationHandler 介面,底下以實例進行說明。例如設計一個 LoggingProxy 類別:

package cc.openhome.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.logging.Logger;

public class LoggingProxy implements InvocationHandler {    
    private Object target;
    private Logger logger;

    public LoggingProxy(Object target) {
        this.target = target;
        logger = Logger.getLogger(target.getClass().getName());
    }

    public Object invoke(Object proxy, Method method, 
                         Object[] args) throws Throwable { 
        Object result = null; 
        try { 
            logger.info(String.format("%s.%s(%s)",
                    target.getClass().getName(), method.getName(), Arrays.toString(args)));
            result = method.invoke(target, args);
        } catch (IllegalAccessException | IllegalArgumentException | 
                InvocationTargetException e){ 
            throw new RuntimeException(e);
        }
        return result; 
    } 
}

接著就可以使用 Proxy.newProxyInstance 方法建立代理物件,呼叫時必須指定類別載入器,告知要代理的介面,以及介面上定義方法被呼叫時的處理者(InvocationHandler 實例),Proxy.newProxyInstance 方法會在執行時期生成代理物件,代理物件實作了指定的介面。

package cc.openhome;

import java.lang.reflect.Proxy;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

import cc.openhome.model.AccountDAO;
import cc.openhome.proxy.LoggingProxy;

@Configuration
@ComponentScan
public class AppConfig {     
    @Autowired
    @Qualifier("accountDAOJdbcImpl")
    private AccountDAO accountDAO;

    @Bean(destroyMethod="shutdown")
    public DataSource dataSource(){
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:testData.sql")
                .build();
    }

    @Bean
    public AccountDAO accountDAO() {
        return (AccountDAO) Proxy.newProxyInstance( 
                accountDAO.getClass().getClassLoader(), 
                accountDAO.getClass().getInterfaces(), 
                new LoggingProxy(accountDAO)
            ); 
    } 
}

如果操作 Proxy.newProxyInstance 傳回的代理物件,在每次操作時會呼叫處理者(InvocationHandler 實例)的 invoke 方法,並傳入代理物件、被呼叫方法的 Method 實例與參數值。

搭配上頭的 AppConfigUserService 可以修改一下:

...略
@Component
public class UserService {
    private final AccountDAO acctDAO;
    private final MessageDAO messageDAO;

    @Autowired
    public UserService(@Qualifier("accountDAO") AccountDAO acctDAO, MessageDAO messageDAO) {
        this.acctDAO = acctDAO;
        this.messageDAO = messageDAO;
    }
...略

LoggingProxy 不再專用於特定型態,如果想再進一步能夠設定哪些方法被呼叫時才進行日誌,可以在 LoggingProxyinvoke 中進行條件判斷。

LoggingProxy 不再專用於特定型態,如果想再進一步能夠設定哪些方法被呼叫時才進行日誌,可以在 LoggingProxyinvoke 中進行條件判斷,若想更進階一些,想使用設定檔來設定要日誌的方法也是可行的,試著這麼發展下去,慢慢就會形成一個具有 AOP 概念的程式庫,當然,Spring AOP 就具備有這類的能力,直接拿來用就省事多了。

你可以在 BeforeAOP2 找到以上的範例專案。