Mybatis,用了这么久,背景自不用说。我还记得,第一次使用,还在成铁科研,做电务那个OA系统的时候,在二代、罗尼玛的带领下,首次接触到的。由于之前的工程一直使用Hibernate,一下切换到Mybatis之后,最大的感受就是:我要成批成批的写sql~然后就是理论上一直在讲的:Hibernate是全自动,Mybatis是半自动。直到多年后的今天,这些依然我是工作生活的主题。另外,最近看到一个点:据统计国外程序员比较喜欢使用Hibernate,而国内的大片是Mybatis的天下。主要原因,和DDD这些还有些关系。当然,这些都不是我当下要深入研究的东西,我就想一探究竟:Mybatis如何将写到xml文件中sql执行的。
一、Java最原始的JDBC方式
由于高校教学的原因,大部分Javaer几乎都是自学出来的,即使是计算机科班也是如此。可是第一次我们使用一本类《21天学会Java》书籍,看到最后几个章节的时候,都会被所谓的JDBC对本地数据库进行CURD的代码所“恶心”到。心想:怎么有这么丑的代码~流程太多,每次都记不住啊~每次用,都要百度一下。恩,下面就是这个代码的片段:
(不要纠结异常捕获的问题,对于一本21天学习xxx的书籍,不要抱有工业代码的期望~~)
我使用本地的数据表,封装了个工具类:
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进行数据的操作
在非集成的情况下,Mybatis使用起来,相对来说比较简单,大概需要四个文件:
- Mybatis的配置文件:mybatis-config.xml
- Mybatis的映射文件:BlogMapper.xml
- 映射接口文件:BlogMapper.java
- 实体类文件:RefTestTable.java
下面是我本地的前三种文件的源码:
mybatis-config.xml:
BlogMapper.xml:
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是如何进行封装的。这篇文章,我们先来看看上面代码中的①
三、各种xml文件的加载与解析
一步步深入进去,先来看看这个类: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"); //这里主要对//这个标签的子标签所有的属性,进行读取,变成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方法上,使用了解析数据源里面的属性,使用反射调用的逻辑。这么一来完全隔离了配置文件中属性名与真实的数据源实现类的属性名。这样可以根据实现类的具体属性,来看看具体配置文件中支持什么配置属性名,实现了完全的解耦和。
四、结束
明天继续来看看映射文件如何读进来,并如何被被执行的