Mybatis攔截器設計原理1
攔截器的實現都是基於代理的設計模式實現的,簡單的說就是要創造一個目標類的代理類,在代理類中執行目標類的方法並在方法之前執行攔截器程式碼。 首先,先不管mybatis的原始碼是怎麼設計的,先假設一下自己要做一個攔截器應該怎麼做。下面我們就利用JDK的動態代理自己設計一個簡單的攔截器。
將被攔截的目標介面:
public interface Target {
public void execute();
}
目標介面的一個實現類:
public class TargetImpl implements Target { public void execute() { System.out.println("Execute"); } }
利用JDK的動態代理實現攔截器:
public class TargetProxy implements InvocationHandler { private Object target; private TargetProxy(Object target) { this.target = target; } //生成一個目標物件的代理物件 public static Object bind(Object target) { return Proxy.newProxyInstance(target.getClass() .getClassLoader(), target.getClass().getInterfaces(), new TargetProxy(target)); } //在執行目標物件方法前加上自己的攔截邏輯 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Begin"); return method.invoke(target, args); } }
客戶端呼叫:
public class Client { public static void main(String[] args) { //沒有被攔截之前 Target target = new TargetImpl(); target.execute(); //Execute //攔截後 // 當代理物件呼叫真實物件的方法時,其會自動的跳轉到代理物件關聯的handler物件的invoke方法來進行呼叫 target = (Target)TargetProxy.bind(target); target.execute(); //Begin //Execute }
上面的設計有幾個非常明顯的不足。首先,攔截邏輯被寫死在代理物件中:
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//攔截邏輯被寫死在代理物件中,導致客戶端無法靈活的設定自己的攔截邏輯
System.out.println("Begin");
return method.invoke(target, args);
}
我們可以將攔截邏輯封裝到一個類中,客戶端在呼叫TargetProxy的bind()方法的時候將攔截邏輯一起當成引數傳入:
定義一個攔截邏輯封裝的介面Interceptor,這才是真正的攔截器介面。
public interface Interceptor {
public void intercept();
}
那麼我們的代理類就可以改成:
public class TargetProxy implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private TargetProxy(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
//將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設定不同的攔截邏輯。
public static Object bind(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target, interceptor));
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
//執行客戶端定義的攔截邏輯
interceptor.intercept();
return method.invoke(target, args);
}
客戶端呼叫程式碼:
/客戶端可以定義各種攔截邏輯
Interceptor interceptor = new Interceptor() {
public void intercept() {
System.out.println("Go Go Go!!!");
}
};
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();
當然,很多時候我們的攔截器中需要判斷當前方法需不需要攔截,或者獲取當前被攔截的方法引數等。我們可以將被攔截的目標方法物件,引數資訊傳給攔截器。
攔截器介面改成:
public interface Interceptor {
public void intercept(Method method, Object[] args);
}
在代理類執行的時候可以將當前方法和引數傳給攔截,即TargetProxy的invoke方法改為:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
interceptor.intercept(method, args);
return method.invoke(target, args);
}
在Java設計原則中有一個叫做迪米特法則,大概的意思就是一個類對其他類知道得越少越好。其實就是減少類與類之間的耦合強度。這是從類成員的角度去思考的。 什麼叫越少越好,什麼是最少?最少就是不知道。 所以我們是不是可以這麼理解,一個類所要了解的類應該越少越好呢?
對於上面的例子,Interceptor介面中需要使用intercept方法傳過去Method類,那麼也需要了解它。那麼既然Interceptor都需要使用Method,還不如將Method的執行也放到Interceptor中,不再讓TargetProxy類對其瞭解。Method的執行需要target物件,所以也需要將target物件給Interceptor。將Method,target和args封裝到一個物件Invocation中,將Invocation傳給Interceptor。
public interface Interceptor {
Object intercept(Invocation invocation)throws Throwable;
}
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
//將自己成員變數的操作儘量放到自己內部,不需要Interceptor獲得自己的成員變數再去操作它們,
//除非這樣的操作需要Interceptor的其他支援。然而這裡不需要。
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
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;
}
}
public interface Target {
public void execute();
}
public class TargetProxy implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private TargetProxy(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
//將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設定不同的攔截邏輯。
public static Object bind(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target, interceptor));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return interceptor.intercept(new Invocation(target, method, args));
}
}
public class Client {
public static void main(String[] args) {
/**
* 相當於:
Interceptor interceptor2 = new Interceptor() {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
} ;
*/
Interceptor interceptor = invocation -> {
System.out.println("Go Go Go!!!");
return invocation.proceed();
};
Target target = (Target) TargetProxy.bind(new TargetImpl(), interceptor);
target.execute();
}
}
根據迪米特法則來講,其實客戶端根本不需要了解TargetProxy類。將繫結邏輯放到攔截器內部,客戶端只需要和攔截器打交道就可以了。
攔截器介面變為:
public interface Inteceptor {
Object intercept(Invocation invocation)throws Throwable;
Object register(Object target);
}
攔截器實現類:
public class InteceptorImpl implements Inteceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
@Override
public Object register(Object target) {
return TargetProxy.bind(target, this);
}
}
測試類:
public class Client {
public static void main(String[] args) {
Inteceptor interceptor = new InteceptorImpl();
Target target = (Target) interceptor.register(new TargetImpl());
target.execute();
}
}
好了,通過一系列調整,設計已經挺好了,不過上面的攔截器還是有一個很大的不足, 那就是攔截器會攔截目標物件的所有方法,然而這往往是不需要的,我們經常需要攔截器攔截目標物件的指定方法。 我們利用在Interceptor上加註解解決。
假設目標物件介面有多個方法:
public interface Target {
public void execute1();
public void execute2();
}
public class TargetImpl implements Target {
@Override
public void execute1() {
System.out.println("Execute");
}
@Override
public void execute2() {
System.out.println("Execute");
}
}
首先簡單的定義一個註解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
public String value();
}
在攔截器的實現類加上該註解:
public interface Interceptor {
Object intercept(Invocation invocation)throws Throwable;
Object register(Object target);
}
@MethodName("execute1")
public class InteceptorImpl implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
@Override
public Object register(Object target) {
return TargetProxy.bind(target, this);
}
}
在TargetProxy中判斷interceptor的註解,看是否實行攔截:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class TargetProxy implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private TargetProxy(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
//將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設定不同的攔截邏輯。
public static Object bind(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target, interceptor));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodName methodName = this.interceptor.getClass().getAnnotation(MethodName.class);
if (methodName == null) {
throw new NullPointerException("xxxx");
}
//如果註解上的方法名和該方法名一樣,才攔截
String name = methodName.value();
if (name.equals(method.getName())){
//攔截方法後 加上自己的邏輯
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(this.target, args);
}
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
//將自己成員變數的操作儘量放到自己內部,不需要Interceptor獲得自己的成員變數再去操作它們,
//除非這樣的操作需要Interceptor的其他支援。然而這裡不需要。
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
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;
}
}
測試類:
public class Client {
public static void main(String[] args) {
Interceptor interceptor = new InteceptorImpl();
Target target = (Target) interceptor.register(new TargetImpl());
target.execute1();
target.execute2();
}
}
列印結果,只攔截了execute1()方法:
Go Go Go!!!
Execute1
Execute2
Process finished with exit code 0
OK,上面的一系列過程其實都是mybatis的攔截器程式碼結構,上面的TargetProxy其實就是mybatis的Plugin類。Interceptor和Invocation幾乎一樣。只是mybatis的Interceptor支援的註解更加複雜。
MyBatis的Plugin類,看到裡面的兩個方法,現在就不那麼陌生了。
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set<Method>> signatureMap;
...
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
...
}
mybatis最終是通過將自定義的Interceptor配置到xml檔案中:
<!-- 自定義處理Map返回結果的攔截器 -->
<plugins>
<plugin interceptor="com.chm.inteceptor.PageInterceptor">
</plugins>