增强代码
MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截的效果。MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
/**
* 插件原理
* 在四大对象创建的时候
* 1、每个创建出来的对象不是直接返回的,而是
* interceptorChain.pluginAll(parameterHandler);
* 2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);
* 调用interceptor.plugin(target);返回target包装后的对象
* 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
* 我们的插件可以为四大对象创建出代理对象;
* 代理对象就可以拦截到四大对象的每一个执行;
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
1、编写插件
/**
* Description:
*
* @author guizy
* @date 2021/4/22 12:58
*/
@SuppressWarnings("all")
/*
完成插件签名: 告诉MyBatis当前插件用来拦截哪个对象哪个方法
*/
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
}
)
public class MyPlugin implements Interceptor {
/**
* 拦截对象目标方法的执行
*
* @param invocation 拦截
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyPlugin.intercept:" + invocation.getMethod());
// 执行目标方法
Object proceed = invocation.proceed();
return proceed;
}
/**
* 包装目标对象, 包装: 为目标对象创建一个代理对象
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
System.out.println("MyPlugin.plugin mybatis将要包装的对象" + target);
// 该方法就是让当前的Interceptor拦截器来包装我们的对象
Object wrap = Plugin.wrap(target, this);
// wrap就是target的动态代理对象
return wrap;
}
/**
* 将插件注册时的property属性设置进来
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息: " + properties);
}
}
2、配置插件
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.sunny.mapper.MyPlugin">
<!-- 可选 -->
<property name="username" value="root"/>
<property name="password" value="8888"/>
</plugin>
</plugins>
3、测试插件
随便执行一个查询方法, 因为拦截的是StatementHandler的parameterize的方法, 因为在mapper.getUser的时候, 底层会调用这个预编译参数方法, 所以会拦截到该方法
@Test
public void testQueryOneUser(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
// 使用方式一: 来找到SQL并执行
//User user = sqlSession.selectOne("com.sunny.dao.UserMapper.getUser", 1L);
// 使用方式二: 来找到SQL并执行
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 实际上底层调用的还是sqlSession的方法,注意:sqlSession调用CRUD方法只能传递一个参数
User user = mapper.getUser(12);
System.out.println(user);
sqlSession.close();
}
打印信息
// 打印了配置的信息
插件配置的信息: {password=8888, username=root}
// 这里在创建4大对象的时候, 都会调用PluginAll方法, 然后里面会拿到所有实现Interceptor的
// 拦截器进行拦截包装
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@77ec78b9
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@42607a4f
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@25bbf683
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7276c8cd
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?;
// 只会为StatementHandler对象创建代理对象, 拦截到parameterize方法
// 因为Executor, parameterHandler, ResultSetHandler的签名不属于StatementHandler
// 所以就没有为它们创建代理对象
MyPlugin.intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
DEBUG [main] - ==> Parameters: 12(Integer)
TRACE [main] - <== Columns: id, name, pwd
TRACE [main] - <== Row: 12, lisi, 44
DEBUG [main] - <== Total: 1
User{id=12, name='lisi', pwd='44'}
为StatementHandler对象创建代理对象
在执行到设置参数预编译的方式时
4、多个插件同时拦截同一对象的方法的运行流程
5、开发插件
- 我们自己编写的插件会为目标对象创建一个代理对象(通过动态代理), 当目标对象的目标方法要执行的时候, 都会来到代理对象的intercept方法, 可以在该方法中在执行目标方法的前后, 做增强/包装
- 完成功能: 偷梁换柱, 传入用户id为1, 实际查出来的是id为2的员工
/**
* Description:
*
* @author guizy
* @date 2021/4/22 12:58
*/
@SuppressWarnings("all")
/*
完成插件签名: 告诉MyBatis当前插件用来拦截哪个对象哪个方法
*/
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
}
)
public class MyPlugin implements Interceptor {
/**
* 拦截对象目标方法的执行
*
* @param invocation 拦截
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyPlugin.intercept:" + invocation.getMethod());
// 因为该插件拦截的是parameterize方法, 所以就动态改变一下sql运行的参数,来测试一下
// 偷梁换柱: 我要原来要查1号员工信息, 实际从数据库查2号员工
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象: " + target);
// 通过原理, 我们只要修改setParameters中的parameterObject的值即可
// 要拿到StatementHandler -> ParameterHandler -> parameterObject
MetaObject metaObject = SystemMetaObject.forObject(target); // 通过该方法拿到target的元数据
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句传入的参数为:" + value);
// 修改sql语句传进来的参数为2
metaObject.setValue("parameterHandler.parameterObject", 2);
// 执行目标方法
Object proceed = invocation.proceed();
return proceed;
}
/**
* 包装目标对象, 包装: 为目标对象创建一个代理对象
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
System.out.println("MyPlugin.plugin mybatis将要包装的对象" + target);
// 该方法就是让当前的Interceptor拦截器来包装我们的对象
Object wrap = Plugin.wrap(target, this);
// wrap就是target的动态代理对象
return wrap;
}
/**
* 将插件注册时的property属性设置进来
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息: " + properties);
}
}
插件配置的信息: {password=8888, username=root}
MyPlugin.plugin mybatis将要包装的对象[email protected]
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@42607a4f
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@25bbf683
MyPlugin.plugin mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7276c8cd
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?;
MyPlugin.intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@7276c8cd
sql语句传入的参数为:1
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <== Columns: id, name, pwd
TRACE [main] - <== Row: 2, lisi, 44
DEBUG [main] - <== Total: 1
User{id=2, name='lisi', pwd='44'}
补充pageHelper分页插件的使用
@Test
public void test01() throws IOException {
// 1、获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Page<Object> page = PageHelper.startPage(5, 1);
List<Employee> emps = mapper.getEmps();
//传入要连续显示多少页
PageInfo<Employee> info = new PageInfo<>(emps, 5);
for (Employee employee : emps) {
System.out.println(employee);
}
/*System.out.println("当前页码:"+page.getPageNum());
System.out.println("总记录数:"+page.getTotal());
System.out.println("每页的记录数:"+page.getPageSize());
System.out.println("总页码:"+page.getPages());*/
///xxx
System.out.println("当前页码:"+info.getPageNum());
System.out.println("总记录数:"+info.getTotal());
System.out.println("每页的记录数:"+info.getPageSize());
System.out.println("总页码:"+info.getPages());
System.out.println("是否第一页:"+info.isIsFirstPage());
// 就是页面右下角可以连续显示的页码数
// 点击第1页, 1 2 3 4 5
// 点击第2页, 1 2 3 4 5
// 点击第3页, 1 2 3 4 5
// 点击第4页, 2 3 4 5 6
// 点击第5页, 3 4 5 6 7 这样的显示规则
System.out.println("连续显示的页码:");
int[] nums = info.getNavigatepageNums();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
//xxxx
} finally {
openSession.close();
}
}