`

由浅入深分析mybatis通过动态代理实现拦截器(插件)的原理

 
阅读更多
最近在用mybatis做项目,需要用到mybatis的拦截器功能,就顺便把mybatis的拦截器源码大致的看了一遍,为了温故而知新,在此就按照自己的理解由浅入深的理解一下它的设计。 
和大家分享一下,不足和谬误之处欢迎交流。直接入正题。 
首先,先不管mybatis的源码是怎么设计的,先假设一下自己要做一个拦截器应该怎么做。拦截器的实现都是基于代理的设计模式设计的,简单的说就是要创造一个目标类的代理类,在代理类中执行目标类的方法并拦截执行拦截器代码。 
那么我们就用JDK的动态代理设计一个简单的拦截器: 

将被拦截的目标接口: 
Java代码  收藏代码
  1. public interface Target {  
  2.     public void execute();  
  3. }  

目标接口的一个实现类: 
Java代码  收藏代码
  1.     public class TargetImpl implements Target {  
  2.     public void execute() {  
  3.         System.out.println("Execute");  
  4.     }  
  5. }  

利用JDK的动态代理实现拦截器: 
Java代码  收藏代码
  1. public class TargetProxy implements InvocationHandler {  
  2.     private Object target;  
  3.     private TargetProxy(Object target) {  
  4.         this.target = target;  
  5.     }  
  6.       
  7.     //生成一个目标对象的代理对象  
  8.     public static Object bind(Object target) {  
  9.         return Proxy.newProxyInstance(target.getClass() .getClassLoader(),   
  10.                 target.getClass().getInterfaces(),  
  11.                        new TargetProxy(target));  
  12.     }  
  13.       
  14.     //在执行目标对象方法前加上自己的拦截逻辑  
  15.     public Object invoke(Object proxy, Method method,  
  16.                              Object[] args) throws Throwable {  
  17.         System.out.println("Begin");  
  18.         return method.invoke(target, args);  
  19.     }  
  20. }  

客户端调用: 
Java代码  收藏代码
  1. public class Client {  
  2. public static void main(String[] args) {  
  3.   
  4.     //没有被拦截之前  
  5.     Target target = new TargetImpl();  
  6.     target.execute(); //Execute  
  7.       
  8.     //拦截后  
  9.     target = (Target)TargetProxy.bind(target);  
  10.     target.execute();   
  11.     //Begin  
  12.     //Execute  
  13. }  

上面的设计有几个非常明显的不足,首先说第一个,拦截逻辑被写死在代理对象中: 
Java代码  收藏代码
  1. public Object invoke(Object proxy, Method method,  
  2.                            Object[] args) throws Throwable {  
  3.         //拦截逻辑被写死在代理对象中,导致客户端无法灵活的设置自己的拦截逻辑  
  4.         System.out.println("Begin");  
  5.        return method.invoke(target, args);  
  6.     }  

我们可以将拦截逻辑封装到一个类中,客户端在调用TargetProxy的bind()方法的时候将拦截逻辑一起当成参数传入: 
定义一个拦截逻辑封装的接口Interceptor,这才是真正的拦截器接口。 
Java代码  收藏代码
  1.     public interface Interceptor {  
  2.     public void intercept();  
  3. }  

那么我们的代理类就可以改成: 
Java代码  收藏代码
  1. public class TargetProxy implements InvocationHandler {  
  2.   
  3. private Object target;  
  4. private Interceptor interceptor;  
  5.   
  6. private TargetProxy(Object target, Interceptor interceptor) {  
  7.     this.target = target;  
  8.     this.interceptor = interceptor;  
  9. }  
  10.   
  11. //将拦截逻辑封装到拦截器中,有客户端生成目标类的代理类的时候一起传入,这样客户端就可以设置不同的拦截逻辑。  
  12. public static Object bind(Object target, Interceptor interceptor) {  
  13.     return Proxy.newProxyInstance(target.getClass().getClassLoader(),   
  14.                        target.getClass().getInterfaces(),  
  15.                        new TargetProxy(target, interceptor));  
  16. }  
  17.   
  18. public Object invoke(Object proxy, Method method,   
  19.                       Object[] args) throws Throwable {  
  20.     //执行客户端定义的拦截逻辑  
  21.     interceptor.intercept();  
  22.     return method.invoke(target, args);  
  23. }  

客户端调用代码: 
Java代码  收藏代码
  1. //客户端可以定义各种拦截逻辑  
  2. Interceptor interceptor = new Interceptor() {  
  3.     public void intercept() {  
  4.         System.out.println("Go Go Go!!!");  
  5.     }  
  6. };  
  7. target = (Target)TargetProxy.bind(target, interceptor);  
  8. target.execute();  

当然,很多时候我们的拦截器中需要判断当前方法需不需要拦截,或者获取当前被拦截的方法参数等。我们可以将被拦截的目标方法对象,参数信息传给拦截器。 
拦截器接口改成: 
Java代码  收藏代码
  1. public interface Interceptor {  
  2.     public void intercept(Method method, Object[] args);  
  3. }  

在代理类执行的时候可以将当前方法和参数传给拦截,即TargetProxy的invoke方法改为: 
Java代码  收藏代码
  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  2.     interceptor.intercept(method, args);  
  3.     return method.invoke(target, args);  
  4. }  

在Java设计原则中有一个叫做迪米特法则,大概的意思就是一个类对其他类知道得越少越好。其实就是减少类与类之间的耦合强度。这是从类成员的角度去思考的。 
什么叫越少越好,什么是最少?最少就是不知道。 
所以我们是不是可以这么理解,一个类所要了解的类应该越少越好呢? 
当然,这只是从类的角度去诠释了迪米特法则。 
甚至可以反过来思考,一个类被其他类了解得越少越好。 
A类只让B类了解总要强于A类让B,C,D类都去了解。 

举个例子: 
我们的TargetProxy类中需要了解的类有哪些呢? 
1. Object target 不需要了解,因为在TargetProxy中,target都被作为参数传给了别的类使用,自己不需要了解它。 
2. Interceptor interceptor 需要了解,需要调用其intercept方法。 
3. 同样,Proxy需要了解。 
4. Method method 参数需要了解,需要调用其invoke方法。 
同样,如果interceptor接口中需要使用intercept方法传过去Method类,那么也需要了解它。那么既然Interceptor都需要使用Method,还不如将Method的执行也放到Interceptor中,不再让TargetProxy类对其了解。Method的执行需要target对象,所以也需要将target对象给Interceptor。将Method,target和args封装到一个对象Invocation中,将Invocation传给Interceptor。 
Invocation: 
Java代码  收藏代码
  1. public class Invocation {  
  2.     private Object target;  
  3.     private Method method;  
  4.     private Object[] args;  
  5.       
  6.     public Invocation(Object target, Method method, Object[] args) {  
  7.         this.target = target;  
  8.         this.method = method;  
  9.         this.args = args;  
  10.     }  
  11.       
  12.     //将自己成员变量的操作尽量放到自己内部,不需要Interceptor获得自己的成员变量再去操作它们,  
  13.     //除非这样的操作需要Interceptor的其他支持。然而这儿不需要。  
  14.     public Object proceed() throws InvocationTargetException, IllegalAccessException {  
  15.         return method.invoke(target, args);  
  16.     }  
  17.         
  18.     public Object getTarget() {  
  19.         return target;  
  20.     }  
  21.     public void setTarget(Object target) {  
  22.         this.target = target;  
  23.     }  
  24.     public Method getMethod() {  
  25.         return method;  
  26.     }  
  27.     public void setMethod(Method method) {  
  28.         this.method = method;  
  29.     }  
  30.     public Object[] getArgs() {  
  31.         return args;  
  32.     }  
  33.     public void setArgs(Object[] args) {  
  34.         this.args = args;  
  35.     }  
  36. }  

Interceptor就变成: 
Java代码  收藏代码
  1. public interface Interceptor {  
  2.     public Object intercept(Invocation invocation)throws Throwable ;  
  3. }  

TargetProxy的invoke方法就变成: 
Java代码  收藏代码
  1. public Object invoke(Object proxy, Method method,   
  2.                           Object[] args) throws Throwable {  
  3.     return interceptor.intercept(new Invocation(target,   
  4.                                                    method, args));  
  5. }  

那么就每一个Interceptor拦截器实现都需要最后执行Invocation的proceed方法并返回。 
客户端调用: 
Java代码  收藏代码
  1. Interceptor interceptor = new Interceptor() {  
  2.     public Object intercept(Invocation invocation)  throws Throwable {  
  3.         System.out.println("Go Go Go!!!");  
  4.         return invocation.proceed();  
  5.     }  
  6. };  

好了,通过一系列调整,设计已经挺好了,不过上面的拦截器还是有一个很大的不足, 
那就是拦截器会拦截目标对象的所有方法,然而这往往是不需要的,我们经常需要拦截器 
拦截目标对象的指定方法。 
假设目标对象接口有多个方法: 
Java代码  收藏代码
  1. public interface Target {  
  2.     public void execute1();  
  3.     public void execute2();  
  4. }  

利用在Interceptor上加注解解决。 
首先简单的定义一个注解: 
Java代码  收藏代码
  1. @Retention(RetentionPolicy.RUNTIME)  
  2. @Target(ElementType.TYPE)  
  3. public @interface MethodName {  
  4.     public String value();  
  5. }  

在拦截器的实现类加上该注解: 
Java代码  收藏代码
  1. @MethodName("execute1")  
  2. public class InterceptorImpl implements Interceptor {...}  

在TargetProxy中判断interceptor的注解,看是否实行拦截: 
Java代码  收藏代码
  1. public Object invoke(Object proxy, Method method,  
  2.                          Object[] args) throws Throwable {  
  3.         MethodName methodName =   
  4.          this.interceptor.getClass().getAnnotation(MethodName.class);  
  5.         if (ObjectUtils.isNull(methodName))  
  6.             throw new NullPointerException("xxxx");  
  7.           
  8.         //如果注解上的方法名和该方法名一样,才拦截  
  9.         String name = methodName.value();  
  10.         if (name.equals(method.getName()))  
  11.             return interceptor.intercept(new Invocation(target,    method, args));  
  12.           
  13.         return method.invoke(this.target, args);  
  14. }  

最后客户端调用: 
Java代码  收藏代码
  1. Target target = new TargetImpl();  
  2. Interceptor interceptor = new InterceptorImpl();  
  3. target = (Target)TargetProxy.bind(target, interceptor);  
  4. target.execute();  

从客户端调用代码可以看出,客户端首先需要创建一个目标对象和拦截器,然后将拦截器和目标对象绑定并获取代理对象,最后执行代理对象的execute()方法。 
根据迪米特法则来讲,其实客户端根本不需要了解TargetProxy类。将绑定逻辑放到拦截器内部,客户端只需要和拦截器打交道就可以了。 
即拦截器接口变为: 
Java代码  收藏代码
  1. public interface Interceptor {  
  2. public Object intercept(Invocation invocation)  throws Throwable ;  
  3. public Object register(Object target);  

拦截器实现: 
Java代码  收藏代码
  1. @MethodName("execute1")  
  2. public class InterceptorImpl implements Interceptor {  
  3.       
  4.     public Object intercept(Invocation invocation)throws Throwable {  
  5.         System.out.println("Go Go Go!!!");  
  6.         return invocation.proceed();  
  7.     }  
  8.       
  9.     public Object register(Object target) {  
  10.         return TargetProxy.bind(target, this);  
  11.     }  
  12. }  

客户端调用: 
Java代码  收藏代码
  1. Target target = new TargetImpl();  
  2. Interceptor interceptor = new InterceptorImpl();  
  3.   
  4. target = (Target)interceptor.register(target);  
  5. target.execute1();  



OK,上面的一系列过程其实都是mybatis的拦截器代码结构,我只是学习了之后用最简单的方法理解一遍罢了。 
上面的TargetProxy其实就是mybatis的Plug类。Interceptor和Invocation几乎一样。只是mybatis的Interceptor支持的注解 
更加复杂。 
mybatis最终是通过将自定义的Interceptor配置到xml文件中: 
Xml代码  收藏代码
  1. <!-- 自定义处理Map返回结果的拦截器 -->  
  2.  <plugins>  
  3.      <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />  
  4.  </plugins>  

通过读取配置文件中的Interceptor,通过反射构造其实例,将所有的Interceptor保存到InterceptorChain中。 
Java代码  收藏代码
  1. public class InterceptorChain {  
  2.   
  3.   private final List<Interceptor> interceptors = new ArrayList<Interceptor>();  
  4.   
  5.   public Object pluginAll(Object target) {  
  6.     for (Interceptor interceptor : interceptors) {  
  7.       target = interceptor.plugin(target);  
  8.     }  
  9.     return target;  
  10.   }  
  11.   
  12.   public void addInterceptor(Interceptor interceptor) {  
  13.     interceptors.add(interceptor);  
  14.   }  
  15.     
  16.   public List<Interceptor> getInterceptors() {  
  17.     return Collections.unmodifiableList(interceptors);  
  18.   }  
  19.   
  20. }  

mybatis的拦截器只能代理指定的四个类:ParameterHandler、ResultSetHandler、StatementHandler以及Executor。 
这是在mybatis的Configuration中写死的,例如(其他三个类似): 
Java代码  收藏代码
  1. public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {  
  2.     ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);  
  3.       
  4.     //将配置文件中读取的所有的Interceptor都注册到ParameterHandler中,最后通过每个Interceptor的注解判断是否需要拦截该ParameterHandler的某个方法。  
  5.     parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  
  6.     return parameterHandler;  
  7. }  

所以我们可以自定义mybatis的插件(拦截器)修改mybatis的很多默认行为, 
例如, 
通过拦截ResultSetHandler修改接口返回类型; 
通过拦截StatementHandler修改mybatis框架的分页机制; 
通过拦截Executor查看mybatis的sql执行过程等等。
分享到:
评论

相关推荐

    mybatis拦截器实现通用权限字段添加的方法

    MyBatis拦截器实现通用权限字段添加的方法 MyBatis拦截器是一种非常实用的技术,可以用来实现各种复杂的数据库操作。本文将详细介绍如何使用MyBatis拦截器来实现通用权限字段添加,达到灵活、可靠、可维护的数据库...

    mybatis使用拦截器实现分页操作

    在MyBatis框架中,拦截器(Interceptor)是一种强大的工具,可以用来扩展MyBatis的行为。在本案例中,我们将探讨如何...通过理解拦截器的工作原理并熟练运用,开发者可以更好地定制MyBatis的行为,满足项目的各种需求。

    mybatis 分页拦截器及拦截器配置

    MyBatis中的拦截器(Interceptor)是基于Java的动态代理机制实现的,它可以拦截执行SQL的生命周期中的某些环节,如:预处理、结果映射等。在分页拦截器中,它会在执行查询之前对SQL进行修改,自动添加LIMIT和OFFSET...

    通过Mybatis拦截器自动定位慢SQL并记录日志

    下面我们将详细介绍如何通过Mybatis拦截器实现这一功能。 首先,了解Mybatis拦截器的基本概念。Mybatis拦截器(Interceptor)是一种插件机制,它允许我们在Mybatis执行SQL语句之前或之后进行自定义操作,比如统计...

    MyBatis拦截器分页与动态修改SQL及其参数值

    1. **MyBatis拦截器**:MyBatis提供了一种插件机制,即拦截器(Interceptor),它基于Java的动态代理,可以在SQL执行前或执行后进行干预。例如,我们可以创建一个自定义的拦截器,来实现特定的功能,如日志记录、...

    Mybatis分页拦截器

    通过对这些源码的分析和学习,可以进一步理解和掌握Mybatis分页拦截器的实现细节。 总的来说,Mybatis分页拦截器是提升项目效率的一个重要工具,它通过插件化的方式,使开发者能方便地为Mybatis添加自定义的分页...

    Mybatis拦截器记录数据更新历史记录到MongoDB

    Mybatis的拦截器是基于Java的动态代理机制实现的,它允许我们在特定的执行点(如SQL语句的执行前、后或结果返回前)插入自定义的行为。这在很多场景下都非常有用,比如日志记录、权限验证或本例中的数据变更监控。 ...

    Mybatis拦截器介绍及分页插件示例

    本文将详细介绍Mybatis拦截器的原理及其应用,并通过一个具体的分页插件示例来展示如何使用拦截器。 #### 1.3 Interceptor接口 Mybatis提供了`Interceptor`接口作为自定义拦截器的基础。此接口包含三个主要方法: ...

    mybatis拦截器的完整实现

    总的来说,这个项目提供了一个完整的MyBatis拦截器示例,不仅可以帮助我们理解和实践拦截器的用法,还能学习如何利用拦截器实现自动化分页功能。同时,它也展示了如何组织一个标准的Java项目结构,包括配置文件、源...

    MyBatis拦截器及分页插件

    MyBatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。Interceptor接口中定义了三个方法:intercept、plugin和setProperties。intercept方法是拦截器要执行的方法,plugin方法是拦截...

    mybatis慢SQL插件

    基于mybatis的慢SQL小插件,原理是mybatis拦截器。只需要在springboot的配置文件做简单的配置,mybatis拦截器将SQL中所有参数自动做了填充。拦截器监控慢SQL并将完整的可执行的SQL语句打印在日志文件中,复制该SQL...

    springmvc +mybatis采用策略设计模式基于拦截器实现按年分表

    总结起来,这个项目是关于如何使用SpringMVC和MyBatis框架,结合策略设计模式和拦截器技术,实现一个动态的按年分表策略。这样的设计不仅可以提升系统的性能,还能灵活适应业务需求的变化,是一个典型的面向现代Web...

    Mybatis实现动态代理,动态SQL

    Mybatis框架可以通过配置的形式为DAO接口生成动态代理实现类,从而简化程序开发,提高开发效率。在实现动态代理时,需要满足以下条件:接口全命名必须和SQL映射文件中的namespace完全一致;接口中的方法命名必须和...

    06实现mybatis分页插件demo

    06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo06实现mybatis分页插件demo...

    MyBatis拦截器 添加查询条件动态修改sql

    通过mybatis的拦截器,实现为所有sql(或指定sql) 统一添加查询条件,譬如通过线程变量传递某参数(日期),来实现对指定参数的数据筛选,而不需要在每个查询前,手动将该条件注入到查询中。因该资料网络较少,故特此...

    SpringBoot整合Mybatis完整详细版含注册、登录、拦截器配置

    在本项目中,我们主要探讨的是如何将SpringBoot与Mybatis进行深度整合,并实现完整的注册、登录功能,以及设置拦截器来控制权限访问。SpringBoot以其简洁的配置和快速的开发能力,配合Mybatis的灵活数据库操作,可以...

Global site tag (gtag.js) - Google Analytics