前言
Mybatis
中的插件又叫做拦截器,通过插件能够在Mybatis
某个行为执行时进行拦挡并扭转这个行为。通常,Mybatis
的插件能够作用于Mybatis
中的四大接口,别离为Executor
,ParameterHandler
,ResultSetHandler
和StatementHandler
,归纳如下表所示。
可作用接口 | 可作用办法 | 拦截器用处 |
---|---|---|
Executor | update() ,query() ,flushStatements() ,commit() ,rollback() ,getTransaction() ,close() ,isClosed() | 拦挡执行器中的办法 |
ParameterHandler | getParameterObject() ,setParameters() | 拦挡对参数的解决 |
ResultSetHandler | handleResultSets() ,handleOutputParameters() | 拦挡对后果集的解决 |
StatementHandler | prepare() ,parameterize() ,batch() ,update() ,query() | 拦挡SQL 构建的解决 |
本篇文章将对插件怎么用和插件的执行原理进行剖析。
注释
一. 插件的应用
插件的应用比较简单,在Mybatis
配置文件中将插件配置好,Mybatis
会主动将插件的性能植入到插件对应的四大接口中。本大节将以一个例子,对自定义插件,插件的配置和插件的执行成果进行阐明。
首先创立两张表,语句如下所示。
CREATE TABLE bookstore( id INT(11) PRIMARY KEY AUTO_INCREMENT, bs_name VARCHAR(255) NOT NULL);CREATE TABLE book( id INT(11) PRIMARY KEY AUTO_INCREMENT, b_name VARCHAR(255) NOT NULL, b_price FLOAT NOT NULL, bs_id INT(11) NOT NULL, FOREIGN KEY book(bs_id) REFERENCES bookstore(id))
往表中插入若干数据,如下所示。
INSERT INTO bookstore (bs_name) VALUES ("XinHua");INSERT INTO bookstore (bs_name) VALUES ("SanYou");INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2)
当初开始搭建测试工程(非Springboot整合工程),新建一个Maven
我的项目,引入依赖如下所示。
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> <optional>true</optional> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency></dependencies>
还须要在POM文件中增加如下配置,以满足打包时能将src/main/java下的XML文件(次要想打包映射文件)进行打包。
<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins></build>
Mybatis
的配置文件mybatis-config.xml
如下所示,次要是开启日志打印,配置事务工厂,配置数据源和注册映射文件/映射接口。
<?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> <settings> <setting name="logImpl" value="STDOUT_LOGGING" /> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <package name="com.mybatis.learn.dao"/> </mappers></configuration>
本示例中,执行一个简略查问,将book表中的所有数据查问进去,查问进去的每条数据用Book
类进行映射,Book
类如下所示。
@Datapublic class Book { private long id; private String bookName; private float bookPrice;}
映射接口如下所示。
public interface BookMapper { List<Book> selectAllBooks();}
依照规定,编写映射文件,如下所示。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mybatis.learn.dao.BookMapper"> <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book"> <result column="b_name" property="bookName"/> <result column="b_price" property="bookPrice"/> </resultMap> <select id="selectAllBooks" resultMap="bookResultMap"> SELECT b.id, b.b_name, b.b_price FROM book b </select></mapper>
最初编写测试程序,如下所示。
public class MybatisTest { public static void main(String[] args) throws Exception { String resource = "mybatis-config.xml"; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream(resource)); SqlSession sqlSession = sqlSessionFactory.openSession(); BookMapper bookMapper = sqlSession.getMapper(BookMapper.class); List<Book> books = bookMapper.selectAllBooks(); books.forEach(System.out::println); }}
整个测试工程的目录构造如下所示。
运行测试程序,日志打印如下。
当初开始自定义插件的编写,Mybatis
官网文档中给出了自定义插件的编写示例,如下所示。
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class TestInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { //获取被拦挡的对象 Object target = invocation.getTarget(); //获取被拦挡的办法 Method method = invocation.getMethod(); //获取被拦挡的办法的参数 Object[] args = invocation.getArgs(); //执行被拦挡的办法前,做一些事件 //执行被拦挡的办法 Object result = invocation.proceed(); //执行被拦挡的办法后,做一些事件 //返回执行后果 return result; }}
当初依照Mybatis
官网文档的示例,编写一个插件,作用于Executor
的query()
办法,行为是在query()
办法执行前和执行后别离打印一些日志信息。编写的插件如下所示。
@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) })public class ExecutorTestPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("Begin to query."); Object result = invocation.proceed(); System.out.println("End to query."); return result; }}
在Mybatis
配置文件中将编写好的插件进行配置,如下所示。
<?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> <settings> <setting name="logImpl" value="STDOUT_LOGGING" /> </settings> <plugins> <plugin interceptor="com.mybatis.learn.plugin.ExecutorTestPlugin"/> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <package name="com.mybatis.learn.dao"/> </mappers></configuration>
再次运行测试程序,打印日志信息如下所示。
能够看到,插件依照预期执行了。
二. 插件的原理
本大节将剖析插件是如何植入Mybatis
四大接口以及插件是如何失效的。因为大节一中自定义的插件是作用于Executor
,所以本大节次要是以Executor
植入插件进行展开讨论,其余三大接口大体相似,就不再赘述。
Mybatis
在获取SqlSession
时,会为SqlSession
构建Executor
执行器,在构建Executor
的过程中,会为Executor
植入插件的逻辑,这部分内容在Mybatis源码-SqlSession获取中曾经进行了介绍。构建Executor
是产生在Configuration
的newExecutor()
办法中,如下所示。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //依据ExecutorType的枚举值创立对应类型的Executor if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } //如果Mybatis配置文件中开启了二级缓存 if (cacheEnabled) { //创立CachingExecutor作为Executor的装璜器,为Executor减少二级缓存性能 executor = new CachingExecutor(executor); } //将插件逻辑增加到Executor中 executor = (Executor) interceptorChain.pluginAll(executor); return executor;}
将插件逻辑植入到Executor
是产生在InterceptorChain
的pluginAll()
办法中。如果在Mybatis
的配置文件中配置了插件,那么配置的插件会在加载配置文件的时候被解析成拦截器Interceptor
并增加到Configuration
的InterceptorChain
中。InterceptorChain
是拦截器链,其实现如下所示。
public class InterceptorChain { //插件会增加到汇合中 private final List<Interceptor> interceptors = new ArrayList<>(); //为四大接口增加插件逻辑时会调用pluginAll()办法 //这里的target参数就是四大接口的对象 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); }}
当为Executor
增加插件逻辑时,就会调用InterceptorChain
的pluginAll()
办法,在pluginAll()
办法中,会遍历插件汇合并调用每个插件的plugin()
办法,所以插件性能的增加在于Interceptor
的plugin()
办法,其实现如下所示。
default Object plugin(Object target) { return Plugin.wrap(target, this);}
Interceptor
的plugin()
办法中,调用了Plugin
的wrap()
静态方法,持续看该静态方法的实现。
public static Object wrap(Object target, Interceptor interceptor) { //将插件的@Signature注解内容获取进去并生成映射构造 //Map[插件作用的接口的Class对象, Set[插件作用的办法的办法对象]] Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); //将指标对象实现的所有接口中是以后插件的作用指标的接口获取进去 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { //为指标对象生成代理对象并返回 //这是JDK动静代理的利用 //Plugin实现了InvocationHandler接口 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target;}
在Plugin
的wrap()
静态方法中,先判断指标对象实现的接口中是否有以后插件的作用指标,如果有,就为指标对象基于JDK
动静代理生成代理对象。同时,Plugin
实现了InvocationHandler
接口,当代理对象执行办法时,就会调用到Plugin
的invoke()
办法,接下来看一下invoke()
办法做了什么事件,如下所示。
@Overridepublic 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); }}
Plugin
的invoke()
办法首先会判断以后插件是否作用于以后代理对象执行的办法,如果不作用于,则以后代理对象执行的办法间接执行,如果作用于,则生成Invocation
并执行插件的逻辑。上面先看一下Invocation
是什么,如下所示。
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 target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } //执行指标办法 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }}
Invocation
用于插件获取插件作用的指标对象的信息,包含:作用对象自身,作用的办法和参数,同时Invocation
的proceed()
办法能够执行被插件作用的办法。所以插件能够在其实现的intercept()
办法中通过Invocation
获取到插件作用指标的残缺信息,也能够通过Invocation
的proceed()
办法运行作用指标的本来逻辑。
所以到这里能够晓得,为Mybatis
的四大对象植入插件逻辑时,就是为Mybatis
的四大对象生成代理对象,同时生成的代理对象中的Plugin
实现了InvocationHandler
,且Plugin
持有插件的援用,所以当代理对象执行办法时,就能够通过Plugin
的invoke()
办法调用到插件的逻辑,从而实现插件逻辑的植入。此外,如果定义了多个插件,那么会依据插件在Mybatis
配置文件中的申明程序,一层一层的生成代理对象,比方如下的配置中,先后申明了两个插件。
<plugins> <plugin intercepter="插件1"></plugin> <plugin intercepter="插件2"></plugin></plugins>
那么生成的代理对象能够用下图进行示意。
即为四大对象植入插件逻辑时,是依据申明插件时的程序从里向外一层一层的生成代理对象,反过来四大对象理论运行时,是从内向里一层一层的调用插件的逻辑。
总结
Mybatis
中的插件能够作用于Mybatis
中的四大对象,别离为Executor
,ParameterHandler
,ResultSetHandler
和StatementHandler
,在插件的@Signature
中能够指定插件的作用指标对象和指标办法,插件是通过为Mybatis
中的四大对象生成代理对象实现插件逻辑的植入,Mybatis
中的四大对象理论运行时,会先调用到插件的逻辑(如果有插件的话),而后才会调用到四大对象自身的逻辑。