1. 插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。一是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。
以 MyBatis
为例,我们可基于 MyBatis
插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
2. Mybatis 插件介绍
Mybatis
作为⼀个应用广泛的优秀的 ORM
开源框架,这个框架具有强大的灵活性,在四大组件 (Executor、StatementHandler、ParameterHandler、ResultSetHandler)
处提供了简单易用的插件扩展机制。Mybatis
对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对 mybatis
来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象
。
MyBatis
所允许拦截的方法如下:
- 执行器
Executor
(update
、query
、commi
t、rollback
等方法); -
SQL
语法构建器StatementHandler
(prepare
、parameterize
、batch
、updates
、query
等方法); - 参数处理器
ParameterHandler
(getParameterObject
、setParameters
方法); - 结果集处理器
ResultSetHandler
(handleResultSets``handleOutputParameters
等方法);
3. Mybatis 插件原理
3.1 在四大对象创建的时候
- 每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler);
- 获取到所有的
Interceptor
(拦截器),插件需要实现的接口;调用interceptor.plugin(target)
;返回target
包装后的对象 - 插件机制,我们可以使用插件为目标对象创建⼀个代理对象;
AOP
(面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每⼀个执行;
3.2 拦截
插件具体是如何拦截并附加额外的功能的呢?以 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;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
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.lagou.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;
/**
* 自定义插件
* @author xiaosong
* @since 2021/4/14
*/
@Intercepts({ //注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Signature(
type = StatementHandler.class,//这是指拦截哪个接口
method = "prepare", //这个接口内的哪个方法名,不要拼错了
args = {Connection.class,Integer.class} // 这是拦截的方法的⼊参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
)
})
public class MyPlugin implements Interceptor {
/**
* //这里是每次执⾏操作的时候,都会进行这个拦截器的方法内
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对方法进⾏了增强....");
//执行原方法
return invocation.proceed();
}
/**
* 主要是为了把这个拦截器⽣成一个代理放到拦截器链中,包装⽬标对象 为⽬标对象创建代理对象
* @param target 为要拦截的对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
System.out.println("将要包装的目标对象:"+target);
return Plugin.wrap(target,this);
}
/**
* 获取配置文件的属性
* 插件初始化的时候调用,也只调用⼀次,插件配置的属性从这里设置进来
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:"+properties);
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.study.plugin.MyPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
-
mapper
接口
public interface UserMapper {
List<User> selectUser();
}
- 测试类
package com.study.mapper;
import com.study.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 缓存的测试
* @author xiaosong
* @since 2021/4/12
*/
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSelect(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
}
-
执行顺序
5. 源码分析
- 执行插件逻辑
Plugin
实现了InvocationHandler
接口,因此它的invoke
方法会拦截所有的方法调用。invoke
方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取被拦截方法列表,比如:signatureMap.get(Executor.class), 可能返回 [query, update,commit]
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);
}
}
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>4.2.1</version>
</dependency>
- 在
mybatis
核心配置文件中配置PageHelper
插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
- 测试分页代码实现
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
- 获得分页相关的其他参数
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
// 其它分页数据
PageInfo<User> pageInfo = new PageInfo<>(userList);
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
7.1 什么是通用 Mapper
通用 Mapper
就是为了解决单表增删改查,基于 Mybatis
的插件机制。开发人员不需要编写 SQL
,不需要在 DAO
中增加方法,只要写好实体类,就能支持相应的增删改查方法
7.2 如何使用
- 首先在
maven
项目,在pom.xml
中引入mapper
的依赖
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
-
Mybatis
配置文件中完成配置
<plugins>
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
</plugins>
- 实体类设置主键
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;
import java.util.List;
/**
* 用户的实体类
* @author xiaosong
* @since 2021/4/1
*/
@Data
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private String birthday;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}';
}
}
- 定义通用
mapper
package com.study.mapper;
import com.study.pojo.User;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
/**
* 用户的持久层
* @author xiaosong
* @since 2021/4/12
*/
public interface UserMapper extends Mapper<User> {
/**
* 查询用户信息
* @return List<User>
*/
List<User> findUserList();
}
- 测试
@Test
public void testMapper(){
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(1);
// mapper 基础接口, select
// 根据实体中的属性进行查询,只能有一个返回值
User user1 = userMapper.selectOne(user);
System.out.println(user1);
// 参数为 null 时,查询全部
List<User> userList = userMapper.select(null);
for (User user2 : userList) {
System.out.println(user2);
}
// 根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
User user2 = userMapper.selectByPrimaryKey(1);
System.out.println(user2);
//根据实体中的属性查询总数,查询条件使用等号
int count = userMapper.selectCount(user);
System.out.println("总数" + count);
// mapper 基础接口, insert
//保存一个实体,null值也会保存,不会使用数据库默认值
user.setId(3);
int insertCount = userMapper.insert(user);
System.out.println("插入条数" + insertCount);
//保存实体,null的属性不会保存,会使用数据库默认值
user.setId(4);
int insertCount1 = userMapper.insertSelective(user);
System.out.println("插入条数" + insertCount1);
// mapper 基础接口, update
//根据主键更新实体全部字段, null值会被更新
user.setId(4);
int updateCount = userMapper.updateByPrimaryKey(user);
System.out.println("更新条数" + updateCount);
// mapper 基础接口, delete
//根据实体属性作为条件进行删除,查询条件使用等号
int deleteCount = userMapper.delete(user);
System.out.println("删除条数" + deleteCount);
// example方法
Example example = new Example(User.class);
example.createCriteria().andEqualTo("id",1)
.andLike("username","E%");
List<User> userList1 = userMapper.selectByExample(example);
for (User user3 : userList1) {
System.out.println(user3);
}
}