MyBatis has been used almost since I first learned programming. I haven't seen 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 The XML file is then invoked 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. You don't need to manage this later
- How to bind mapper interface and mapper XML file
- How to generate a proxy object, let the methods 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.
JDBC & native MyBatis call review
Firstly, 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 the 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 There is a problem with the statement. The query cannot succeed!"); return null; } return connection;//Return to the connection } public User getUser(int id) { //Get a 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()) { //Retrieve 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; } /** * Determine whether the database is closed * @param rs Check whether the result set is closed * @param stmt Close preprocessing SQL * @param conn Is the 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 Closing failed!"); } 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 Closing 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, which will be described later on
- DriverManager: when the method getConnection is called, DriverManager will try to find the appropriate driver from the driver loaded in initialization and explicitly load 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 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 the 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, we 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 class Configuration is created through the XMLConfigBuilder::parse method. Mybatis loads all configurations during initialization, and basically saves the results into Configuration. It only creates one during initialization, which contains mybatis config All configurations in the XML, including all parsed mapper s 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 first feel the mybatis source code without almost no comments in advance, and then 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 look at the construction method of XMLConfigBuilder
public class XMLConfigBuilder extends BaseBuilder { private boolean parsed;//Has mybatis config. Been resolved xml //Parsing 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 an XPathParser object. 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 } }
XPathParser encapsulates the JDK's native Document, EntityResolver, XPath and Properties. It is more convenient to parse XML files. I wrote an example above for specific usage, so that all values can be obtained.
Node resolution
After initializing XMLConfigBuilder, it will call its parse() method. Parsing xml, mapper parsing and mapper binding are all completed in this method. Let's look at this method
/** * Parsing 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. The step of evalnode ("/ configuration") is to obtain the configuration node, and the next step is to resolve each child node in the parseConfiguration method
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.xml nodes placed in + Element are the corresponding parsing methods. Since we only configured environments and mappers, let's take a 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
Here, TransactionFactory is responsible for creating Transaction. There are two subclasses of Transaction in mybatis
Transaction defines
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; }
JdbcTransaction encapsulates 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(); } } ..Other strategies }
Another is ManagedTransaction, which gives 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
After creating the TransactionFactory, it's the turn of the DataSourceFactory. There's nothing to say. A bunch of class aliases will be registered in the Configuration constructor, and then created through reflection. There's nothing to say.
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 take a look at how MyBatis is encapsulated.
The core method of UnpooledDataSource 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 multiple can be supported and 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; //password 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 is 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); } } }
It should be clear to see the whole line 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. When I look at the code, I also see a PooledDataSource. As we all know, each time I use it, I create a Connection. It's wasteful to use it up and destroy it. It's best to take it. Of course, there is a better implementation, such as Druid. I think it's good to learn from 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 take a look at PoolState. The author maintains the Connection object with two list s.
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 implements PooledDataSource. The core is to get and put back the Connection. Personally, I think 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()) {//Rollback 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 as 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()) {//Detect that 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 from 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 }
The following is to encapsulate the TransactionFactory and DataSource into the environment, and then insert the environment into the Configuration. The 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() { //Determine 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."); } } } } }
Due to mybatis_config specifies the path of mapper, and the specified resource, mapperparser, will be loaded parse(); Finally, the following method will be called to parse the select tag and add the parsed results to configuration::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 it is implemented using 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 class object in typeAliasRegistry, get the corresponding class object 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 the 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
The annotation is through MapperRegistry::addMapper. The specific process is similar to the above process. It is also used to parse the values in the above methods`
MapperBuilderAssistant
`Add to configuration
Because all the parsing results are stuffed into the configuration. Finally, the configuration is stuffed into the configuration`
DefaultSqlSessionFactory
`
The parsing part is finished
Get SqlSession
Here is how to get SqlSession. Mybatis gives you DefaultSqlSession by default.
//Get database connection from 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 statements 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 internally encapsulates a PlainMethodInvoker `, 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 resolved parameters will be used and a MapperMethod will be constructed
public class MapperMethod { //Specific implementation public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) {//Call different methods according to 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(); } }
Finally, the cacheingexecution is called. Remember that the previous sql statement is encapsulated in SqlSource? The following ms.getBoundSql(parameterObject); Eventually called`
sqlSource.getBoundSql(parameterObject);
Create a new BoundSql `. Now you have the sql and 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 will be executed, and finally BaseExecutor will be called to execute 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. The value here introduces the query process, and the process of addition, deletion and modification is similar. I won't repeat it here.
Reference - MyBatis technology insider