MyBatis-框架使用和分析
- 2021 年 6 月 3 日
- 笔记
- mybatis, mybatis源码分析, 自定义插件
一、基础知识
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口为数据库中的记录。
参考优秀博文: MyBatis源码解读,MyBatis的架构设计以及实例分析(老版本参考)
本文测试和源码分析参考版本: Mybatis-version:3.4.1
1.1 名词术语
名称
|
说明
|
SqlSession
|
作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 。
|
Executor
|
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。
|
StatementHandler
|
封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
|
ParameterHandler
|
封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
|
ResultSetHandler
|
负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
|
TypeHandler
|
负责java数据类型和jdbc数据类型之间的映射和转换
|
MappedStatement
|
MappedStatement维护了一条<select|update|delete|insert>节点的封装
|
SqlSource
|
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
|
BoundSql
|
表示动态生成的SQL语句以及相应的参数信息
|
Configuration
|
MyBatis所有的配置信息都维持在Configuration对象之中
|
MetaObject
|
MetaObject是MyBatis提供的工具类,它可以有效的获取或修改一些重要对象的属性。
|
1.2 流程原理分析
MyBatis执行流程节点说明
二、插件使用
与其称为Mybatis插件,不如叫Mybatis拦截器,更加符合其功能定位,实际上它就是一个拦截器,应用代理模式,在方法级别上进行拦截。Mybatis3 插件采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的)。
责任链模式:责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。
2.1 插件原理
// 插件声明格式 @Intercepts({ @Signature(type =StatementHandler.class, method="prepare" , args={Connection.class})}) public class MyPlugin implements Interceptor{ ...... }
支持拦截的方法
- 执行器Executor(update、query、commit、rollback等方法);
- 参数处理器ParameterHandler(getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法);
- SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
- @Signature(type = StatementHandler.class, method = “query”, args = {Statement.class, ResultHandler.class}),编写逻辑,基于StatementHandler接口,查看里面对应方法,配置的参数,args为StatementHandler.query()方法的入参。参考代码:
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
在实际的拦截器中,可通过参数获取对象如:
Statement statement = (Statement)invocation.getArgs()[0];
ResultHandler resultHandler = (ResultHandler)invocation.getArgs()[1];
流程说明
Mybatis的拦截器实现机制,使用的是JDK的InvocationHandler,当调用ParameterHandler,ResultSetHandler,StatementHandler,Executor的对象的时候,实际上使用的是Plugin这个代理类的对象,这个类实现了InvocationHandler接口。在调用上述被代理类的方法的时候,就会执行Plugin的invoke方法。
Plugin在invoke方法中根据@Intercepts的配置信息(方法名,参数等)动态判断是否需要拦截该方法,再然后使用需要拦截的方法Method封装成Invocation,并调用Interceptor的proceed方法达到了拦截目标方法的结果。截器代理类对象->拦截器->目标方法,如:
Executor->Plugin->Interceptor->Invocation
Executor.Method->Plugin.invoke->Interceptor.intercept->Invocation.proceed->method.invoke
2.2 应用场景
场景
|
描述
|
分页
|
mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可。
|
公共字段统一赋值
|
一般业务系统都会有创建者,修改者等基础字段字段,对于基础字段的赋值,可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可。
|
性能监控
|
对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间。
|
其他
|
其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
|
三、框架使用
3.1 基于Java API模式
MyBatis 3.0版本,框架支持不使用配置xml文件,配置可走Java API的模式,便于项目管理和维护。
基础配置信息
设置数据库的连接基础信息,放入到application.properties文件中。
配置项目的pom依赖文件,具体参考项目中的配置信息。
基础测试对象类:
** * @author wl * @description 单据生命流程日志 * @date 2019/10/9 21:06 */ @Data public class PayLifeLog implements Serializable { /** * 日志主键编码 */ private Integer logId; /** * 单据包编码 */ private String packageId; /** * 单据编码 */ private String billCode; /** * 节点执行结果 */ private Byte lifeStatus; /** * 节点 */ private Byte lifeState; /** * 备注 */ private String markMsg; /** * 更新时间 */ private LocalDateTime updatetime; }
View Code
构建DataBaseConfiguration配置类
package com.trace.base.tool.configuration; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 数据库层配置类 * * @author wl * @date 2021-4-27 */ @Configuration @MapperScan(basePackages = {"com.trace.base.tool.mapper.**"}, sqlSessionFactoryRef = "sqlSessionFactory") public class DataBaseConfiguration { @Value("${hikaricp.dataSource.jdbc.driverClassName}") private String driverClassName; @Value("${hikaricp.dataSource.url}") private String jdbcUrl; @Value("${hikaricp.dataSource.username}") private String username; @Value("${hikaricp.dataSource.password}") private String password; @Value("${hikaricp.dataSource.connectionTestQuery}") private String connectionTestQuery; @Value("${hikaricp.dataSource.connectionTimeout}") private long connectionTimeout; @Value("${hikaricp.dataSource.idleTimeout}") private long idleTimeout; @Value("${hikaricp.dataSource.maxLifetime}") private long maxLifetime; @Value("${hikaricp.dataSource.maximumPoolSize}") private int maximumPoolSize; @Value("${hikaricp.dataSource.minimumIdle}") private int minimumIdle; /** * 注入一个hikaricp dataSource */ @Bean(value = "dataSource", destroyMethod = "close") public HikariDataSource hikariDataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setDriverClassName(driverClassName); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setUsername(username); hikariConfig.setPassword(password); hikariConfig.setConnectionTestQuery(connectionTestQuery); hikariConfig.setConnectionTimeout(connectionTimeout); hikariConfig.setIdleTimeout(idleTimeout); hikariConfig.setMaxLifetime(maxLifetime); hikariConfig.setMaximumPoolSize(maximumPoolSize); hikariConfig.setMinimumIdle(minimumIdle); return new HikariDataSource(hikariConfig); } }
数据库配置类功能:
- 读取application.properties配置文件的信息,并生成一个hikariDataSource对象。
- 通过注解@MapperScan限定数据库应用的包,支持项目中多数据源应用。
构建Mapper接口
package com.trace.base.tool.mapper; import com.trace.base.tool.domain.base.PayLifeLog; import com.trace.base.tool.sqlprovider.LifeLogSqlProvider; import com.trace.base.tool.web.Page; import org.apache.ibatis.annotations.*; import org.springframework.stereotype.Repository; import java.util.List; /** * @author wl * @description 支付请求日志-数据接口 * @date 2019/10/10 18:42 */ @Mapper @Repository public interface LifeLogMapper { /** * 获取批次支付失败日志 * * @param billCode 单号 * @return 结果 */ @SelectProvider(type = LifeLogSqlProvider.class, method = "getPayLifeLogByIdSql") PayLifeLog getPayLifeLogById(@Param("billCode") String billCode); }
设置查询参数:
- 参考mybatis 3.4版本,带上@param注解,保证参数传递正常。
- @SelectProvider注解,指定基于Java API模式,提供具体sql信息的配置类信息。
构建SqlProvider配置类
package com.trace.base.tool.sqlprovider; import com.trace.base.tool.domain.base.PayLifeLog; import com.trace.base.tool.util.sql.SQL; import com.trace.base.tool.web.Page; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.annotations.Param; /** * @author wl * @description 请求日志sql生成器 * @date 2019/10/8 18:43 */ public class LifeLogSqlProvider { public String getPayLifeLogByIdSql(@Param("billCode") String billCode) { String sql = " select log_id as logId, package_id as packageId,bill_code as billCode,life_state as lifeState,life_status as lifeStatus,mark_msg as markMsg,updatetime " + "from caiwu_pay_life_log where " + "bill_code = #{billCode}" + "limit 0,1"; return sql; } }
- 编写一个基本类,类似xml文件功能,生成对应执行的sql。
- sql可参考网上或者Mybatis自身的一sql生成工具类 SQL(),扩展开发,方便生成sql语句。
构建RestController业务控制器
package com.trace.base.tool.controller; import com.trace.base.tool.domain.base.PayLifeLog; import com.trace.base.tool.mapper.LifeLogMapper; import com.trace.base.tool.web.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * mybatis测试demo * * @author wl * @date 2020-12-01 */ @RestController @RequestMapping("mybatis") @Validated public class MyBatisController { @Autowired private LifeLogMapper lifeLogMapper; /** * 获取当前日志信息 * * @return 返回存储数据 */ @GetMapping("/log") public PayLifeLog getTraceService() { return lifeLogMapper.getPayLifeLogById("5"); } }
注入Mapper接口,测试获取数据。(正式项目不应该直接在Controller里面调用Mapper,需设置一个Service层,流程为:Controller->Service->Mapper)。
启动微服务,利用Postman测试效果如下图:
3.2 编写慢sql监控日志插件
分析了sql插件的原理,再做一个自定义插件做测试。需求:
- 基于环境配置变量,控制该插件是否需要启用(如测试环境启用,线上关闭)。
- 当sql运行时长超过1秒时,记录运行的sql信息,写入到慢sql日志中。
基础配置信息
设置数据库的连接基础信息,放入到application.properties文件中。
设置慢sql监控配置信息:sql.slow.enable=true
配置项目的pom依赖文件,具体参考项目中的配置信息。
构建DataBaseConfiguration配置类
package com.trace.base.tool.configuration; import com.trace.base.tool.mybatis.study.StudySqlSessionFactoryBean; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; /** * 数据库层配置类 * * @author wl * @date 2021-4-27 */ @Configuration @MapperScan(basePackages = {"com.trace.base.tool.mapper.**"}, sqlSessionFactoryRef = "sqlSessionFactory") public class DataBaseConfiguration { @Value("${hikaricp.dataSource.jdbc.driverClassName}") private String driverClassName; @Value("${hikaricp.dataSource.url}") private String jdbcUrl; @Value("${hikaricp.dataSource.username}") private String username; @Value("${hikaricp.dataSource.password}") private String password; @Value("${hikaricp.dataSource.connectionTestQuery}") private String connectionTestQuery; @Value("${hikaricp.dataSource.connectionTimeout}") private long connectionTimeout; @Value("${hikaricp.dataSource.idleTimeout}") private long idleTimeout; @Value("${hikaricp.dataSource.maxLifetime}") private long maxLifetime; @Value("${hikaricp.dataSource.maximumPoolSize}") private int maximumPoolSize; @Value("${hikaricp.dataSource.minimumIdle}") private int minimumIdle; /** * 注入一个hikaricp dataSource */ @Bean(value = "dataSource", destroyMethod = "close") public HikariDataSource hikariDataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setDriverClassName(driverClassName); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setUsername(username); hikariConfig.setPassword(password); hikariConfig.setConnectionTestQuery(connectionTestQuery); hikariConfig.setConnectionTimeout(connectionTimeout); hikariConfig.setIdleTimeout(idleTimeout); hikariConfig.setMaxLifetime(maxLifetime); hikariConfig.setMaximumPoolSize(maximumPoolSize); hikariConfig.setMinimumIdle(minimumIdle); return new HikariDataSource(hikariConfig); } /** * 注入一个sqlSessionFactory */ @Bean(value = "sqlSessionFactory") public StudySqlSessionFactoryBean sqlSessionFactory(HikariDataSource dataSource) { StudySqlSessionFactoryBean sessionFactoryBean = new StudySqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); return sessionFactoryBean; } /** * 主动注入一个transactionManger,适用多数据库事务管理器环境 */ @Bean(value = "transactionManager") public DataSourceTransactionManager dataSourceTransactionManager(HikariDataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
数据库配置类功能:
- 读取application.properties配置文件的信息,并生成一个hikariDataSource对象。
- 自定义一个StudySqlSessionFactoryBean继承SqlSessionFactoryBean,便于扩展默认的SqlSessionFactoryBean功能,并基于MyBatis Java API功能,做插件或其他配置。
慢sql监控插件编写
package com.trace.base.tool.mybatis.study.plugin; import com.mysql.jdbc.PreparedStatement; import com.trace.base.tool.logging.BaseLog; import com.trace.base.tool.logging.Channel; import com.trace.base.tool.logging.LevelEnum; import com.trace.base.tool.mybatis.monitor.SlowSqlEnum; import com.trace.base.tool.mybatis.monitor.SlowSqlLog; import com.trace.base.tool.util.LocalDateTimeUtil; import com.zaxxer.hikari.pool.ProxyStatement; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.logging.jdbc.PreparedStatementLogger; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.postgresql.jdbc.PgStatement; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.sql.Statement; import java.util.Properties; /** * 监控慢SQL插件 * * @author wl */ @Intercepts({ @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}) }) public class SlowSqlMonitorPlugin implements Interceptor { public static final String SLOW_SQL_ENABLE = "sql.slow.enable"; private static boolean POSTGRESQL_DRIVER_AVAILABLE; private static boolean MYSQL_DRIVER_AVAILABLE; private static boolean HIKARICP_AVAILABLE; private static Field DELEGATE_FIELD; static { try { Class.forName("org.postgresql.jdbc.PgPreparedStatement"); POSTGRESQL_DRIVER_AVAILABLE = true; } catch (ClassNotFoundException e) { // ignore POSTGRESQL_DRIVER_AVAILABLE = false; } try { Class.forName("com.mysql.jdbc.PreparedStatement"); MYSQL_DRIVER_AVAILABLE = true; } catch (ClassNotFoundException e) { // ignore MYSQL_DRIVER_AVAILABLE = false; } try { Class.forName("com.zaxxer.hikari.pool.HikariProxyPreparedStatement"); DELEGATE_FIELD = ProxyStatement.class.getDeclaredField("delegate"); DELEGATE_FIELD.setAccessible(true); HIKARICP_AVAILABLE = true; } catch (ClassNotFoundException | NoSuchFieldException e) { // ignore HIKARICP_AVAILABLE = false; } } @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { Object obj = invocation.proceed(); return obj; } finally { long end = System.currentTimeMillis(); long used = end - start; // >= 1s final long max = 1000L; if (used >= max) { try { Object target = invocation.getTarget(); String sql = "unknown"; if (target instanceof StatementHandler) { sql = ((StatementHandler) target).getBoundSql().getSql(); } // 外部提前做一次猜测是否为预处理语句,只用instanceof PreparedStatement有可能没有?,这种情况不需要执行下面逻辑 boolean mightPreparedSql = sql.contains("?"); Object statementArg = invocation.getArgs()[0]; // 可能是预处理语句才处理 if (mightPreparedSql) { // 这里还要区分是否为debug模式,debug模式下,生成的connection和statement都是被mybatis logger类代理 if (Proxy.isProxyClass(statementArg.getClass())) { // 获取到真实被代理的statement statementArg = ((PreparedStatementLogger) Proxy.getInvocationHandler(statementArg)).getPreparedStatement(); } // 被HikariProxyPreparedStatement代理,通过反射才能获取到真实的PreparedStatement if (HIKARICP_AVAILABLE && statementArg instanceof ProxyStatement) { java.sql.PreparedStatement preparedStatement = (java.sql.PreparedStatement) DELEGATE_FIELD.get(statementArg); // postgresql,前提是SQL为预处理语句,避免非预处理语句也执行了toString()造成拿到内存地址 if (POSTGRESQL_DRIVER_AVAILABLE && preparedStatement instanceof PgStatement) { // 因为PgPreparedStatement是保护类,只能使用PgStatement转换,实际是执行子类的toString() sql = preparedStatement.toString(); } // mysql else if (MYSQL_DRIVER_AVAILABLE && preparedStatement instanceof PreparedStatement) { sql = ((PreparedStatement) preparedStatement).asSql(); } } } // 记录日志信息 SlowSqlLog slowSqlLog = new SlowSqlLog(); slowSqlLog.setTraceId("idnum-0001"); slowSqlLog.setType(SlowSqlEnum.DML); slowSqlLog.setMessage("执行DML[" + sql + "]超时1秒"); slowSqlLog.setStart(LocalDateTimeUtil.formatMilliPlus8(start)); slowSqlLog.setEnd(LocalDateTimeUtil.formatMilliPlus8(end)); slowSqlLog.setUsed(used); BaseLog<SlowSqlLog> baseLog = new BaseLog<>(); baseLog.setContext(slowSqlLog); baseLog.setLevel(LevelEnum.WARNING.getLevel()); baseLog.setLevelName(LevelEnum.WARNING.getLevelName()); baseLog.setChannel(Channel.SYSTEM); baseLog.setMessage("slowsql log"); baseLog.setDatetime(LocalDateTimeUtil.getMicroSecondFormattedNow()); String sqlLog=slowSqlLog.toString(); // todo 记录日志信息 } catch (Throwable ex) { // ignore } } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
View Code
自定义插件说明:
- 参考插件的使用说明文档,指定@Intercepts,设置产生拦截的触发条件。
- 重新实现intercept方法,监听invocation.proceed()方法执行前后的时间,记录sql运行的相关日志信息。
- 插件中获取的原生sql的方法,在不同的驱动,数据库版本下,方法不一样,仅供参考。
配置插件
编写完成自定义的插件信息后,需将插件配置到MyBatis中,告诉它有这个插件,才可以运行插件功能。插件配置可通过xml设置,或者Java API配置,基于Java配置参考如下:
package com.trace.base.tool.mybatis.study; import com.trace.base.tool.mybatis.monitor.MonitorSpringManagedTransactionFactory; import com.trace.base.tool.mybatis.study.plugin.PagePlugin; import com.trace.base.tool.mybatis.study.plugin.SlowSqlMonitorPlugin; import org.apache.commons.lang3.ArrayUtils; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.AutoMappingBehavior; import org.apache.ibatis.session.Configuration; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; /** * 自定义的sqlSessionFactoryBean * * @author wl * @date 2021-3-9 */ public class StudySqlSessionFactoryBean extends SqlSessionFactoryBean implements EnvironmentAware { private Interceptor[] plugins; public static Configuration CONFIGURATION; private boolean slowSqlEnabled = false; public StudySqlSessionFactoryBean() { this(null); } public StudySqlSessionFactoryBean(Configuration configuration) { super(); if (configuration == null) { configuration = new Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setAutoMappingBehavior(AutoMappingBehavior.FULL); } CONFIGURATION = configuration; setConfiguration(configuration); } @Override public void setPlugins(Interceptor[] plugins) { this.plugins = plugins; } /** * 真实执行设置插件,setPlugins只用于记录客户端自定义的plugin,便于后续拷贝 */ private void actualSetPlugins() { if (slowSqlEnabled) { // 使用自定义监控功能的事务管理器工厂类 setTransactionFactory(new MonitorSpringManagedTransactionFactory()); this.plugins = ArrayUtils.add(plugins == null ? new Interceptor[0] : plugins, new SlowSqlMonitorPlugin()); } super.setPlugins(plugins); } @Override public void setEnvironment(Environment environment) { slowSqlEnabled = environment.getProperty(SlowSqlMonitorPlugin.SLOW_SQL_ENABLE, boolean.class, true); actualSetPlugins(); } }
View Code
配置插件关键点:
- setConfiguration只是设置Mybatis的全局配置信息,如设置统一的下划线转驼峰功能。
- 重新实现setEnvironment方法,可以获取配置信息,用于环境变量标识,在指定的环境下,设置插件是否运行。
- 重新实现setPlugins方法,添加自定义的插件。
再次启动3.1中的测试用例,调试代码,进入插件运行函数,可查看到自定义的插件已经生效,效果如下:
3.3 Mybatis应用的一些思考
- 到底是xml模式设置sql好,还是Java API模式更好?
- 若基于Java API模式,MyBatis提供的动态sql工具类SQL()如何,有无局限性?
- 若扩展SQL()或者自实现,有哪些方向和注意点?
- 插件开发便于项目做监控管理,一般需要考虑哪些自定义的插件?
- 是否存在一些开源优秀的插件(自定义插件可以作为参考的方向)?
- Mapper配置一些关联查询如何处理,如一个Mapper查询集成其他mapper中的查询功能?
- 多个插件先后顺序如何定义?
四、Mybatis扩展
Mybatis框架操作数据库已经很优秀了,有没有其他的ORM框架了,欢迎了解MybatisPlus,Spring Data Jpa 。优缺点项目开发者自己确定,也是一个不错的方向(Mybatis Plus个人觉得里面的部分成熟的插件可以参考源代码,引入到项目中)。参考文档: