8-Mybatis插件

8.1 插件简介

⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯作。以 MyBatis 为例,我们可基于 MyBati s 插件机制实现分⻚、分表,监控等功能。由于插件和业务⽆关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能。

8.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 等⽅法 )

8.3 Mybatis插件原理

在四⼤对象创建的时候

  1. 每个创建出来的对象不是直接返回的,⽽是 interceptorChain.pluginAll(parameterHandler);
  2. 获取到所有的 Interceptor ( 拦截器 )( 插件需要实现的接⼝ ) ;调⽤interceptor.plugin(target); 返回 target 包装后的对象
  3. 插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象; 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;
}

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 ExampleInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return null;
}

@Override
public Object plugin(Object target) {
return null;
}

@Override
public void setProperties(Properties properties) {

}
}

除此之外,我们还需将插件配置到 sqlMapConfig.xml 中。

<plugins>
<plugin interceptor="com.lemon.plugin.ExamplePlugin"/>
</plugins>

这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象 (InterceptorChain ,拦截器链 ) 中。待准备⼯作做完后, MyBatis 处于就绪状态。我们在执⾏ SQL 时,需要先通过 DefaultSqlSessionFactory创建 SqlSession 。 Executor 实例会在创建 SqlSession 的过程中被创建, Executor 实例创建完毕后,MyBatis 会通过** JDK 动态代理为实例⽣成代理类**。这样,插件逻辑即可在 Executor 相关⽅法被调⽤前执⾏。

以上就是 MyBatis 插件机制的基本原理。

8.4 ⾃定义插件

8.4.1 插件接⼝

Mybatis 插件接⼝ -Interceptor

  • Intercept ⽅法,插件的核⼼⽅法
  • plugin ⽅法,⽣成 target 的代理对象
  • setProperties ⽅法,传递插件所需参数

8.4.2⾃定义插件

设计实现⼀个⾃定义插件

@Intercepts({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Signature(type = StatementHandler.class, //这是指拦截哪个接口
method = "prepare",//这个接口内的哪个方法名,不要拼错了
args = {Connection.class, Integer.class}),// 这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
})
public class MyPlugin implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); //这里是每次执行操作的时候,都会进行这个拦截器的方法内

/**
* 拦截方法:只要被拦截的目标对象的目标方法被执行时,每次都会执行 intercept 方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable { //增强逻辑
System.out.println("对方法进行了增强....");
return invocation.proceed(); //执行原方法
}

/**
* 主要是为了把这个拦截器生成一个代理放到拦截器链中
*/
@Override
public Object plugin(Object target) {
System.out.println("将要包装的目标对象:" + target);
return Plugin.wrap(target, this);
}

/**
* 获取配置文件的参数
* 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:" + properties);
}
}

sqlMapConfig.xml配置自定义插件

<!--拦截器-->
<plugins>
<plugin interceptor="com.lemon.plugin.MyPlugin">
<!--配置参数-->
<property name="name" value="syj"/>
</plugin>
</plugins>

测试

@Test
public void SecondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();

IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);

User user1 = mapper1.findUserById(1);
sqlSession1.close(); //清空一级缓存


User user = new User();
user.setId(1);
user.setUsername("lisi");
mapper3.updateUser(user);
sqlSession3.commit();

User user2 = mapper2.findUserById(1);

System.out.println(user1==user2);


}

8.5 源码分析

执⾏插件逻辑
Plugin 实现了 InvocationHandler 接⼝,因此它的 invoke ⽅法会拦截所有的⽅法调⽤。 invoke ⽅法会对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取被拦截方法列表,比如:
//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 主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看⼀下该类的定义:

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 proceed() throws InvocationTargetException, IllegalAccessException {
//调用被拦截的方法
return method.invoke(target, args);
}

}

8.6 PageHelper分页插件

MyBati s 可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿ PageHelper 是将分⻚的复杂操作进⾏封装,使⽤简单的⽅式即可获得分⻚的相关数据。

开发步骤:

  • 导⼊通⽤ PageHelper 的坐标
  • 在 mybatis 核⼼配置⽂件中配置 PageHelper 插件
  • 测试分⻚数据获取

8.6.1 导⼊通⽤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>

8.6.2 sqlMapConfig.xml中加入配置

<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>

8.6.3 测试分页数据的获取

@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());
}

8.7 通⽤mapper

8.7.1 什么是通⽤Mapper

通⽤ Mapper 就是为了解决单表增删改查,基于 Mybatis 的插件机制。开发⼈员不需要编写 SQL, 不需要在 DAO 中增加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法。

8.7.2 使用步骤

1.⾸先在 maven 项⽬,在 pom.xml 中引⼊ mapper 的依赖

<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
  1. 在sqlMapConfig.xml中完成配置

    <!--分⻚插件:如果有分⻚插件,要排在通用mapper之前-->
    <plugin interceptor="com.github.pagehelper.PageHelper">
    <property name="dialect" value="mysql"/>
    </plugin>
    <!--通用Mapper接口,多个通用接口用逗号隔开-->
    <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
    <!--指定当前通用 mapper 使用的是哪一个-->
    <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
    </plugin>
  2. 实体类注解

    @Table(name="user") //指定与数据库哪一张表进行映射
    public class User implements Serializable {

    @Id //对应的是注解Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //设置主键生成策略
    private Integer id;
    private String username;

    ...

  3. 定义通用mapper

    public interface UserMapper extends Mapper<User> {
    }
  4. 测试

    @Test
    public void mapperTest() {
    User user = new User();
    user.setId(1);

    //(1)mapper基础接口
    //select 接口
    User user1 = userMapper.selectOne(user); //根据实体中的属性进行查询,只能有一个返回值
    List<User> users = userMapper.select(null); //查询全部结果
    userMapper.selectByPrimaryKey(1); //根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
    userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使用等号

    // insert 接口
    int insert = userMapper.insert(user); //保存一个实体,null值也会保存,不会使用数据库默认值
    int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存, 会使用数据库默认值

    // update 接口
    int i1 = userMapper.updateByPrimaryKey(user);//根据主键更新实体全部字段, null值会被更新

    // delete 接口
    int delete = userMapper.delete(user); //根据实体属性作为条件进行删除,查询条件使用等号
    userMapper.deleteByPrimaryKey(1); //根据主键字段进行删除,方法参数必须包含完整的主键属性

    //(2)example方法
    Example example = new Example(User.class);
    example.createCriteria().andEqualTo("id", 1);
    example.createCriteria().andLike("val", "1");
    //自定义查询
    List<User> users1 = userMapper.selectByExample(example);
    }