Configuration存储着mybatis运行时所需要的全部配置信息,那么它是如何从mybatis-config.xml转换过来的呢?实际运行中它又起到什么作用呢?今天我通过一个小例子,结合源码一步一步探索一下Configuration的解析流程,以便更加深入的了解其运行机制。
# 从Demo开始
下面是一个小小的示例。指定了配置文件mybatis-config.xml,通过SqlSessionFactoryBuilder创建SqlSessionFactory,打开SqlSession获取Mapper,执行Mapper方法。
~~~java
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 获取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
CompanyDO companyDO = dao.selectById(1, "test");
System.out.println(companyDO);
}
}
复制代码
~~~
从main方法中我们并没有看到Configuration的影子,是因为在实际运行中,操作数据库是使用SqlSession,而SqlSession是由SqlSessionFactory按需创建的,Configuration对象保存在SqlSessionFactory中,当创建SqlSession时会把Configuration传递到SqlSession。
# 配置解析流程
示例代码3-6行就完成了Configuration的解析工作,这个过程发生在方法SqlSessionFactoryBuilder#build()中,进入方法内部会发现实际执行xml解析的是类XMLConfigBuilder。
~~~java
public SqlSessionFactory build(InputStream inputStream) {
// 调用重载方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建XMLConfigBuilder对象,这个是Configuration解析类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 首先把xml解析为Configuration对象,然后创建SqlSessionFactory对象。
// 我们下来主要关心parse()方法
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
复制代码
~~~
在mybatis中XMLConfigBuilder负责解析mybatis-config.xml,实现配置信息从文件到内存对象的转换与映射。首先看下构造方法。
~~~java
// 接收配置文件流对象、environment以及通过代码传入的属性配置。
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;
}
复制代码
~~~
XMLConfigBuilder构造方法创建了xml的解析器parser,parser负责处理复杂的xml读取工作。另外,构造方法还接收了props对象,它会覆盖从配置文件中解析的同名属性信息。
parse()方法及parseConfiguration()是解析流程的主干流程,其中,parseConfiguration中逐个完成了xml中各部分的解析,每种配置封装为了独立的方法,理解起来比较容易。每个XMLConfigBuilder只能解析一次,重复解析会导致异常。
~~~java
public Configuration parse() {
//只能解析一次,否则异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 标记为已解析
parsed = true;
//解析configuration节点下的内容
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析属性
propertiesElement(root.evalNode("properties"));
//解析配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载vfs
loadCustomVfs(settings);
//加载自定义日志
loadCustomLogImpl(settings);
//加载类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//加载插件
pluginElement(root.evalNode("plugins"));
//加载对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//加载对象包装器工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//加载反射器工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//设置配置信息
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//加载环境配置
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//加载类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//加载mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
复制代码
~~~
以上每部分配置的加载方法,其内部都会为configuration赋值,最终解析完成所有配置,再由parse方法返回给调用方。下面逐个了解各部分mybatis配置的解析流程。
## 属性(properties)
用于定义mybatis运行所需的属性信息,如数据库连接相关配置。属性的定义有三种方式:在mybatis-config.xml#properties中定义属性、通过外部properties文件定义,然后使用resource方式引入mybatis-config.xml、通过 SqlSessionFactoryBuilder.build() 方法中传入属性值。
~~~java
// 入参为root.evalNode("properties"),即properties节点下所有子节点
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//读取所有子节点中配置的属性信息,所有信息存储defaults
Properties defaults = context.getChildrenAsProperties();
//若以resource或url方式导入外部配置,则加载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.");
}
//外部配置写入defaults,这里需要注意:外部配置会覆盖上面从properties中读取的配置信息
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//从configuration读取以代码方式传入的配置信息,如果有也写入defaults,
//此时也会覆盖前两步加载的同名配置
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//属性解析完成,存储。
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
复制代码
~~~
从这个方法的执行流程可知,如果同一个属性在不同的位置进行重复配置,那么MyBatis 将按照下面的顺序来加载:
* 首先读取在 properties 元素体内指定的属性。
* 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
* 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。
## 设置(settings)
### 设置项
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。通过以下代码+注释解释各个setting及其用途。其中,大部分setting都拥有默认值,实际可按需更改。
~~~java
public class Configuration {
//环境配置
protected Environment environment;
//是否允许在嵌套语句中使用分页(RowBounds),默认false
protected boolean safeRowBoundsEnabled;
//是否允许在嵌套语句中使用结果处理器(ResultHandler),默认true
protected boolean safeResultHandlerEnabled = true;
//是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。
protected boolean mapUnderscoreToCamelCase;
//开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。
protected boolean aggressiveLazyLoading;
//是否允许单个语句返回多结果集(需要数据库驱动支持),默认值true
protected boolean multipleResultSetsEnabled = true;
//允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。
// 尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。
protected boolean useGeneratedKeys;
//使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。
protected boolean useColumnLabel = true;
//全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认true
protected boolean cacheEnabled = true;
//指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。
// 注意基本类型(int、boolean 等)是不能设置成 null 的。
// 默认false
protected boolean callSettersOnNulls;
//允许使用方法签名中的名称作为语句参数名称。
//为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。
//默认值true
protected boolean useActualParamName = true;
//当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。
// 请注意,它也适用于嵌套的结果集(如集合或关联),默认false
protected boolean returnInstanceForEmptyRow;
//指定 MyBatis 增加到日志名称的前缀。默认无
protected String logPrefix;
//指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
//SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
//默认无
protected Class <? extends Log> logImpl;
//指定 VFS 的实现
protected Class <? extends VFS> vfsImpl;
//MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。
// 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
// 可选值:SESSION | STATEMENT
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
//当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。
//某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。
//默认值:OTHER
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
//指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。
//例如:equals,clone,hashCode,toString
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
//设置超时时间,它决定数据库驱动等待数据库响应的秒数。默认未设置。
protected Integer defaultStatementTimeout;
//为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。默认未设置
protected Integer defaultFetchSize;
//配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。
//可选值:SIMPLE,REUSE,BATCH,默认值:SIMPLE
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
//指定 MyBatis 应如何自动映射列到字段或属性。
// NONE 表示关闭自动映射;
// PARTIAL 只会自动映射没有定义嵌套结果映射的字段。
// FULL 会自动映射任何复杂的结果集(无论是否嵌套)。
//默认值:PARTIAL
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
//指定发现自动映射目标未知列(或未知属性类型)的行为。
//NONE: 不做任何反应
//WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN)
//FAILING: 映射失败 (抛出 SqlSessionException)
//默认值:NONE
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
// 属性列表
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
//每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。
//默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。
//如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
//延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。
//默认值:false
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
*
* @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
//语言驱动注册中心
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
//存储所有Mapper中的映射声明,k-v结构
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
//数据缓存,k-v结构
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
//存储所有结果映射,来自Mapper
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
//存储所有参数迎神,来自Mapper
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
//存储已加载的资源文件,如mapper.xml
protected final Set<String> loadedResources = new HashSet<>();
//存储sql片段
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
//存储未解析完成的声明
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
//存储未解析完成的缓存解析
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
//存储未解析完成的结果映射
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
//存储未解析完成的方法
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
//……
}
复制代码
~~~
### 加载流程
mybatis提供的配置项很多,为了方便使用,都提供了默认值。多数情况下,我们只需要按需更改即可。settingsAsProperties用于完成setting的配置解析工作:
~~~java
private Properties settingsAsProperties(XNode context) {
//节点为空,返回空的属性对象
if (context == null) {
return new Properties();
}
//读取所有设置信息,存入props
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 通过反射方式检查props加载的配置信息是否为已知的配置项,如果存在未知配置项,抛出异常。
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;
}
复制代码
~~~
设置加载过程中采用了反射方式检查配置文件中配置项的合法性,存在未知配置项会导致异常。另外,这个方法把解析到的配置项返回,并没有直接为configuration设置,而是通过后面的settingsElement方法进行初始化。因为还要继续加载vfs、logImpl的配置信息。
## 类型别名
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:
~~~java
<typeAliases>
<typeAlias type="com.raysonxin.dataobject.CompanyDO" alias="CompanyDO"/>
</typeAliases>
复制代码
~~~
别名降低了使用的复杂度,它在类型查找、映射方面起到很大的作用,typeAliasesElement负责完成别名的解析加载工作:
~~~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);
//别名为空,默认类名首字母小写;不是空,按照alias注册
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);
}
}
}
}
}
复制代码
~~~
从代码可知,mybatis支持两种别名的注册方式:
* 按照包名注册:按照指定的包扫描类,若存在别名注解,则使用注解指定的名称;否则使用类的首字母小写默认;
* 按照类全限定名称注册:指定了别名则使用其作为别名,否则使用类的首字母小写默认;
**另外需要注意的是,这是mybatis提供的自定义别名的方式,其实mybatis已经提供了绝大多数常用的别名,具体在Configuration的构造方法中,大家可以自行查看。**
## 插件(plugins)
MyBatis 允许我们在映射语句执行过程中的某一点进行拦截调用,这可以很方便的让我们扩展mybatis的能力。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
* ParameterHandler (getParameterObject, setParameters)
* ResultSetHandler (handleResultSets, handleOutputParameters)
* StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。先看下接口Interceptor的定义:
~~~java
public interface Interceptor {
//拦截接口,这里面实现拦截处理逻辑
Object intercept(Invocation invocation) throws Throwable;
//为上述目标增加拦截,增强
Object plugin(Object target);
//设置插件属性
void setProperties(Properties properties);
}
复制代码
~~~
### 自定义插件
自定义插件只需实现 Interceptor 接口,并指定想要拦截的方法签名即可,这里的接口方法和签名必须与Mybatis源码中的定义完全一致,下面通过一个例子进行说明。
~~~java
@Intercepts(
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
)
public class CustomInterceptor implements Interceptor {
Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before CustomInterceptor");
Object object = invocation.proceed();
System.out.println("after CustomInterceptor");
return object;
}
@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
复制代码
~~~
CustomInterceptor对Executor#query方法进行了拦截,在query前后输出一些埋点信息(可以替换为自己的业务逻辑);plugin方法实现逻辑是:当目标组件为Executor时使用Plugin.wrap方法会为其增加插件功能。接下来在mybatis-config.xml中进行配置:
~~~xml
<plugins>
<plugin interceptor="com.raysonxin.plugin.CustomInterceptor">
<property name="name" value="abcd"/>
</plugin>
</plugins>
复制代码
~~~
看下运行效果:
**![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e04ba08ff039414ab97508bd57949daf~tplv-k3u1fbpfcp-zoom-1.image)**
### 插件加载与执行流程
直接上代码:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
~~~java
//入参为root.evalNode("plugins")节点
private void pluginElement(XNode parent) throws Exception {
//节点为空不处理
if (parent != null) {
//依次遍历子节点
for (XNode child : parent.getChildren()) {
//获取插件名称
String interceptor = child.getStringAttribute("interceptor");
//获取插件属性列表
Properties properties = child.getChildrenAsProperties();
//实例化插件对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//设置插件属性字段
interceptorInstance.setProperties(properties);
//添加插件信息到configurtation
configuration.addInterceptor(interceptorInstance);
}
}
}
复制代码
~~~
插件加载流程比较简单:获取名称,获取属性,实例化插件,设置插件属性,最终添加到configuration。XMLConfigBuilder完成插件加载后把插件保存在Configuration#interceptorChain,那实际运行中是如何应用的呢?
如前文所说,mybatis支持在Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件的若干方法上通过插件增强,mybatis正是在四大组件的创建过程中采用动态代理的方式应用插件的。四大组件的创建方法在Configuration中,如下所示,以newExecutor为例对插件应用过程进行梳理。
~~~java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//添加插件逻辑,采用动态代理进行包装
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
复制代码
~~~
组件对象创建完成后通过interceptorChain.pluginAll()方法依次应用插件,四个方法的逻辑类似。interceptor.plugin()由插件实现,它会调用Plugin#wrap方法进行增强。看下这一段的源码逻辑:
~~~java
public Object pluginAll(Object target) {
//循环依次遍历插件
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
//获取插件类的签名:就是确认插件为哪个方法进行增强
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//获取插件类实现的所有接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//采用动态代理创建代理类实现,在我们的例子中相当于为Executor对象进行了增强。
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
复制代码
~~~
所以,经过Plugin#wrap包装后的Executor对象其实是一个代理对象,其中包含了Executor原有的逻辑,按照动态代理的原理,我们看下Plugin#invoke的执行逻辑:
~~~java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//从缓存中查询缓存的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//判断当前方式是否为插件增强的方法
if (methods != null && methods.contains(method)) {
//如果是则执行插件的intercept方法,内部会调用原有的method,入口是:Invocation#proceed
return interceptor.intercept(new Invocation(target, method, args));
}
//未被增强,直接调用原来的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
复制代码
~~~
编写插件需要注意对原有mybatis的影响,防止破坏 MyBatis 的核心模块。通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
## 类型处理器(typeHandler)
MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。通过TypeHandler接口定义也可以清晰的看到TypeHandler的作用:
~~~java
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
复制代码
~~~
### 自定义类处理器
如果默认的类型处理器不能满足使用需求,可以按照要求自定义类型处理器。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。
以第二种方式举例,自定义类型处理器CustomTypeHandler,该处理器对应的java类型是String,jdbc类型是VARCHAR,includeNullJdbcType为true。
~~~java
@MappedJdbcTypes(value = JdbcType.VARCHAR,includeNullJdbcType = true)
public class CustomTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
复制代码
~~~
然后在mybatis-config.xml中添加如下配置:
~~~xml
<typeHandlers>
<typeHandler handler="com.raysonxin.typehandler.CustomTypeHandler"/>
</typeHandlers>
复制代码
~~~
### 加载流程
类型处理器的注册过程比较复杂,原因是代码中包含了许多个重载方法,看上去晕头转向,我们先来认识一下TypeHandlerRegistry是如何设计的,直接看代码。
~~~java
public final class TypeHandlerRegistry {
//这个字段存储了jdbcType到类型处理器的映射,使用EnumMap
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
//存储javaType对应的处理器映射,需要根据jdbcType选择处理器
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
//未知类型处理器,内部会进一步解析处理器类型并路由
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
//存储所有的类型与类型处理器的映射,jdbcType或者javaType
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
//NULL处理器
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
//默认枚举类型处理器
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
//构造方法内默认注册了常用的类型处理器
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
//....
}
//...
}
复制代码
~~~
TypeHandlerRegistry内部维护了java类型、jdbc类型与类型处理器之间的关系,分别从不同的角度进行映射:
* JdbcType对应的处理器(JDBC\_TYPE\_HANDLER\_MAP):意思是当处理一个jdbc类型时,可以从这里获取处理器;
* javaType对应的处理器(TYPE\_HANDLER\_MAP):这个字段使用了两层映射。第一层是javaType,代表了该javaType具有的处理器列表;第二层是jdbcType与处理器之间的对应关系。综合下来,确认一个处理器需要进行最终确认处理器。
* 全类型映射(ALL\_TYPE\_HANDLERS\_MAP):维护了jdbc类型和java类型所对应的所有映射器。
* 空类型处理器和默认枚举类型处理器。
~~~java
//入参为root.evalNode("typeHandlers")节点
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 {
//单个类型处理器注册的配置方式
//获取javaType,这个是类型的别名,应该注册在typeAliasRegistry
String javaTypeName = child.getStringAttribute("javaType");
//获取jdbcType
String jdbcTypeName = child.getStringAttribute("jdbcType");
//获取handler名称,这是类的全限定名称
String handlerTypeName = child.getStringAttribute("handler");
//通过javaType获取其对应的类型,可能为空;若指定了但是没有找到,会报异常
Class<?> javaTypeClass = resolveClass(javaTypeName);
//获取JdbcType,枚举类型
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
//获取处理器对应的类型
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
//Case-1:按照java类型、处理器类型注册
//a、根据typeHandlerClass创建处理器对象typeHandler;
//b、获取typeHandler注解中的jdbcTypes,可能为空
//c、jdbcTypes!=null:注册javaType-jdbcType-handler,如果注解中includeNullJdbcType=true,注册空类型;
// jdbcTypes==null:仅注册javaType-handler;
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
//Case-2:按照java类型、jdbc类型、处理器类型注册
//a、创建typeHandler对象;
//b、注册关联javaType-jdbcType-handler
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
//Case-3:按照处理器类型注册
//a、获取typeHandlerClass的MappedTypes注解;
//b、javaType不为空:按照javatype注册,内部会获取MappedJdbcTypes,关联三者。
//c、这种情况还没看明白。。。
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
//Case-1~3,最终都会走到这里
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
//从map中查询已有的javaType对应的处理器
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
//不存在或者为空,执行初始化与添加操作
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
TYPE_HANDLER_MAP.put(javaType, map);
}
//关联javaType-jdbcType-handler
map.put(jdbcType, handler);
}
//加入总的映射
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
复制代码
~~~
通过代码可以发现,JDBC\_TYPE\_HANDLER\_MAP仅仅是在构造方法逻辑中进行了添加,相当于是mybatis默认的处理器。我们自定义的处理器会添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,虽然有不同的注册方式,但是殊途同归,都是通过上述方法实现了注册。
总结一下:
* 通过配置文件自定义的类型处理器,会添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,而获取类型处理器是优先使用javaType来查询的。如果自定义的类型处理器了重复定义了javaType或jdbcType,会对系统默认的处理器产生覆盖。
* 如果在类型处理器的注解和xml配置中同时声明了javaType,那么xml中配置的javaType会生效。
## 映射器(mapper)
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。——*摘自《XML 映射器》*
映射器是mybatis面向接口编程的利器,它使用动态代理技术,实现了接口与mapper中sql语句的动态绑定,免去了JDBC中手动实现执行Statement的繁杂过程。mybatis提供多种方式支持mapper资源查找:可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。
### 映射器元素
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
* cache – 该命名空间的缓存配置。
* cache-ref – 引用其它命名空间的缓存配置。
* resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
* ~parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。~
* sql – 可被其它语句引用的可重用语句块。
* insert – 映射插入语句。
* update – 映射更新语句。
* delete – 映射删除语句。
* select – 映射查询语句
以上顶级元素还有较多的属性用于控制其行为,大家可以自行查阅官方文档,这里不再贴出来了。
### 加载流程
我们随着示例及源码来看下mybatis是如何加载解析mapper的,然后通过另外一篇文章来剖析其执行过程。示例配置信息,示例中定义了CompanyMapper.xml,以资源方式引入到mybatis-config.xml。
~~~xml
<!--mybatis-config.xml-->
<mappers>
<mapper resource="mapper/CompanyMapper.xml"/>
</mappers>
<!--CompanyMapper.xml-->
<mapper namespace="com.raysonxin.dao.CompanyDao">
<resultMap id="baseResultMap" type="com.raysonxin.dataobject.CompanyDO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="cpy_type" property="cpyType"/>
</resultMap>
<sql id="BaseColumns">
id,name,cpy_type
</sql>
<select id="selectById" resultMap="baseResultMap">
select
<include refid="BaseColumns"></include>
from company
where id= #{id} and name = #{name}
</select>
</mapper>
复制代码
~~~
mapper解析入口为XMLConfigBuilder#mapperElement,通过代码注释走下流程。
~~~java
//入参为root.evalNode("mappers"),即示例中的mappers节点,它可包含多个mapper子节点
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 整包方式添加mapper
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//这里包含了三种方式:资源导入、资源全限定路径、类名,分别获取mapper节点属性值
//三个值中只能配置一个,否则会报异常(最后的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类进行mapper解析,这里是重点。
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类进行mapper解析,同上。
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.");
}
}
}
}
}
复制代码
~~~
示例代码以resource方式引入mapper,按照这条线走下去,创建XMLMapperBuilder后调用其parse方法。这里与XMLConfigBuilder类似,同样使用XPathParser进行xml解析,另外使用MapperBuilderAssistant构建MappedStatement。
~~~java
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
复制代码
~~~
接下来进入解析流程,即XMLMapperBuilder#parse方法。大体流程是:若资源未被加载过,则解析mapper文件中的元素,把mapper添加到已加载资源,绑定mapper与namespace。无论是否加载过当前资源,都会处理那些之前未完成的ResultMap、CacheRef、Statement。
~~~java
public void parse() {
//判断资源是否已经加载
if (!configuration.isResourceLoaded(resource)) {
//这个是mapper解析的核心方法,入参是mapper对应的资源文件,示例中的CompanyMapper.xml
configurationElement(parser.evalNode("/mapper"));
//添加已加载资源到configuration
configuration.addLoadedResource(resource);
//这个方法是绑定mapper与namespace
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
复制代码
~~~
核心解析方式是configurationElement,它依次解析mapper文件中的cache-ref、cache、parameterMap、resultMap、sql、select|insert|update|delete,本次重点分析后面三者的解析过程。
~~~java
private void configurationElement(XNode context) {
try {
//获取mapper中的namespace,示例中为:com.raysonxin.dao.CompanyDao
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置builderAssistant的namespace
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref内容
cacheRefElement(context.evalNode("cache-ref"));
//解析cache内容
cacheElement(context.evalNode("cache"));
//解析parameterMap节点
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
//构建statement
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);
}
}
复制代码
~~~
#### resultMap
下可以定义多个resultMap,通常我们会把常用的返回字段定义为一个resultMap,方便在查询语句中引用。我们从resultMapElements开始看下源码:
~~~java
private void resultMapElements(List<XNode> list) throws Exception {
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList(), null);
}
//真正的解析工作从这里开始
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
//获取resultMap的id属性,示例中baseResultMap
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
//获取resultMap的type属性,后面是默认值设置顺序
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
//获取extends属性
String extend = resultMapNode.getStringAttribute("extends");
//获取autoMapping属性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
//获取type对应的类型对象,首先从typeAliasRegistry获取,否则按照全路径限定名称反射创建
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
//依次解析子节点
for (XNode resultChild : resultChildren) {
//是否为构造方法
if ("constructor".equals(resultChild.getName())) {
//处理构造方法
processConstructorElement(resultChild, typeClass, resultMappings);
}
//discriminator鉴别器
else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
}
//其他节点
else {
List<ResultFlag> flags = new ArrayList<>();
// 是否为id节点
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
//构建resultMap:buildResultMappingFromContext
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
复制代码
~~~
不考虑复杂的resultMap,我们示例中的节点类型为id和result两种,我们直接进入方法buildResultMappingFromContext:
~~~java
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
//获取property值,对应javaType的字段
property = context.getStringAttribute("property");
}
//数据表的字段
String column = context.getStringAttribute("column");
//获取javaType
String javaType = context.getStringAttribute("javaType");
//获取jdbcType
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList(), resultType));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
//类型处理器
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
//获取javaType类型对象
Class<?> javaTypeClass = resolveClass(javaType);
//获取类型处理器
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
//jdbc类型枚举
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
//创建ResultMapping对象并返回
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
复制代码
~~~
这个解析过程包含了resultMap高级用法的逻辑,比如association、collections等,我们先从简单、常用的方式入手。buildResultMappingFromContext最终返回了resultMap中某个字段的映射信息,构建了java类型与数据库字段的映射关系。这样,通过遍历所有字段把resultMap下的所有字段一一映射。
我们再回到方法resultMapElement,遍历所有节点后得到resultMappings,创建了ResultMapResolver对象。从名称可以知道,这个类是ResultMap的解析器,它最终把resultMap配置信息转为mybaits的ResultMap对象并添加到configuration。
#### sql
实际开发中,我们可以使用sql标签定义那些可复用的sql片段,简化mapper文件的配置。在mybatis解析sql时,它也只是一个中间过程,它是解析select|update|insert|delete语句的基础。
~~~java
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
//拼接namespace,结果为:namespace+"."+id
id = builderAssistant.applyCurrentNamespace(id, false);
//是否符合datbaseId要求
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
//sqlFragments保存所有sql片段
sqlFragments.put(id, context);
}
}
}
复制代码
~~~
#### select|insert|update|delete
select|insert|update|delete对于sql语句中的同名命令,是我们使用最频繁的部分,最终mybatis会把这些标签转为MappedStatement,存储在configuration,同时与Mapper接口方法在运行时绑定。
~~~java
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
复制代码
~~~
从代码可以看到XMLStatementBuilder,它是命令标签的处理器,核心方法是parseStatementNode。到这里,我们已经见过了XMLConfigBuilder、XMLMapperBuilder。看下处理过程:
~~~java
public void parseStatementNode() {
//获取标签中的id属性,如:selectById
String id = context.getStringAttribute("id");
//databaseId,为空
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//属性fetchSize
Integer fetchSize = context.getIntAttribute("fetchSize");
//属性timeout
Integer timeout = context.getIntAttribute("timeout");
//属性parameterMap
String parameterMap = context.getStringAttribute("parameterMap");
//属性parameterType
String parameterType = context.getStringAttribute("parameterType");
//解析参数类型信息
Class<?> parameterTypeClass = resolveClass(parameterType);
//获取属性resultMap
String resultMap = context.getStringAttribute("resultMap");
//获取属性resultType
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
//获取属性statementType,未设置时使用默认值PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//获取nodeName,就是标签的类型,select、insert之类
String nodeName = context.getNode().getNodeName();
//命令类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//判断是否为select命令
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//是否刷新缓存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否使用缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
//这里解析sql语句中的include标签
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//解析selectKey标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//创建SqlSource,必须在inclue和selectKey解析完成后执行,
//内部会根据sql语句是否包含动态标签创建不同类型的sqlSource,解析sql所需的参数映射信息
//这个地方需要一篇单独的文章来分析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
//处理主键生成器
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//创建MappedStatement,添加到configuration
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
复制代码
~~~
源码的过程梳理一下:
* 首先解析标签的基础属性,如id、fetchSize等,
* 解析sql语句中inclue标签(把include的内容替换拼接进来),
* 处理selcetKey标签(特定的databaseProvider需要),
* 接下来创建SqlSource:这个是mybatis的核心内容,涉及到sql语句中参数提取、动态赋值等,以后在详细说明;
* 处理KeyGenerator;
* 使用builderAssistant创建MappedStatement,添加到configuration
# 总结
Configuration是mybatis的全局配置类,mybatis运行时所需的一切都缓存在这里,它伴随mybatis的整个生命周期。理论上讲,Configuration是配置文件mybatis-config.xml的代码映射,mybatis提供了充分的灵活性、可扩展性,方便开发人员通过配置文件改变其运行行为。
本文通过示例及源码把mybatis-config.xml的解析过程、Configuration配置的大部分内容进行了梳理,通过梳理确实获益匪浅。由于水平优先,文中很多内容确实没有描述清楚,随着学习的不断深入,再做完善。
作者:码路印记
链接:https://juejin.cn/post/6878979290252361741
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 一.JVM
- 1.1 java代码是怎么运行的
- 1.2 JVM的内存区域
- 1.3 JVM运行时内存
- 1.4 JVM内存分配策略
- 1.5 JVM类加载机制与对象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面试相关文章
- 2.1 可能是把Java内存区域讲得最清楚的一篇文章
- 2.0 GC调优参数
- 2.1GC排查系列
- 2.2 内存泄漏和内存溢出
- 2.2.3 深入理解JVM-hotspot虚拟机对象探秘
- 1.10 并发的可达性分析相关问题
- 二.Java集合架构
- 1.ArrayList深入源码分析
- 2.Vector深入源码分析
- 3.LinkedList深入源码分析
- 4.HashMap深入源码分析
- 5.ConcurrentHashMap深入源码分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的设计模式
- 8.集合架构之面试指南
- 9.TreeSet和TreeMap
- 三.Java基础
- 1.基础概念
- 1.1 Java程序初始化的顺序是怎么样的
- 1.2 Java和C++的区别
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字节与字符的区别以及访问修饰符
- 1.7 深拷贝与浅拷贝
- 1.8 字符串常量池
- 2.面向对象
- 3.关键字
- 4.基本数据类型与运算
- 5.字符串与数组
- 6.异常处理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 数据流(Stream)
- 8.3 Java 8 并发教程:线程和执行器
- 8.4 Java 8 并发教程:同步和锁
- 8.5 Java 8 并发教程:原子变量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、数值、算术和文件
- 8.7 在 Java 8 中避免 Null 检查
- 8.8 使用 Intellij IDEA 解决 Java 8 的数据流问题
- 四.Java 并发编程
- 1.线程的实现/创建
- 2.线程生命周期/状态转换
- 3.线程池
- 4.线程中的协作、中断
- 5.Java锁
- 5.1 乐观锁、悲观锁和自旋锁
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平锁和非公平锁
- 5.3.1 说说ReentrantLock的实现原理,以及ReentrantLock的核心源码是如何实现的?
- 5.5 锁优化和升级
- 6.多线程的上下文切换
- 7.死锁的产生和解决
- 8.J.U.C(java.util.concurrent)
- 0.简化版(快速复习用)
- 9.锁优化
- 10.Java 内存模型(JMM)
- 11.ThreadLocal详解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的实现原理
- 1.DelayQueue的实现原理
- 14.Thread.join()实现原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的实际使用场景
- 五.Java I/O NIO
- 1.I/O模型简述
- 2.Java NIO之缓冲区
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之选择器
- 6.基于 Java NIO 实现简单的 HTTP 服务器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面试题
- 六.Java设计模式
- 1.单例模式
- 2.策略模式
- 3.模板方法
- 4.适配器模式
- 5.简单工厂
- 6.门面模式
- 7.代理模式
- 七.数据结构和算法
- 1.什么是红黑树
- 2.二叉树
- 2.1 二叉树的前序、中序、后序遍历
- 3.排序算法汇总
- 4.java实现链表及链表的重用操作
- 4.1算法题-链表反转
- 5.图的概述
- 6.常见的几道字符串算法题
- 7.几道常见的链表算法题
- 8.leetcode常见算法题1
- 9.LRU缓存策略
- 10.二进制及位运算
- 10.1.二进制和十进制转换
- 10.2.位运算
- 11.常见链表算法题
- 12.算法好文推荐
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事务管理
- 4.SpringMVC 运行流程和手动实现
- 0.Spring 核心技术
- 5.spring如何解决循环依赖问题
- 6.springboot自动装配原理
- 7.Spring中的循环依赖解决机制中,为什么要三级缓存,用二级缓存不够吗
- 8.beanFactory和factoryBean有什么区别
- 九.数据库
- 1.mybatis
- 1.1 MyBatis-# 与 $ 区别以及 sql 预编译
- Mybatis系列1-Configuration
- Mybatis系列2-SQL执行过程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-参数设置揭秘(ParameterHandler)
- Mybatis系列8-缓存机制
- 2.浅谈聚簇索引和非聚簇索引的区别
- 3.mysql 证明为什么用limit时,offset很大会影响性能
- 4.MySQL中的索引
- 5.数据库索引2
- 6.面试题收集
- 7.MySQL行锁、表锁、间隙锁详解
- 8.数据库MVCC详解
- 9.一条SQL查询语句是如何执行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能优化神器 Explain 使用分析
- 12.mysql中,一条update语句执行的过程是怎么样的?期间用到了mysql的哪些log,分别有什么作用
- 十.Redis
- 0.快速复习回顾Redis
- 1.通俗易懂的Redis数据结构基础教程
- 2.分布式锁(一)
- 3.分布式锁(二)
- 4.延时队列
- 5.位图Bitmaps
- 6.Bitmaps(位图)的使用
- 7.Scan
- 8.redis缓存雪崩、缓存击穿、缓存穿透
- 9.Redis为什么是单线程、及高并发快的3大原因详解
- 10.布隆过滤器你值得拥有的开发利器
- 11.Redis哨兵、复制、集群的设计原理与区别
- 12.redis的IO多路复用
- 13.相关redis面试题
- 14.redis集群
- 十一.中间件
- 1.RabbitMQ
- 1.1 RabbitMQ实战,hello world
- 1.2 RabbitMQ 实战,工作队列
- 1.3 RabbitMQ 实战, 发布订阅
- 1.4 RabbitMQ 实战,路由
- 1.5 RabbitMQ 实战,主题
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 实战 – 整合 RabbitMQ 发送邮件
- 1.8 RabbitMQ 的消息持久化与 Spring AMQP 的实现剖析
- 1.9 RabbitMQ必备核心知识
- 2.RocketMQ 的几个简单问题与答案
- 2.Kafka
- 2.1 kafka 基础概念和术语
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志机制
- 2.4 kafka是pull还是push的方式传递消息的?
- 2.5 Kafka的数据处理流程
- 2.6 Kafka的脑裂预防和处理机制
- 2.7 Kafka中partition副本的Leader选举机制
- 2.8 如果Leader挂了的时候,follower没来得及同步,是否会出现数据不一致
- 2.9 kafka的partition副本是否会出现脑裂情况
- 十二.Zookeeper
- 0.什么是Zookeeper(漫画)
- 1.使用docker安装Zookeeper伪集群
- 3.ZooKeeper-Plus
- 4.zk实现分布式锁
- 5.ZooKeeper之Watcher机制
- 6.Zookeeper之选举及数据一致性
- 十三.计算机网络
- 1.进制转换:二进制、八进制、十六进制、十进制之间的转换
- 2.位运算
- 3.计算机网络面试题汇总1
- 十四.Docker
- 100.面试题收集合集
- 1.美团面试常见问题总结
- 2.b站部分面试题
- 3.比心面试题
- 4.腾讯面试题
- 5.哈罗部分面试
- 6.笔记
- 十五.Storm
- 1.Storm和流处理简介
- 2.Storm 核心概念详解
- 3.Storm 单机版本环境搭建
- 4.Storm 集群环境搭建
- 5.Storm 编程模型详解
- 6.Storm 项目三种打包方式对比分析
- 7.Storm 集成 Redis 详解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初识ElasticSearch
- 2.文档基本CRUD、集群健康检查
- 3.shard&replica
- 4.document核心元数据解析及ES的并发控制
- 5.document的批量操作及数据路由原理
- 6.倒排索引
- 十七.分布式相关
- 1.分布式事务解决方案一网打尽
- 2.关于xxx怎么保证高可用的问题
- 3.一致性hash原理与实现
- 4.微服务注册中心 Nacos 比 Eureka的优势
- 5.Raft 协议算法
- 6.为什么微服务架构中需要网关
- 0.CAP与BASE理论
- 十八.Dubbo
- 1.快速掌握Dubbo常规应用
- 2.Dubbo应用进阶
- 3.Dubbo调用模块详解
- 4.Dubbo调用模块源码分析
- 6.Dubbo协议模块