MyBati简介
MyBatis 是一种半自动化的 Java 持久层框架(persistenceframework),其通过注解或 XML的方式将对象和 SQL 关联起来。之所以说它是半自动的,是因为和 Hibernate 等一些可自动生成 SQL 的 ORM(ObjectRelationalMapping)框架相比,使用 MyBatis 需要用户自行维护 SQL。维护 SQL 的工作比较繁琐,但也有好处。比如我们可控制 SQL 逻辑,可对其进行优化,以提高效率。
mbatis-plus是mybatis的增强工具,他完美的兼容了mybatis的原有功能,又提供很多简化开发的功能,提高了开发效率。mp的出现使得mybatis在一些简单的curd场景上趋于自动化。
在实际开发中,我们常常把mybatis和spring整合在一起使用。这样的话,我们可以使用bean的方式便捷的操作dao接口。mybatis和spring是两个不相关的框架,使用了mybatis-spring中间框架,将两者整合起来,该框架一方面解析和加载mybatis相关信息,另一方面将dao接口和相关对象放入到bean工厂中。
下面就是将 MyBatis 整合到 Spring 中所需的一些配置:
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">
<property name="driverClassName" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
<property name="initialSize" value="${initialSize}"/>
<property name="maxActive" value="${maxActive}"/>
<property name="minIdle" value="${minIdle}"/>
<property name="maxWait" value="${maxWait}"/>
<property name="filters" value="stat"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!--mapping.xml 文件 -->
<property name="mapperLocations" value="classpath:mapper/test/*.xml"/>
<!--配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--插件-拦截器-->
<property name="plugins">
<array>
<bean id="mybatisInterceptor" class="SQLExecInterceptor.class"/>
</array>
</property>
</bean>
<!-- mapper接口所在包名 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.demo.mapper.test"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
配置⽂件解析过程分析
我们在使用 MyBatis 框架时,通常会进行一定的设置,使其能更好的满足我们的需求。我们在使用 MyBatis 时,第一步要做的事情一般是根据配置文件构建 SqlSessionFactory对象。我们首先会使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory
对象。这里的 build 方法是我们分析配置文件解析过程的入口方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
// 创建配置文件解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 通过parser.parse()方法获取到一个Configuration 对象
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
通过分析parser.parse()方法找到了配置文件的入口方法parseConfiguration(XNode root),传入配置文件的< configuration>根节点,遍历子节点来完成配置文件的解析过程。
private void parseConfiguration(XNode root) {
try {
// 解析properties配置
this.propertiesElement(root.evalNode("properties"));
// 解析settings配置并将其转换成Properties对象,加载配置
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
// 解析别名配置
this.typeAliasesElement(root.evalNode("typeAliases"));
// 解析插件配置
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析类型转换器配置
this.typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mapper配置
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
到此,一个完整的配置解析过程就呈现出来了,每个节点的的解析逻辑均封装在了相应的方法中,接下来具体分析下常用的几个配置。
1.解析< properties >节点
< properties >节点常用配置:
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
</properties>
propertiesElement(properties节点对象)源码:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 获取properties节点下的所有子节点
Properties defaults = context.getChildrenAsProperties();
// 获取properties节点中的resource或者url的属性值,注意此处获得的同名配置会覆盖子节点中的值
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = this.configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
this.parser.setVariables(defaults);
// 将属性值设置到 configuration 中
this.configuration.setVariables(defaults);
}
}
/**
遍历properties节点所有子节点
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
// 获取所有子节点的迭代器
Iterator var2 = this.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
// 获取 property 节点的 name 和 value 属性
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
/**
* 获取子节点列表, 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中
*/
public List<XNode> getChildren() {
List<XNode> children = new ArrayList();
NodeList nodeList = this.node.getChildNodes();
if (nodeList != null) {
int i = 0;
for(int n = nodeList.getLength(); i < n; ++i) {
Node node = nodeList.item(i);
if (node.getNodeType() == 1) {
children.add(new XNode(this.xpathParser, node, this.variables));
}
}
}
return children;
}
< properties >节点总结:
- 解析< properties >节点的子节点
- 从文件系统或网络读取配置信息,会导致同名属性覆盖的问题
- 将包含属性信息的 Properties 对象设置到XPathParser 和 Configuration中
2.解析< settings >节点
< settings >节点常用配置:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
< settings >节点解析源码分析:
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
} else {
// 获取 settings 子节点中的内容
Properties props = context.getChildrenAsProperties();
// 创建 Configuration 类的“元信息”对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, this.localReflectorFactory);
Iterator var4 = props.keySet().iterator();
Object key;
// 检测 Configuration 中是否存在相关属性,不存在则抛出异常
do {
if (!var4.hasNext()) {
return props;
}
key = var4.next();
} while(metaConfig.hasSetter(String.valueOf(key)));
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
< settings >节点总结:
- 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
- 为 Configuration 创建元信息对象
- 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法,
不存在则抛异常 - 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束
MetaClass元对象的构建是个比较复杂的过程,有兴趣的可以翻阅源码研究下。
3.解析< typeAliases >节点
在 MyBatis 中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输入别名即可,无需再把全限定的类名写出来。在 MyBatis 中,我们有两种方式进行别名配置。第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名(全小写类名),可以搭配注解使用,第二种方式是通过手动的方式,明确为某个类型配置别名。
< typeAliases >节点常用配置:
<!-- 第一种方式-->
<typeAliases>
<package name="xyz.coolblog.chapter2.model1"/>
<package name="xyz.coolblog.chapter2.model2"/>
</typeAliases>
<!-- 第二种方式-->
<typeAliases>
<typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article" />
<typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author" />
</typeAliases>
< typeAliases >节点解析源码分析:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 获取typeAliases子节点
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String alias;
// 第一种方式,从包中解析别名
if ("package".equals(child.getName())) {
alias = child.getStringAttribute("name");
this.configuration.getTypeAliasRegistry().registerAliases(alias);
} else {
// 第二种方式,自定义别名
alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
// 别名可以为空,为空则设置为type对应的类名全小写
if (alias == null) {
this.typeAliasRegistry.registerAlias(clazz);
} else {
this.typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException var7) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + var7, var7);
}
}
}
}
}
第二种方式具体的实现方法:
public void registerAlias(Class<?> type) {
// 获取全路径类名的简称
String alias = type.getSimpleName();
// 获取注解中的别名,如果使用注解设置别名,则优先使用注解中的名称
Alias aliasAnnotation = (Alias)type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 配置别名
this.registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
} else {
// 别名转换成小写
String key = alias.toLowerCase(Locale.ENGLISH);
// 如果在缓存中已经存在该别名,则判断当前类型与缓存中类型是否一致,不一致则报错
if (this.TYPE_ALIASES.containsKey(key) && this.TYPE_ALIASES.get(key) != null && !((Class)this.TYPE_ALIASES.get(key)).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + ((Class)this.TYPE_ALIASES.get(key)).getName() + "'.");
} else {
// 缓存别名到类型映射
this.TYPE_ALIASES.put(key, value);
}
}
}
第一种方式具体实现方法:
public void registerAliases(String packageName) {
this.registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
// 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
// superType = Object.class,所以 ResolverUtil 将查找所有的类。
// 查找完成后,查找结果将会被缓存到内部集合中。
resolverUtil.find(new IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
Iterator var5 = typeSet.iterator();
// 遍历结果,忽略匿名类,接口,内部类,依次调用registerAlias(type)方法设置别名(默认类名简称,注解优先)
while(var5.hasNext()) {
Class<?> type = (Class)var5.next();
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
this.registerAlias(type);
}
}
}
< typeAliases >节点总结以及默认配置:
- 通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,
- 比如 xyz/coolblog/model/Article.class
- 筛选以.class 结尾的文件名
- 将路径名转成全限定的类名,通过类加载器加载类名
- 对类型进行匹配,若符合匹配规则,则将其放入内部集合中
总结一些mybatis已经默认创建的类型别名:
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
registerAlias("ResultSet", ResultSet.class);
}
了解这些默认创建的别名可以更好的帮助我们开发,比如为List对象设置别名为list,这样我们在配置标签的 resultType 属性时,就可以使用list来表示返回类型啦~
4.解析< typeHandlers >节点
在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有 CHAR 和 VARCHAR 等类型,但 Java 中没有这些类型,不过 Java 有 String类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成String。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler 去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。
< typeHandlers >节点常用配置:
<!-- 自动扫描 -->
<typeHandlers>
<package name="xyz.coolblog.handlers"/>
</typeHandlers>
<!-- 手动配置 -->
<typeHandlers>
<typeHandler jdbcType="TINYINT"
javaType="xyz.coolblog.constant.ArticleTypeEnum"
handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>
< typeHandlers >节点解析源码分析:
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String typeHandlerPackage;
// 从指定的包中注册 TypeHandler
if ("package".equals(child.getName())) {
typeHandlerPackage = child.getStringAttribute("name");
// 注册方法
this.typeHandlerRegistry.register(typeHandlerPackage);
} else {
// 从 typeHandler 节点中解析别名到类型的映射
typeHandlerPackage = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
// 解析上面获取到的属性值
Class<?> javaTypeClass = this.resolveClass(typeHandlerPackage);
JdbcType jdbcType = this.resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = this.resolveClass(handlerTypeName);
// 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
if (javaTypeClass != null) {
if (jdbcType == null) {
this.typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
this.typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
this.typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
上面的代码中调用了 4 个不同的类型处理器注册方法,类型处理器的解析过程不复杂,但是注册过程由于重载方法间相互调用,导致调用路线比较复杂。这里就不展开四个重载方法详细说明了。大致流程为,将JdbcType通过TypeHandler进行映射,返回结果后,将javaType和结果存储到缓存中。当JdbcType或javaType为空时,第一步则是先尝试从注解中获取JdbcType 的值或通过注解的方式或者反射的方式了解析出 javaType 的值。
< typeHandlers >节点一些常见类型的类型处理器:
在看到这里的时候有个疑问,在公司开发过程中,库中的timestamp类型被默认自动转成java中的string类型,但是在默认的typehandlers中并未找到这个转换,查看代码发现公司配置了拦截器,在拦截器中做了该类型的转换~