插件原理mybatis插件涉及的几个类分析:我将以Executor为例,分析MyBatis是如何为Executor实例植入插件的。Executor实例是在打开SqlSession的时候创建的,所以我们从源头上分析。我们先看一下SqlSession的开启过程。publicSqlSessionopenSession(){returnopenSessionFromDataSource(configuration.getDefaultExecutorType(),null,false);}privateSqlSessionopenSessionFromDataSource(ExecutorTypeexecType,TransactionIsolationLevellevel,booleanautoCommit){Transactiontx=null;try{//省略部分逻辑//创建ExecutorfinalExecutorexecutor=configuration.newExecutor(tx,execType);returnnewDefaultSqlSession(configuration,executor,autoCommit);}catch(Exceptione){...}finally{...}}Executor的创建过程封装在Configuration中,我们跟进看看。//publicExecutornewExecutor(Transactiontransaction,ExecutorTypeexecutorType){executorType=executorType==null?defaultExecutorType:executorType;executorType=executorType==null?ExecutorType.SIMPLE:executorType;Executorexecutor;//根据executorType创建对应的Executor实例if(ExecutorType.BATCH==executorType){...}elseif(ExecutorType.REUSE==executorType){...}else{executor=newSimpleExecutor(this,transaction);}if(cacheEnabled){executor=newCachingExecutor(executor);}//植入插件executor=(Executor)interceptorChain.pluginAll(executor);returnexecutor;}如上,newExecutor方法创建Executor实例后,再通过拦截器链interceptorChain为Executor实例植入代理逻辑。我们来看看InterceptorChain的代码长什么样。publicclassInterceptorChain{privatefinalListinterceptors=newArrayList();publicObjectpluginAll(Objecttarget){//遍历拦截器集合for(Interceptorinterceptor:interceptors){//调用拦截器的plugin方法植入对应的插件逻辑target=interceptor.plugin(target);}returntarget;}/**添加插件实例到拦截器集合*/publicvoidaddInterceptor(Interceptorinterceptor){interceptors.add(interceptor);}/**获取插件列表*/publicListgetInterceptors(){returnCollections.unmodifiableList(interceptors);}}for上面的循环表示只要是插件,都会以责任链的形式一个一个执行(别指望它会跳过某个节点)。所谓插件,其实就是类似于拦截器。这里使用责任链设计模式。责任链设计模式相当于我们在OA系统中发起审批,由领导层层审批。以上就是InterceptorChain的全部代码,比较简单。它的pluginAll方法会调用具体插件的plugin方法植入相应的插件逻辑。如果有多个插件,就会多次调用插件方法,最终生成一个嵌套的代理类。形式如下:当调用Executor的某个方法时,插件逻辑会先执行。执行顺序是从外到内。比如上图中的执行顺序是plugin3→plugin2→Plugin1→Executor。plugin方法是由具体的插件类实现的,但是这个方法的代码一般都是固定的,所以下面找个例子来分析一下。//TianPluginclasspublicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}//PluginpublicstaticObjectwrap(Objecttarget,Interceptorinterceptor){/**获取插件类的@Signature注解内容,生成对应的映射结构。形式如下:*{*Executor.class:[query,update,commit],*ParameterHandler.class:[getParameterObject,setParameters]*}*/Map,Set>signatureMap=getSignatureMap(interceptor);Class>type=target.getClass();//获取目标类实现的接口Class>[]interfaces=getAllInterfaces(type,signatureMap);if(interfaces.length>0){//通过JDK动态代理为目标类生成代理类returnProxy.newProxyInstance(type.getClassLoader(),interfaces,newPlugin(target,interceptor,signatureMap));}returntarget;}同上,plugin方法调用wrapPlugin类的内部方法,用于为目标对象生成代理。Plugin类实现了InvocationHandler接口,因此它可以作为参数传递给Proxy的newProxyInstance方法。至此,插件植入的逻辑分析完毕。接下来我们看一下插件逻辑是如何执行的。执行插件逻辑Plugin实现了InvocationHandler接口,所以它的invoke方法会拦截所有的方法调用。invoke方法会检测被拦截的方法来决定是否执行插件逻辑。该方法的逻辑如下://在Plugin类中publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{try{/**获取拦截方法列表,如:*signatureMap.get(Executor.class),mayreturn[query,update,commit]*/Setmethods=signatureMap.get(method.getDeclaringClass());//检查方法列表是否包含拦截的方法if(methods!=null&&methods.contains(method)){//执行插件逻辑returninterceptor.intercept(newInvocation(target,method,args));}//执行拦截方法returnmethod.invoke(target,args);}catch(Exceptione){throwExceptionUtil.unwrapThrowable(e);}}invoke方法的代码比较少,逻辑也不难理解。首先invoke方法会检测插件的@Signature注解中是否配置了被拦截的方法。如果是,则执行插件逻辑,否则执行拦截的方法。插件逻辑封装在intercept中,该方法的参数类型为Invocation。Invocation主要用来存放目标类、方法和方法参数列表。我们简单看一下这个类的定义。publicclassInvocation{privatefinalObjecttarget;privatefinalMethodmethod;privatefinalObject[]args;publicInvocation(Objecttarget,Methodmethod,Object[]args){this.target=target;this.method=method;this.args=args;}//省略部分代码publicObjectproceed()throwsInvocationTargetException,IllegalAccessException{//反射调用拦截方法returnmethod.invoke(target,args);}}这是对插件执行逻辑的分析。整个过程并不难理解。看看吧。自定义插件为了让大家更好的理解Mybatis的插件机制,我们来模拟一个慢sql监控插件。/***慢查询sql插件*/@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})publicclassSlowSqlPluginimplementsInterceptor{privatelongslowTime;//拦截后面需要处理的业务@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{//通过StatementHandlerstatementHandler=(StatementHandler)invocation.getTarget();BoundSqlboundSql=statementHandler.getBoundSql();Stringsql=boundSql.getSql();longstart获取执行的sqlStatementHandler=系统。currentTimeMillis();//结束拦截Objectproceed=invocation.proceed();longend=System.currentTimeMillis();longf=end-start;System.out.println(sql);System.out.println("耗时="+f);if(f>slowTime){System.out.println("本次数据库操作为慢查询,sql为:");System.out.println(sql);}returnproceed;}//得到拦截对象,底层也是通过proxy实现的,实际得到一个target代理对象@OverridepublicObjectplugin(Objecttarget){//触发拦截方法returnPlugin.wrap(target,this);}//设置属性@OverridepublicvoidsetProperties(Propertiesproperties){//获取我们定义的慢sql的时间阈值valueslowTimethis.slowTime=Long.parseLong(properties.getProperty("slowTime"));}}然后将这个插件类注入到容器中,然后我们执行查询方法。如果耗时28秒,大于我们定义的10毫秒,那么这个SQL就是我们认为的慢SQL。通过这个插件,我们可以很容易的理解setProperties()方法的作用。查看分页插件也实现了mybatis接口Interceptor。@SuppressWarnings({"rawtypes","unchecked"})@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class}),})publicclassPageInterceptorimplementsInterceptor{@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{...}拦截方法中//AbstractHelperDialect类中@OverridepublicStringgetPageSql(MappedStatementms,BoundSqlboundSql,ObjectparameterObject,RowBoundsrowBounds,CacheKeypageKey){Stringsql=boundSql.getSql();Pagepage=getLocalPage();//支持orderbyStringorderBy=page.getOrderBy();if(StringUtil.isNotEmpty(orderBy)){pageKey.update(orderBy);sql=OrderByParser.converToOrderBySql(sql,orderBy);}if(page.isOrderByOnly()){returnsql;}//获取页面sqlreturngetPageSql(sql,page,pageKey);}//模板方法模板中的钩子方法publicabstractStringgetPageSql(Stringsql,Pagepage,CacheKeypageKey);AbstractHelperDialect类的实现类如下(即本分页插件支持的数据库如下):我们用的是MySQL,这里有对应的。@OverridepublicStringgetPageSql(Stringsql,Pagepage,CacheKeypageKey){StringBuildersqlBuilder=newStringBuilder(sql.length()+14);sqlBuilder.append(sql);if(page.getStartRow()==0){sqlBuilder.append("LIMIT?");}else{sqlBuilder.append("LIMIT?,?");}pageKey.update(page.getPageSize());returnsqlBuilder.toString();}这里就知道了,无非就是在执行TheLimit的时候拼接在SQL上。同样,Oracle使用rownum来处理分页。下面是Oracle处理分页的方式@OverridepublicStringgetPageSql(Stringsql,Pagepage,CacheKeypageKey){StringBuildersqlBuilder=newStringBuilder(sql.length()+120);if(page.getStartRow()>0){sqlBuilder.append("SELECT*FROM(");}if(page.getEndRow()>0){sqlBuilder.append("SELECTTMP_PAGE.*,ROWNUMROW_IDFROM(");}sqlBuilder.append(sql);if(page.getEndRow()>0){sqlBuilder.append(")TMP_PAGEWHEREROWNUM<=?");}if(page.getStartRow()>0){sqlBuilder.append(")WHEREROW_ID>?");}returnsqlBuilder.toString();}其他数据库分页操作类似。关于具体原理的分析,这里就不用赘述了,因为分页插件源码里面的注释基本都是中文的。Mybatis插件应用场景水平分表访问控制数据加解密总结Spring-Boot+Mybatis继承分页插件,以及用例,插件原理分析,源码分析,如何自定义插入。涉及技术点:JDK动态代理、责任链设计模式、模板方法模式。Mybatis插件关键对象总结:Inteceptor接口:自定义拦截必须实现的类。InterceptorChain:用于存储插件的容器。Plugin:h对象,提供创建代理类的方法。调用:代理对象的封装。本文转载自微信公众号《Java后端技术全栈》,可通过以下二维码关注。转载本文请联系Java后端技术全栈公众号。