1. 程式人生 > >自己實現Spring AOP(二)JDK代理實現AOP

自己實現Spring AOP(二)JDK代理實現AOP

前言

上一篇文章自己實現Spring AOP(一)環境搭建及知識準備,我搭建好了專案也寫了一個例子,接下來我就要實現這個例子中的AOP功能。

在Spring中如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP,如果目標物件沒有實現介面,必須採用CGLib(Code Generation Library)方式,下面我就先用JDK的動態代理實現一下。

準備工作

在實現AOP功能前,先準備好要測試的相關類

準備好目標物件相關類和介面

UserDao介面

package edu.jyu.dao;

public interface UserDao {
    public
void add(String user); public String getUser(String id); }

UserDaoImpl類,實現了UserDao介面

package edu.jyu.dao;

public class UserDaoImpl implements UserDao {

    @Override
    public void add(String user) {
        System.out.println("add " + user);
    }

    @Override
    public String getUser
(String id) { System.out.println("getUser " + id); return id + ":Jason"; } }

前置通知

還是以前置通知為例,我先仿造Spring弄個前置通知介面MethodBeforeAdvice,要自定義前置通知就必須實現它,它裡面有個before方法,就是在目標方法執行前執行的

package edu.jyu.aop;

import java.lang.reflect.Method;

/**
 * 前置通知介面
 * 
 * @author Jason
 */
public interface
MethodBeforeAdvice {
/** * * @param method * 目標方法 * @param args * 目標方法所需的引數 * @param target * 目標物件 */ void before(Method method, Object[] args, Object target); }

然後自定義一個前置通知類MyBeforeAdvice,它實現了MethodBeforeAdvice介面

package edu.jyu.advice;

import java.lang.reflect.Method;

import edu.jyu.aop.MethodBeforeAdvice;

/**
 * 自定義前置通知類
 * 
 * @author Jason
 */
public class MyBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) {
        System.out.println("前置通知");
    }

}

ProxyFactoryBean

還記得上一章我用的那個例子嘛,配置生成代理物件時需要指定的class
org.springframework.aop.framework.ProxyFactoryBean

<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">

所以我也來定義一個ProxyFactoryBean類來專門生成的代理物件,現在還沒有任何方法

package edu.jyu.aop;
/**
 * 用於生產代理物件的類
* @author Jason
 */
public class ProxyFactoryBean {

}

配置

實現AOP相關的類和介面都準備好了,現在要在applicationContext.xml檔案中配置生成代理物件了

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!-- 目標物件 -->
    <bean name="userDao" class="edu.jyu.dao.UserDaoImpl"></bean>

    <!-- 前置通知 -->
    <bean name="beforeAdvice" class="edu.jyu.advice.MyBeforeAdvice"></bean>

    <!-- 配置生成代理物件 -->
    <bean name="userDaoProxy" class="edu.jyu.aop.ProxyFactoryBean">

        <!-- 代理的目標物件 -->
        <property name="target" ref="userDao" />

        <!-- 代理要實現的介面 -->
        <property name="proxyInterface" value="edu.jyu.dao.UserDao" />

        <!-- 需要織入目標的通知 -->
        <property name="interceptor" ref="beforeAdvice" />
    </bean>
</beans>

這個配置檔案和上一章的例子中的很像,比較多的區別在於配置生成代理物件,在配置織入目標通知那裡,Spring中的nameinterceptorNames,我的是
interceptor,為了簡單,我只打算讓一個目標物件只能有一個通知去增強。

那麼根據這個配置檔案ProxyFactoryBean檔案就應該修改成如下

package edu.jyu.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用於生產代理物件的類
 * 
 * @author Jason
 */
public class ProxyFactoryBean {
    // 目標物件
    private Object target;
    // 通知
    private Object interceptor;
    // 代理實現的介面
    private String proxyInterface;

    // 提供setter讓容器注入屬性

    public void setTarget(Object target) {
        this.target = target;
    }

    public void setInterceptor(Object interceptor) {
        this.interceptor = interceptor;
    }

    public void setProxyInterface(String proxyInterface) {
        this.proxyInterface = proxyInterface;
    }
}

開發功能

現在建立物件的類edu.jyu.core.ClassPathXmlApplicationContext就需要進行修改了,因為它建立物件時得分兩種情況,一種是建立一般物件,另外一種是建立代理物件。我們只需要修改它的createBeanByConfig方法

/**
 * 根據bean的配置資訊建立bean物件
 * 
 * @param bean
 * @return
 */
private Object createBeanByConfig(Bean bean) {
    // 根據bean資訊建立物件
    Class clazz = null;
    Object beanObj = null;
    try {
        clazz = Class.forName(bean.getClassName());
        // 建立bean物件
        beanObj = clazz.newInstance();
        // 獲取bean物件中的property配置
        List<Property> properties = bean.getProperties();
        // 遍歷bean物件中的property配置,並將對應的value或者ref注入到bean物件中
        for (Property prop : properties) {
            Map<String, Object> params = new HashMap<>();
            if (prop.getValue() != null) {
                params.put(prop.getName(), prop.getValue());
                // 將value值注入到bean物件中
                BeanUtils.populate(beanObj, params);
            } else if (prop.getRef() != null) {
                Object ref = context.get(prop.getRef());
                // 如果依賴物件還未被載入則遞迴建立依賴的物件
                if (ref == null) {
                    ref = createBeanByConfig(config.get(prop.getRef()));
                }
                params.put(prop.getName(), ref);
                // 將ref物件注入bean物件中
                BeanUtils.populate(beanObj, params);
            }
        }

        // 說明是要建立代理物件
        if (clazz.equals(ProxyFactoryBean.class)) {
            ProxyFactoryBean factoryBean = (ProxyFactoryBean) beanObj;
            // 建立代理物件
            beanObj = factoryBean.createProxy();
        }

    } catch (Exception e1) {
        e1.printStackTrace();
        throw new RuntimeException("建立" + bean.getClassName() + "物件失敗");
    }
    return beanObj;
}

也沒改什麼程式碼,只是新增了下面幾句程式碼

// 說明是要建立代理物件
if (clazz.equals(ProxyFactoryBean.class)) {
    ProxyFactoryBean factoryBean = (ProxyFactoryBean) beanObj;
    // 建立代理物件
    beanObj = factoryBean.createProxy();
}

就是判斷一下建立好的物件的類是否為ProxyFactoryBean,如果是的話,就將這個物件強轉成ProxyFactoryBean型別物件,然後呼叫它的createProxy()方法來建立一個代理物件,並將這個代理物件作為最終結果beanObj

此時ProxyFactoryBean還沒有createProxy()方法,所以現在就來建立這個方法並且實現它要完成的功能。程式碼如下

/**
 * 建立代理物件
 * 
 * @return
 */
public Object createProxy() {
    // 判斷有沒有指定proxyInterface,沒有指定就用CGLib方式
    if (proxyInterface == null || proxyInterface.trim().length() == 0)
        return createCGLibProxy();
    // 使用JDK中的代理
    return createJDKProxy();
}

這個方法很簡單,就是根據有沒有指定proxyInterface來判斷使用哪種動態代理方式去生成代理物件,沒有介面的那就用CGLib,有的話就用JDK中的代理。

那現在用CGLib建立代理物件的createCGLibProxy()方法和用JDK建立代理物件的createJDKProxy()方法都還沒有,我們要先建立這兩個方法,createCGLibProxy()方法留到下一章再實現,現在先來實現一下createJDKProxy()方法

/**
 * JDK方式建立代理物件
 * 
 * @return
 */
private Object createJDKProxy() {
    Class<?> clazz = null;
    try {
        clazz = Class.forName(proxyInterface);// 實現的介面
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        throw new RuntimeException(proxyInterface + "找不到,請注意填寫正確");
    }
    // JDK方式生成的代理物件
    Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object result = null;

                    // 要先判斷interceptor是哪種通知型別,以決定執行目標方法的位置
                    // 判斷interceptor是否為前置通知型別
                    if (interceptor instanceof MethodBeforeAdvice) {
                        MethodBeforeAdvice advice = (MethodBeforeAdvice) interceptor;
                        // 在目標方法執行前執行前置通知程式碼
                        advice.before(method, args, target);
                        // 執行目標方法
                        result = method.invoke(target, args);
                    }
                    return result;
                }
            });
    return proxyInstance;
}

使用JDK建立代理物件的方法也不難,主要關注在InvocationHandler那裡,我先要判斷一下interceptor是屬於哪種通知型別,因為不同的通知型別在於目標方法的執行順序上有所不同,比如說前置通知在目標方法前執行,後置通知在目標方法後執行,現在的實現其實是很有問題的,比如說我新加了一個後置通知,那麼我就要在加一條判斷分支判斷interceptor是否為後置通知。這個問題我會在下一章我解決。

我再把ProxyFactoryBean整個類的程式碼貼上來吧

package edu.jyu.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用於生產代理物件的類
 * 
 * @author Jason
 */
public class ProxyFactoryBean {
    // 目標物件
    private Object target;
    // 通知
    private Object interceptor;
    // 代理實現的介面
    private String proxyInterface;

    // 提供setter讓容器注入屬性

    public void setTarget(Object target) {
        this.target = target;
    }

    public void setInterceptor(Object interceptor) {
        this.interceptor = interceptor;
    }

    public void setProxyInterface(String proxyInterface) {
        this.proxyInterface = proxyInterface;
    }

    /**
     * 建立代理物件
     * 
     * @return
     */
    public Object createProxy() {
        // 判斷有沒有指定proxyInterface,沒有指定就用CGLib方式
        if (proxyInterface == null || proxyInterface.trim().length() == 0)
            return createCGLibProxy();
        // 使用JDK中的代理
        return createJDKProxy();
    }

    /**
     * JDK方式建立代理物件
     * 
     * @return
     */
    private Object createJDKProxy() {
        Class<?> clazz = null;
        try {
            clazz = Class.forName(proxyInterface);// 實現的介面
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(proxyInterface + "找不到,請注意填寫正確");
        }
        // JDK方式生成的代理物件
        Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;

                        // 要先判斷interceptor是哪種通知型別,以決定執行目標方法的位置
                        // 判斷interceptor是否為前置通知型別
                        if (interceptor instanceof MethodBeforeAdvice) {
                            MethodBeforeAdvice advice = (MethodBeforeAdvice) interceptor;
                            // 在目標方法執行前執行前置通知程式碼
                            advice.before(method, args, target);
                            // 執行目標方法
                            result = method.invoke(target, args);
                        }
                        return result;
                    }
                });
        return proxyInstance;
    }

    /**
     * CGLib方式建立代理物件
     * 
     * @return
     */
    private Object createCGLibProxy() {
        return null;
    }

}

測試

到此,JDK方式實現AOP就已經能行了,現在就可以寫個測試類測試一下

package edu.jyu.aop;

import org.junit.Test;

import edu.jyu.core.BeanFactory;
import edu.jyu.core.ClassPathXmlApplicationContext;
import edu.jyu.dao.UserDao;

public class TestProxy {

    @Test
    public void testJDKProxy(){
        BeanFactory ac = new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserDao userDao = (UserDao) ac.getBean("userDaoProxy");
        System.out.println(userDao.getClass());
        userDao.add("Jason");
        String user = userDao.getUser("132");
        System.out.println(user);
    }
}

輸出結果

class com.sun.proxy.$Proxy4
前置通知
add Jason
前置通知
getUser 132
132:Jason

第一行輸出可以看到確實是JDK生成的代理物件,然後再執行userDaoaddgetUser方法前也執行了前置通知的方法,最後的getUser的結果也沒有錯

現在,整個專案就已經完成一半了,另外一半就是增加CGLib方式建立代理物件還有優化一下架構。