本次所谈的原理仅仅只涉及基本的框架和核心代码,并不会全部都说到,比如关于MyBatis是如何解析XML文件和其他配置文件从而的到内容,还有就是JDBC如何使用,关于JDBC如何使用,可以参考我的这篇博客:单例模式和JDBC
还有就是关于Java基础方面的内容,个人建议大家抽空看看《Java编程思想》这本书,这本书可以作为一本参考书来看,不要从头开始看,有选择的阅读是最好的,从头开始看,别说看懂问题,估计你看的都想睡觉了。另外最好的话还是可以通过知识付费看看人家是怎么讲Java的,通过知识付费获取的Java相关知识,应该是不错的,比如在极客时间这个APP中看到的杨晓峰《Java核心技术36讲》,口碑目前还不错,不过建议朋友们最好还是自己时不时根据一些参考书或者官网再加上自己工作用到时刻复习一下,总结一下。这样还是有很大好处的。
MyBatis的运行分为两大部分,第一部分是读取配置文件缓存到Configuration对象,用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。相对而言,SqlSessionFactory的创建比较容易理解,而SqlSession的执行过程远远不是那么简单了,它将包括许多复杂的技术,我们需要讨论反射技术和动态代理技术,这是揭示MyBatis底层架构的基础。
当我们掌握了MyBatis的运行原理,我们就可以知道MyBatis是怎么运行的,同时当我们在深入理解MyBatis相关的源码和涉及到的设计模式后,我们也许就能像MyBatis-Plus的开发者那样,开发出一个比MyBatis或者MyBatis-Plus还要好的持久层框架。
一、涉及的技术难点简介
Mapper仅仅只是一个接口,而不是一个包含逻辑的实现类。我们知道一个接口是没有办法去执行的,那么它是怎么运行的呢?这不是违反教科书上说的接口不能运行的道理吗?相信不少初学者会对此有疑惑。
答案就是动态代理。
首先,什么是代理模式?所谓的代理模式就是在原有的服务商多加以占位,通过这个占位去控制服务的访问。这句话不太容易理解,举例而言,假设你是一个公司的工程师,能提供一些技术服务,公司的客服就一个美女,她不懂技术。而我是一个客户需要你们公司提供技术服务。显然,我只会找到你们的客服,和客服沟通,而不是找你沟通。客服会根据公司的规章制度和业务规则来决定找不找你服务。那么这个时候客服就等同于你的一个代理,她通过和我的交流来控制对你的访问,当然她也可以提供一些你们公司对外的服务。而我只能通过她的代理访问你。对我而言,根本不需要认识你,只需要认识客服就可以了。事实上,站在我的角度,我会认为客服就代表你们公司,而不管真正为我服务的你是怎么样的。
其次,为什么要使用代理模式?通过代理,一方面可以控制如何访问真正的服务对象,提供额外服务。另外一方面有机会通过重写一些类来满足特定的需要,正如客服也可以根据公司的业务规则,提供一些服务,这个时候就不需要劳你大驾。
动态代理示意图:
一般而言,动态代理分为两种,一种是JDK反射机制提供的代理,另一种是CGLIB代理。在JDK提供的代理,我们必须要提供接口,而CGLIB则不需要提供接口,在MyBatis里面两种动态代理技术都已经使用了。但是在此之前我们需要学习的技术就是反射。
1.反射技术
关于反射技术详细可以参考我的这篇博客:MyBatis之反射技术+JDK动态代理+cglib代理
不过在此基础上,我还是要说说什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
JAVA反射(放射)机制:“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。但是JAVA有着一个非常突出的动态相关机制:Reflection,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
2.JDK动态代理
JDK的动态代理,是由JDK的java.lang,reflect.*包提供支持的,我们需要完成这么几个步骤?
(1)编写服务类和接口,这个是真正的服务提供者,在JDK代理中接口是必须的;
(2)编写代理类,提供绑定和代理的方法;
JDK的代理最大的缺点是需要提供接口,而MyBatis的Mapper就是一个接口,它采用的就是JDK的动态代理。
关于示例还是可以参考我的这篇博客(虽然前面提到过):MyBatis之反射技术+JDK动态代理+cglib代理
3.CGLIB动态代理
JDK提供的动态代理存在一个缺陷,就是你必须提供接口才可以使用,为了克服这个缺陷,我们可以使用开源框架-CGLIB,它是一种流行的动态代理。
关于示例还是可以参考我的这篇博客:MyBatis之反射技术+JDK动态代理+cglib代理
二、构建SqlSessionFactory过程
SqlSessionFactory是MyBatis的核心类之一,其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以我们需要先创建SqlSessionFactory,为此我们需要提供配置文件和相关的参数。而MyBatis是一个复杂的系统,采用构造模式去创建SqlSessionFactory,我们可以通过SqlSessionFactoryBeanBuilder去构建。构建分为两步:
第一步,通过org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML文件,读出配置参数,并将读取的数据存入这个org.apache.ibatis.session.Configuration类中。注意,MyBatis几乎所有的配置都是存在这里的。
第二步,使用Configuration对象去创建SqlSessionFactory。MyBatis中的SqlSessionFactory是一个接口,而不是实现类,为此MyBatis提供了一个默认的SqlSessionFactory实现类,我们一般都会使用它org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。注意,在大部分情况下我们都没有必要自己去创建新的SqlSessionFactory的实现类。
这种创建的方式就是一种Builder模式。对于复杂的对象而已,直接使用构造方法构建是有困难的,这会导致大量的逻辑放在构造方法中,由于对象的复杂性,在构建的时候,我们更希望一步一步有秩序的来构建它,从而降低其复杂性。这个时候使用一个参数类总领全局。例如,Configuration类,然后分布构建,例如,DefaultSqlSessionFactory类,就可以构建一个复杂的对象,例如SqlSessionFactory,这种方式值得我们在工作中学习和使用。
1.构建Configuration
在SqlSessionFactory构建中,Configuration是最重要的,它的作用如下:
(1)读取配置文件,包括基础配置的XML和映射器的XML文件;
(2)初始化基础配置,比如MyBatis的别名等,一些重要的类对象,例如插件、映射器、ObjectFactory和typeHandler对象;
(3)提供单例,为后续创建SessionFactory服务并提供配置的参数;
(4)执行一些重要的对象方法,初始化配置信息;
显然Configuration不会是一个很简单的类,MyBatis的配置信息都会来自于此。有兴趣的朋友可以读读源码,几乎所有的配置都可以在这里找到踪影。
比如在MyBatis实战之初步 提到的单例
Configuration是通过XMLConfigBuilder去构建。首先,MyBatis会读出所有的XML配置的信息。然后,将这些信息保存到Configuration类的单例中。它会做如下初始化。
a.properties全局参数;
b.settings设置;
c.typeAliases别名:
d.typeHandler类型处理器;
e.ObjectFactory对象;
f.plugin插件
g.environment环境;
h.DatabaseIdProvier数据库标识;
i.Mapper映射器;
2.映射器的内部组成
一般而言,一个映射器是由3个部分组成:
(1)MappedStatement,它保存映射器的一个节点。包括许多我们配置的SQL、SQL的id、缓存信息、resultMap、parameterType、resultType、languageDriver等重要配置内容;
(2)SqlSource,它是提供BoundSql对象的地方,它是MappedStatement的一个属性;
(3)BoundSql,它是建立SQL和参数的地方。它有3个常用的属性:SQL、parameterObject、parameterMappings;
这些都是映射器的重要内容,也是MyBatis的核心内容。在插件的应用中常常会用到它们。映射器的解析过程是比较复杂的,但是在大部分的情况下,我们并不需要去理会解析和组装SQL的规则,因为大部分的插件只要做很小的改变即可,无需做很大的改变。大的改变可能导致重写这些内容。所以我们主要关注参数和SQL。
映射器的组成部分,如图所示:
注意,这张图并没有将所有的方法和属性都列举出来,只列举了主要的属性和方法。
MappedStatement对象涉及的东西较多,我们一般都不去修改它,因为容易产生不必要的错误。SqlSource是一个接口,它的主要作用是根据参数和其他的规则组装SQL。这些都是很复杂的东西,好在MyBatis本身已经实现了它,一般也不需要去修改它。对于参数和SQL而言,主要的规则都反映在BoundSql类对象上,在插件中往往需要拿到它进而可以拿到当前运行的SQL的参数以及参数规则,做出适当的修改,来满足我们特殊的需求。
BoundSql会提供3个主要的属性:parameterMappings、paramterObject和sql。
(1)其中parameterObject为参数本身,前面我们说到过,参数可以是简单对象,Pojo、Map或者@Param注解的参数,由于它在插件中相当常用,后面我们有必要讨论一下它的规则;
(2)传递简对象(包括int、String、float、double等),比如当我们传递int类型时,MyBatis会把参数变为Integer对象传递,类似的long、String、float、double也是如此;
(3)如果我们传递的是Pojo或者Map,那么这个parameterObject就是你传入的Pojo或者Map不变;
(4)当然我们也可以传递多个参数,如果没有@Param注解,那么MyBatis就会把parameterObject变为一个Map对象,其键值的关系是按顺序来规划的;
(5)如果我们使用@Param注解,那么MyBatis就会把parameterObject变为一个Map对象,类似于没有@Param注解,只是把其数字的键值对应置换为@Param注解的键值;
(6)parameterMappings,它是一个List,每个元素都是ParameterMapping的对象。这个对象会描述我们的参数。参数包括属性、名称、表达式、javaType、jdbcType、typeHandler等重要信息,我们一般不需要去改变它。通过它可以实现参数和SQL的结合,以便PreparedStatement能够通过它找到parameterObject对象的属性并设置参数,使得程序准确运行;
(7)sql属性就是我们书写在映射器里面的一条SQL,在大多数时候无需修改它,只有在插件的情况下,我们可以根据需要进行改写。改写SQL将是一件危险的事情,请务必慎重行事;
3.构建SqlSessionFactory
有了Configuration对象构建SqlSessionFactory就很简单了,我们只要写很简短的代码便可以了。
例如:
复制代码
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
复制代码
MyBatis会根据Configuration的配置读取所配置的信息,构建SqlSessionFactory对象。
三、SqlSession运行过程
SqlSession的运行过程是整个MyBatis最难以理解的部分。SqlSession是一个接口,使用它并不复杂。我们构建SqlSessionFactory就可以轻易地拿到SqlSession了。SqlSession给出了查询、插入、更新、删除的方法,在旧版本的MyBatis或iBatis中常常使用这些接口方法,而在新版的MyBatis中我们建议使用Mapper,所以它就是MyBatis最为常用和重要的接口之一。
SqlSession内部并没有那么容易,因为它的内部实现相当复杂。
1.映射器的动态代理
Mapper映射是通过动态代理来实现的,我们来看看代码清单:
MapperProxyFactory源码如下:
复制代码
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.binding;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.ibatis.session.SqlSession;
/**
* @author Lasse Voss
*/
public class MapperProxyFactory {
private final Class mapperInterface;
private final Map methodCache = new ConcurrentHashMap();
public MapperProxyFactory(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class getMapperInterface() {
return mapperInterface;
}
public Map getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
复制代码
这里我们可以看到动态代理对接口的绑定,它的作用就是生成动态代理对象(占位)。
而代理的方法则被放到MapperProxy类中。
MapperProxy源码如下:
复制代码
/**
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.binding;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import org.apache.ibatis.lang.UsesJava7;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class MapperProxy implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class mapperInterface;
private final Map methodCache;
public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
@UsesJava7
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
}
复制代码
上面运用了invoke方法。一旦mapper是一个代理对象,那么它就会运行到invoke方法里面,invoke首先判断它是否是一个类,显然这里Mapper是一个接口而不是类,所以判定失败。那么就会生成MapperMethod对象,它是通过cachedMapperMethod方法对其初始化的,然后执行execute方法,把sqlSession和当前运行的参数传递进去。
这个exexute方法的源码如下:
复制代码
/**
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.binding;
import org.apache.ibatis.annotations.Flush;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.reflection.TypeParameterResolver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author Clinton Begin
* @author Eduardo Macarron
* @author Lasse Voss
*/
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(comman