怒肝一夜

您所在的位置:网站首页 源码解析 怒肝一夜

怒肝一夜

2024-05-06 04:28| 来源: 网络整理| 查看: 265

本文:12006字,阅读时长:10分15秒

前面已经发过Mybatis源码解析的文章了,本文是对前面文章进行精简以及部分调整优化,总结出来的一篇万字Mybatis源码分析。

主要内容

我们从一个简单案例入手,接着就是一步一步的剥开Mybatis的源码,大量的图文结合。

Mybatis使用案例

添加mybatis和MySQL相关pom依赖。

代码语言:javascript复制 org.mybatis mybatis 3.5.2 mysql mysql-connector-java 8.0.16 runtime

本地创建数据库,创建一张表。同时可以初始化几条数据,方便后面debug。

代码语言:javascript复制CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `pwd` varchar(255) DEFAULT NULL, `gender` int DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

实体类(entity):

代码语言:javascript复制public class User { private Integer id; private String userName; private Integer age; private Integer gender; //省略..... }

创建UserMapper.java接口:

代码语言:javascript复制public interface UserMapper { User selectById(Integer id); }

创建UserMapper.xml配置:

代码语言:javascript复制 select * from t_user id = #{id}

创建mybatis-config.xml配置文件。

代码语言:javascript复制

使用案例:

代码语言:javascript复制public class MybatisApplication { public static final String URL = "jdbc:mysql://localhost:3306/mblog?useUnicode=true"; public static final String USER = "root"; public static final String PASSWORD = "123456"; public static void main(String[] args) { String resource = "mybatis-config.xml"; InputStream inputStream = null; SqlSession sqlSession = null; try { //读取mybatis-config.xml inputStream = Resources.getResourceAsStream(resource); //解析mybatis-config.xml配置文件,创建sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //创建sqlSession sqlSession = sqlSessionFactory.openSession(); //创建userMapper对象(UserMapper并没有实现类) UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //调用userMapper对象的方法 User user = userMapper.selectById(1); System.out.println(user); } catch (Exception e) { e.printStackTrace(); } finally { //关闭资源 try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } sqlSession.close(); } } }

运行输出:

代码语言:javascript复制User{id=1, userName='田维常', age=23, gender=1}

就这么简单的搞定了,下面我们来详细的分析这个案例背后的设计原理。

猜想Mybatis是如何设计的

从上面的案例中,我们可以大致可以猜测到Mybatis一共做了哪些步骤。

1.定位到mybatis-config.xml并读取装载。获取输入流InputStream。

2.解析输入流InputStream,把mybatis-config.xml配置文件中相关配置项解析,校验,保存起来。

3.创建sqlSessionFactory对象,在我们的印象里,session就是一次会话,所以我们可以理解sqlSessionFactory就是个工厂类,就专门创建sqlSession对象,并且这个sqlSessionFactory工厂类是唯一不变的(单例)。

4.创建sqlSession,SqlSession中肯定保存了配置文件内容信息和执行数据库相关的操作。

5.获取userMapper对象,但是UserMapper是接口,并且没有实现类。怎么就可以调用其方法呢?这里猜想可能用到了动态代理。

6.userMapper接口中的方法是如何关联到SQL的,这个猜想可能是有个专门映射的类,另外,肯定使用到了接口全路径名+方法名称,这个才能确保方法和SQL关联(主要是使用的时候,都是方法名必须和SQL中statementId一致,由此猜想的)。

7.最后底层使用JDBC去操作数据库。

8.作为一个持久化框架,很有可能会使用到缓存,用来存储每次查询数据。

仅仅是个人假设不清楚源码的情况,仅仅从这个简单的案例出发的,案例中没有差距、缓存,但是下面源码分析中是有的。

面试中遇到,让你来设计一个Mybatis如何设计?

配置文件解析,并保存于Configuration中

因为第一步是读取mybatis-config.xml配置文件,这里我们就没必要阅读这部分源码了,这里得到是InputStream输入流。接下来就是基于这个输入流进行一系列牛逼的操作。

我们将从下面这行代码开始:

代码语言:javascript复制SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactory没有构造方法,那么这里使用的就是默认无参构造方法,所以我们直接进去build方法。

代码语言:javascript复制//这个方法啥也没干 public SqlSessionFactory build(InputStream inputStream) { //调用的是另外一个build方法 return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //创建一个XMLConfigBuilder对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); 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. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }

该类中的build重载的方法如下:

SqlSessionFactory中提供了三种读取配置信息的方法后:字节流、字符流和Configuration配置类。

创建XMLConfigBuilder对象,这个类是BaseBuilder的子类,BaseBuilder类图:

看到这些子类基本上都是以Builder结尾,所以这里使用的是建造者设计模式。

这个类名可以猜出给类就是解析xml配置文件的。然后我们继续进入

代码语言:javascript复制public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser(inputStream,...); }

Mybatis对应解析包org.apache.ibatis.parsing:

XPathParser基于 Java XPath 解析器,用于解析 MyBatis中

mybatis-config.xmlmapper.xml

等 XML 配置文件 。

XPathParser主要内容:

继续上面的源码分析:

代码语言:javascript复制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对象,给属性设置相应值。

然后我们再回到SqlSessionFactoryBuilder中的build方法里:

代码语言:javascript复制XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); build(parser.parse());

先看parser.parse()方法:

代码语言:javascript复制public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //mybatis-config.xml的一级标签 parseConfiguration(parser.evalNode("/configuration")); return configuration; }

继续parseConfiguration()方法:

代码语言:javascript复制private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); 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")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }

结合 mybatis-config.xml配置文件和解析方法,可以得出如何关联:

mybatis-config.xml中如果把标签当做一级标签,那么有多少个二级标签可与定义呢?

在org.apache.ibatis.builder.xml下的mybatis-3-config.dtd中已经定义了

代码语言:javascript复制

与之对应的mybatis-config.xsd中

代码语言:javascript复制

同理,我们最关心的Mapper.xml中能定义哪些标签,也在mybatis-3-mapper.dtd中也定义了,另外也有与之对应的

mybatis-mapper.xsd文件中也能找到:

关于Mybatis中标签相关就介绍到此。关于Mybatis配置文件中有哪些标签现在是不是觉得很轻松就能找到了?其实在IDEA中也会提示的。

下面我们来看看这些标签内容是如何存入configuration对象中?(这里例举部分,挑几个相对重要的)。

propertiesElement()方法:

typeAliasesElement()方法:

插件plugins解析

pluginElement()方法:

代码语言:javascript复制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); configuration.addInterceptor(interceptorInstance); } } }

Configuration中interceptorChain用来存储所有定义的插件。

代码语言:javascript复制//interceptorChain中有个List interceptors protected final InterceptorChain interceptorChain = new InterceptorChain(); //存入interceptors中 public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }

InterceptorChain插件链(连接链),责任链模式。

代码语言:javascript复制public class InterceptorChain { private final List interceptors = new ArrayList(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } }

我们继续看看Mapper.xml是如何解析的。

mapper.xml解析

我们的Mapper.xml在mybatis-config.xml中的配置是这样的:

使用方式有以下四种:

代码语言:javascript复制

源码分析:

代码语言:javascript复制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中 configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); //使用java类名 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); //根据文件存放目录,读取XxxMapper.xml InputStream inputStream = Resources.getResourceAsStream(resource); //映射器比较复杂,调用XMLMapperBuilder //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); //使用绝对url路径 } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); //映射器比较复杂,调用XMLMapperBuilder 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."); } } } } } public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } private XMLMapperBuilder(.....) { super(configuration); this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; }

把这些UserMapper类似接口保存到configuration对象中。

代码语言:javascript复制configuration.addMapper(mapperInterface);

到这里,配置文件mybatis-config.xml和我们定义映射文件XxxMapper.xml就全部解析完成。

关于其他配置项,解析方式类似,最终都保存到了一个Configuration大对象中。

Configuration对象类似于单例模式,就是整个Mybatis中只有一个Configuration对象。

回到SqlSessionFactoryBuilder类

前面讲到了XMLConfigBuilder中的parse方法,并返回了一个Configuration对象。

代码语言:javascript复制build(parser.parse());

这个build方法就是传入一个Configuration对象,然后构建一个DefaultSqlSession对象。

代码语言:javascript复制public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }

继续回到我们的demo代码中这一行代码里。

代码语言:javascript复制SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

这一行代码就相当于:

代码语言:javascript复制SqlSessionFactory sqlSessionFactory = new new DefaultSqlSessionFactory();

到此。配置文件解析完毕。

配置文件解析流程

既然已经获取到了SqlSessionFactory,那么我们就可以构建SqlSession了。下面我们来看看构建SqlSession的整个过程。

构建SqlSession

前面已经做了配置文件的解析,那么现在我们来构建SqlSession。

代码语言:javascript复制sqlSession = sqlSessionFactory.openSession();

前面已经分析了,这里的sqlSessionFactory是DefaultSqlSessionFactory。那么此时调用的openSession()方法为DefaultSqlSessionFactory中的方法。

代码语言:javascript复制public class DefaultSqlSessionFactory implements SqlSessionFactory { //配置文件所有内容 private final Configuration configuration; //创建session @Override public SqlSession openSession() { //调用的是另外一个openSessionFromDataSource方法 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } //其实是调用这个方法 //protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //对应xml标签 ,这个在配置文件解析的时候就已经存放到configuration中了。 final Environment environment = configuration.getEnvironment(); //构建事务工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); //构建一个失误对象对象 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); //创建一个executor来执行SQL final Executor executor = configuration.newExecutor(tx, execType); //创建一个DefaultSqlSession对象并返回 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) { if (environment == null || environment.getTransactionFactory() == null) { return new ManagedTransactionFactory(); } return environment.getTransactionFactory(); } 创建事务Transaction

Transaction类图:

事务工厂类型可以配置为JDBC类型或者MANAGED类型。

JdbcTransactionFactory生产JdbcTransaction。ManagedTransactionFactory生产ManagedTransaction。

如果配置的JDBC,则会使用Connection对象的commit()、rollback()、close()方法来管理事务。

如果我们配置的是MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们是本地跑的程序,如果配置成MANAGED就会不有任何事务。

但是,如果是Spring+Mybatis,则没有必要配置,因为我们会直接在applicationContext.xml里配置数据源和事务管理器,从而覆盖Mybatis的配置。

把事务传给newExecutor()方法创建执行器Executor对象。

代码语言:javascript复制configuration.newExecutor(tx, execType) 创建Executor

调用configuration的newExecutor方法创建Executor。

代码语言:javascript复制final Executor executor = configuration.newExecutor(tx, execType); //Configuration中 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; }

此方法分三个步骤。

第一步:创建执行器

Executor的基本类型有三种:SIMPLE为默认类型。

代码语言:javascript复制public enum ExecutorType { SIMPLE, REUSE, BATCH }

Executor类图:

为什么要让抽象类BaseExecutor实现Executor接口,然后让具体实现类继承抽象类呢?

这就是模板方法模式的实现。

模板方法模式就是定义一个算法骨架,并允许子类为一个或者多个步骤提供实现。模板方法是得子类可以再不改变算法结构的情况下,重新定义算法的某些步骤。

抽象方法是在子类汇总实现的,每种执行器自己实现自己的逻辑,BaseExecutor最终会调用到具体的子类。

抽象方法

代码语言:javascript复制protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; protected abstract List doFlushStatements(boolean isRollback) throws SQLException; protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; 第二步:缓存装饰

在上面代码中的第二步

代码语言:javascript复制if (cacheEnabled) { executor = new CachingExecutor(executor); }

如果cacheEnabled=true,会用装饰器设计模式对Executor进行装饰。

第三步:插件代理

缓存装饰完后,就会执行

代码语言:javascript复制executor = (Executor) interceptorChain.pluginAll(executor);

这里会对Executor植入插件逻辑。

关于插件:推荐阅读,mybatis插件原理详解

比如:分页插件中就需要把插件植入的Executor

好了,到此,执行器创建的就搞定了。

Executor创建完毕后,就该创建DefaultSqlSession了,请看代码:

代码语言:javascript复制//创建一个DefaultSqlSession对象并返回 return new DefaultSqlSession(configuration, executor, autoCommit);

进入DefaultSqlSession的构造方法中:

代码语言:javascript复制public class DefaultSqlSession implements SqlSession { private final Configuration configuration; private final Executor executor; public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; } }

DefaultSqlSession中包含两个重要属性:

自此,SqlSession对象构建完毕。

代码语言:javascript复制sqlSession = sqlSessionFactory.openSession();

这里的sqlSession就是:

代码语言:javascript复制sqlSession = new DefaultSqlSession();

整个构建过程:

获取UserMapper接口的代理对象

前面我们已经Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

代码语言:javascript复制UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

我们已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。

所以,我们的故事就从这个类开始,直接进入DefaultSqlSession的getMapper方法。

代码语言:javascript复制//DefaultSqlSession中 private final Configuration configuration; //type=UserMapper.class @Override public T getMapper(Class type) { return configuration.getMapper(type, this); }

三个疑问:

问题1:getMapper返回的是个什么对象?

上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中

代码语言:javascript复制//Configuration中 protected final MapperRegistry mapperRegistry = new MapperRegistry(this); ////type=UserMapper.class public T getMapper(Class type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }

这里也没做什么,继续调用MapperRegistry中的getMapper:

代码语言:javascript复制//MapperRegistry中 public class MapperRegistry { //主要是存放配置信息 private final Configuration config; //MapperProxyFactory 的映射 private final Map> knownMappers = new HashMap(); //获得 Mapper Proxy 对象 //type=UserMapper.class,session为当前会话 public T getMapper(Class type, SqlSession sqlSession) { //这里是get,那就有add或者put final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { //创建实例 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } //解析配置文件的时候就会调用这个方法, //type=UserMapper.class public void addMapper(Class type) { // 判断 type 必须是接口,也就是说 Mapper 接口。 if (type.isInterface()) { //已经添加过,则抛出 BindingException 异常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //添加到 knownMappers 中 knownMappers.put(type, new MapperProxyFactory(type)); //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); //标记加载完成 loadCompleted = true; } finally { //若加载未完成,从 knownMappers 中移除 if (!loadCompleted) { knownMappers.remove(type); } } } } }

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在这个类里可以理解使用了单例模式methodCache(注册式单例模式),和工厂模式getMapper()。

代码语言:javascript复制public class MapperProxyFactory { private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap(); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public T newInstance(SqlSession sqlSession) { //创建MapperProxy对象 final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } //最终以JDK动态代理创建对象并返回 protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } }

继续看这行重点代码:

代码语言:javascript复制 return mapperProxyFactory.newInstance(sqlSession);

newInstance()方法:

依然是稳稳的基于 JDK Proxy 实现,而 InvocationHandler 参数是 MapperProxy 对象。

代码语言:javascript复制//UserMapper 的类加载器 //接口是UserMapper //h是mapperProxy对象 public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h){ } 问题2:为什么就可以调用他的方法?

上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

代码语言:javascript复制//果然实现了InvocationHandler接口 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; } //调用userMapper.selectById()实质上是调用这个invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //如果是Object的方法toString()、hashCode()等方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (method.isDefault()) { //JDK8以后的接口默认实现方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //创建MapperMethod对象 final MapperMethod mapperMethod = cachedMapperMethod(method); //下一篇再聊 return mapperMethod.execute(sqlSession, args); } }

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。

userMapper=$Proxy6@2355。

现在我们回答前面的问题:

为什么要在MapperRegistry中保存一个工厂类,原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

MapperProxy如何实现对接口的代理?

我们知道,JDK动态代理有三个核心角色:

被代理类(即就是实现类)接口实现了InvocationHanndler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:

在Mybatis中,JDK动态代理为什么不需要实现类呢?

这里我们的目的其实就是根据一个可以执行的方法,直接找到Mapper.xml中statement ID ,方便调用。

最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象,

问题3:到底是怎么根据Mapper.java找到Mapper.xml的?

最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

自此上面三个问题已经全部解决。

整个流程

自此,我们已经拿到了UserMapper接口的代理对象。接下来我们就去调用这个代理对象的方法。

UserMapp中的方法和SQL关联

加油坚持,我们已经到从这一行代码开始:

代码语言:javascript复制User user = userMapper.selectById(1));

这一行代码搞完,也就代表着我们Mybatis源码第一遍搞完。想想就很开心,嘿嘿~,加油

继续开撸。

通过前面的分析,我们已经知道了userMapper是通过动态代理生成的代理对象,所以调用这个代理对象的任意方法都是执行触发管理类MapperProxy的invoke()方法。

分为两部分,

1- MapperProxy.invoke()到Executor.query(方法和SQL关联)。2- Executor.query到JDBC中的SQL执行

第一部分流程图:

MapperProxy.invoke()

先来看看这个invoke()方法里有些什么逻辑。

代码语言:javascript复制//MapperProxy类 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //首先判断是否为Object本身的方法,是则不需要去执行SQL, //比如:toString()、hashCode()等方法。 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (method.isDefault()) { //判断是否JDK8以后的接口默认实现方法。 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // final MapperMethod mapperMethod = cachedMapperMethod(method); // return mapperMethod.execute(sqlSession, args); }

从缓存获取MapperMethod,这里加入了缓存主要是为了提升MapperMethod的获取速度。这个设计非常的有意思,缓存的使用在Mybatis中也是非常之多。

代码语言:javascript复制private final Map methodCache; private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }

Map的computeIfAbsent方法:根据key获取值,如果值为null,则把后面的Object的值付给key。

继续看MapperMethod这个类,定义了两个属性command和method,以及两个静态内部类。

代码语言:javascript复制public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public static class SqlCommand { private final String name; private final SqlCommandType type; public SqlCommand(Configuration configuration, Class mapperInterface, Method method) { final String methodName = method.getName(); final Class declaringClass = method.getDeclaringClass(); //获得 MappedStatement 对象 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); // 找不到 MappedStatement if (ms == null) { // 如果有 @Flush 注解,则标记为 FLUSH 类型 if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } else { // 抛出 BindingException 异常,如果找不到 MappedStatement //(开发中容易见到的错误)说明该方法上,没有对应的 SQL 声明。 throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } //找到 MappedStatement } else { // 获得 name //id=com.tian.mybatis.mapper.UserMapper.selectById name = ms.getId(); // 获得 type=SELECT type = ms.getSqlCommandType(); //如果type=UNKNOWN if (type == SqlCommandType.UNKNOWN) { // 抛出 BindingException 异常,如果是 UNKNOWN 类型 throw new BindingException("Unknown execution method for: " + name); } } } private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, Class declaringClass, Configuration configuration) { // 获得编号 //com.tian.mybatis.mapper.UserMapper.selectById String statementId = mapperInterface.getName() + "." + methodName; //如果有,获得 MappedStatement 对象,并返回 if (configuration.hasStatement(statementId)) { //mappedStatements.get(statementId); //解析配置文件时候创建并保存Map mappedStatements中 return configuration.getMappedStatement(statementId); // 如果没有,并且当前方法就是 declaringClass 声明的,则说明真的找不到 } else if (mapperInterface.equals(declaringClass)) { return null; } // 遍历父接口,继续获得 MappedStatement 对象 for (Class superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } // 真的找不到,返回 null return null; } //.... } public static class MethodSignature { private final boolean returnsMap; private final Class returnType; private final Integer rowBoundsIndex; //.... }

SqlCommand封装了statement ID,比如说:

代码语言:javascript复制com.tian.mybatis.mapper.UserMapper.selectById

和SQL类型。

代码语言:javascript复制public enum SqlCommandType { UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH; }

另外还有个属性MethodSignature,主要是封装的是返回值的类型和方法入参。这里我们debug看看这个MapperMethod对象返回的内容和我们案例中代码的关联。

妥妥的,故事继续,我们接着看MapperMethod中execute方法。

MapperMethod.execute

先来看看这个方法的整体逻辑:

代码语言:javascript复制 public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case SELECT: //部分代码省略 Object param = method.convertArgsToSqlCommandParam(args); //本次是QUERY类型,所以这里是重点 result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } return result; }

这个方法中,根据我们上面获得的不同的type(INSERT、UPDATE、DELETE、SELECT)和返回类型:

1.调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。2.调用sqlSession的insert()、update()、dalete()、selectOne()方法。我们这个案例是查询,这里回到了DefaultSqlSession中selectOne方法中。

SqlSession.selectOne方法

继续DefaultSqlSession中的selectOne()方法:

代码语言:javascript复制//DefaultSqlSession中 @Override public T selectOne(String statement, Object parameter) { //这是一种好的设计方法 //不管是执行多条查询还是单条查询,都走selectList方法(重点) List list = this.selectList(statement, parameter); if (list.size() == 1) { //如果只有一条就返回第一条 return list.get(0); } else if (list.size() > 1) { //(开发中常见错误)方法定义的是返回一条数据,结果查出了多条数据,就会报这个异常 throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { //数据库中没有数据就返回null return null; } }

这里调用的是selectList方法。

代码语言:javascript复制@Override public List selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); } @Override public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { //从configuration获取MappedStatement //此时的statement=com.tian.mybatis.mapper.UserMapper.selectById MappedStatement ms = configuration.getMappedStatement(statement); //调用执行器中的query方法 return executor.query(...); } catch (Exception e) { //..... } finally { ErrorContext.instance().reset(); } }

在这个方法里是根据statement从configuration对象中获取MappedStatement。

代码语言:javascript复制MappedStatement ms = configuration.getMappedStatement(statement);

在configuration中getMappedStatement方法:

代码语言:javascript复制//存放在一个map中的 //key是statement=com.tian.mybatis.mapper.UserMapper.selectById,value是MappedStatement protected final Map mappedStatements = new StrictMap(); public MappedStatement getMappedStatement(String id) { return this.getMappedStatement(id, true); } public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) { return mappedStatements.get(id); }

而MappedStatement里面有xml中增删改查标签配置的所有属性,包括id、statementType、sqlSource、入参、返回值等。

到此,我们UserMapper类中的方法已经和UserMapper.xml中的sql给彻底关联起来了。继续

代码语言:javascript复制executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

这里执行的是执行器Executor中的query()方法。

Executor.query()方法

这里的Executor对象是在调用openSession方法的时候创建的。

下面来看看调用执行器的query()放的整个流程(第二部分流程):

下面我们看看具体源码是如何实现的。

CachingExecutor.query()

在CachingExecutor中

代码语言:javascript复制@Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }

BoundSql中主要是SQL和参数:

既然是缓存,我们肯定想到key-value数据结构。

下面来看看这个key生成规则:

这个二级缓存是怎么构成的呢?并且还要保证在查询的时候必须是唯一。

也就说,构成key主要有:

方法相同、翻页偏移量相同、SQL相同、参数相同、数据源环境相同才会被认为是同一个查询。

这里大家知道这个层面就已经阔以了。如果向更深入的搞,就得把hashCode这些扯进来了,请看上面这个张图里前面的几个属性。

处理二级缓存

首先是从ms中取出cache对象,判断cache对象是否为null,如果为null,则没有查询二级缓存和写入二级缓存的流程。

代码语言:javascript复制@Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); //判断是否有二级缓存 if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }

那么这个Cache对象是什么创建的呢?

二级缓存如何开启?

配置项:

代码语言:javascript复制

cacheEnabled=true表示二级缓存可用,但是要开启话,需要在Mapper.xml内配置。

代码语言:javascript复制

对配置项属性说明:

flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。readOnly="true",表示任何获取对象的操作,都将返回同一实例对象。如果readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。

在解析Mapper.xml的XMLMapperBuilder类中的cacheElement()方法里。

解析二级缓存中的标签:

创建Cache对象:

二级缓存处理完了,就来到BaseExecutor的query方法中。

BaseExecutor.query()

第一步,清空缓存

代码语言:javascript复制if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }

queryStack用于记录查询栈,防止地柜查询重复处理缓存。

flushCache=true的时候,会先清理本地缓存(一级缓存)。

如果没有缓存会从数据库中查询

代码语言:javascript复制 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

在看看这个方法的逻辑:

代码语言:javascript复制private List queryFromDatabase(...) throws SQLException { List list; //使用占位符的方式,先抢占一级缓存。 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { //删除上面抢占的占位符 localCache.removeObject(key); } //放入一级缓存中 localCache.putObject(key, list); return list; }

先在缓存使用占位符占位,然后查询,移除占位符,将数据放入一级缓存中。

执行Executor的doQuery()方法,默认使用SimpleExecutor。

代码语言:javascript复制list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

下面就来到了SimpleExecutor中的doQuery方法。

SimpleExecutor.doQuery代码语言:javascript复制@Override public List doQuery(....) throws SQLException { Statement stmt = null; try { //获取配置文件信息 Configuration configuration = ms.getConfiguration(); //获取handler StatementHandler handler = configuration.newStatementHandler(....); //获取Statement stmt = prepareStatement(handler, ms.getStatementLog()); //执行RoutingStatementHandler的query方法 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } 创建StatementHandler

在configuration中newStatementHandler()里,创建了一个StatementHandler,先得到RoutingStatementHandler(路由)。

代码语言:javascript复制public StatementHandler newStatementHandler() { StatementHandler statementHandler = new RoutingStatementHandler //执行StatementHandler类型的插件 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }

RoutingStatementHandler创建的时候是用来创建基本的StatementHandler的。这里会根据MapperStament里面的statementType决定StatementHandler类型。

默认是PREPARED。

StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的ResultHandler。

上面说的这几个对象正式被插件拦截的四大对象,所以在创建的时都要用拦截器进行包装的方法。

对于插件相关的,请看前面发的插件的文章。

创建Statement

创建对象后就会执行RoutingStatementHandler的query方法。

代码语言:javascript复制//RoutingStatementHandler中 @Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { //委派delegate=PreparedStatementHandler return delegate.query(statement, resultHandler); }

这里设计很有意思,所有的处理都要使用RoutingStatementHandler来路由,全部通过委托的方式进行调用。

然后执行到PreparedStatementHandler中的query方法。

代码语言:javascript复制@Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; //JDBC的流程了 ps.execute(); //处理结果集,如果有插件代理ResultHandler,会先走到被拦截的业务逻辑中 return resultSetHandler.handleResultSets(ps); }

看到了ps.execute();表示已经到JDBC层面了,这时候SQL就已经执行了。后面就是调用DefaultResultSetHandler类进行处理。

到这里,SQL语句就执行完毕,并将结果集赋值并返回了。

整个流程

从调用userMapper的selectById()方法开始,到JDBC中SQL执行的整个流程图。

感兴趣的小伙伴,可以对照着这张流程图就行一步一步的debug。

总结

本文从一个案例代码出发,到解析Mybatis的配置文件,到创建SqlSession对象,到获取UserMapper接口的代理对象,到调用代理对象方法,再到让方法和SQL关联起来,最后执行SQL,返回结果集。

涉及到的设计模式:单例模式、建造者设计模式、模板方法模式、代理模式、装饰器模式等。

技术难点:动态代理。



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3