一:插件的使用
以分页插件PageHelper为例,看一下mybatis的插件如何工作
首先添加pageHelper的maven依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.2</version> </dependency>
在mybatis-config.xml中配置插件plugins:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 引入外部资源文件 resource:默认引入classpath路径下的资源文件 url:引入物理路径下的资源文件(如:d:\jdbc.properties) --> <properties resource="application.properties"></properties> <!-- 设置参数 --> <settings> <!-- 开启驼峰匹配:完成经典的数据库命名到java属性的映射 相当于去掉数据中的名字的下划线,和java进行匹配 --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!-- 配置别名 --> <typeAliases> <!-- typeAlias:用来配置别名,方便映射文件使用,type:类的全限定类名,alias:别名 --> <typeAlias type="com.example.mybatis.model.User" alias="User"/> </typeAliases> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"/> <!--<plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin"> <property name="someProperty" value="100"/> </plugin>--> </plugins> <!-- 配置环境:可以配置多个环境,default:配置某一个环境的唯一标识,表示默认使用哪个环境 --> <environments default="development"> <!-- 配置环境,id:环境的唯一标识 --> <environment id="development"> <!-- 事务管理器,type:使用jdbc的事务管理器 --> <transactionManager type="JDBC" /> <!-- 数据源,type:池类型的数据源 --> <dataSource type="POOLED"> <!-- 配置连接信息 --> <property name="driver" value="${jdbc.driverClass}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <!-- 配置映射文件:用来配置sql语句和结果集类型等 --> <mappers> <mapper resource="UserMapper.xml" /> </mappers> </configuration>
在使用的上一行语句中写上PageHelper.startPage(pageNo,pageSize) 页码,每页页数
PageHelper.startPage(3,2); List<User> list = userMapper.selectUser("hello105");
这样就可以工作了,下面我们测试一下
通过日志可以看到,可以实现正常的分页工作了,下面我们来研究一下它的工作原理
二:插件工作原理
1:插件的注册,我们在第一节分析XMLConfigBuilder解析mybatis-config.xml的时候看过解析mappers,这里重点
看一下如何解析plugins元素
看一下解析plugins元素下面的plugin元素,
最后注册到configuration中的interceptorChain中
到这里,解析mybatis-config.xml时注册插件的过程就完成了。
2:对数据库操作做增强
看一下PageInterceptor这个类,这是一个拦截器类,从注解数据可以看出它主要拦截Executor的query方法
这个类里有个plugin方法,入参是被代理对象,通过静态方法wrap包装,返回代理对象
首先读取拦截Interceptor注解上的信息,判断代理类型是否匹配注解拦截信息,如果匹配则代理,不匹配则直接返回原对象
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; }
PageInterceptor类的具体的拦截动作是在intercept这个方法里
看完这个类,我们看一下到底是在哪里对Executor做的增强,一定是在创建executor对象的时候,创建executor是在创建DefaultSqlSession的时候,
那来看一下SqlSessionFactory类的方法
创建Executor后,会通过拦截链对Executor进行增强,如果interceptor为空,或者拦截链不匹配executor是就会返回原来的executor
注册插件的时候我们看到过这个类,addInterceptor被调用过,现在就是用到第一步注册时候的插件来拦截
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); } }
这个plugin一般就是对target进行代理,在上面看PageInterceptor这个类的时候,我们已经分析过,这里PageInterceptor是可以
匹配Executor的,所以会被拦截,增强类Plugin,内部维护了PageInterceptor这个对象,所以当Executor对象调用query方法时,
会调用到Plugin的Invoke方法,然后会被委托给PageInterceptor对象的intercept方法
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); } }
这样又回到了这个主要的方法里。
我们来看一下查询的地方,这个查询的地方,四个入参的在selectList中,DefaultSqlSession中的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
调用这个方法,最终会调到intercept方法,这个方法里面是怎么分页的逻辑,这里忽略
3:自定义一个拦截插件
这里我们自定义一个拦截的插件,只是在拦截的时候把信息拿出来打印一下
/** * 告诉MyBatis当前插件用来拦截哪个对象的哪个方法 */ @Intercepts({@Signature( type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class} ) }) public class MyFirstPlugin implements Interceptor { /** * * 拦截目标对象的目标方法的执行 */ @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod()); Object target = invocation.getTarget(); System.out.println("当前拦截到的对象:"+target); //拿到target的元数据 MetaObject metaObject = SystemMetaObject.forObject(target); Object value = metaObject.getValue("parameterHandler.parameterObject"); System.out.println("sql语句用的参数是:"+value); //执行目标方法 Object proceed = invocation.proceed(); //返回执行后的返回值 return proceed; } /** * *包装目标对象的:为目标对象创建一个代理对象 */ @Override public Object plugin(Object target) { //我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象 System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target); Object wrap = Plugin.wrap(target, this); //返回为当前target创建的动态代理 return wrap; } /** * *将插件注册时 的property属性设置进来 */ @Override public void setProperties(Properties properties) { System.out.println("插件配置的信息:"+properties); } }
定义好插件后,要在mybatis-config.xml中配置一下,这样才能在解析xml的时候实现注册缓存到configuration中
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"/> <plugin interceptor="com.example.mybatis.plugin.MyFirstPlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
看一下运行结果:
具体是在哪里调用的呢,那就要找到创建statement的地方
SimpleExecutor类中有doQuery这个方法,方法里面有创建statementHandler对象的方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
newStatementHandler方法,会使用拦截链过滤这个statementHandler,看是否和拦截链中的interceptor匹配,如果匹配就会生成代理。
如果匹配,那么返回的statementHandler对象就是代理对象,statementHandler调用query时,调用的是Plugin的invoke方法,
然后委托给MyFirstPlugin这个拦截器的intercept方法执行。
总结:
插件的使用可以在不修改原有逻辑的基础上,对功能进行增强,这也是动态代理的特性,在mybatis中可以支持插件拦截的地方有四个,上面已经分析,executor、statementHandler、parameterHandler、resultHanlder
,原理就是在mybatis-config配置插件信息,在解析mybatis-config.xml的时候会注册拦截信息到configuration的拦截链,然后在创建上面四个对象的时候实现增强,在具体调用拦截方法的时候,会
调用到Plugin的invoke方法,在invoke中委托给插件处理。