一. 插件简介
⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅ 的,⼀是增 加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。以MyBatis
为例,我 们可基于MyBati s
插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆关,业务也⽆法感知插件的存在。因 此可以⽆感植⼊插件,在⽆形中增强功能。
二. Mybatis插件介绍
Mybati s
作为⼀个应⽤⼴泛的优秀的
ORM
开源框架,这个框架具有强⼤的灵活性,在四⼤组件
(Executor
、
StatementHandler
、
ParameterHandler
、
ResultSetHandler)
处提供了简单易⽤的插 件扩展机制。
Mybatis
对持久层的操作就是借助于四⼤核⼼对象。
MyBatis
⽀持⽤插件对四⼤核⼼对象进 ⾏拦截,对
mybatis
来说 插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说, MyBatis中的四⼤对象都是代理对象。
![](https://img-blog.csdnimg.cn/d9a9a016f3b54ef6b18e9b7b3edefbc1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAZW50ZXJwYw==,size_20,color_FFFFFF,t_70,g_se,x_16)
MyBatis所允许拦截的⽅法如下:
执⾏器
Executor (update
、
query
、
commit
、
rollback
等⽅法
)
;
SQL
语法构建器
StatementHandler (prepare
、
parameterize
、
batch
、
updates query
等⽅ 法
)
;
参数处理器
ParameterHandler (getParameterObject
、
setParameters
⽅法
)
;
结果集处理器
ResultSetHandler (handleResultSets
、
handleOutputParameters
等⽅法
)
;
三. Mybatis插件原理
在四⼤对象创建的时候
1
、每个创建出来的对象不是直接返回的,⽽是
interceptorChain.pluginAll(parameterHandler);
2
、获取到所有的
Interceptor (
拦截器
)(
插件需要实现的接⼝
)
;调⽤
interceptor.plugin(target);
返回
target
包 装后的对象
3
、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;
AOP (
⾯向切⾯
)
我们的插件可 以为四⼤对象 创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
拦截
插件具体是如何拦截并附加额外的功能的呢?以
ParameterHandler
来说
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
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.xm l
中。
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
</plugin>
</plugins>
这样
MyBatis
在启动时可以加载插件,并保存插件实例到相关对象
(InterceptorChain
,拦截器链
)
中。待准备⼯作 做完后,MyBatis
处于就绪状态。我们在执⾏
SQL
时,需要先通过
DefaultSqlSessionFactory
创建
SqlSession
。
Executor
实例会在创建
SqlSession
的过程中被创建,
Executor
实例创建完毕后,
MyBatis
会通过
JDK
动态代理为 实例⽣成代理类。这样,插件逻辑即可在 Executor
相关⽅法被调⽤前执⾏。
以上就是
MyBatis
插件机制的基本原理。
四. ⾃定义插件
(一)插件接⼝
Mybatis
插件接⼝
-Interceptor
• Intercept
⽅法,插件的核⼼⽅法
• plugin
⽅法,⽣成
target
的代理对象
• setProperties
⽅法,传递插件所需参数
(二)⾃定义插件
设计实现⼀个⾃定义插件
package com.lagou.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 =后面这个大刮号中主要是填写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(); //这里返回invocation.proceed()是指增强逻辑执行后,按照原方法执行
}
/*
主要为了把当前的拦截器生成代理存到拦截器链中,(这个方法先执行)
*/
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);//这里的target代表被拦截对象
return wrap;
}
/*
获取配置文件的参数,主要是获取在SqlMapConfig配置文件中配置插件时所设置的一些参数
*/
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件的参数是:"+properties);
}
}
sqlMapConfig.xml 配置
<plugins>
<plugin interceptor="com.lagou.plugin.plugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
mapper接⼝
public interface UserMapper {
List<User> selectUser();
}
mapper.xml
<mapper namespace="com.lagou.mapper.UserMapper">
<select id="selectUser" resultType="com.lagou.pojo.User">
SELECT
id,username
FROM
user
</select>
</mapper>
测试类
public class PluginTest {
@Test
public void test() throws IOException {
InputStream resourceAsStream =
Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> byPaging = userMapper.selectUser();
for (User user : byPaging) {
System.out.println(user);
}
} }
五. 源码分析
执⾏插件逻辑
Plugin
实现了
InvocationHandler
接⼝,因此它的
invoke
⽅法会拦截所有的⽅法调⽤。
invoke
⽅法会 对所拦截的⽅ 法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:
// -Plugin
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){
}
}
invoke
⽅法的代码⽐较少,逻辑不难理解。⾸先
,invoke
⽅法会检测被拦截⽅法是否配置在插件的
@Signature
注解 中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept
中,该 ⽅法的参数类型为 Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看 ⼀下该类的定义
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object targetf Method method, Object[] args) {
this.target = target;
this.method = method;
//省略部分代码
public Object proceed() throws InvocationTargetException, IllegalAccessException { //调
⽤被拦截的⽅法
>> —
关于插件的执⾏逻辑就分析结束
六. pageHelper分⻚插件
MyBati s
可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿
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插件
<!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper">
<!—指定⽅⾔ —>
<property name="dialect" value="mysql"/>
</plugin>
(三)测试分⻚代码实现
@Test
public void testPageHelper() {
//设置分⻚参数
PageHelper.startPage(1, 2);
List<User> select = userMapper2.select(null);
for (User user : select) {
System.out.println(user);
}
}
}
获得分⻚相关的其他参数
//其他分⻚的数据
PageInfo<User> pageInfo = new PageInfo<User>(select);
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());
七. 通⽤ mapper
(一)什么是通⽤Mapper
通⽤
Mapper
就是为了解决单表增删改查,基于
Mybatis
的插件机制。开发⼈员不需要编写
SQL,
不需要 在
DAO
中增 加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法
如何使⽤
1. ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
2. Mybatis配置⽂件中完成配置
<plugins>
<!--分⻚插件:如果有分⻚插件,要排在通⽤mapper之前-->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
</plugins>
3. 实体类设置主键
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username; }
4. 定义通⽤mapper
import com.lagou.domain.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
5. 测试
public class UserTest {
@Test
public void test1() throws IOException {
Inputstream resourceAsStream =
Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory build = new
SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = build.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(4);
//(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);
}
}
节选自拉钩教育JAVA系列教程