文章目录
上一章中已经了解到mybatis-config和mapper文件中使用的类似int/string/JDBC/POOLED等字面常量最终解析为具体的java类型都是在
typeAliasRegistry
构造器和
Configuration
构造器执行期间初始化的。本章主要分析每个配置具体是如何解析的
属性标签解析(properties)
解析properties的方法为:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 加载properties节点为property
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
// 必须至少包含resource或者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 = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
总体逻辑比较简单,首先加载properties节点下的property属性,比如:
<properties resource="resources/config.properties">
<property name="salary" value="30000"/>
</properties>
然后从url或resource加载配置文件,都先和configuration合并,然后赋值到XMLConfigBuilder.parser
和BaseBuilder.configuration
。此时开始所有的属性就可以在随后的整个配置文件中使用了。值得注意的是,如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:
- 首先读取在 properties 元素体内指定的属性。
- 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
- 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。
设置标签解析(settings)
解析settings的方法为settingsAsProperties
:
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// 检查所有从settings加载的设置,确保它们都在Configuration定义的范围内
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException(
"The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
首先加载settings下面的setting节点为property,然后检查所有属性,确保它们都在Configuration中已定义,而非未知的设置。得到setting之后,调用settingsElement(Properties props)将各值赋值给configuration,同时在这里有重新设置了默认值,所有这一点很重要,configuration中的默认值不一定是真正的默认值。
MetaClass是一个保存对象定义比如getter/setter/构造器等的元数据类。代码如下:
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
构造方法中的ReflectorFactory
是mybatis硬编码为DefaultReflectorFactory
,其代码结构为:
public class DefaultReflectorFactory implements ReflectorFactory {
private boolean classCacheEnabled = true;
private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();
public DefaultReflectorFactory() {
}
@Override
public boolean isClassCacheEnabled() {
return classCacheEnabled;
}
@Override
public void setClassCacheEnabled(boolean classCacheEnabled) {
this.classCacheEnabled = classCacheEnabled;
}
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) {
// synchronized (type) removed see issue #461
return reflectorMap.computeIfAbsent(type, Reflector::new);
} else {
return new Reflector(type);
}
}
}
重点关注return reflectorMap.computeIfAbsent(type, Reflector::new);
,这里直接new了一个Reflector
而Reflector
采用了facade设计模式,简化反射的使用。简单看一下它的构造方法:
public Reflector(Class<?> clazz) {
type = clazz;
addDefaultConstructor(clazz);
addGetMethods(clazz);
addSetMethods(clazz);
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
这个反射类主要是将反射对象的方法,属性放到指定的集合中,方便查询使用。java反射中的Type
可参考Java中的Type
加载自定义VFS
VFS主要用来加载容器内的各种资源,比如jar或者class文件。mybatis提供了2个实现 JBoss6VFS 和 DefaultVFS,并提供了用户扩展点,用于自定义VFS实现,加载顺序是自定义VFS实现 > 默认VFS实现
类型别名解析( typeAliases)
mybatis主要提供两种类型的别名设置,具体类的别名以及包的别名设置。类型别名是为 Java 类型设置一个短的名字,存在的意义仅在于用来减少类完全限定名的冗余
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
<typeAliases>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>
当这样配置时,Blog可以用在任何使用domain.blog.Blog的地方
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
设置为package之后,MyBatis 会在包名下面搜索需要的 Java Bean。每一个在包 domain.blog
中的 Java Bean,在没有注解的情况下,会使用 Bean的字母小写的非限定类名来作为它的别名。 比如 org.apache.test.dom.JAXPTest
的别名为jaxptest;若有注解,则别名为其注解值。
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
//获取注解
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
//使用注解的别名
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
所以所有的别名,无论是内置的还是自定义的,都一开始被保存在configuration.typeAliasRegistry
中了。内置的别名在初始化Configuration的时候,new了一个TypeAliasRegistry,在其构造方法内部初始化了java自带的一些别名,代码如下
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);
}
加载插件(plugin)
mybatis支持插件来插入自定制的处理过程,所有的plugin都需实现Interceptor接口,自定制的处理过程可以在Executor(执行器),ParameterHandler(参数处理器),ResultSetHandler(结果集处理器),**StatementHandler(SQL语法构建器)**四个处理过程中插入,
1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 拦截执行器的方法,其中包含了query、update、commit、rollback方法
2.ParameterHandler (getParameterObject, setParameters) 拦截参数的处理;
3.ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理;
4.StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理
它的实现原理是在使用这四种类型处理数据的时候使用的都是经过plugin处理过的代理对象。同一个处理过程支持配置多个plugin,则plugin的执行顺序是根据包装的顺序,从最外部向内部执行,直到执行到目标对象的调用方法。包装的顺序是根据配置顺序,也就是说配置越靠前,包装的越深,越后执行。mybatis调用pluginElement(root.evalNode(“plugins”));
加载mybatis插件。使用案例:
<!-- 拦截器 -->
<plugins>
<plugin interceptor="cn.com.mybatis.plugins.MyInterceptor" />
<plugin interceptor="cn.com.mybatis.plugins.MyInterceptor2">
<property name="name" value="mybatis"/>
<property name="age" value="10"/>
</plugin>
</plugins>
上面的配置在第二个拦截器中新增了两个属性name和age,这两个属性会被解析为Properties对象。解析的时候会解析properties属性,具体实现如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
//读取properties
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance =
(Interceptor)resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
插件在具体实现的时候,采用的是拦截器模式,要注册为mybatis插件,必须实现org.apache.ibatis.plugin.Interceptor
接口,每个插件可以有自己的属性。interceptor属性值既可以完整的类名,也可以是别名,只要别名在typealias中存在即可,如果启动时无法解析,会抛出ClassNotFound异常。实例化插件后,将设置插件的属性赋值给插件实现类的属性字段。mybatis提供了两个内置的插件例子,如下所示:
具体的插件实现方式,后续有专门的文章解释。
加载对象工厂(objectFactory)
MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂DefaultObjectFactory
做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。
public class Configuration {
...省略代码...
// 对象工厂, 所有的类resultMap类都需要依赖于对象工厂来实例化
protected ObjectFactory objectFactory = new DefaultObjectFactory();
...省略代码...
}
DefaultObjectFactory代码结构如下:
public class DefaultObjectFactory implements ObjectFactory, Serializable {
private static final long serialVersionUID = -8855120656740914948L;
@Override
public <T> T create(Class<T> type) {
return create(type, null, null);
}
@SuppressWarnings("unchecked")
@Override
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
Class<?> classToCreate = resolveInterface(type);
// we know types are assignable
return (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
}
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
Constructor<T> constructor;
if (constructorArgTypes == null || constructorArgs == null) {
constructor = type.getDeclaredConstructor();
try {
return constructor.newInstance();
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance();
} else {
throw e;
}
}
}
constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
try {
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} else {
throw e;
}
}
} catch (Exception e) {
String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList)
.stream().map(Class::getSimpleName).collect(Collectors.joining(","));
String argValues = Optional.ofNullable(constructorArgs).orElseGet(Collections::emptyList)
.stream().map(String::valueOf).collect(Collectors.joining(","));
throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
}
}
protected Class<?> resolveInterface(Class<?> type) {
Class<?> classToCreate;
if (type == List.class || type == Collection.class || type == Iterable.class) {
classToCreate = ArrayList.class;
} else if (type == Map.class) {
classToCreate = HashMap.class;
} else if (type == SortedSet.class) { // issue #510 Collections Support
classToCreate = TreeSet.class;
} else if (type == Set.class) {
classToCreate = HashSet.class;
} else {
classToCreate = type;
}
return classToCreate;
}
@Override
public <T> boolean isCollection(Class<T> type) {
return Collection.class.isAssignableFrom(type);
}
}
无论是创建集合类型、Map类型还是其他类型,都是同样的处理方式。如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。ObjectFactory
接口很简单,它包含两个创建用的方法,一个是处理默认构造方法的,另外一个是处理带参数的构造方法的。最后,setProperties
方法可以被用来配置 ObjectFactory
,在初始化你的 ObjectFactory
实例后,objectFactory
元素体中定义的属性会被传递给setProperties
方法。
解析对象包装器工厂(objectWrapperFactory)
对象包装器工厂主要用来包装返回result对象,比如说可以用来设置某些敏感字段脱敏或者加密等。默认对象包装器工厂是DefaultObjectWrapperFactory
,也就是不使用包装器工厂。
注意:mybatis提供了一个什么都不做的默认实现DefaultObjectWrapperFactory
。
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ObjectWrapperFactory factory =
(ObjectWrapperFactory)resolveClass(type).getDeclaredConstructor().newInstance();
configuration.setObjectWrapperFactory(factory);
}
}
对象包装器结构如下:
其中BeanWrapper
是BaseWrapper
的默认实现。其中的两个关键接口是getBeanProperty
和setBeanProperty
,它们是实现包装的主要位置。
要实现自定义的对象包装器工厂,只要实现ObjectWrapperFactory
中的两个接口hasWrapperFor
和getWrapperFor
即可。
案例如下:
public class MyObjectWrapperFactory implements ObjectWrapperFactory {
@Override
public boolean hasWrapperFor(Object object) {
return object != null && object instanceof User;
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new MyObjectWrapper(metaObject, object);
}
}
ObjectWrapperFactory
是一个对象包装器工厂,用于对返回的结果对象进行二次处理,它主要在org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue
方法中创建对象MetaObject
时作为参数设置进去,这样MetaObject
中的objectWrapper
属性就可以被设置为我们自定义的ObjectWrapper
实现而不是mybatis内置实现。
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
final String resultMapId = resultMap.getId();
Object rowValue = partialObject;
....省略代码....
final MetaObject metaObject = configuration.newMetaObject(rowValue);
....省略代码....
return rowValue;
}
//MetaObject构造方法
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory;
if (object instanceof ObjectWrapper) {
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
// 如果有自定义的ObjectWrapperFactory,就不会总是返回false了,这样对于特定类就启用了的我们自定义的ObjectWrapper
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
this.objectWrapper = new BeanWrapper(this, object);
}
}
例如数据库中某些字段和Javabean中的属性对应不上,我们就可以使用ObjectWrapperFactory
来做处理。当时实际生产上我们一般不会用这种方式,而是使用ResultMap
去映射实现。ObjectWrapperFactory
接口如下:
public interface ObjectWrapperFactory {
boolean hasWrapperFor(Object object);
ObjectWrapper getWrapperFor(MetaObject metaObject, Object object);
}
通过实现这个接口,可以判断当object是特定类型时,返回true,然后在下面的getWrapperFor
中返回一个可以处理key为特定值的ObjectWrapper
实现类即可。值得一提的是ObjectWrapper
这个类可以说是对象反射信息的f外观模式。
当然,我们不需要从头实现ObjectWrapper
接口,可以选择继承BeanWrapper
或者MapWrapper
。比如对于JavaBean
类型,我们可以继承BeanWrapper
,让参数useCamelCaseMapping起作用。BeanWrapper
默认的findProperty方法并没有做特定值转换处理,如下:
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return metaClass.findProperty(name, useCamelCaseMapping);
}
我们可以改成,将特定的wrapper
数据库列转换成JavaBean
的test
属性
public class MyObjectWrapper extends BeanWrapper {
public MyObjectWrapper(MetaObject metaObject, Object object) {
super(metaObject, object);
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return "wrapper".equals(name) ? "test" : name;
}
}
同时,创建一个自定义的ObjectWrapperFactory
如下:
public class MyObjectWrapperFactory implements ObjectWrapperFactory {
@Override
public boolean hasWrapperFor(Object object) {
return object != null && object instanceof User;
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new MyObjectWrapper(metaObject, object);
}
}
然后,在 MyBatis 配置文件中配置上ObjectWrapperFactory:
<objectWrapperFactory type="org.apache.test.model.MyObjectWrapperFactory"/>
解析反射工厂(reflectorFactory)
因为加载配置文件中的各种插件类等等,为了提供更好的灵活性,mybatis支持用户自定义反射工厂,不过总体来说,用的不多,要实现反射工厂,只要实现ReflectorFactory
接口即可。默认的反射工厂是DefaultReflectorFactory
。一般来说,使用默认的反射工厂就可以了。
解析环境配置(environments)
环境可以说是mybatis-config配置文件中最重要的部分,它类似于spring和maven里面的profile,允许给开发、生产环境同时配置不同的environment,根据不同的环境加载不同的配置,这也是常见的做法,如果在SqlSessionFactoryBuilder
调用期间没有传递使用哪个环境的话,默认会使用一个名为default的环境。找到对应的environment之后,就可以加载事务管理器和数据源了。事务管理器和数据源类型这里都用到了类型别名,JDBC/POOLED都是在mybatis内置提供的,在Configuration
构造器执行期间注册到TypeAliasRegister
。
mybatis内置提供JDBC和MANAGED两种事务管理方式,前者主要用于简单JDBC模式,后者主要用于容器管理事务,一般使用JDBC事务管理方式。mybatis内置提供JNDI、POOLED、UNPOOLED三种数据源工厂,一般情况下使用POOLED数据源。
环境配置,配置了两个,一个是default
的development
,一个是prod
,如果在SqlSessionFactoryBuilder
期间没有指定environment
的话,默认使用default
//指定使用prod的环境
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "prod");
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatisdb?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
<environment id="prod">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatisdb?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
标签解析实现
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 没有指定,就使用default
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// 查找匹配的environment
if (isSpecifiedEnvironment(id)) {
// 事务配置并创建事务工厂
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 数据源配置加载并实例化数据源, 数据源是必备的
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 创建Environment.Builder
Environment.Builder environmentBuilder =
new Environment.Builder(id).transactionFactory(txFactory).dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
数据源反射类图
- XMLConfigBuilder从别名内存中获取到数据源工厂,代码如下:
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//解析数据源配配置的类型,一般是POOLED或者UNPOOLED或者JNI
String type = context.getStringAttribute("type");
//获取数据远的配置属性入driver,url,username,password等等
Properties props = context.getChildrenAsProperties();
//从别名内存中取出数据源工厂,别名是在初始化Configuration时生成的
DataSourceFactory factory = (DataSourceFactory)resolveClass(type).getDeclaredConstructor().newInstance();
//设置数据源属性,利用的是反射
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
- 利用反射获取对象元信息
public void setProperties(Properties properties) {
Properties driverProperties = new Properties();
//在设置属性的时候,利用反射将数据源类的类元信息反射过来
MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
...省略代码....
}
public static MetaObject forObject(Object object) {
//获取类元信息用的是默认的反射工厂和默认的对象包装器工厂和默认的对象工厂
return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
}
- 实例化目标类
这里用的是BeanWrapper,代码如下:
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory;
if (object instanceof ObjectWrapper) {
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
//默认使用的是BeanWrapper
this.objectWrapper = new BeanWrapper(this, object);
}
}
- 使用BeanWrapper实例化数据源类元信息,并利用反射工厂,拿到反射器
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
//这里用的反射工厂其实就是上面传来的默认反射工厂
this.reflectorFactory = reflectorFactory;
//利用反射工厂,拿到反射器(反射器其实是外观模式)
this.reflector = reflectorFactory.findForClass(type);
}
- 利用反射设置属性值
解析数据库厂商标识(databaseIdProvider)
MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId
属性。 MyBatis 会加载不带 databaseId
属性和带有匹配当前数据库 databaseId
属性的所有语句。 如果同时找到带有 databaseId
和不带 databaseId
的相同语句,则后者会被舍弃。 为支持多厂商特性只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider
即可
<databaseIdProvider type="DB_VENDOR" />
这里的 DB_VENDOR 会通过 DatabaseMetaData#getDatabaseProductName()
返回的字符串进行设置。
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
try (Connection con = dataSource.getConnection()) {
//这里用的是动态代理模式
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
}
我们来看下动态代理的实现过程:
从uml图中可以看到,PooledConnection
是我们的代理类,真正被代理的类其实是ConnectionImpl
,我们在看PooledDataSource
是什么时候去生成代理类的。先看下方法调用栈
前面有省略
getDatabaseId:50, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
-->getDatabaseName:63, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
-->getDatabaseProductName:77, VendorDatabaseIdProvider (org.apache.ibatis.mapping)
-->getConnection:89, PooledDataSource (org.apache.ibatis.datasource.pooled)
-->popConnection:432, PooledDataSource (org.apache.ibatis.datasource.pooled)
--><init>:53, PooledConnection (org.apache.ibatis.datasource.pooled)
从方法的调用栈看出,PooledDataSource
在getConnection
时调用了popConnection
,并在此时去实例化PooledConnection
,我们看下代码:
private PooledConnection popConnection(String username, String password) throws SQLException {
//省略代码
conn = new PooledConnection(dataSource.getConnection(), this);
//省略代码
}
//构造方法
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
//实际被代理者
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
//生成代理对象
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
所以实际在执行con.getMetaData()
时是由代理类去执行的,而不是接口。详细的代理模式请参考静态代理和JDK动态代理
解析类型处理器(typeHandlers)
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
mybatis提供了两种方式注册类型处理器,package自动检索方式和显示定义方式。使用自动检索(autodiscovery)功能的时候,只能通过注解方式来指定 JDBC 的类型。
<typeHandlers>
<typeHandler handler="org.apache.ibatis.submitted.uuid_test.UUIDTypeHandler"></typeHandler>
</typeHandlers>
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
register(type);
}
}
}
为了简化使用,mybatis在初始化TypeHandlerRegistry
期间,自动注册了大部分的常用的类型处理器比如字符串、数字、日期等。
public class Configuration {
//省略代码
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
//省略代码
}
对于非标准的类型,用户可以自定义类型处理器来处理。要实现一个自定义类型处理器,只要实现 org.apache.ibatis.type.TypeHandler
接口, 或继承一个实用类 org.apache.ibatis.type.BaseTypeHandler
, 并将它映射到一个 JDBC 类型即可。
MyBatis 不会窥探数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明那是 VARCHAR 类型的字段或者其他类型, 以使其能够绑定到正确的类型处理器上。 这是因为:MyBatis 直到语句被执行才清楚数据类型。
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:
- 在类型处理器的配置元素(
typeHandler element
)上增加一个javaType
属性(比如:javaType=”String”
); - 在类型处理器的类上(
TypeHandler class
)增加一个@MappedTypes
注解来指定与其关联的 Java 类型列表。 如果在javaType
属性中也同时指定,则注解方式将被忽略。
可以通过两种方式来指定被关联的 JDBC 类型:
- 在类型处理器的配置元素上增加一个
jdbcType
属性(比如:jdbcType="VARCHAR"
); - 在类型处理器的类上(
TypeHandler class
)增加一个@MappedJdbcTypes
注解来指定与其关联的 JDBC 类型列表。 如果在两个位置同时指定,则注解方式将被忽略。
当决定在ResultMap
中使用某一TypeHandler
时,此时java类型是已知的(从结果类型中获得),但是JDBC类型是未知的。 因此Mybatis使用javaType=[TheJavaType], jdbcType=null
的组合来选择一个TypeHandler
。 这意味着使用@MappedJdbcTypes
注解可以限制TypeHandler
的范围,同时除非显示的设置,否则TypeHandler
在ResultMap
中将是无效的。 如果希望在ResultMap
中使用TypeHandler
,那么设置@MappedJdbcTypes
注解的includeNullJdbcType=true
即可。 然而从Mybatis 3.4.0开始,如果只有一个注册的TypeHandler
来处理Java类型,那么它将是ResultMap
使用Java类型时的默认值(即使没有includeNullJdbcType=true
)。
解析mappers文件(mappers)
mapper文件是mybatis框架的核心之处,所有的用户sql语句都编写在mapper文件中,所以理解mapper文件对于所有的开发人员来说都是必备的要求。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser =
new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException(
"A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
mybatis提供了两类配置mapper的方法,第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,另外一类是明确指定mapper,这又可以通过resource、url或者class进行细分,例如:
<mappers>
<mapper resource="org/apache/test/model/mapper/UserMapper.xml"/>
<package name="org.apache.test.model.mapper" />
</mappers>
需要注意的是,如果要同时使用package
自动扫描和通过mapper
明确指定要加载的mapper
,则必须先声明mapper
,然后声明package
,否则DTD
校验会失败。对于通过package
加载的mapper
文件,调用mapperRegistry.addMappers(packageName)
进行加载,其核心逻辑在org.apache.ibatis.binding.MapperRegistry
中,对于每个找到的接口或者mapper
文件,最后调用用XMLMapperBuilder
进行具体解析。对于明确指定的mapper
文件或者mapper
接口,则主要使用XMLMapperBuilder
进行具体解析。
我们先来看通过package自动搜索加载的方式,它的范围由addMappers的参数packageName指定的包名以及父类superType确定,其整体流程如下:
public void addMappers(String packageName, Class<?> superType) {
// mybatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或者继承于某个类/接口)的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
//无条件的加载所有的类,因为调用方传递了Object.class作为父类
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 所有匹配的class都被存储在ResolverUtil.matches集合字段中
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
//调用addMapper方法进行具体的mapper类/接口解析
addMapper(mapperClass);
}
}
public <T> void addMapper(Class<T> type) {
// 对于mybatis mapper接口文件,必须是interface,不能是class
if (type.isInterface()) {
if (hasMapper(type)) {
// 判重,确保只会加载一次不会被覆盖
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 为mapper接口创建一个MapperProxyFactory代理
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//剔除解析出现异常的接口
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
knownMappers
是MapperRegistry
的主要字段,维护了Mapper
接口和代理类的映射关系,key是mapper
接口类,value是MapperProxyFactory
,其定义如下:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethodInvoker> 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<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
MapperProxyFactory
方法利用JDK的动态代理创建了代理对象,其中InvocationHandler
是MapperProxy
再进一步地查看MapperProxy
,这个类实现了InvocationHandler
接口,也就是Mapper
的每个方法被调用时,都是调用MapperProxy
的invoke
方法
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache;
......省略部分代码......
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 调用从Object继承的方法,则直接调用,不做任何特殊的处理
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else { // 否则调用缓存的对应的方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// 如果缓存中没有,则新建一个放到缓存中并返回
return methodCache.computeIfAbsent(method, m -> {
//接口中的default修饰的方法
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//非default方法返回一个PlainMethodInvoker
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
......省略部分代码......
}
cachedInvoker
方法就是从methodCache
中拿一个MapperMethodInvoker
,如果拿不到,就新建一个。新建时会查看当前调用的方法是不是又默认实现(m.isDefault()
),如果是,就新建一个DefaultMethodInvoker
,否则,新建一个PlainMethodInvoker
,这两个类都是MethodInvoker
的实现类,作用就是一个处理默认方法的调用,另一个处理没有被实现的方法的调用。
PlainMethodInvoker
内部维护了一个MapperMethod
,在调用PlainMethodInvoker
的invoke()
时会最终调用MapperMethod
的execute
方法
interface MapperMethodInvoker {
Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
}
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
MapperMethod介绍
MapperMethod
一个公有构造方法和一个公共方法execute()
构造方法
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
SqlCommand构造方法
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
//获取statement
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
获取Statement
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
//从configuration里面获取,是在解析mapper.xml里赋值进去的
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
execute()
方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断何种类型的sql
switch (command.getType()) {
case INSERT: {
//把方法参数转换成sql语句中的参数
Object param = method.convertArgsToSqlCommandParam(args);
//调用SqlSession执行命令,并包装结果
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
//把方法参数转换成sql语句中的参数
Object param = method.convertArgsToSqlCommandParam(args);
//调用SqlSession执行命令,并包装结果
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
//把方法参数转换成sql语句中的参数
Object param = method.convertArgsToSqlCommandParam(args);
//调用SqlSession执行命令,并包装结果
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
//改方法没有返回值时,并且方法中有ResultHandler
if (method.returnsVoid() && method.hasResultHandler()) {
//使用ResultHandler的方式调用
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//方法返回数组获集合时,使用数组方式调用
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//返回Map时使用Map方式调用
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
//使用游标的方式调用
result = executeForCursor(sqlSession, args);
} else {
//把方法参数转换成sql语句中的参数
Object param = method.convertArgsToSqlCommandParam(args);
//调用SqlSession的selectOne
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
//处理批量操作
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
由上面的流程可以看出 MapperProxyFactory
主要是维护mapper
接口的方法与对应mapper
文件中具体CRUD节点的关联关系。其中每个Method
与对应MapperMethod
维护在一起。MapperMethod
是mapper
中具体映射语句节点的内部表示
mapper接口创建MapperProxyFactory
首先为mapper
接口创建MapperProxyFactory
,然后创建MapperAnnotationBuilder
进行具体的解析,MapperAnnotationBuilder
在解析前的构造器中完成了下列工作:
public class MapperAnnotationBuilder {
//这里使用了java8新特性流
private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
.of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
InsertProvider.class, DeleteProvider.class)
.collect(Collectors.toSet());
private final Configuration configuration;
//继承自BaseBuilder
private final MapperBuilderAssistant assistant;
private final Class<?> type;
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
}
Select.class/Insert.class
等注解指示该方法对应的真实sql语句类型分别是select/insert。SelectProvider.class/InsertProvider.class
主要用于动态SQL,它们允许你指定一个类名和一个方法在具体执行时返回要运行的SQL语句。MyBatis会实例化这个类,然后执行指定的方法。其中的MapperBuilderAssistant
和XMLConfigBuilder
一样,都是继承于BaseBuilder
。
mapper接口文件加载与解析
MapperBuilderAssistant
初始化完成之后,就调用build.parse()
进行具体的mapper
接口文件加载与解析,如下所示:
public void parse() {
String resource = type.toString();
//首先根据mapper接口的字符串表示判断是否已经加载,避免重复加载,正常情况下应该都没有加载
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
//每个mapper文件自成一个namespace,通常自动匹配就是这么来的,约定俗成代替人工设置最简化常见的开发
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
整体流程为:
- 首先加载
mapper
接口对应的xml文件并解析。loadXmlResource
和通过resource、url解析相同,都是解析mapper
文件中的定义,他们的入口都是XMLMapperBuilder.parse()
,后序有专门分析,先来看通过注解方式配置的mapper的解析。
注意:一个mapper接口,不能同时使用注解方式和xml方式。下面的配置就是无效的,因为及配置注解又配置了xml
public interface UserMapper {
@Select("select * from user where id = #{id}")
User selectOne(String id);
User selectUser(String id);
}
- 解析缓存注解;
mybatis中缓存注解的定义为
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
/*默认的缓存实现类是PerpetualCache.*/
Class<? extends Cache> implementation() default PerpetualCache.class;
/*默认的缓存回收类是LruCache.*/
Class<? extends Cache> eviction() default LruCache.class;
/** Returns the flush interval.*/
long flushInterval() default 0;
/** Return the cache size.*/
int size() default 1024;
/**Returns whether use read/write cache.*/
boolean readWrite() default true;
/** Returns whether block the cache at request time or not.*/
boolean blocking() default false;
/** Returns property values for a implementation object.*/
Property[] properties() default {};
}
解析mapper文件XMLMapperBuilder
Mapper
文件的解析主要由XMLMapperBuilder
类完成,Mapper文件的加载流程如下
我们以package扫描中的loadXmlResource()为入口开始。
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// 约定俗称从classpath下加载接口的完整名
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// 对于从package和class进来的mapper,如果找不到对应的文件,就忽略
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// 这种情况下是允许SQL语句作为注解打在接口上的
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
根据package自动搜索加载的时候,约定俗称从classpath下加载接口的完整名,比如org.mybatis.example.mapper.BlogMapper
,就加载org/mybatis/example/mapper/BlogMapper.xml
。对于从package
和class
进来的mapper
,如果找不到对应的文件,就忽略,因为这种情况下是允许SQL语句作为注解打在接口上的,所以xml
文件不是必须的,而对于直接声明的xml mapper
文件,如果找不到的话会抛出IOException
异常而终止。
异常抛出位置:
加载到对应的mapper.xml
文件后,调用XMLMapperBuilder
进行解析。在创建XMLMapperBuilder
时,我们发现用到了configuration.getSqlFragments()
,这就是我们在mapper
文件中经常使用的可以被包含在其他语句中的SQL片段,但是我们并没有初始化过,所以很有可能它是在解析过程中动态添加的,创建了XMLMapperBuilder
之后,在调用其parse()
接口进行具体xml
的解析,这和mybatis-config
的逻辑基本上是一致的思路。
再来看XMLMapperBuilder
的初始化逻辑
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
this(inputStream, configuration, resource, sqlFragments);
this.builderAssistant.setCurrentNamespace(namespace);
}
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
加载的基本逻辑和加载mybatis-config
一样的过程,使用XPathParser
进行总控,XMLMapperEntityResolver
进行具体判断。
注:mybatis-config的初始化过程
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
接下去来看XMLMapperBuilder.parse()的具体实现。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
其中,解析mapper
的核心又在configurationElement
中,如下所示:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值
//也可以在 include 元素的 refid 属性或内部语句中使用属性值
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
其主要过程是:
- 解析缓存参照cache-ref。参照缓存顾名思义,就是共用其他缓存的设置。(缓存机制参考文章缓存)
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。如果想要在多个命名空间中共享相同的缓存配置和实例,则可以使用 cache-ref 元素来引用另一个缓存
private void cacheRefElement(XNode context) {
if (context != null) {
//在全局配置对象的cacheRefMap字段中维护缓存引用的关系,
//cacheRefMap中的映射关系为,当前命名空间--->配置中声明的名称空间
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
//构造CacheRefResolver对象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//解析缓存引用,实际的解析工作是委派给builderAssistant对象来完成
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//如果在解析的过程中抛出了IncompleteElementException异常,则将当前的配置解析器对象添加到configuration对象的待完成缓存引用的列表中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
<cache-ref namespace="org.apache.test.cache.ClazzMapper"/>
缓存参照因为通过namespace
指向其他的缓存。所以会出现第一次解析的时候指向的缓存还不存在的情况,所以需要在所有的mapper
文件加载完成后进行二次处理,不仅仅是缓存参照,其他的CRUD
也一样。所以在XMLMapperBuilder.configuration
中有很多的incompleteXXX
,这种设计模式类似于JVM GC
中的mark and sweep
,标记、然后处理(标记清除算法)。所以当捕获到IncompleteElementException
异常时,没有终止执行,而是将指向的缓存不存在的cacheRefResolver
添加到configuration.incompleteCacheRef
中。
CacheRefResolver类
根据resolveCacheRef()
方法的定义可以发现,mybatis
对于缓存引用的解析是委派给了MapperBuilderAssistant
类的实例去完成的。
public class CacheRefResolver {
//构造小助手
private final MapperBuilderAssistant assistant;
//引用的命名空间
private final String cacheRefNamespace;
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
this.assistant = assistant;
this.cacheRefNamespace = cacheRefNamespace;
}
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
}
MapperBuilderAssistant#useCacheRef
方法的定义如下:
/**
*
* @param namespace 配置文件中引用的名称空间
* @return
*/
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//在全局配置对象中查找该 namespace 对应的缓存对象
Cache cache = configuration.getCache(namespace);
//如果在configuration对象中没有找到对应的缓存实例,则抛出异常
//并将当前的解析任务放进incompleteCacheRefs集合中,后续会调用
//XmlMapperBuilder#parsePendingCacheRefs()方法对其进行再次解析
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
- 解析缓存cache
//解析<cache/>配置,最后委派给MapperBuilderAssistant的实例去完成缓存对象的创建任务
private void cacheElement(XNode context) {
if (context != null) {
//获取type属性的配置,如果未配置,则默认值为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取eviction属性的配置,如果未配置,则默认值为LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//获取eviction属性的配置
Long flushInterval = context.getLongAttribute("flushInterval");
//获取size属性的配置
Integer size = context.getIntAttribute("size");
//获取readOnly属性的配置
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
//获取blocking属性的配置
boolean blocking = context.getBooleanAttribute("blocking", false);
//获取<cache/>标签中配置的property属性
Properties props = context.getChildrenAsProperties();
//根据用户配置创建一个缓存对象,并将其添加到configuration的caches属性中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
type
配置的是缓存的类型,默认为PERPETUAL
,也就是使用HashMap作为缓存的容器。eviction
配置的是缓存的清除策略,可用的清除策略有LRU
、FIFO
、SOFT
和WEAK
,其中LRU
为默认策略。flushInterval
获取缓存的清除间隔,可以配置为任意正整数,单位为毫秒,默认为0。size
配置的是最大缓存的引用个数,可以是任意正整数,默认为缓存1024个引用。readOnly
属性可以被设置为true
或false
。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是false
。blocking
表示当在缓存中获取不到数据时,是否会阻塞后续的请求,默认为false。
MapperBuilderAssistant#useNewCache
方法定义如下:
该方法会创建一个缓存建造者对象(建造者设计模式),并通过其创建一个缓存对象
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//将缓存对象保存在Configuration配置对象中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
-
解析参数映射
parameterMap
总体来说,目前已经不推荐使用参数映射,而是直接使用内联参数,有必要可以参考文章 -
解析结果集映射
resultMap
结果集映射早期版本可以说是用的最多的辅助节点了,不过有了mapUnderscoreToCamelCase
属性之后,如果命名规范控制做的好的话,resultMap
也是可以省略的。每个mapper
文件可以有多个结果集映射。下面我们来看resultMap
在运行时到底是如何表示的。