品讀Mybatis源碼---(1)解析配置文件

Mybatis,用了這麼久,背景自不用說。我還記得,第一次使用,還在成鐵科研,作電務那個OA系統的時候,在二代、羅尼瑪的帶領下,首次接觸到的。因爲以前的工程一直使用Hibernate,一下切換到Mybatis以後,最大的感覺就是:我要成批成批的寫sql~而後就是理論上一直在講的:Hibernate是全自動,Mybatis是半自動。直到多年後的今天,這些依然我是工做生活的主題。另外,最近看到一個點:據統計國外程序員比較喜歡使用Hibernate,而國內的大片是Mybatis的天下。主要緣由,和DDD這些還有些關係。固然,這些都不是我當下要深刻研究的東西,我就想一探究竟:Mybatis如何將寫到xml文件中sql執行的。java

1、Java最原始的JDBC方式

因爲高校教學的緣由,大部分Javaer幾乎都是自學出來的,即便是計算機科班也是如此。但是第一次咱們使用一本類《21天學會Java》書籍,看到最後幾個章節的時候,都會被所謂的JDBC對本地數據庫進行CURD的代碼所「噁心」到。心想:怎麼有這麼醜的代碼~流程太多,每次都記不住啊~每次用,都要百度一下。恩,下面就是這個代碼的片斷:mysql

節放假在家翻出了第一本學習Java書籍拍了個照

(不要糾結異常捕獲的問題,對於一本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的整個框架,就是將上面的代碼進行進一步封裝,固然封裝的源碼遠遠多於上面。數據庫

2、使用Mybatis進行數據的操做

在非集成的狀況下,Mybatis使用起來,相對來講比較簡單,大概須要四個文件:apache

  • Mybatis的配置文件:mybatis-config.xml
  • Mybatis的映射文件:BlogMapper.xml
  • 映射接口文件:BlogMapper.java
  • 實體類文件:RefTestTable.java

下面是我本地的前三種文件的源碼:session

mybatis-config.xmlmybatis

<?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.xmlapp

<?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是如何進行封裝的。這篇文章,咱們先來看看上面代碼中的①

3、各類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");
            //這裏主要對<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方法上,使用瞭解析數據源裏面的屬性,使用反射調用的邏輯。這麼一來徹底隔離了配置文件中屬性名與真實的數據源實現類的屬性名。這樣能夠根據實現類的具體屬性,來看看具體配置文件中支持什麼配置屬性名,實現了徹底的解耦和。

4、結束

明天繼續來看看映射文件如何讀進來,並如何被被執行的

相關文章
相關標籤/搜索