原文:http://www.programering.com/a/MjN1UjNwATE.html(非直译,如有错误欢迎指正)
我们都知道mybatis自定义拦截器大概的格式类似于下面这种方式:
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class XXXInterceptor implements Interceptor {
}
关于注解的详细说明是这样子的,其中type表示需要拦截的类,method表示需要拦截的方法,args表示方法的参数。
拦截的类 | 拦截的方法 |
---|---|
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
StatementHandler | prepare, parameterize, batch, update, query |
ResultSetHandler | handleResultSets, handleOutputParameters |
这里就不再详细说明了。这里我们来讨论下,如果是我们自己来实现一个拦截器的话,我们会如何去进行设计。
先从大的思路说起,假设我们使用JDK自带的动态代理工具去实现简单的拦截器。
需要先定义被代理类的接口:
public interface Target {
public void execute();
}
下面是实现类:
public class TargetImpl implements Target {
public void execute() {
System.out.println("跑一把");
}
}
下面我们来实现动态代理的代理类:
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));
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
System.out.println("我要代理一下你");
return method.invoke(target, args);
}
}
以上我们实现了代理类的具体内容,与一般的Java动态代理的实现没有特殊的地方,下面看一下客户端的调用:
public class Client {
public static void main(String[] args) {
Target target = new TargetImpl();
target.execute();
//使用代理类获取执行实例
target = (Target)TargetProxy.bind(target);
target.execute();
}
}
OK,到这里我们已经完成了最基础的拦截器功能,但是离mybatis的拦截器还有很大的距离,这里我们先考虑一个问题,上面的设计有哪些问题,我们先思考一下代理类的实现中有没有什么问题?
--------------------------------------------分割线------------------------------------------
一般我们开发业务代码,不同场景的业务一般我们会怎么做?写死!!!哈哈,这是业务风险最低的方式,其实我不反对写死,因为很多时候业务方提出的需求都是暂时的,我们可能花的一些心思去做设计,结果,过度设计,哈哈。。。
好了,不扯那么远了,回过头来我们继续说说这个动态代理的实现类中TargetProxy中的问题,我们把代理类要实现的内容,写死了,如果以后我要把输出的内容改为“我要代理你一下下”那么咱们就要改代码了,改代码毕竟是有风险的,只有新写一份才保险,这就是著名的开闭原则。(这里有个小故事要提一下,有朋友所在的公司对于出的产线问题的追责还是蛮严重了,所以一般开发对于新接的需求,如果牵涉到原有的功能,一般都会新写一个,所以这个公司的项目是不是都很好维护,毕竟符合了开闭原则,哈哈不是,因为是if写死的)
Ok,那么我们要怎么才能解决动态代理中写死的问题呢,遇事不决接口来试(开个玩笑),好我们定义个拦截器接口。
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) {
return Proxy.newProxyInstance(target.getClass() .getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxy(target));
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
// 这里的执行拦截器需要执行的方法
interceptor.intercept();
return method.invoke(target, args);
}
}
相对于前面的改动其实不大,这里就不贴客户端的实现了,简单说下吧,定义XXXInterceptor实现刚刚咱们声明的接口,然后定义的动态代理类的构造参数加入这个拦截器的实例,后面都一样的,就不详细说了。当然大部分情况,我们的拦截器的那个方法都是有入参的,不然能实现的拦截功能有限,我们把接口声明稍微修改一下,
public interface Interceptor {
public void intercept(Method method, Object[] args);
}
这里,可能有些人可能会有疑惑,加参数就加参数呗,为什么要加个method,这里是因为我们可以对这个类的特定的方法进行拦截。
其实看到这里我们也已经大概看到了mybatis拦截器的影子,其实这里只解决了我们刚刚提出来的第一个问题,也是比较容易想到跟理解的问题,后面我再想想另一个问题,这个类还有什么问题,先抛出个提示,迪米特原则(又叫作最少知识原则(Least Knowledge Principle 简写LKP),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话)。
其实总结来说就是对于TargetProxy最少知道什么就可以正常工作呢?
-------------------------------------------------分割线-----------------------------------------------------------
我们先来分析一波,一个类的组成分为属性及方法。
1.TargetProxy这个类有2个属性,第一个是target,这个是需要代理的类,第二个是我们后面加的interceptor。
2.然后这个类有三个方法:
- 构造方法,入参为:target,interceptor
- bind方法,入参为:target
- invoke方法,入参为:proxy,method,args
这里需要指明一点,方法的入参也是这个类知道其它的类的含义,那这里我们来总结一下,现在TargetProxy这个类知道了target,interceptor,method,proxy
我们先来看target,这个类我们需要在TargetProxy中知道吗,我们现在来把他去掉,貌似能跑通,那么暂时列为不需要的类,interceptor肯定是需要知道的,proxy也是,method也是,目前为止我们排除掉一个target。
接下来我们来看封装的问题,这个问题跟上面的问题是相互关联的,因为到这里始终觉得TargetProxy需要知道的类太多了,其实代理类就是一个中间人,他只需要知道怎么调用就行了,OK,刚刚标粗的就是我们要思考的重点,我们是不是可以把target,method,args封装为一个调用Invocation,这里为什么又有target的事了呢,因为调用中需要知道。话不多说,上代码:
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;
}
//执行实际的调用
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 Interceptor {
public Object intercept(Invocation invocation)throws Throwable ;
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
return interceptor.intercept(new Invocation(target,
method, args));
}
拦截器的使用(这里必须调用invocation.proceed):
Interceptor interceptor = new Interceptor() {
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
};
到这里,这个设计已经蛮OK了,但是还有些问题,我们一开始有提到按照方法的粒度拦截,mybatis注解中的method,假设target中有2个方法,
public interface Target {
public void execute1();
public void execute2();
}
我们定义一个注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
public String value();
}
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {...}
然后代理类中的invoke方法改造一下:
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
MethodName methodName =
this.interceptor.getClass().getAnnotation(MethodName.class);
if (ObjectUtils.isNull(methodName))
throw new NullPointerException("xxxx");
//If the method name and the method of annotation on the name, to intercept
String name = methodName.value();
if (name.equals(method.getName()))
return interceptor.intercept(new Invocation(target, method, args));
return method.invoke(this.target, args);
}
客户端调用:
Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl();
target = (Target)TargetProxy.bind(target, interceptor);
target.execute();
这里客户端其实也是有问题的,因为它可以不需要知道TargetProxy这个类,我们再来改造一把。
public interface Interceptor {
public Object intercept(Invocation invocation) throws Throwable ;
public Object register(Object target);
}
@MethodName("execute1")
public class InterceptorImpl implements Interceptor {
public Object intercept(Invocation invocation)throws Throwable {
System.out.println("Go Go Go!!!");
return invocation.proceed();
}
public Object register(Object target) {
return TargetProxy.bind(target, this);
}
}
Target target = new TargetImpl();
Interceptor interceptor = new InterceptorImpl();
target = (Target)interceptor.register(target);
target.execute1();
完美。
现在我们再回过头来看下mybatis,我们自定义插件的时候一般是这样子的
<!-- Custom processing of Map results returned interceptor -->
<plugins>
<plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
</plugins>
mybatis通过读取配置文件,将拦截器注册到InterceptorChain中
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
mybatis中只能代理4个类, ParameterHandler, ResultSetHandler, StatementHandler and Executor,mybatis中写死的(不是说不能写吗,嘿嘿),下面是其中一个ParameterHandler,其它三个的源码类似。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//Reads the configuration file in all Interceptor are registered to ParameterHandler, finally through each Interceptor annotations to determine whether to intercept a method of the ParameterHandler.
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
- 拦截ResultSetHandler可以改变返回类型
- 自动分页机制通过拦截StatementHandler
- 拦截Executor可以查看SQL执行过程
最后上一下mybatis部分源码:
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
Object plugin(Object var1);
void setProperties(Properties var1);
}
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return this.target;
}
public Method getMethod() {
return this.method;
}
public Object[] getArgs() {
return this.args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args);
}
}
最后一个是代理类,其中有些方法我给删除了,看一下需要关注的方法就好了
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map signatureMap = getSignatureMap(interceptor);
Class type = target.getClass();
Class[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0?Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)):target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set e = (Set)this.signatureMap.get(method.getDeclaringClass());
return e != null && e.contains(method)?this.interceptor.intercept(new Invocation(this.target, method, args)):method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
}