😀 这里写文章的前言: 一个简单的开头,简述这篇文章讨论的问题、目标、人物、背景是什么?并简述你给出的答案。
可以说说你的故事:阻碍、努力、结果成果,意外与转折。
📝 MyBatis 的工程流程分析 MyBatis 是我们在学习Java框架,也就是学习完JavaWeb的知识后,要学习到的一个ORM的框架. 我也是学习&使用过后,再次对源码进行阅读的. 所以这篇文章记录 MyBatis 的一个 work flow.
先放上项目地址 : https://github.com/baoyang23/mybtatis-analysis/tree/master/mybatis-work-flow
有兴趣的同学,可以clone下来看看.
代码案例 先放上案列的代码, 然后我们可以挨个的分析
public class InitHelloMyBatis { public static void main (String[] args) throws IOException { InputStream mybatisInputStream = Resources.getResourceAsStream("mybatis-config.xml" ); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ().build(mybatisInputStream); SqlSession session = sqlSessionFactory.openSession(); BlogMapper blogMapper = session.getMapper(BlogMapper.class); TbBlog tbBlog = blogMapper.selectBlog(1 ); System.out.println(tbBlog); } }
这里说下大致流程:
使用 Resources 来读取 mybatis-config.xml配置文件, 如果该文件不存在或者读取出来 InputStream 是 null 的话,程序就会抛出 IOException 的错误来.
读取配置没有问题,来到 new SqlSessionFactoryBuilder().build(io) 来构建出一个 SqlSessionFactory 来, 这里构建出来的 SqlSessionFactory 肯定是有已经讲配置文件给全部加载进去了的.
SqlSessionFactory.openSession() 从 SqlSessionFactory 中获取一次会话, 然后可以从会话中获取出接口(BlogMapper)来,这里是不是有点好奇,明明这就是一个接口,也没有实现类,怎么就可以get出一个接口对象来?获取出接口来,然后就可以调用接口中的方法, 根据id查询出数据来.
可以看到,根据从官网写的一个列子,从表面来看,代码量并不是很多. 所以接下来点去源码,去跟进源码中的每个方法,到底做了些什么事情.
读取配置文件 InputStream mybatisInputStream = Resources.getResourceAsStream("mybatis-config.xml" );
org.apache.ibatis.io.Resources (Class).
可以看到MyBatis源码还写了一个 ClassLoader的包装类,通过ClassLoaderWrapper包装类来讲配置文件转化为InputSream.
如果返回的InputStream是null,就会抛出IOException来.
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper ();public static InputStream getResourceAsStream (ClassLoader loader, String resource) throws IOException { InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader); if (in == null ) { throw new IOException ("Could not find resource " + resource); } return in; }
于是我们接着看 ClassLoaderWrapper 是怎么 读取配置文件 & 转化为 InputStream 流的.
ClassLoader[] getClassLoaders(ClassLoader classLoader) { return new ClassLoader []{ classLoader, defaultClassLoader, Thread.currentThread().getContextClassLoader(), getClass().getClassLoader(), systemClassLoader}; } InputStream getResourceAsStream (String resource, ClassLoader[] classLoader) { for (ClassLoader cl : classLoader) { if (null != cl) { InputStream returnValue = cl.getResourceAsStream(resource); if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } if (null != returnValue) { return returnValue; } } } return null ; }
至此,MyBatis读取 mybatis-config.xml 配置文件也就是解析完毕,可以看到采用了自己写的 ClassLoaderWrapper来操作的, 传递一种 ClassLoader进来,其默认的&系统&线程的,加一起也是有四种. 最后挨个进来迭代,满足条件的会读取文件转化为InputStream,如果都是null的话,也会返回null.
获取SqlSessionFactory & 解析配置文件 new SqlSessionFactoryBuilder() 也是new了一个 SqlSessionFactoryBuild,个人理解 SqlSessionFactoryBuilder 就是专程用来构建出 SqlSessionFactory 来的,毕竟其后面有一个 build 方法.
Problem ? 这里有个问题,为什么不将 SqlSessionFactoryBuilder 的build 方法,修改为静态的 ? 如果修改为静态的话,那就不用new了,就可以直接 SqlSessionFactoryBuilder.build(mybatisInputStream);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ().build(mybatisInputStream);
SqlSessionFactory 接着我们来到 SqlSessionFactory 的 build 方法.
这里在 finnaly 中, 可以看到 ErrorContext 利用了 ThreadLocal
public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties) { try { 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) { } } }
new XmlConfigBuilder() 方法: 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; } ---------------------------------------- public XPathParser (InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) { commonConstructor(validation, variables, entityResolver); this .document = createDocument(new InputSource (inputStream)); } private Document createDocument (InputSource inputSource) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true ); factory.setValidating(validation); factory.setNamespaceAware(false ); factory.setIgnoringComments(true ); factory.setIgnoringElementContentWhitespace(false ); factory.setCoalescing(false ); factory.setExpandEntityReferences(true ); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler () { @Override public void error (SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError (SAXParseException exception) throws SAXException { throw exception; } @Override public void warning (SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException ("Error creating document instance. Cause: " + e, e); } } ------------- public Document parse (InputSource is) throws SAXException, IOException { if (is == null ) { throw new IllegalArgumentException ( DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "jaxp-null-input-source" , null )); } if (fSchemaValidator != null ) { if (fSchemaValidationManager != null ) { fSchemaValidationManager.reset(); fUnparsedEntityHandler.reset(); } resetSchemaValidator(); } domParser.parse(is); Document doc = domParser.getDocument(); domParser.dropDocumentReferences(); return doc; } --------------- 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; }
到这里,就可以看到 this构造方法以及其之前还有new对象的方法,都已经走完了. 这上面的方法,基本都是再为后面的解析xml文件做准备, 并且还有一些初始化数据的赋值操作.
Note : 注意这里的 BaseBuilder是抽象类,其实现类是有好几个的. 这种写法,其实是将子类的一些common的方法,写入到 BaseBuilder父类中,然后不同的方法,需要子类自己去重写这个方法实现自己的业务逻辑. 当然一些参数也是可以放在抽象类中.
build(parser.parse())方法 parser.parse() 方法 :
public Configuration parse () { if (parsed) { throw new BuilderException ("Each XMLConfigBuilder can only be used once." ); } parsed = true ; parseConfiguration(parser.evalNode("/configuration" )); return configuration; } --------------------------------- private void parseConfiguration (XNode root) { try { 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); 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); } }
这里是对 parser.parse() 调用玩返回的 Configuration 传入到新创建的 DefaultSqlSessionFactory 对象中.
也就是说,我们拿到的 SqlSessionFactory 是 DefaultSqlSessionFactory.
public SqlSessionFactory build (Configuration config) { return new DefaultSqlSessionFactory (config); }
获取 SqlSession SqlSession session = sqlSessionFactory.openSession(); private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null ; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession (configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
至此, 可以看到 MyBatis 从SqlSessionFactory中获取出来SqlSession会话, 也可以理解为几个步骤.
首先获取事务工厂, 然后再从事务工厂中获取一个事务来, JdbcTransaction 有兴趣的同学可以看下这个类,里面也是封装了写 commit / rollback等方法. 再接着获取出 执行器(Executor),这里从代码哪里看,执行器还是有几种类型的,也执行自定义. 最后new了一个 DefaultSqlSession 回去.
session.getMapper(BlogMapper.class) 接着看,上一步返回的session,是怎么获取到我们写的Mapper接口文件(Mapper这种文件,在解析配置文件的时候,其实就已经解析到MyBatis的configuration里面去了).
@SuppressWarnings("unchecked") public <T> T getMapper (Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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); } } ------------------------------------------- public T newInstance (SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy <>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } @SuppressWarnings("unchecked") protected T newInstance (MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class [] { mapperInterface }, mapperProxy); } ------- BlogMapper blogMapper = session.getMapper(BlogMapper.class);
从SqlSession 中获取 BlogMapper我们写的mapper流程, 先从 knownMappers 中根据key获取出来之前加载配置已经加载完毕的信息,如果没用的话,就会抛出没有的异常. 最后使用 Proxy.newProxyIntsance来生成的一个类似接口实现类的代码,不同的是, 在 new MapperProxy 的时候,就已经将接下来需要的信息全部传入进去.
blogMapper.selectBlog(1) 方法 竟然 BlogMapper是通过Proxy.newInstance获取出来的,那它是怎么查询的数据库? 又是怎么将字段给映射到 Object一一对应的呢 ?
debug会走到 MapperProxy的invoke方法来
@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 { return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } ------------------ public Object execute (SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break ; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break ; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break ; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null ; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); 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; }
这里可以看到,先是对 INSERT / UPDATE / DELETE / SELECT 进行分类处理, 然后对再分别根据不同的类型进行处理. 都是先有转化为sql,然后将执行结果赋值给result.
至于里面详细的查询执行sql,还有动态sql,每次会话缓存等,后面看到详细的情况再一一说明. 这里只是对MyBatis的基本工作进行了一个梳理. 然后后面再根据基础梳理,再来挨个击碎他们.
至此, MyBatis的入门分析流程是结束的. 理解起来,应该还不是那么难.
🤗 总结归纳 总结文章的内容
根据 com.iyang.mybatis.InitHelloMyBatis , 也就是入门的demo来梳理下流程.
读取配置文件,也就是将配置文件读取,转化为inptStream流.
利用 SqlSessionFactoryBuilder 来 解析流, 起内部又利用 BaseBuilder(其又很多实现类,这里用的XMLConfigBuilder)也解析xml配置文件. Configuration configuration 该类中是保存着xml配置文件的很多信息. 然后 DefaultSqlSessionFactory 中有configuration字段,也就是属性.
然后从 DefaultSqlSessionFactory 中获取 SqlSession来, 并且也会是否开启事务(参考:org.apache.ibatis.transaction.jdbc.JdbcTransaction)类,然后获取 Executor,Executor也是有几种种类的,也可以自己自定义,最后返回一个 DefaultSqlSession 来.
然后从 SqlSession 中获取我们的接口Mapper, 最后也是利用 Proxy.newProxyInstance 来生成的接口,也就是代理(这里打印出地址池或者debug看地址池,就会很明显的看到是代理对象).
最后走查询的方法, 也就是走到了 MapperProxy 来. 可以看到MapperProxy里面是有sqlSession的,而SqlSession是有 Executor/configuration/autoCommit等信息的, 有了sqlSession,就剩下执行sql和映射sql查询出来的结果来了(这里是 mapperMethod.execute(sqlSession, args) —> org.apache.ibatis.binding.MapperMethod#execute 走到这里来了,这里之后就会分类进行处理,然后映射sql语句).
至此,一个 MyBatis 的 HelloWorld分析流程是完毕的.
📎 参考文章
💡 有关文章的问题,欢迎您在底部评论区留言,一起交流~