自己實現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中的name
是interceptorNames
,我的是
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生成的代理物件,然後再執行userDao
的add
和getUser
方法前也執行了前置通知的方法,最後的getUser
的結果也沒有錯
現在,整個專案就已經完成一半了,另外一半就是增加CGLib方式建立代理物件還有優化一下架構。