前言
最近在参加金石计划,在考虑写什么的时,想到自己在项目中使用过的mybatis的插件,就想趁这个机会聊一聊我们接触频繁的Mybatis.
如果是使用过Mybatis的小伙伴,那么我们接触过的第一个Mybatis的插件自然就是分页插件(Mybatis-PageHelper)啦。
你有了解过它是如何实现的吗?你有没有自己编写 Mybatis 插件去实现一些自定义需求呢?
插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或改变框架原有的功能。
Mybatis 中也提供了插件的功能,虽然叫插件,但是实际上是通过拦截器( Interceptor )实现的,通过拦截某些方法的调用,在执行目标逻辑之前插入我们自己的逻辑实现。另外在 MyBatis 的插件模块中还涉及责任链模式和 JDK 动态代理~
文章大纲:
一、应用场景
- 一些字段的自动填充
- SQL语句监控、打印、数据权限等
- 数据加解密操作、数据脱敏操作
- 分页插件
- 参数、结果集的类型转换
这些都是一些可以使用Mybatis插件实现的场景,当然也可以使用其他的方式来实现,只不过拦截的地方不一样罢了,有早有晚。
二、Mybatis实现自定义拦截器
我们用自定义拦截器实现一个相对简单的需求,在大多数表设计中,都会有create_time和update_time
等字段,在创建或更新时需要更新相关字段。
如果是使用过MybatisPlus
的小伙伴,可能知道在MybatisPlus
中有一个自动填充功能,通过实现MetaObjectHandler
接口中的方法来进行实现(主要的实现代码在com.baomidou.mybatisplus.core.MybatisParameterHandler
中).
但使用Mybatis
,并没有相关的方法或 API 可以直接来实现。所以我们这次就用以此处作为切入点,自定义拦截器来实现类似的自动填充功能。
编写步骤
- 编写一个拦截器类实现 Interceptor 接口
- 添加拦截注解 @Intercepts
- 在xml文件中配置拦截器或者添加到Configuration中
基础的环境我就不再贴出来啦哈,直接上三个步骤的代码
2.1、编写拦截器
package com.nzc.interceptor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.springframework.beans.factory.annotation.Value; import java.lang.reflect.Field; import java.util.*; /** * @author 宁在春 * @version 1.0 * @description: 通过实现拦截器来实现部分字段的自动填充功能 * @date 2023/4/6 21:49 */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Slf4j public class MybatisMetaInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String sqlId = mappedStatement.getId(); log.info("------sqlId------" + sqlId); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); Object parameter = invocation.getArgs()[1]; log.info("------sqlCommandType------" + sqlCommandType); log.info("拦截查询请求 Executor#update 方法" + invocation.getMethod()); if (parameter == null) { return invocation.proceed(); } if (SqlCommandType.INSERT == sqlCommandType) { Field[] fields = getAllFields(parameter); for (Field field : fields) { log.info("------field.name------" + field.getName()); try { // 注入创建时间 if ("createTime".equals(field.getName())) { field.setAccessible(true); Object local_createDate = field.get(parameter); field.setAccessible(false); if (local_createDate == null || local_createDate.equals("")) { field.setAccessible(true); field.set(parameter, new Date()); field.setAccessible(false); } } } catch (Exception e) { } } } if (SqlCommandType.UPDATE == sqlCommandType) { Field[] fields = getAllFields(parameter); for (Field field : fields) { log.info("------field.name------" + field.getName()); try { if ("updateTime".equals(field.getName())) { field.setAccessible(true); field.set(parameter, new Date()); field.setAccessible(false); } } catch (Exception e) { e.printStackTrace(); } } } return invocation.proceed(); } @Override public Object plugin(Object target) { return Interceptor.super.plugin(target); } // 稍后会展开说的 @Override public void setProperties(Properties properties) { System.out.println("=======begin"); System.out.println(properties.getProperty("param1")); System.out.println(properties.getProperty("param2")); Interceptor.super.setProperties(properties); System.out.println("=======end"); } /** * 获取类的所有属性,包括父类 * * @param object * @return */ public static Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } }
2.2、添加到Mybatis配置
我这里使用的JavaConfig的方式
package com.nzc.config; import com.nzc.interceptor.*; import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisConfig { @Bean public ConfigurationCustomizer configurationCustomizer() { return new ConfigurationCustomizer() { @Override public void customize(org.apache.ibatis.session.Configuration configuration) { // 开启驼峰命名映射 configuration.setMapUnderscoreToCamelCase(true); MybatisMetaInterceptor mybatisMetaInterceptor = new MybatisMetaInterceptor(); Properties properties = new Properties(); properties.setProperty("param1","javaconfig-value1"); properties.setProperty("param2","javaconfig-value2"); mybatisMetaInterceptor.setProperties(properties); configuration.addInterceptor(mybatisMetaInterceptor); } }; } }
如果是xml配置的话,则是如下: property 是设置 拦截器中需要用到的参数
<configuration> <plugins> <plugin interceptor="com.nzc.interceptor.MybatisMetaInterceptor"> <property name="param1" value="value1"/> <property name="param2" value="value2"/> </plugin> </plugins> </configuration>
2.3、测试
测试代码:实现了一个SysMapper的增删改查
package com.nzc.mapper; import com.nzc.entity.SysUser; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import java.util.List; /** * @author 宁在春 * @description 针对表【sys_user】的数据库操作Mapper */ @Mapper public interface SysUserMapper { @Select("SELECT * FROM tb_sys_user") List<SysUser> list(); @Insert("insert into tb_sys_user(id,username,realname,create_time,update_time) values (#{id}, #{username}, #{realname}, #{createTime}, #{updateTime})") Boolean insert(SysUser sysUser); @Update("update tb_sys_user set username=#{username} , realname=#{realname},update_time=#{updateTime} where id=#{id}") boolean update(SysUser sysUser); }
/** * @author 宁在春 * @version 1.0 * @description: TODO * @date 2023/4/6 21:38 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class SysUserMapperTest { @Autowired private SysUserMapper sysUserMapper; @Test public void test1(){ System.out.println(sysUserMapper.list()); } @Test public void test2(){ SysUser sysUser = new SysUser(); sysUser.setId("1235"); sysUser.setUsername("nzc5"); sysUser.setRealname("nzc5"); System.out.println(sysUserMapper.insert(sysUser)); } @Test public void test3(){ SysUser sysUser = new SysUser(); sysUser.setId("1235"); sysUser.setUsername("nzc7"); sysUser.setRealname("nzc5"); System.out.println(sysUserMapper.update(sysUser)); } }
当然重点不在这里,而是在我们打印的日志上,一起来看看效果吧
此处相关日志对应Interceptor中的日志打印,想要了解的更为详细的可以debug查看一番。
2.4、小结
通过这个小小的案例,我想大伙对于Mybatis中的拦截器应当是没有那般陌生了吧,接下来再来仔细聊聊吧
如果你使用过
MybatisPlus
的话,在读完这篇博文后,可以思考思考下面这个问题,或去看一看源码,将知识串联起来,如果可以的话,记得把答案贴到评论区啦~~~思考:还记得这一小节开始我们聊到的
MybatisPlus
实现的自动填充功能吗?它是怎么实现的呢?
学会自己编写Mybatis插件(拦截器)实现自定义需求2:https://developer.aliyun.com/article/1394846