Ten thousand words sorting mybatis source code

Time:2022-5-4

Mybatis has been used almost since I first learned programming. I haven’t looked at its source code. This time, I happened to study it. After reading the source code, I still got a lot of harvest. I specially sorted it out. If there are any problems, please point them out

summary

The ORM framework of mybatis actually encapsulates JDBC. Friends who have used it must know to create a mybatis_ config. XML configuration file, create a mapper interface, and create a mapper XML file, and then called in the service layer. Let’s not analyze the source code for the moment. If you develop such an ORM framework with the same functions as mybatis, there are three problems in front of you

  • How to encapsulate the configuration (database link address, user name, password) and achieve registration only once, so you don’t need to take care of it in the future
  • How to bind mapper interface and mapper XML file
  • How to generate a proxy object, let the method in the interface find the corresponding mapper statement, and then bring the parameters in for execution
    With these problems, it is better to learn the source code step by step. Of course, these problems alone cannot be fully developed. Here I will talk about them as much as possible. If there are some unpopular configurations, I may have to study them in depth by myself.

JDBC & native mybatis call review

First of all, mybatis is a layer of encapsulation of traditional JDBC. First, let’s review the traditional JDBC

JDBC

public class User {

    //ID of user table
    private Integer id;

    //User name
    private String username;
    
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username ;
    }

}
public class JDBCDemo {
    //Create a connection to a database
    private static Connection getConnection() {
        Connection connection = null;
        try {
            //Load user driver
            Class.forName("com.mysql.cj.jdbc.Driver");
            //Address to connect to the database
            String url = "jdbc:mysql://127.0.0.1:3306/test1";
            //User name of the database
            String user = "root";
            //Password for database
            String password = "12345678";
            //Get a database connection
            connection = DriverManager.getConnection(url, user, password);
        } catch (ClassNotFoundException e) {
            System. out. Println (jdbcdemo. Class. Getname() + "database driver package not found!");
            return null;
        } catch (SQLException e) {
            System. out. Println (jdbcdemo. Class. Getname() + "SQL statement has a problem, unable to query successfully!");
            return null;
        }
        return connection;// Return to the connection
    }

    public User getUser(int id) {
        //Get the connection to the database
        Connection connection = getConnection();
        //Declare a null preprocessed statement
        PreparedStatement ps = null;
        //Declare a result set to store the results of SQL query
        ResultSet rs = null;
        try {
            //Preprocess and compile the SQL of the user table of the query
            ps = connection.prepareStatement("select * from user where id=?");
            //Set the parameter ID to the condition of the data
            ps.setInt(1, id);
            //Execute the query statement. Returns the result to the resultset result set
            rs = ps.executeQuery();
            //Traversal fetching from result set
            while (rs.next()) {
                //Get the user ID of the statement
                int user_id = rs.getInt("id");
                //Get the user name of the statement
                String username = rs.getString("username");
                User user = new User();
                //Stored in user object
                user.setId(user_id);
                user.setUsername(username);
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            this.close(rs, ps, connection);
        }
        return null;
    }

    /**
     *Judge whether the database is closed
     *@ param RS check whether the result set is closed
     *@ param stmt preprocessing SQL closed
     *@ param conn database connection closed
     */
    private void close(ResultSet rs, Statement stmt, Connection conn) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            System. out. Println (jdbcdemo. Class. Getname() + "resultset failed to close!");
        }
        try {
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException e) {
            System. out. Println (jdbcdemo. Class. Getname() + "statement closing failed!");
        }
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            System. out. Println (jdbcdemo. Class. Getname() + "connection shutdown failed!");
        }
    }
    
    public static void main(String[] args) {
        //We query the user whose ID is 1
        User user = new JDBCDemo().getUser(1);
        //Print out the queried data
        System.out.println(user);

    }

}

Here is a brief introduction to the next three main classes. Later, we will introduce how to encapsulate them

  • Drivermanager: when calling a methodgetConnectionWhenDriverManagerAttempts to find the appropriate driver from the drivers loaded in initialization and explicitly loads the driver using the same class loader as the current applet or application.
  • Connection: connection to the database. Execute the SQL statement and return the result in the context of the connection.
  • Statement: an object used to execute a static SQL statement and return the results it generates.
  • Resultset: represents the database result set

    Native mybatis call

    After a general understanding, we can take a look at the writing method of native mybatis
    mybatis_config

    <?xml version="1.0" encoding="UTF-8" ?>
    <!--
    
         Copyright 2009-2017 the original author or authors.
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
            http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    
    -->
    <!DOCTYPE configuration
          PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
          "http://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
    
      <!-- autoMappingBehavior should be set in each test case -->
    
      <environments default="development">
          <environment id="development">
             <!-- Configure transaction manager -- >
              <transactionManager type="JDBC" />
              <!-- Configure data source type and database link information -- >
              <dataSource type="UNPOOLED">
                  <property name="driver" value="org.hsqldb.jdbcDriver"/>
                  <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
                  <property name="username" value="root"/>
                  <property name="password" value="12345678"/>
    
              </dataSource>
          </environment>
      </environments>
      <!-- Mapper file location configuration -- >
      <mappers>
          <mapper resource="org/apache/ibatis/autoconstructor/AutoConstructorMapper.xml"/>
      </mappers>
    
    </configuration>

    mapper

    public interface AutoConstructorMapper {
    PrimitiveSubject getSubject(final int id);
    
    @Select("SELECT * FROM subject")
    List<PrimitiveSubject> getSubjects();
    
    @Select("SELECT * FROM subject")
    List<AnnotatedSubject> getAnnotatedSubjects();
    
    @Select("SELECT * FROM subject")
    List<BadSubject> getBadSubjects();
    
    @Select("SELECT * FROM extensive_subject")
    List<ExtensiveSubject> getExtensiveSubjects();
    }

    xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    
         Copyright 2009-2017 the original author or authors.
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
            http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    
    -->
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
    <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
      SELECT * FROM subject WHERE id = #{id}
    </select>
    </mapper>

    use

    private static SqlSessionFactory sqlSessionFactory;
    
    @BeforeAll
    static void setUp() throws Exception {
    // create a SqlSessionFactory
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/autoconstructor/mybatis-config.xml")) {
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }
    }
    
    @Test
    void fullyPopulatedSubject() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      final Object subject = mapper.getSubject(1);
      assertNotNull(subject);
    }
    }

Parsing mybatis_ config. xml

First, let’s come to the first question, how does mybatis parse mybatis_ config. xml

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.
    }
  }
}

Mybatis core classConfigurationIs throughXMLConfigBuilder::parseMethod is created. Mybatis puts all configuration loads at the time of initialization, and the results are basically saved inConfiguration, it only creates one during initialization, which contains mybatis config All configurations in XML, including all parsed mappers and their mapping relationships, can be found here after loading. It is the core class in mybatis. The member variables are as follows. You can see a lot of mybatis by name_ The figure of config. (the member variables are as follows. You can feel the source code of mybatis in advance. There is no code with almost no comments. By the way, you can feel the pain of watching the source code, QAQ)

public class Configuration {
    protected Environment environment;
    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls;
    protected boolean useActualParamName = true;
    protected boolean returnInstanceForEmptyRow;
    protected boolean shrinkWhitespacesInSql;

    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected Class<? extends VFS> vfsImpl;
    protected Class<?> defaultSqlProviderType;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
    protected Integer defaultStatementTimeout;
    protected Integer defaultFetchSize;
    protected ResultSetType defaultResultSetType;
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

    protected Properties variables = new Properties();
    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected boolean lazyLoadingEnabled = false;
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

    protected String databaseId;
    /**
     * Configuration factory class.
     * Used to create Configuration for loading deserialized unread properties.
     *
     * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
     */
    protected Class<?> configurationFactory;

    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
    //Aliases for common classes
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
        .conflictMessageProducer((savedValue, targetValue) ->
            ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
    protected final Set<String> loadedResources = new HashSet<>();
    protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

    /*
     * A map holds cache-ref relationship. The key is the namespace that
     * references a cache bound to another namespace and the value is the
     * namespace which the actual cache is bound to.
     */
    protected final Map<String, String> cacheRefMap = new HashMap<>();
...  Method skimming
}

Xpathparser usage

Let’s take a lookXMLConfigBuilderConstruction method of

public class XMLConfigBuilder extends BaseBuilder {
    private boolean parsed;// Has mybatis config. Been resolved xml
    //Parse mybatis config xml
    private final XPathParser parser;
    private String environment;
    //Create and cache reflctor objects
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
      this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    }
}

Its interior will encapsulate aXPathParserObject, let’s take a look at its usage first

<employee id="${id_var}">
  <blah something="that"/>
  <first_name>Jim</first_name>
  <last_name>Smith</last_name>
  <birth_date>
    <year>1970</year>
    <month>6</month>
    <day>15</day>
  </birth_date>
  <height units="ft">5.8</height>
  <weight units="lbs">200</weight>
  <active bot="YES" score="3.2">true</active>
</employee>
  @Test
  void constructorWithInputStreamValidationVariablesEntityResolver() throws Exception {

    try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
      XPathParser parser = new XPathParser(inputStream, false, null, null);
      System.out.println(parser.evalLong("/employee/birth_date/year").equals(1970L));//true
      System.out.println(parser.evalNode("/employee/birth_date/year").getLongBody().equals(1970L));//true
      System.out.println(parser.evalNode("/employee").evalString("@id"));//${id_var}
      System.out.println(parser.evalNode("/employee/active").getDoubleAttribute("score"));//3.2    }
  }

XPathParserIt encapsulates the JDK nativeDocumentEntityResolverXPathPropertiesThese objects are more convenient for parsing XML files. I wrote an example above for specific usage, so that all values can be obtained.

Node resolution

After initializing xmlconfigbuilder, it will be calledparse()Method, parsing XML, mapper parsing and mapper binding are all completed in this method. Let’s look at this method

/**
 *Parse mybatis config xml
 *Initialization call
 * @return
 */
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  //Find configuration node resolution
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

parser.evalNode("/configuration")This step is to getconfigurationNode, the next step is toparseConfigurationMethod

private void parseConfiguration(XNode root) {
  try {
    //Resolve each node
    // issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    //Class alias registration
    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"));
    //Parsing typehandler
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

It should be familiar to see here,mybatis_config.xmlEach node placed on + element is the corresponding parsing method. Because we only configureenvironmentsandmappersLet’s look at these two methods.

Parsing environment

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //Create datasource and datasourcefactory and set corresponding property values
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
        break;
      }
    }
  }
}

Transaction

hereTransactionFactoryResponsible for creatingTransaction, in mybatisTransactionThere are 2 subclasses of


TransactionDefined

public interface Transaction {

  /**
   *Get the corresponding database connection object
   */
  Connection getConnection() throws SQLException;

  /**
   *Commit transaction
   */
  void commit() throws SQLException;

  /**
   *Rollback transaction
   */
  void rollback() throws SQLException;

  /**
   *Close database connection
   */
  void close() throws SQLException;

  /**
   *Get transaction timeout
   */
  Integer getTimeout() throws SQLException;

}

JdbcTransactionEncapsulates the transaction isolation level, connection and data source. The method is basically to call the corresponding method of connection

public class JdbcTransaction implements Transaction {

  private static final Log log = LogFactory.getLog(JdbcTransaction.class);

     //Database connection corresponding to transaction
     protected Connection connection;
     //Datasource to which the database connection belongs
     protected DataSource dataSource;
     //Transaction isolation level
     protected TransactionIsolationLevel level;
     //Auto submit
     protected boolean autoCommit;
     @Override
     public void rollback() throws SQLException {
       if (connection != null && !connection.getAutoCommit()) {
         if (log.isDebugEnabled()) {
           log.debug("Rolling back JDBC Connection [" + connection + "]");
         }
         connection.rollback();
       }
     }
     .. Others omitted
   }

Another one isManagedTransactionIt leaves the commit and rollback to the container implementation

public class ManagedTransaction implements Transaction {
       ... Other strategies
    //Container implementation
    @Override
    public void commit() throws SQLException {
      // Does nothing
    }

    @Override
    public void rollback() throws SQLException {
      // Does nothing
    }

}

DataSource

Finished creatingTransactionFactoryNow it’s your turnDataSourceFactory, there’s nothing to say about this,ConfigurationThe construction method of will register a bunch of class aliases and then create them through reflection. There’s nothing to say about this.

public Configuration() {
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
。。。 Other strategies
}

Let’s mainly talk about datasource, that is, data source, which is actually the encapsulation of driver. Let’s see how mybatis is encapsulated.

UnpooledDataSourceThe core method is as follows. Is it very friendly? In fact, it is an encapsulation of traditional JDBC. It’s not much different from the example written above. The main difference is that it can support multiple and is encapsulated.

/**
 *The getconnection () method and its overloaded method are implemented
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class UnpooledDataSource implements DataSource {

  //Class loader to load driver class
  private ClassLoader driverClassLoader;
  //Related configuration of database connection driver
  private Properties driverProperties;
  //Cache all registered database connection drivers
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();

  //The name of the driver for the database connection
  private String driver;
  //URL of the database connection
  private String url;
  //User name
  private String username;
  //Code
  private String password;
  //Auto submit
  private Boolean autoCommit;
  //Transaction isolation level
  private Integer defaultTransactionIsolationLevel;
  private Integer defaultNetworkTimeout;

  static {
    //Register JDBC drivers with drivermanager
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }

  private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }

  //Create a new connection at a time
  private Connection doGetConnection(Properties properties) throws SQLException {
    //Initialize database driver
    initializeDriver();
    //Create a real database connection
    Connection connection = DriverManager.getConnection(url, properties);
    //Configure autocommit and isolation levels for database connections
    configureConnection(connection);
    return connection;
  }

  private synchronized void initializeDriver() throws SQLException {
    If (! Registereddrivers. Containskey (driver)) {// check whether the driver has been registered
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class. forName(driver, true, driverClassLoader);// Register driver
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver) driverType. getDeclaredConstructor(). newInstance();// Create driver object
        //Register the driver. Driverproxy is an internal class defined in unpooleddatasource and a static proxy class of the driver
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        //Add drivers to registereddrivers
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

The whole line should be clear when you see it here. Let’s take a look at the tradition

private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      //Set transaction isolation level
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }

  private static class DriverProxy implements Driver {
    private Driver driver;

    DriverProxy(Driver d) {
      this.driver = d;
    }

    。。。 slightly
  }
  。。。 slightly

}

You think it’s over here, far from it. I saw one when I looked at the codePooledDataSourceAs we all know, it’s a waste to create a connection every time you use it. It’s best to take it. Of course, it has a better implementation, such as Druid. I think it’s good to learn the one encapsulated by mybatis. If you are interested, you can understand it. If you are not interested, you can not see it (you can directly see the Druid source code).

Let’s have a look firstPoolState, the author maintains it with two listsConnectionObject.

public class PoolState {

  //Data source object
  protected PooledDataSource dataSource;
  //Idle state connection collection
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  //Active state connection set
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
  protected long requestCount = 0;// Number of database connections requested
  protected long accumulatedRequestTime = 0;// Cumulative time of connection
  protected long accumulatedCheckoutTime = 0;// Connection cumulative checkouttime duration
  protected long claimedOverdueConnectionCount = 0;// Number of timeout connections
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;// Cumulative timeout
  protected long accumulatedWaitTime = 0;// Cumulative waiting time
  protected long hadToWaitCount = 0;// Waiting times
  protected long badConnectionCount = 0;// Invalid number of connections
}

Now let’s take a look at how mybatis is implementedPooledDataSource, the core is to get and put backConnectionI feel that the logic of thread pool is similar.

public class PooledDataSource implements DataSource {
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
state.activeConnections.remove(conn);//
If (conn.isvalid()) {// is the connection valid
//Check whether the number of idle connections has reached the online limit
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
state. accumulatedCheckoutTime += conn.getCheckoutTime();// Cumulative checkout duration
if (!conn.getRealConnection(). Getautocommit()) {// roll back uncommitted transactions
conn.getRealConnection().rollback();
}
//Create pooledconnection
//The proxy object is actually eliminated, and the realconnection is actually used
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state. idleConnections. add(newConn);// Add to inactive collection
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate();// Set the original pooledconnection object to invalid
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
state.notifyAll();
} else {
//The free collection is full. Close it directly
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
//Spin without getting the connection object
while (conn == null) {
synchronized (state) {
/**
*Processing whether there are idle connections
*/
//Idleconnections idle state connections
If (! State. Idleconnections. Isempty()) {// idle connection
// Pool has available connection
conn = state. idleConnections. remove(0);// Get connection
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
//Pool does not have available connection if the number of active connections does not reach the maximum, a new connection can be created
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//Can create new connection creates a new database connection and encapsulates it into a pooledconnection object
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
}Else {// if the number of active connections has reached the maximum, you cannot create a new connection
//Cannot create new connection gets the first active connection created
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
If (longestcheckouttime > poolmaximumcheckouttime) {// check whether the connection timed out
//Can claim excess connection statistics the information of timeout connections
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
//Move timed out connections out of the activeconnections collection
state.activeConnections.remove(oldestActiveConnection);
//If the timeout connection is not submitted, it will be rolled back automatically
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happened.
Wrap the bad connection with a new PooledConnection, this will help
to not interrupt current executing thread and give current thread a
chance to join the next competition for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
//Create a new pooledconnection object and reuse the old collection object
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
//Multiplex timestamp
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
//Timeout pooledconnection set to invalid
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
//If there are no idle connections, no new connections can be created, and no timeout connections, you can only block the wait
try {
if (!countedWait) {
state. hadToWaitCount++;// Count waiting times
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// ping to server and check the connection is valid or not
If (conn.isvalid()) {// check whether the connection is valid
//
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state. activeConnections. add(conn);// Statistics
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
//The current connection is invalid, continue to choose
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
}
return conn;
}
。。。 slightly
}

Here is theTransactionFactoryandDataSourceEncapsulated inEnvironment, put the environment inConfigurationThe builder mode is used. If you are interested in the builder mode, you can see my blog https://juejin.cn/post/698594…

Parse mapper


There are about four types of mapper configurations. It’s easy to understand from the source code. The contents are similar. Let’s take a look at the analysis of resource

public void parse() {
//Judge whether the mapping file has been loaded
if (!configuration.isResourceLoaded(resource)) {
//Bind SQL via XML
configurationElement(parser.evalNode("/mapper"));// Processing mapper nodes
//The parsed XML is added to loadedresources
configuration.addLoadedResource(resource);
//Scan annotation binding SQL
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//Package is processed separately
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//Resolve corresponding configuration
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
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.");
}
}
}
}
}

becausemybatis_configIf the path of mapper is specified, the specified resource will be loaded,mapperParser.parse();The resolved tag will be added to the final resolved result of the select methodconfiguration::mappedStatements

public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
//Process the include node first
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
//According to the usegeneratedkeys configuration in the global
//Whether it is an insert statement determines whether to use the keygenerator interface
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//Parse the original SQL statement and replace #{} with?, Put parameters
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
//Gets the value of resulttype in the select tag
String resultType = context.getStringAttribute("resultType");
//If there is no corresponding in typealiasregistry, get the corresponding class object by reflection
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//Generate mappedstatements and add them to the mappedstatements of configuration
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

I believe you can also see the annotation version. It is in
Annotation is throughMapperRegistry::addMapperThe specific process is similar to the above process. It is also used to parse the values in the above method`
MapperBuilderAssistant
`Add to configuration
Because all the parsing results are stuffed intoConfigurationInside, finally, put the configuration into`
DefaultSqlSessionFactory
`
The parsing part is finished

Get sqlsession

The following is to get sqlsession. Mybatis is for you by defaultDefaultSqlSession

//Get database connection through data source
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
//Get the transactionfactory in the environment and create one without
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//Execute SQL statement
final Executor executor = configuration.newExecutor(tx, execType);
//Create sqlsession
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();
}
}

When you get the mapper, a proxy object is generated for you through dynamic proxy`
final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
`The source code is as follows

@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//Mapper interface proxy object
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) {
//Create proxy object
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);
}

Mapper method execution

When executing, we just need to see how the agent executes, that is`
MapperProxy
It encapsulates aPlainmethodinvoker `, and the final execution is to call the invoke method of this internal class

private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//
return mapperMethod.execute(sqlSession, args);
}
}

During execution, the previously parsed parameters will be used and aMapperMethod

public class MapperMethod {
//Specific implementation
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Switch (command. Gettype()) {// call different methods according to the SQL type
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 {
//Analytical parameters
Object param = method.convertArgsToSqlCommandParam(args);
//Call sqlsession to execute
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;
}
}
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> 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 {
return null;
}
}
//Finally call this one
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
//Mybatis was parsed earlier_ config. XML has been loaded into the configuration
MappedStatement ms = configuration.getMappedStatement(statement);
//
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

Final callCachingExecutor, remember the previous SQL statement encapsulated inSqlSourceWhat’s down therems.getBoundSql(parameterObject);Will eventually call`
sqlSource.getBoundSql(parameterObject);
Create a newBoundsql `, now you have all the SQL parameters

//
@Override
public <E> List<E> 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);
}

Next, the query operation is executed, and finally theBaseExecutorTo perform the query operation

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//Query L1 cache
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//Processing for stored procedures
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//Query complete
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//Add placeholder to cache
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//Call doquery
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//Delete placeholder
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
//Get statement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
//Get connection
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
//Get through basestatementhandler
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
}
}
//Create Preparedstatement
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
//Preparedstatementhandler executes the query, which should be very friendly
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}

So far, the whole query process of the native mybatis is over. Here, the value introduces the query process, and the process of addition, deletion and modification is similar. I won’t repeat it here.

Reference – mybatis technology insider