Java EE数据持久化框架 • 【第6章 MyBatis插件开发】
全部章节 >>>>
本章目录
6.1 MyBatis拦截器接口
6.1.1 MyBais拦截器接口介绍
MyBatis支持使用插件对四个接口对象进行拦截,对MyBatis来说,插件就是拦截器,它用来拦截在执行映射语句过程中添加额外操作,实现一些扩展功能。
MyBatis允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的接口为以下四个核心接口:
- Executor:MyBatis的执行器,用于执行增、删、改、查操作。
- ParameterHandler:处理SQL的参数对象。
- ResultSetHandler:处理SQL的返回结果集。
- StatementHandler:数据库的处理对象,用于执行SQL语句。
拦截器会针对该4个接口下的方法进行控制
MyBatis拦截器的开发步骤如下:
- 创建拦截器类实现Interceptor 接口,重写接口中定义的方法
- 在拦截器类上进行签名(告诉拦截器针对mybatis中哪一种操作而拦截)
- 在mybatis-config.xml核心配置文件中使用<plugin>标签定义使用拦截器
MyBatis插件类的定义必须实现Interceptor 接口,该接口所在位置在org.apache.ibatis.plugin.Interceptor,在实现类中对拦截对象和方法进行处理。
//拦截器的接口定义如下,有3个抽象方法
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
setProperties()方法功能:该方法用来传递插件的参数,可以通过参数来改变插件的行为
<plugins>
<plugin interceptor="jack.mybatis.plug.XXXInterceptor">
<property name="prop1" value="value1"/>
<property name="prop2" value="value2"/>
</plugin>
</plugins>
这个是自定义的拦截器,通过property传递数据
plugin()方法功能
该方法的参数target就是拦截器需要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。
只需要调用MyBatis提供的org.apache.ibatis.plugin.Plugin类的wrap()静态方法,就可以通过Java的动态代理拦截目标对象
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
intercept()方法功能
intercept()方法是MyBatis运行时要执行的拦截方法。通过该方法的参数invocation可以得到很多有用的信息。
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget(); //获取当前被拦截的对象
Method method = invocation.getMethod(); //获取被拦截的方法
Object[] args = invocation.getArgs(); //获取被拦截方法中的参数
Object result = invocation.proceed(); //执行被拦截的方法,理解为放行
return result;
}
例如自定义拦截器类实现如下:
//这里需要设置签名
public class MyTetPlugin implements Interceptor {
// 这里是每次执行操作的时候,都会进行这个拦截器的方法内
public Object intercept(Invocation invocation) throws Throwable {
//自已的业务处理代码可以写在这
return invocation.proceed();
}
// 主要是为了把这个拦截器生成一个代理放到拦截器链中
public Object plugin(Object target) {
return Plugin.wrap(target, this); //官方推荐写法
}
// 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
public void setProperties(Properties properties) {
}
}
6.1.2 MyBais拦截器签名介绍
定义MyBatis拦截器实现类除了需要实现拦截器接口外,还需要给实现类配置以下两个拦截器注解以进行签名:
-
拦截器注解:@Intercepts,全限定类名为org.apache.ibatis.plugin.Intercepts。
-
签名注解:@Signature,全限定类名为org.apache.ibatis.plugin.Signature。
签名:通过两个注解用来配置拦截器要拦截的接口的方法。@Intercepts注解中的属性是一个@Signature签名数组,可以在同一个拦截器中同时拦截不同的接口和方法。
以拦截ResultSetHandler接口的handleResultSets()方法为例,配置签名的代码如下:
//在创建拦截器类的上方使用注解签名
@Intercepts({
@Signature(
type = ResultSetHandler.class, //指定要拦截的接口
method = “handleResultSets”, //设置拦截接口中的方法名
args = {Statement.class}) //设置拦截方法的参数类型数组
})
public class MyTestInterceptor implements Interceptor{ }
Executor接口下方法:
int update(MappedStatement ms, Object parameter) throws SQLException
说明:在所有的SQL语句的insert、update、delete执行时被调用
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException
说明:在所有select查询方法执行时被调用。通过这个接口参数可以获取很多有用的信息,这是最常被拦截的一个方法。
<E> Course<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException
说明:在查询的返回值类型为Cursor(即游标)时被调用
List<BatchResult> flushStatements() throws SQLException
说明:在通过SqlSession对象调用flushStatements()方法,或执行的接口方法中带有@Flush注解时才被调用
@Signature(
type = Executor.class,
method = “query",
args = { MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class }
)
void commit(boolean required) throws SQLException
说明:在通过SqlSession对象调用commit()方法时才被调用
void rollback(boolean required) throws SQLException
说明:在通过SqlSession对象调用rollback()方法时才被调用
Transaction getTransaction()
说明:在通过SqlSession对象获取数据库连接时才被调用
void close(boolean forceRollback)
说明:在延迟加载获取新的Executor后才会被执行
boolean isClosed()
说明:在延迟加载执行查询方法前才会被执行
@Signature(
type = Executor.class,
method = "close",
args = {boolean.class}
)
ParameterHandler接口下方法:
Object getParameterObject()
说明:在执行存储过程处理参数值传出的时候被调用
签名:@Signature(
type = ParameterHandler.class,
method = "getParameterObject",
args = { })
void setParameters(PreparedStatement ps) throws SQLException
说明:在所有数据库方法设置SQL参数时被调用。
签名:@Signature(
type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class})
ResultSetHandler接口下方法:
<E> List<E> handleResultSets(Statement stmt) throws SQLException
说明:在除存储过程及返回值类型为org.apache.ibatis.cursor.Cursor<T>以外的查询方法中被调用
签名:@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException
说明:在返回值类型为Cursor<T>的查询方法中被调用。
签名:@Signature(
type = ResultSetHandler.class,
method = "handleCursorResultSets",
args = {Statement.class})
void handleOutputParameters(CallableStatement cs) throws SQLException
说明:在使用存储过程处理出参时被调用
签名:@Signature(
type = ResultSetHandler.class,
method = "handleOutputParameters",
args = {CallableStatement.class})
StatementHandler接口下方法:
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException
说明:在数据库执行前被调用,优先于当前接口中的其他方法而被执行
签名:@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
void parameterize(Statement statement) throws SQLException
说明:在prepare()方法之后执行,用于处理参数信息。
签名:@Signature(
type = StatementHandler.class,
method = "preparerize",
args = {Statement.class})
void batch(Statement statement) throws SQLException
说明:在全局设置配置defaultExecutorType="BATCH"时,执行数据操作才会调用该方法
签名:@Signature(
type = StatementHandler.class,
method = "batch",
args = {Statement.class})
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException
说明:执行select查询时,该方法才会被调用。
签名:@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class})
虽然拦截器可以拦截的接口方法很多,但是实际上使用最为频繁的就是固定的几种,比如增删改查语句执行时、查询结果返回时等等。
6.1.3 实践练习
6.2 下划线键值转小写驼峰形式插件
6.2.1 下划线键值转小写驼峰形式的三种方法
数据库中经常使用下划线的列字段命名方式,而Java中则不推荐,Java中的规范是驼峰命名规则,在使用MyBatis返回Map数据时,key部分则是表中列字段名,会造成和Java数据无法正常映射。
为了实现数据库表的列名与Java实体类属性名不匹配的问题,我们已有有两种解决方案:
- 在mybatis主配置文件中配置mapUnderscoreToCamelCase=true开启驼峰命名转换全局配置。
- 通过<resultMap>标签手动配置映射,以保证列名和实体属性名一一对应,但是这种做法过于繁琐。
除了上述两种方案,通过对mybatis拦截器的了解,我们可以通过拦截器的方式在返回结果后进行处理转换。
- 通过拦截ResultSetHandler接口中的handleResultSets()方法去处理Map类型的结果
- 在结果返回之前将Map中的key部分下划线进行处理
6.2.2 拦截器实现下划线键值转小写驼峰
首先需要创建拦截器类,并且要实现Interceptor接口,且当前拦截器类进行处理的方法签名,如下:
@Intercepts(
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
)
public class CamelHumpInterceptor implements Interceptor{
//拦截器中的方法
}
type = ResultSetHandler.class,那块属于签名部分:声明针对那些操作进行拦截
通过拦截器方法后,以Map类型为结果的key部分都会进行处理
为了避免把己经是驼峰的值转换为纯小写,因此通过首字母是否为大写或是否包含下划线来判断,如果符合其中一个条件就转换为驼峰形式,然后删除对应的键值,使用新的键值来代替。
<plugins>
<!--其他插件-->
<!--下划线键值转驼峰插件-->
<plugin interceptor="jack.mybatis.plugin.CamelHumpInterceptor" />
</plugins>
在mybatis-config核心配置中加入插件声明
测试返回Map类型结果时的映射问题:
Map<String, Object> selectByIdCamelHump(Long id);
<select id="selectByIdCamelHump" resultType="java.util.Map">
select * from sys_user where id = #{id}
</select>
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = userMapper.selectByIdCamelHump(1L);
Set<String> keySet = map.keySet();
Iterator<String> it = keySet.iterator();
while(it.hasNext()) {
String key = it.next();
Object value = map.get(key);
System.out.println(key+"-->"+value);
}
6.2.3 实践练习
6.3 日志记录插件
6.3.1 创建针对日志记录的MyBatis应用
设计一个日志记录表sys_sqllog,所有针对数据表的维护操作的具体信息都将记载到日志记录表,结构如下:
列名 | 含义 | 类型 | 长度 | 允许空 | 约束 |
id | 编号 | int | —— | NOT | 主键,自动增长 |
sql_caluse | DML语句字符串 | varchar | 500 |
|
|
result | DML语句执行结果影响行数 | int | —— |
|
|
when_created | DML语句执行时间 | datetime | —— |
|
|
创建日志记录实体类,用于封装日志信息:
public class SysSqlLog {
private Integer result;
private String sqlClause;
private Date whenCreated;
// 属性的getter()方法和setter()方法省略
}
创建日志操作的接口LogMapper,并且定义插入日志的方法:
public interface LogMapper {
int insertSqlLog(SysSqlLog log);
// 其他接口方法
}
创建日志接口LogMapper.xml,用于配置SQL和映射:
<mapper namespace="jack.mybatis.authority.mapper.LogMapper">
<insert id="insertSqlLog">
insert into sys_sqllog(sql_clause, result, when_created)
values(#{sqlClause}, #{result}, #{whenCreated})
</insert>
</mapper>
6.3.2 创建日志记录插件
创建拦截器类实现Interceptor接口,并且定义在更新方法时进行拦截:
所以针对的是Executor接口中的update()方法
@Intercepts(
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class})
)
public class SqlInterceptor implements Interceptor{
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
//拦截方法intercept见下一页
}
针对update更新方法进行签名
日志记录中最为核心的拦截处理方法是intercept方法:
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs(); // 获取被拦截方法中的参数数据
// MappedStatement维护了一个<select|update|delete|insert>节点的封装
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1]; // 被拦截方法的具体参数列表
SysSqlLog log = new SysSqlLog(); // 创建一个日志对象
/ *Configuration保存了所有MyBatis的配置信息,主要包括MyBatis基础配置信息
和Mapper映射信息* /
Configuration configuration = ms.getConfiguration();
Object target = invocation.getTarget(); // 获取被拦截的对象
// StatementHandler负责处理MyBatis与JDBC之间Statement的交互
StatementHandler handler = configuration.newStatementHandler((Executor)target,ms, parameter, RowBounds.DEFAULT, null, null);
BoundSql boundSql = handler.getBoundSql(); // BoundSql维护了一条SQL语句
log.setSqlClause(boundSql.getSql()); // 记录SQL
Object result = invocation.proceed(); // 执行真正的方法
log.setResult(Integer.valueOf(Integer.parseInt(result.toString()))); // 记录影响的行数
log.setWhenCreated(new Date()); // 记录操作时间
// 获取insertSqlLog()方法的MappedStatement对象
ms = ms.getConfiguration().getMappedStatement("insertSqlLog");
args[0] = ms; // 替换当前的参数为新的MappedStatement
args[1] = log; // insertSqlLog方法的参数为SysSqlLog对象,即log
// 执行insertSqlLog()方法
invocation.proceed();
return result; // 返回真正方法的执行结果
使用该插件,还需要在mybatis-config.xml中配置该插件。代码如下:
<plugins>
<!--其他插件-->
<!--日志记录插件-->
<plugin interceptor="jack.mybatis.plugin.SqlInterceptor" />
</plugins>
在UserMapper接口中添加方法和对应的xml映射配置,在测试类中新增三个用户信息,查看日志表中是否存在一条新增日志信息:
int addUserBatch(List<SysUser> userList);
<insert id="addUserBatch">
insert into
sys_user(user_name,user_password,user_email,user_info,head_img,create_time)
values
<foreach collection="list" item="user" separator=",">
(#{user.userName},#{user.userPassword},#{user.userEmail},#{user.userInfo},
#{user.headImg,jdbcType=BLOB},#{user.createTime,jdbcType=TIMESTAMP})
</foreach>
</insert>
6.3.3 实践练习
6.4 动态修改SQL插件
6.4.1 动态修改SQL的场景和解决思路
在MyBatis的实际运用过程中,经常会遇到动态修改SQL的场景:
- 公司的销售数据的检索逻辑已经很稳定了,公司日常的管理业务也是依赖于这个常规的检索规则进行查询。
- 如果公司销售数据的检索逻辑发生了一些比较小的变化,在保持系统相对稳定的前提下,需要尽可能地在非常小的范围内进行修改。
- 特别是公司想隐藏这些查询逻辑的更改细节(例如,更新了商品关注度、热度排名规则,只显示销售额最高的前100件商品),有没有更好的方法让这些查询逻辑的修改悄无声息地默默进行?
利用MyBatis的插件可以很好地解决上述需求,思路如下:
- 创建一个实现了Interceptor接口的拦截器
- 拦截器拦截StatementHandler接口中的prepare()方法,该方法优先于当前接口中的其他方法而被执行,它会在数据库执行前被调用
- 可以在拦截器的intercept()方法中拦截到需要的执行的SQL,然后对这条SQL进行一系列的追加、拼接和更新操作,再执行这条经过改造之后的SQL,所有的这些操作都不露痕迹
6.4.2 创建动态修改SQL插件
原系统显示用户信息的逻辑是显示所有用户,现在改为按页动态显示(假设每页显示5条用户,按照用户创建时间升序排序)。如何在显示用户的接口不发生改变的情况下实现以上需求。利用MyBatis的插件可以很好地实现以上需求:
- 创建拦截器类StatementPrepareInterceptor实现Interceptor接口
- 在改拦截器类上添加拦截签名(StatementHandler接口中的prepare()方法)
- 在拦截器类的intercept()方法中实现具体的修改SQL的逻辑
- 在mybatis-config.xml核心配置中加入<plugin>配置
动态修改SQL的拦截器核心代码如下:
@Intercepts(
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
)
public class StatementPrepareInterceptor implements Interceptor{
public Object intercept(Invocation invocation) throws Throwable { }
public Object plugin(Object target) { return Plugin.wrap(target, this); }
public void setProperties(Properties properties) { // 接收到配置文件的property参数 }
}
private String appendSql; // 追加的SQL
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); // BoundSql维护了一条SQL语句
String sql = boundSql.getSql(); // 获取到原始SQL语句
int pageIndex = 2; // 第2页
int pageSize = 5; // 每页显示5条数据
// 按照用户创建日期升序显示第2页用户数据
appendSql = " order by create_time limit "+(pageIndex-1)*pageSize+", "+pageSize;
String mSql = sql + appendSql; // 将附加的SQL与原始SQL合并
Field field = boundSql.getClass().getDeclaredField("sql"); // 通过反射获得字段对象
field.setAccessible(true); // 属性允许访问
field.set(boundSql, mSql); // BoundSql对象设置新的属性值
return invocation.proceed();
}
配置后测试拦截器插件的结果:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<SysUser> allUsers = userMapper.selectAllUsers();
System.out.println("共查询出"+allUsers.size()+"个用户");
for (SysUser user : allUsers) {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("用户名:"+user.getUserName()+",创建时间:"+
format.format(user.getCreateTime()));
}
6.4.3 实践练习
总结
MyBatis允许在已映射语句执行过程中某一点进行拦截操作,具体拦截的是4个核心接口代理对象和对应的方法,MyBatis本身数据持久层的操作也是借助于这4个接口。
四个核心接口是Executor用于执行CRUD操作、ParameterHandler处理SQL的参数、ResultSetHandler处理返回结果集、StatementHandler用于执行SQL语句。
开发插件的步骤包括:
- 1)创建拦截器类实现Interceptor接口;
- 2)添加注解进行签名;
- 3)实现其3个抽象方法,其中intercept为拦截处理方法;
- 4)在主配置文件中使用<plugin>标签声明使用
利用拦截器可以在执行SQL语句过程中添加很多额外操作,如日志添加、SQL语句的动态修改处理等操作。
本文来自博客园,作者:明金同学,转载请注明原文链接:https://www.cnblogs.com/vmuu/p/15663317.html
公众号:【明金同学】