1.What is Mybatis?
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。上面是官网的解释,简单的说就是如何把Java中的对象映射到数据库中,把数据库中的记录映射成为Java对象的一个过程,并且封装好jdbc相关的功能,从mybatis的运作机理中可以发现其实它只是一个半ORM框架,因为在实现增删改查操作的时候需要我们自己手动编写数据库语句。
2.Mybatis应用(整合SpringBoot)
SpringBoot作为现如今主流的开发框架,为你提供非常便捷简单的项目搭建流程,再加上丰富工具集成而且out-of-the-box(开箱即用),不需要繁琐的配置,大大的提升了开发效率,SpringBoot本文不多做叙述,有兴趣的可以自行了解,下面开始使用SpringBoot+Mybatis做一简单的demo,实现快速使用mybatis来构建你的Dao层。
2-1.创建SpringBoot空项目,引入相关依赖和项目结构
2-2.自定义数据源配置类,完成数据源的自定义配置
@Configuration
@MapperScan(basePackages = DataSourceConfig.PACKAGE, sqlSessionFactoryRef = "mybatisSqlSessionFactory")
public class DataSourceConfig {
//项目中的mapper接口包路径
static final String PACKAGE = "com.lzl.demo.mapper";
//mapper接口对应的xml文件的所在路径
private static final String MAPPER_LOCATION = "classpath:mapper/*.xml";
@Value("${datasource.master.url}")
private String url;
@Value("${datasource.master.username}")
private String user;
@Value("${datasource.master.password}")
private String password;
@Value("${datasource.master.driverClassName}")
private String driverClass;
/**
* 自定义数据源配置
* @return
*/
@Bean(name = "mybatisDataSource")
public DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
dataSource.setValidationQuery("select 1");
dataSource.setTestOnBorrow(true);
return dataSource;
}
/**
* 事务管理
* @param dataSource
* @return
*/
@Bean(name = "mybatisTransactionManager")
public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mybatisDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* 初始化 mybatisSqlSessionFactory
* @param mybatisDataSource
* @return
* @throws Exception
*/
@Bean(name = "mybatisSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(mybatisDataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(DataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
|
这里就已经完成了配置,因为这里我使用的是自定义配置,如果使用的是默认的配置会更简单。SpringBoot+Mybatis就配置好了,非常简单,这里的Mapper接口、**.xml和**PO文件都可以使用 mybatis-generator来生成,如果使用的是IDEA,会有一个插件better-mybatis-generator,安装之后直接可以连接数据库生成上述文件。
3.Mybatis初始化流程
看了上面简单的配置,估计会一脸懵,太简单导致你都看不懂为啥它就可以使用了,下面结合项目说下Mybatis是怎么初始化的。
3-1.启动SpringBoot
一般一个SpringBoot工程的启动类DemoApplication都是在package路径的根目录下,这样你的@SpringBootApplication注解就可以不用指定scanBasePackages了,因为@SpringBootApplication注解里的@ComponentScan默认扫描的就是DemoApplication所在的路径及其子路径下的所有class。
3-2.@MapperScan注解
当SpringBoot启动的时候,扫描到这个@MapperScan 类的时候,就开始了Mybatis的加载。首先看下@MapperScan的源码:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
.../* 省略 */...
/**
* dao接口路径
*/
String[] basePackages() default {};
/**
* SqlSessionFactory的bean名称
*/
String sqlSessionFactoryRef() default "";
/**
* Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
*
*/
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
} |
其中最主要的就是 @Import(MapperScannerRegistrar.class) 这句,导入MapperScannerRegistrar.class这个类并把它注册到容器中,看下这个类的关系图
实现了2个接口ImportBeanDefinitionRegistrar和ResourceLoaderAware,其中ImportBeanDefinitionRegistrar这个接口就是Spring留给开发者的扩展接口,通常可以通过实现这个接口来初始化配置项,在这里Mybatis就是使用这个方式来初始化的。当MapperScannerRegistrar这个bean被注册之后,Spring就会调用ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars)方法来对各个register进行BeanDefinitions的注册,然后就开始调用MapperScannerRegistrar这个实现类的方法进行各项属性的注册,源码如下
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// this check is needed in Spring 3.1
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
.../* 省略 */...
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
.../* 省略 */...
for (String pkg : annoAttrs.getStringArray("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(basePackages));
} |
以上源码主要就是设置 basePackages 和 SqlSessionFactoryBeanName,并且扫描basePackages下的所有class注册为BeanDefinition,注册的是交给Spring处理的,注册完成之后Mybatis会进行一些操作来修改这个BeanDefinition的属性,如下:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
//这里将这些bean的class全部设置为 mapperFactoryBean.class,
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
} |
这里对bean定义做了修改,主要就是将beanClass设置为MapperFactoryBean.class,并且将原className作为MapperFactoryBean的构造函数入参,这两步就为后面的初始化做好了准备。OK到此为止完成了所有mapper接口bean的注册。
3-3.Spring实例化bean
完成BeanDefinition的注册,IOC的过程就完成一半了,接下来根据BeanDefinition的信息来对bean进行实例化。这里由于这里将所有mapper接口bean的beanClass设置为MapperFactoryBean.class,所以Spring对于实现了FactoryBean接口的类实例化的时候会调用该类的getObject方法,源码如下:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
//intentionally empty
}
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
.../* 省略 */...
/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
.../* 省略 */...
} |
继续追踪代码,最后到MapperProxyFactory这个类里取初始化Mapper接口的实例。
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
} |
这里使用了jdk的动态代理生成了 Mapper接口的真正bean实例,这里的生成细节就不说了。到这里,整个Mapper接口实例化完成。
3-4.Mapper接口实例化时序图
3-5.Mybatis的核心配置的初始化
接下来就是比较明显的Mybatis的核心配置的初始化,在自定义的数据源配置类中,方法如下:
/**
* 初始化 mybatisSqlSessionFactory
* @param mybatisDataSource
* @return
* @throws Exception
*/
@Bean(name = "mybatisSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
//设置数据源配置
sessionFactory.setDataSource(mybatisDataSource);
//设置Mybatis的mapper.xml的路径,由于初始化sql语句
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(DataSourceConfig.MAPPER_LOCATION));
//factoryBean 通过getObject获取实例
return sessionFactory.getObject();
} |
调用的时序大体如下:
这里其实大多数工作都是在解析xml的sql语句到内存,还有就是Configuration的初始化了,这个类是Mybatis里非常核心的一个类,也非常的重,里面包含了几乎所有Mybatis运行时需要的组件。经过上面的初始化之后,Mybatis的主要组件SqlSessionFactory就被Spring注册为Bean放到Spring的容器里,用来初始化前面的Mapper接口的代理类实例。
4.Mybatis的架构及各个组件作用
Mybatis分为三层架构,大体分为接口层,数据处理层和基础支撑层,每层负责的职责不一样,模块划分清晰
4-1.接口层
这里就是我们能够看到的框架最上层的地方,它定义了对于数据库的基本操作,比较简单,要注意Mapper接口和Mapper.xml对应(或者基于注解,更简单,0配置),实例如下
4-2.数据处理层
这一层是Mybatis的核心层,包括了我们上面分析的加载和初始化都属于这个层,它包含了配置解析、参数解析、SQL解析、SQL执行、结果处理、插件等模块,其功能如下:
- 配置解析:在Mybatis初始化过程中,会加载mybatis-config.xml配置文件(我们使用的是注解)、映射配置文件以及Mapper接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configration对象中。之后,根据该对象创建SqlSessionFactory对象。待Mybatis初始化完成后,可以通过SqlSessionFactory创建SqlSession对象并开始数据库操作。这一过程上面已经分析过了。
- 参数解析:参数映射主要是将java类型和JDBC类型对应起来,Mybatis已经有默认的映射了,初始化的时候就已经默认赋值了,具体可以看看TypeHandlerRegistry类。对应之后就会根据映射将入参由java类型转换为JDBC类型的参数。
- SQL解析:Mybatis实现的动态SQL语句,几乎可以编写出所有满足需要的SQL,Mybatis中scripting模块会根据用户传入的参数,解析映射文件中定义的动态SQL节点,形成数据库能执行的sql语句。
- SQL执行:Executor主要维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给StatementHandler完成,这里一般情况下默认使用SimpleExecutor来执行sql。
- 结果处理:这里和上面参数解析的过程反过来,由于JDBC返回的结果集是JDBC类型的参数,所以也需要对JDBC类型的参数转换为java类型,再转换为对应的ResultType,返回给调用者。
- 插件:这里没使用到插件,但是一般分页都会使用到插件,参数解析的时候对ParameterHandler进行了增强。
4-3.基础支撑层
这一层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。其中比较重要有几个模块:
- 反射模块:对java反射进行了很好的封装,提供给上层使用,并且对反射操作进行了一系列的优化,比如,缓存了类的元数据(MetaClass)和对象的元数据(MetaObject),提高了反射操作的性能。
- 缓存模块:Mybatis对数据查询进行优化,所以引入了缓存机制,分为一级缓存和二级缓存,两种缓存的区别就是作用域和机制,一级缓存默认开启,作用域是同一SqlSession会话,即当会话被销毁则一级缓存也就没了,但是当SqlSession执行update操作(update()、delete()、insert())的时候,一级缓存同样被删掉。SpringBoot中同样默认帮我们全局开启了二级缓存,但是是需要在相应的mapper接口中加@CacheNamespace注解才有用(或者xml里配置 <cache/>,但是无论如何返回的java对象一定要可以序列化),而且他的作用域是mapper的namespace,多个SqlSession可以使用改缓存,但任何一个SqlSession执行了该Mapper里的update操作(update()、delete()、insert())的时候都会刷新二级缓存。这里就简单说下缓存模块,Mybatis的二级缓存使用不多。
- 解析器模块:该模块有两个主要功能:一个是封装了XPath,为Mybatis初始化时解析mybatis-config.xml配置文件以及映射配置文件提供支持;另一个为处理动态SQL语句中的占位符提供支持。
- 数据源和连接池模块:在数据源模块中,Mybatis自身提供了相应的数据源实现,也提供了与第三方数据源集成的接口。
- 类型转换模块:类型转换模块的另一个功能是实现JDBC类型与Java类型间的转换。
- 事务管理模块:Mybatis的事务管理分为实现JdbcTransaction 和 ManagedTransaction两种,一个自己管理,一个交给外部容器管理,感兴趣的话可以看看具体的原理。
5.Mybatis如何完成一条SQL的执行
5-1.首先看执行的时序图
这是一个比较漫长的流程,涉及到的类主要就是有以下几个:
- SqlSession接口:Mybatis默认使用的是DefaultSession实现,这里主要就是Mybatis的操作的会话接口,里面定义了一系列的数据库操作方法,例如增删改查。持有2个重要的属性,配置类Configuration和Executor,前者可以获取各种加载的配置如Mapper.xml里的内容;后者则是sql执行器。
- Executor类:执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。关系图如下,Executor总体来说大同小异,只是以不同场景需要有了4个实现类。
- Configuration类:Mybatis整个架构的大总管,内部持有几乎所有Mybatis的配置。
- MappedStatement类:这个类主要是用来存储Mapper.xml的节点信息的,如select、update等,是在Mybatis初始化Configuration的时候解析xml或者注解存到这个对象中的,这个对象是被Configuration对象持有的。
- SqlSource类:这个接口第一个作用是在Mybatis初始化的时候存放解析出来的原始SQL语句的,根据不同类型的SQL初始化为DynamicSqlSource或StaticSqlSource实例,原始SQL语句存放于M,负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。在这里动态SQL会被分为很多节点,大体上是根据<if>、<set>这些节点来分的sql,并且通过不同的SqlNode接口实现循环解析sql,直到全部解析为StaticTextSqlNode为止,然append到sql结尾,组成jdbc可识别的sql语句。
- SqlNode接口:在动态SQL的解析中至关重要。
- BoundSql类:存放解析出来的jdbc识别的SQL语句和其参数和值。
- TypeHandler类:jdbc类型和java类型的映射表和其处理类。
- StatementHandler类:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
- ParameterHandler类:负责对用户传递的参数转换成JDBC Statement 所需要的参数。
- ResultSetHandler类:对jdbc返回的结果集进行类型转换,使其变为java类型的对象。
5-2.Mybatis执行流程分析
上面的较为详细的描述了整个sql执行的流程,其中比较重要的步骤就是第三步SQL的构成BoundSql对象,这里涉及到了对动态sql的处理,而且这块设计也比较巧妙,这里对于动态SQL的组装是用递归解析的,我们分析下复杂的SQL的解析,首先会调用MappedStatement.getBoundSql(Object parameterObject)方法,由于本次执行的SQL是动态SQL,所以它最终调用的是DynamicSqlSource类的getBoundSql方法,源码如下
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 初始化上下文 主要是sqlBuilder和bindings模块初始化
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 这里就是递归调用,根据node的不同类型来进行apply解析,node的种类上面的类图已经画出,这里使用的是MixdSqlNode
rootSqlNode.apply(context);
// 从配置类中拿出 typeAliasRegister和TypeHandlerRegister
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 解析完整的SQL语句,找到#{},替换为? 输出预编译sql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
} |
这里首先初始化一个上下文,主要作用是初始化2个属性 sqlBuilder和bindings,前者为存放完整SQL的,后者则是存放参数的。然后进入MixedSqlNode的apply方法,源码如下:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
} |
可以看到这里循环调用不同类型的SqlNode的qpply方法,最终都是解析为StaticTextSqlNode,并将SQL语句拼接到context的SqlBuilder中。这里构建之后就得到的完整的sql了,但是还不是预编译的sql,后面会在SqlSourceParser类的parse()方法中实例化真正的解析器GenericTokenParser对sql语句进行解析,相关代码为:
qlSourceBuilder类
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
GenericTokenParser类 对#{}的部分进行替换为 ?
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
} |
从代码可以清楚的看到sql中的#{}被找到 然后替换为"?"的,完成了这个替换之后,sql这块基本就准备完成了,后面就是对参数的处理了,这里就不多说了。OK到此为止,Mybatis的运行流程和设计基本上介绍完成。
5-3.Mybatis使用注意事项
了解了Mybatis的大体运行原理,在实战过程中难免会遇到一些问题,我这里列举下注意事项: