Java EE数据持久化框架 • 【第6章 MyBatis插件开发】

全部章节   >>>>


本章目录

6.1 MyBatis拦截器接口

6.1.1 MyBais拦截器接口介绍

6.1.2 MyBais拦截器签名介绍

6.1.3 实践练习

6.2 下划线键值转小写驼峰形式插件

6.2.1 下划线键值转小写驼峰形式的三种方法

6.2.2 拦截器实现下划线键值转小写驼峰

6.2.3 实践练习

6.3 日志记录插件

6.3.1 创建针对日志记录的MyBatis应用

6.3.2 创建日志记录插件

6.3.3 实践练习

6.4 动态修改SQL插件

6.4.1 动态修改SQL的场景和解决思路

6.4.2 创建动态修改SQL插件

6.4.3 实践练习

总结


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语句的动态修改处理等操作。

posted @ 2021-05-11 21:42  明金同学  阅读(50)  评论(0编辑  收藏  举报