插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBatis插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能
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等方法);
Mybatis插件原理
在四大对象创建的时候 - 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
- 获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返回target包装后的对象
- 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
拦截
插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说
1 | public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){ |
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象
如果我们想要拦截Executor的query方法,那么可以这样定义插件:
1 |
|
除此之外,我们还需将插件配置到sqlMapConfig.xml中。
1 | <plugins> |
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory创建SqlSession。Executor实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在Executor相关方法被调用前执行。
以上就是MyBatis插件机制的基本原理
自定义插件
插件接口
Mybatis插件接口-Interceptor
- Intercept方法,插件的核心方法
- plugin方法,生成target的代理对象
- setProperties方法,传递插件所需参数
自定义插件
设计实现一个自定义插件sqlMapConfig.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32Intercepts ({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
//这是指拦截哪个接口 (type = StatementHandler .class ,
method = "prepare",//这个接口内的哪个方法名,不要拼错了
args = { Connection.class, Integer .class}),//// 这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
})
public class MyPlugin implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
//这里是每次执行操作的时候,都会进行这个拦截器的方法内
Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对方法进行了增强....");
return invocation.proceed(); //执行原方法
}
/**
* //主要是为了把这个拦截器生成一个代理放到拦截器链中
* ^Description包装目标对象 为目标对象创建代理对象
* @Param target为要拦截的对象
* @Return代理对象
*/
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 );
}
}mapper接口1
2
3
4
5
6<plugins>
<plugin interceptor="com.lagou.plugin.MySqlPagingPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>mapper.xml1
2
3public interface UserMapper {
List<User> selectUser();
}测试类1
2
3
4
5<mapper namespace="com.lagou.mapper.UserMapper">
<select id="selectUser" resultType="com.lagou.pojo.User">
SELECT id,username FROM
</select>
</mapper>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class PluginTest {
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方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截方法是否配置在插件的@Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept中,该方法的参数类型为Invocationo Invocation主要用于存储目标类,方法以及方法参数列表。下面简单看一下该类的定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// -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){
}
}
}关于插件的执行逻辑就分析结束1
2
3
4
5
6
7
8
9
10
11
12
13public 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分页插件
MyBatis可以使用第三方的插件来对功能进行扩展,分页助手PageHelper是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据开发步骤: - 导入通用PageHelper的坐标
- 在mybatis核心配置文件中配置PageHelper插件
- 测试分页数据获取
导入通用PageHelper的坐标
1 | <dependency> |
在mybatis核心配置文件中配置PageHelper插件
1 | <!--注意:分页助手的插件 配置在通用馆mapper之前*-->* |
测试分页数据获取
1 |
|
获得分页相关的其他参数
1 | //其他分页的数据 |
通用mapper
什么是通用Mapper
通用Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法
如何使用
首先在maven项目,在pom.xml中引入mapper的依赖
1 | <dependency> |
Mybatis配置文件中完成配置
1 | <plugins> |
实体类设置主键
1 |
|
定义通用mapper
1 | import com.lagou.domain.User; |
测试
1 | public class UserTest { |