文章目录
1. 插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页,分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
2. Mybatis 插件介绍
Mybatis 作为一个应用广泛的优秀的 ORM 开源框架,这个框架具有强大的灵活性,在四大组件( Executor、StatementHandler、ParameterHandler、ResultSetHandler )处提供了简单易用的插件扩展机制。Mybatis 对持久层的操作就是借助于四大核心对象。MyBatis ⽀持用插件对四大核心对象进行拦截,对 MyBatis 来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis 中的四大对象都是代理对象。

MyBatis 所允许拦截的方法如下:
- 执行器
Executor(update、query、commit、rollback 等方法); - SQL 语法构建器
StatementHandler(prepare、parameterize、batch、updates 、query 等方法); - 参数处理器
ParameterHandler(getParameterObject、setParameters 方法); - 结果集处理器
ResultSetHandler(handleResultSets 、handleOutputParameters 等方法);
3. Mybatis 插件原理
在四大对象创建的时候
- 每个创建出来的对象不是直接返回的,而是 interceptorChain.pluginAll(parameterHandler)。
- 获取到所有的 Interceptor (拦截器:插件需要实现的接口);调用 interceptor.plugin(target);返回target 包装后的对象。
- 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行。
拦截
插件具体是如何拦截并附加额外的功能的呢?以 ParameterHandler 来说
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.plugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Clinton Begin
*/
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);
}
}
interceptorChain 保存了所有的拦截器(interceptors),是 mybatis 初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target) 中的 target 就可以理解为 mybatis 中的四大对象。返回的 target 是被重重代理后的对象。
如果我们想要拦截 Executor 的 query 方法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
除此之外,我们还需将插件配置到 sqlMapConfig.xml 中。
<plugins>
<plugin interceptor="com.study.plugin.ExamplePlugin"/>
</plugins>
这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL 时,需要先通过 DefaultSqlSessionFactory 创建 SqlSession。
Executor 实例会在创建 SqlSession 的过程中被创建,Executor 实例创建完毕后,MyBatis 会通过 JDK 动态代理为实例生成代理类。这样,插件逻辑即可在 Executor 相关方法被调用前执行。
以上就是MyBatis插件机制的基本原理。
4. 自定义插件
4.1. 插件接口
Mybatis 插件接口 Interceptor
Intercept 方法,插件的核心方法plugin 方法,生成 target 的代理对象setProperties 方法,传递插件所需参数
4.2. 自定义插件
设计实现一个自定义插件
package com.study.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
@Intercepts(
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class})
)
public class MyPlugin implements Interceptor {
// 拦截方法:只要被拦截的目标对象的目标方法被执行时,每次都会执行intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("对方法进行了增强....");
//原方法执行
return invocation.proceed();
}
// 主要为了把当前的拦截器生成代理存到拦截器链中
@Override
public Object plugin(Object o) {
Object wrap = Plugin.wrap(o, this);
return wrap;
}
// 获取配置文件的参数
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件的参数是:"+properties);
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.study.plugin.MyPlugin">
<property name="name" value="tom"/>
</plugin>
</plugins>
mapper 接口
package com.study.mapper;
import com.study.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface UserMapper {
//查询用户
@Select("select * from user")
@Options(useCache = false)
List<User> selectUser();
}
测试类
@Test
public void test() {
List<User> userList = userMapper.selectUser();
for (User user : userList) {
System.out.println(user);
}
}
5. 源码分析
执行插件逻辑
Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取被拦截方法列表,比如:
// signatureMap.get(Executor.class), 可能返回 [query, update, commit]
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//检测方法列表是否包含被拦截的方法
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) /*执⾏插件逻辑*/ : method.invoke(this.target, args);//执⾏被拦截的方法
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
invoke 方法的代码比较少,逻辑不难理解。首先 ,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation Invocation ;主要用于存储目标类,方法以及方法参数列表。下面简单看 一下该类的定义
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Clinton Begin
*/
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);
}
}
关于插件的执行逻辑就分析结束
6. pageHelper 分页插件
MyBatis 可以使用第三方的插件来对功能进行扩展,分页助手 PageHelper 是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。
开发步骤:
- 导入通用 PageHelper 的坐标
- 在 mybatis 核心配置文件中配置 PageHelper 插件
- 测试分页数据获取
导入通用 PageHelper 坐标
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>3.7.5</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
在 mybatis 核心配置文件中配置 PageHelper 插件
<plugins>
<!--注意:分⻚助手的插件 配置在通用馆mapper之前 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!-- 指定方⾔ -->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
测试分页代码实现
@Test
public void pageHelperTest(){
PageHelper.startPage(1,1);
List<User> users = userMapper.selectUser();
for (User user : users) {
System.out.println(user);
}
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总页数:"+pageInfo.getPages());
System.out.println("当前页:"+pageInfo.getPageNum());
System.out.println("每页显示的条数:"+pageInfo.getPageSize());
System.out.println("是否第⼀⻚:" + pageInfo.isIsFirstPage());
System.out.println("是否最后⼀⻚:" + pageInfo.isIsLastPage());
}
7. 通用 mapper
什么是通用 Mapper?
通用 Mapper 就是为了解决单表增删改查,基于 Mybatis 的插件机制。开发人员不需要编写 SQL,不需要在 DAO 中增加方法,只要写好实体类,就能支持相应的增删改查方法。
如何使用
首先在 maven 项目,在 pom.xml 中引入 mapper 的依赖
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
Mybatis 配置文件中完成配置
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<!-- 通用 Mapper 接口,多个通用接口用逗号隔开 -->
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
实体类设置主键
package com.study.pojo;
import lombok.Data;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Data
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
}
定义通用 mapper
package com.study.mapper;
import com.study.pojo.User;
import tk.mybatis.mapper.common.Mapper;
public interface IUserMapper extends Mapper<User> {
}
测试
@Test
public void mapperTest() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
User user = new User();
user.setId(1);
User user1 = mapper.selectOne(user);
System.out.println(user1);
//2.example方法
Example example = new Example(User.class);
example.createCriteria().andEqualTo("id",1);
List<User> users = mapper.selectByExample(example);
for (User user2 : users) {
System.out.println(user2);
}
}
1346

被折叠的 条评论
为什么被折叠?



