MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口为数据库中的记录。
官网地址:https://mybatis.org/mybatis-3/zh/index.html
参考优秀博文: MyBatis源码解读,MyBatis的架构设计以及实例分析(老版本参考)
本文测试和源码分析参考版本: Mybatis-version:3.4.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维护了一条
SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql
表示动态生成的SQL语句以及相应的参数信息
Configuration
MyBatis所有的配置信息都维持在Configuration对象之中
MetaObject
MetaObject是MyBatis提供的工具类,它可以有效的获取或修改一些重要对象的属性。
明细参考:mybatis原理分析(六)---MetaObject
MyBatis执行流程节点说明
参考文档:Mybatis框架启动流程分析,跟着源码学-启动流程解读
参考文档:Mybatis插件开发指南,MyBatis 插件之拦截器Interceptor源码解析(五)
与其称为Mybatis插件,不如叫Mybatis拦截器,更加符合其功能定位,实际上它就是一个拦截器,应用代理模式,在方法级别上进行拦截。Mybatis3 插件采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的)。
责任链模式:责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。
// 插件声明格式
@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
场景
描述
分页
mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可。
公共字段统一赋值
一般业务系统都会有创建者,修改者等基础字段字段,对于基础字段的赋值,可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可。
性能监控
对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间。
其他
其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
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;
}
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);
}
}
数据库配置类功能:
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);
}
设置查询参数:
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;
}
}
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测试效果如下图:
分析了sql插件的原理,再做一个自定义插件做测试。需求:
设置数据库的连接基础信息,放入到application.properties文件中。
设置慢sql监控配置信息:sql.slow.enable=true
配置项目的pom依赖文件,具体参考项目中的配置信息。
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);
}
}
数据库配置类功能:
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) {
}
}
自定义插件说明:
编写完成自定义的插件信息后,需将插件配置到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();
}
}
配置插件关键点:
再次启动3.1中的测试用例,调试代码,进入插件运行函数,可查看到自定义的插件已经生效,效果如下:
Mybatis框架操作数据库已经很优秀了,有没有其他的ORM框架了,欢迎了解MybatisPlus,Spring Data Jpa 。优缺点项目开发者自己确定,也是一个不错的方向(Mybatis Plus个人觉得里面的部分成熟的插件可以参考源代码,引入到项目中)。参考文档:
手机扫一扫
移动阅读更方便
你可能感兴趣的文章