代理模式的實際運用-以mybatis攔截器實現原理為例
之前在寫mybatis攔截器的時候,因為不懂原理,琢磨了很久,不知道怎麼寫,在網上找了很多資料,才知道mybatis的攔截器主要還是通過代理實現的,而且我在之前的博文中剛好學習了代理模式。更精細的是,在mybatis對代理的應用上,不管是封裝易用性,減少程式碼耦合度上,都可以讓我之前寫的代理模式demo進一步改進,也讓我加深了對代理模式的理解。
之前代理模式博文地址:http://blog.csdn.net/lovejj1994/article/details/74932311,上一篇博文中,我們討論了靜態代理和動態代理的區別,靜態代理需要自己寫代理類,比較麻煩,代理的東西一多就很不方便,動態代理只要簡單的實現InvocationHandler介面,讓jvm自己在執行時生成所需的代理類.
但是第一個問題就是邏輯程式碼在InvocationHandler的invoke方法裡被寫死了,不同的被代理類可以有不同的邏輯,邏輯程式碼被寫死就無法保證程式碼的可拓展性。所以我們可以定義一個Interceptor介面,把需要的邏輯放在介面的intercept方法中。
public interface Interceptor {
Object intercept() throws Throwable;
}
隨後我們將Interceptor 放到代理類中,讓Interceptor 的intercept方法在invoke裡執行。
/**
* 動態代理,新增Interceptor介面,讓攔截邏輯分離出來
* Created by panqian on 2017/7/31.
*/
public class DynamicProxy implements InvocationHandler {
private Interceptor interceptor;
private DynamicProxy(Object object, Interceptor interceptor) {
super();
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object intercept = interceptor.intercept();
System.out.println("我收到了:" + intercept);
return intercept;
}
}
編寫測試類,檢視效果:
public class DynamicProxyTest {
public static void main(String[] args) {
Sourceable source = new Source();
//lambda表示式直接實現,返回 [0,9] 之間的數
Interceptor interceptor = () -> new Double(Math.random() * 10).intValue();
DynamicProxy dynamicProxy = new DynamicProxy(source, interceptor);
Sourceable sourceable = (Sourceable) Proxy.newProxyInstance(Source.class.getClassLoader(), Source.class.getInterfaces(), dynamicProxy);
sourceable.method();
System.out.println("=========");
sourceable.method1();
}
}
我收到了:9
=========
我收到了:4
通過上面對邏輯程式碼的封裝,作為客戶端程式設計師就可以隨意改變程式碼邏輯,不需要直接在InvocationHandler介面上定義程式碼邏輯。
現在引出第二個問題,Interceptor 的引入對我們操作代理類還不是很自由,因為在invoke方法中,還有三個引數,除了第一個proxy物件用的很少,其餘兩個對於代理操作的靈活性非常重要,method可以返回當前的方法物件,args則是方法的引數陣列。那這些引數是否也可以封裝進Interceptor介面供 客戶端程式設計師使用呢?
invoke(Object proxy, Method method, Object[] args)
先新建一個Invocation類,裡面有method和args。
public class Invocation {
private Method method;
private Object[] args;
public Invocation(Method method, Object[] args) {
this.method = method;
this.args = args;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
@Override
public String toString() {
return "Invocation{" +
"method=" + method +
", args=" + Arrays.toString(args) +
'}';
}
}
Interceptor 介面做個改造,intercept方法放入Invocation物件。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
}
最後在代理類將Invocation 物件封裝好並傳入intercept方法,這樣客戶端程式設計師就對這個攔截器操作有更大的靈活性。
public class DynamicProxy implements InvocationHandler {
private Interceptor interceptor;
private DynamicProxy(Object object, Interceptor interceptor) {
super();
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//method和args通過intercept()方法傳給了客戶端程式設計師
Object intercept = interceptor.intercept(new Invocation(method, args));
System.out.println("我收到了:" + intercept);
return intercept;
}
}
最後的測試類,這裡為了列印方法引數,將被代理類的method1方法多加了一個引數,用來演示效果。
public static void main(String[] args) {
Sourceable source = new Source();
//lambda表示式直接實現,返回 [0,9] 之間的數
Interceptor interceptor = (invocation) -> {
System.out.println("invocation :" + invocation.toString());
return new Double(Math.random() * 10).intValue();
};
DynamicProxy dynamicProxy = new DynamicProxy(source, interceptor);
Sourceable sourceable = (Sourceable) Proxy.newProxyInstance(Source.class.getClassLoader(), Source.class.getInterfaces(), dynamicProxy);
sourceable.method();
System.out.println("=========");
sourceable.method1(666);
}
invocation :Invocation{method=public abstract void designmode.代理模式.evolution02.Sourceable.method(), args=null}
我收到了:2
=========
invocation :Invocation{method=public abstract void designmode.代理模式.evolution02.Sourceable.method1(int), args=[666]}
我收到了:0
這時候我們從客戶端程式設計師的角度開始看生成代理物件的程式碼,我們只需要提供被代理物件(source)和代理邏輯(interceptor),但是我們還寫了生成代理物件的邏輯程式碼(Proxy.newProxyInstance),這不是我們客戶端程式設計師需要乾的事,所以我們決定把這部分程式碼繼續提取出來,封裝到DynamicProxy代理類中。因為在mybatis攔截器中,Plugin類對應我們的DynamicProxy類,我們也在逐步靠近mybatis攔截器的寫法,所以下面直接把DynamicProxy更名為Plugin,便於理解。
對Plugin新增一個靜態方法wrap,通過此方法生成代理物件,客戶端不需要寫過多跟自己業務無關的程式碼,生成代理物件的任務全部轉移到Plugin類。
public class Plugin implements InvocationHandler {
private Interceptor interceptor;
private Plugin(Object object, Interceptor interceptor) {
super();
this.interceptor = interceptor;
}
//wrap方法生成代理物件
public static Object wrap(Object target, Interceptor interceptor) {
Class<?> type = target.getClass();
Class<?>[] interfaces = target.getClass().getInterfaces();
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor)) : target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object intercept = interceptor.intercept(new Invocation(method, args));
System.out.println("我收到了:" + intercept);
return intercept;
}
}
測試類部分程式碼,sourceable 代理物件 依靠Plugin.wrap生成 :
Sourceable sourceable = (Sourceable)Plugin.wrap(source, interceptor);
等等!我們似乎忘記了一個很嚴重的問題,我們似乎忘了執行 被代理類本身的method程式碼。。。雖然這也很好解決,只要在Plugin 的invoke方法中執行以下方法即可:
method.invoke(this.target, this.args);
但是現在已經用了Interceptor ,這個也乾脆剝離出來 給客戶端程式設計師 自由發揮吧,Invocation做個小小的改造,新增了target被代理物件 和 proceed方法,proceed方法用來執行原方法的邏輯:
public class Invocation {
private Method method;
private Object[] args;
private Object target;
public Invocation(Method method, Object[] args,Object target) {
this.method = method;
this.args = args;
this.target = target;
}
....
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args);
}
}
Plugin 也改動一下,新增object被代理物件。
public class Plugin implements InvocationHandler {
private Object object;
private Interceptor interceptor;
private Plugin(Object object, Interceptor interceptor) {
super();
this.object = object;
this.interceptor = interceptor;
}
public static Object wrap(Object target, Interceptor interceptor) {
Class<?> type = target.getClass();
Class<?>[] interfaces = target.getClass().getInterfaces();
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor)) : target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object intercept = interceptor.intercept(new Invocation(method, args, object));
System.out.println("我收到了:" + intercept);
return intercept;
}
}
被代理類也新增一些邏輯程式碼:
public class Source implements Sourceable {
@Override
public void method() {
System.out.println("the original method!");
}
@Override
public void method1(int i) {
System.out.println("the original method1!");
}
}
最後測試類測試一下:
public static void main(String[] args) {
Sourceable source = new Source();
//lambda表示式直接實現,返回 [0,9] 之間的數
Interceptor interceptor = (invocation) -> {
System.out.println("原方法開始執行");
//執行原方法的邏輯
invocation.proceed();
System.out.println("原方法執行完畢");
return new Double(Math.random() * 10).intValue();
};
Sourceable sourceable = (Sourceable) Plugin.wrap(source, interceptor);
sourceable.method();
System.out.println("=========");
sourceable.method1(666);
}
測試成功~!
原方法開始執行
the original method!
原方法執行完畢
我收到了:4
=========
原方法開始執行
the original method1!
原方法執行完畢
我收到了:9
到現在為止mybatis攔截器的原理也講了十之六七,在攔截器裡,並不是所有的方法都需要攔截,你可以在攔截邏輯裡判斷method的方法名,這是一個方法,mybatis用了註解解決這個問題。
先新建一個註解,methods加入要攔截的方法名:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Intercepts {
String[] methods();
}
新建一個Interceptor實現類,加上註解@Intercepts。
@Intercepts(methods = {"method1"})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Intercepts annotation = this.getClass().getAnnotation(Intercepts.class);
if (Objects.nonNull(annotation)) {
List<String> methods = Arrays.asList(annotation.methods());
if (methods.contains(invocation.getMethod().getName())) {
System.out.println(invocation.getMethod().getName() + " :該方法可以執行");
} else {
System.out.println(invocation.getMethod().getName() + " :該方法不能執行");
}
}
return null;
}
}
測試類:
public static void main(String[] args) {
Sourceable source = new Source();
//lambda表示式貌似不能加註解,所以換成傳統實現類
Interceptor interceptor = new MyInterceptor();
Sourceable sourceable = (Sourceable) Plugin.wrap(source, interceptor);
sourceable.method();
System.out.println("=========");
sourceable.method1(666);
}
結果,可以看出攔截起到了效果:
method :該方法不能執行
=========
method1 :該方法可以執行
總結:
這是一篇以mybatis攔截器實現原理為例子的 講解代理模式運用 的文章,單純的學習代理模式不用於實戰是理解不了設計模式的核心和用法。 在設計模式中,代理模式算是運用的很廣泛了,比如spring的aop也運用的很經典,自己以後還要多多學習