Mybatis,用了这么久,背景自不用说。我还记得,第一次使用,还在成铁科研,作电务那个OA系统的时候,在二代、罗尼玛的带领下,首次接触到的。因为以前的工程一直使用Hibernate,一下切换到Mybatis以后,最大的感觉就是:我要成批成批的写sql~而后就是理论上一直在讲的:Hibernate是全自动,Mybatis是半自动。直到多年后的今天,这些依然我是工做生活的主题。另外,最近看到一个点:据统计国外程序员比较喜欢使用Hibernate,而国内的大片是Mybatis的天下。主要缘由,和DDD这些还有些关系。固然,这些都不是我当下要深刻研究的东西,我就想一探究竟:Mybatis如何将写到xml文件中sql执行的。java
因为高校教学的缘由,大部分Javaer几乎都是自学出来的,即便是计算机科班也是如此。但是第一次咱们使用一本类《21天学会Java》书籍,看到最后几个章节的时候,都会被所谓的JDBC对本地数据库进行CURD的代码所“恶心”到。心想:怎么有这么丑的代码~流程太多,每次都记不住啊~每次用,都要百度一下。恩,下面就是这个代码的片断:mysql
(不要纠结异常捕获的问题,对于一本21天学习xxx的书籍,不要抱有工业代码的指望~~)程序员
我使用本地的数据表,封装了个工具类:sql
public class BDUtil { static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost:3306/my_test"; static final String USER = "root"; static final String PASS = "root"; static Connection conn = null; static Statement stmt = null; static { try { Class.forName(JDBC_DRIVER); conn = DriverManager.getConnection(DB_URL, USER, PASS); stmt = conn.createStatement(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public static void printSqlResult(String sql) { try { ResultSet resultSet = stmt.executeQuery(sql); int columnCount = resultSet.getMetaData().getColumnCount(); while (resultSet.next()) { for (int i = 1; i <= columnCount; i++) { System.out.print(resultSet.getObject(i) + " "); } System.out.println(); } } catch (SQLException e) { e.printStackTrace(); } } public static void insertSqlRseult(String insertSql) { try { stmt.execute(insertSql); } catch (SQLException e) { e.printStackTrace(); } } public static void close() { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void main(String[] args) { LocalDateTime now = LocalDateTime.now(); insertSqlRseult("insert into ref_test_table (ref_id,col,create_date) values (7,'98_ef','" + now.toString() + "')"); close(); } }
那这么下来,复杂的流程不说,我要修改个什么sql,耦合性过高了吧,要直接改源代码。明显不是工业代码的首选。那么Mybatis的整个框架,就是将上面的代码进行进一步封装,固然封装的源码远远多于上面。数据库
在非集成的状况下,Mybatis使用起来,相对来讲比较简单,大概须要四个文件:apache
下面是我本地的前三种文件的源码:session
mybatis-config.xml:mybatis
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/my_test"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/BlogMapper.xml"/> </mappers> </configuration>
BlogMapper.xml:app
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.mybatis.mapper.BlogMapper"> <resultMap id="BaseResultMap" type="org.mybatis.example.entity.RefTestTable"> <id column="id" property="id" jdbcType="INTEGER" /> <id column="ref_id" property="refId" jdbcType="INTEGER" /> <id column="col" property="col" jdbcType="VARCHAR" /> <id column="create_date" property="createDate" jdbcType="INTEGER" /> </resultMap> <select id="selectBlog" resultMap="BaseResultMap"> select * from ref_test_table where id = #{id} </select> </mapper>
BlogMapper:框架
package org.mybatis.mapper; import org.mybatis.example.entity.RefTestTable; /** * @ClassName: BlogMapper * @Author: jicheng * @CreateDate: 2019/1/26 下午5:43 */ public interface BlogMapper { RefTestTable selectBlog(int id); }
接下俩就直接可使用这些配置,直接操做数据库:
package org.mybatis.example; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.io.Resources; import org.mybatis.example.entity.RefTestTable; import org.mybatis.mapper.BlogMapper; import java.io.InputStream; /** * @ClassName: Main * @Author: jicheng * @CreateDate: 2019/1/26 下午5:40 */ public class Main { public static void main(String[] args) { String resource = "mybatis-config.xml"; SqlSession session = null; try { InputStream inputStream = Resources.getResourceAsStream(resource); // ①初始化过程 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // ② session = sqlSessionFactory.openSession(); // ③ BlogMapper mapper = session.getMapper(BlogMapper.class); // ④ RefTestTable refTestTable = mapper.selectBlog(3); System.out.println(refTestTable); } catch (Exception e) { e.printStackTrace(); } finally { if (session != null) { session.close(); } } } }
可见,如此下来,代码的可配置性、解耦合性、简洁性大幅度提高。那接下来,咱们就一探究竟,一步步解开,Mybatis是如何进行封装的。这篇文章,咱们先来看看上面代码中的①
一步步深刻进去,先来看看这个类:org.apache.ibatis.session.SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder { public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //内部使用jdk中xpath解析xml的功能 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); /** * 这一步是重点解析xml: * 1:将配置文件xml解析出来; * 2:建立默认的sessionFactory,内部持有解析出来的配置属性 */ 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) { // 使用默认的SqlSessionFactory return new DefaultSqlSessionFactory(config); } }
进一步的,咱们来看看进一步调用XMLConfigBuilder的细节:
public class XMLConfigBuilder extends BaseBuilder { public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { /** * XPathParser是一个具体的使用xpath的解析工具类 */ this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } /** * environment,props都是null * 正常初始化其实没有对这些进行赋值 */ private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { // 这个Configuration对象也是有猫腻,后面看 super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); // 这个标识位表示这个配置没有被解析过,后面调用了parse()方法,会进行翻转 this.parsed = false; this.environment = environment; this.parser = parser; } }
下面就是XPathParser :
public class XPathParser { private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor //(jicheng:可见,老外的代码也不完美) try { // xml文档解析构建类工厂 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 下面是对建立xml文档构建器的一些参数的设置,例如:是否要进行DTD校验 factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); // 建立xml文档构建器 DocumentBuilder builder = factory.newDocumentBuilder(); // 这里的DTD解析实现类是:org.apache.ibatis.builder.xml.XMLMapperEntityResolver 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); } } /** * * @param inputStream 这里对应的就是配置文件mybatis-config.xml * @param validation 是否对配置文件使用dtd解析器进行校验 * @param variables 这个暂时为null * @param entityResolver MyBatis dtd的脱机实体解析器,主要是对inputStream的文件进行校验 */ public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) { commonConstructor(validation, variables, entityResolver); this.document = createDocument(new InputSource(inputStream)); } private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; // JDK的xpath工厂对象 XPathFactory factory = XPathFactory.newInstance(); // 使用xpath工厂对象生成xpath对象,用于后面解析xml文件使用 this.xpath = factory.newXPath(); } }
到此,一个xpath的解析器就建立完成,并读入了配置文件,下面咱们来看看如何对配置文件进行进一步的属性读取的,让咱们先回到下面的代码:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 重点来看看这个parse.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. } } }
重点,要看看org.apache.ibatis.builder.xml.XMLConfigBuilder#parse方法:
public class XMLConfigBuilder extends BaseBuilder { public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 控制每一个配置文件只被解析一次 parsed = true; // 包装根节点 XNode root = parser.evalNode("/configuration"); // 重点是解析、加载configuratio节点下面的各个配置属性 parseConfiguration(root); return configuration; } private void parseConfiguration(XNode root) { try { /** * 很是明显的能够看出: * 咱们在配置文件中配置的每一个xml标签,在 * 这个方法中都会被读取,解析出来属性 */ 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); } } }
接下来先详细讲解一个小点
首先咱们来看看前面的猫腻Configuration对象的构造函数,到底干了啥:
public Configuration() { // 这里的注册主要实现了别名对应关系,对于后面的属性直接使用这些别名,就能够直接加载对应的类了 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); }
mybatis的内部大量使用了这种别名的机制,利于咱们配置的时候,直接使用一个单词,就能够对应的找到实现类,有点像IoC的概念。这个对后面咱们xml解析,有很大的帮助。下面就看看如何将配置文件中的数据库相关配置,加载到程序里面的:
public class XMLConfigBuilder extends BaseBuilder { private void environmentsElement(XNode context) throws Exception { // 这里的context表示的是environments标签 if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { // 遍历全部environments标签下面的子标签environment String id = child.getStringAttribute("id"); // 判断当前读取的结点是否是默认的结点 if (isSpecifiedEnvironment(id)) { // 加载事物管理器工程,这里配置是JDBC, // 对应的实现类是: //org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 记载对应的数据源生成工厂,重点加载数据库配置属性的地方 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); // 获取数据源 DataSource dataSource = dsFactory.getDataSource(); // 最终生成一个数据库的环境对象,设置到公共的Configuration配置对象中 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } } private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); //这里主要对<dataSource type="POOLED"> //这个标签的子标签全部的属性,进行读取,变成Properties Properties props = context.getChildrenAsProperties(); //这里对应的DataSourceFactory,若是type是POOLED,那么类就是PooledDataSourceFactory DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); //这里是对数据源的初始化,结合配置文件中配置的参数 factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); } }
下面是数据源生成工厂的实现类:
public class UnpooledDataSourceFactory implements DataSourceFactory { private static final String DRIVER_PROPERTY_PREFIX = "driver."; private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); protected DataSource dataSource; public UnpooledDataSourceFactory() { this.dataSource = new UnpooledDataSource(); } @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); /** * 这个是将配置文件中对于数据源的配置与具体的datasource对象创建关联的核心! * 主要是将具体的DataSource实现类中的属性的setter与getter方法进行读取, * 而后根据具体的配置名称(name属性的值),反射调用对应DataSource中的 * setter方法。因此具体的DataSource实现类中有什么属性,配置文件中就能够 * 配置什么属性,没有的配置了,会报错 */ MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); for (Object key : properties.keySet()) { String propertyName = (String) key; if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { String value = properties.getProperty(propertyName); driverProperties .setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); // 这里内部其实反射调用了datasource实现类的set方法 metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException("Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { metaDataSource.setValue("driverProperties", driverProperties); } } }
重点落在了SystemMetaObject.forObject方法上,使用了解析数据源里面的属性,使用反射调用的逻辑。这么一来彻底隔离了配置文件中属性名与真实的数据源实现类的属性名。这样能够根据实现类的具体属性,来看看具体配置文件中支持什么配置属性名,实现了彻底的解耦和。
明天继续来看看映射文件如何读进来,并如何被被执行的