1, Environment construction
1. mysql environment
1.1 mysql installation address
Linux environment:
You can download mysql using yum or docker
Windows Environment:
You can download it from the official website of mysql. It is recommended here Tsinghua tuna mirror network , download faster.
1.2 table building
Here we need to create a test table for debug ging mybatis
The table creation statement is as follows:
CREATE TABLE `user` ( `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `age` int(0) NOT NULL, `create_time` datetime(0) NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
Here, a user table is created. The fields are id, name, age, and create_time
2, Introduction to Mybatis
MyBatis is an excellent persistence layer framework, which supports custom SQL, stored procedures and advanced mapping. MyBatis eliminates almost all JDBC code and the work of setting parameters and obtaining result sets. MyBatis can configure and map primitive types, interfaces and Java POJO s (Plain Old Java Objects) as records in the database through simple XML or annotations, greatly simplifying the development efficiency.
Studying the source code of Mybatis can have the following benefits:
- Understand the operation process of Mybatis and better locate bug s when Mybatis has exceptions
- When mybatis has a performance bottleneck, it can perform performance tuning on mybatis and further encapsulate mybatis
- Query the executed SQL statements, especially for projects using tk.mybatis. You can know whether the executed SQL statements meet the expectations, etc
3, Native JDBC statement
Let's take a look at how we use JDBC to query all the data in the user table without using tools (JDBC template, etc.) and frameworks (Mybatis, etc.)
import java.sql.*; public class Main { // JDBC information static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8"; static final String USER = "root"; static final String PASS = "123456"; public static void main(String[] args) { Connection conn = null; Statement stmt = null; try{ //STEP 2: register JDBC Driver Class.forName(JDBC_DRIVER); //STEP 3: establishing a connection conn = DriverManager.getConnection(DB_URL,USER,PASS); //STEP 4: execute query stmt = conn.createStatement(); String sql; sql = "SELECT * FROM user"; ResultSet rs = stmt.executeQuery(sql); //STEP 5: encapsulating result sets while(rs.next()){ String id = rs.getString("id"); String name = rs.getString("age"); Integer age = rs.getInt("age"); Date createTime = rs.getDate("create_time"); User user = new User(id, name, age, createTime); System.out.println(user); System.out.println("---------------"); } //STEP 6: closing resources rs.close(); stmt.close(); conn.close(); }catch(SQLException e){ e.printStackTrace(); }catch(Exception e){ e.printStackTrace(); }finally{ try{ if(stmt!=null) stmt.close(); } catch (SQLException se2){ } try{ if(conn!=null) conn.close(); } catch (SQLException e){ e.printStackTrace(); } } System.out.println("Goodbye!"); } }
Querying data using JDBC statements is summarized as follows
- Register JDBC Driver
- Establish a Connection and get the Connection object
- Execute query statement
- Encapsulate mysql result sets into Java objects
- close resource
We found that in the above operations, the above steps of different DML statements are similar, such as obtaining connections, executing statements (only different SQL statements), encapsulating result sets (different encapsulated objects), and closing resources. Therefore, we can encapsulate these general operations into a tool or framework. We only need to provide the executed SQL statements and encapsulate them into the result set.
Frameworks such as JDBC template and Mybatis provide tools and frameworks for these functions.
4, The core class of Mybatis source code
To deeply understand the process and source code of Mybatis, we first introduce the core classes and functions of Mybatis. Readers only need to remember these core classes and their functions, and then go deep into their source code.
1. SqlSessionFactory
The factory class of SqlSession, which is used to produce SqlSession, can be configured through Configuration getConfiguration(); Gets the Configuration object.
DefaultSqlSessionFactory is the implementation class of SqlSessionFactory
2. SqlSession
Key classes of Mybatis, execute statements through SqlSession class, obtain Mapper class, etc.
For example, there are selectList and selectMap methods for query and update methods for update
DefaultSqlSession and SqlSessionTemplate are the implementation classes of SqlSession. DefaultSqlSession has an important attribute Executor executor.
3. Executor
Executor, real connection, SQL statement parsing, real working class. The previous SqlSession combines the executor class to complete the function.
Among them, cacheingexecution is the implementation of L1 cache, which will be introduced later.
4. Configuration
For the Configuration file class, we need to use a global mybatis Configuration file and multiple xxmapper.xml files when writing mybatis. Java is an object-oriented language, so the Configuration class is used to encapsulate the Configuration file information of mybatis.
5. MapperProxyFactory
You can guess the role of this class by listening to the class name. MapperProxyFactory is a factory class used to create MapperProxy.
6. MapperProxy
MapperProxy. Similarly, the literal meaning of the class name is Mapper's proxy class. In the Mybatis framework, we usually use Mapper's interface without creating a real entity class. It is the MapperProxy proxy class that helps us complete the real logic.
7. Summary
Next, we summarize the above class diagram (non-standard UML class diagram) with a diagram
- The proxy class MapperProxy of UserMapper is created by MapperProxyFactory
- SqlSessionFactory created SqlSession
- SqlSession aggregates the Executor (Executor is the real working class)
- Both SqlSession and SqlSessionFactory aggregate the Configuration class (the xml Configuration file of Mybatis)
- MapperProxy aggregates SqlSession
5, Simple Demo of Mybatis
Let's start with a simple Demo to see how to use Mybatis. The database table used is the User table mentioned above.
The following is a code link to the Demo , you only need to install MySQL and modify the corresponding database configuration information to run correctly.
1. Create the entity User corresponding to the database table
package com.junehua.pojo; import java.util.Date; public class User { private String id; private String name; private Integer age; private Date createTime; public User() { } public User(String id, String name, Integer age, Date createTime) { this.id = id; this.name = name; this.age = age; this.createTime = createTime; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } @Override public String toString() { final StringBuffer sb = new StringBuffer("User{"); sb.append("id='").append(id).append('\''); sb.append(", name='").append(name).append('\''); sb.append(", age=").append(age); sb.append(", createTime=").append(createTime); sb.append('}'); return sb.toString(); } }
2. Create Mapper interface corresponding to User object
package com.junehua.mapper; import com.junehua.pojo.User; import java.util.List; public interface UserMapper { int insert(User user); List<User> listAll(); }
3. Mybatis configuration file
First, we create a mybatisConfig.xml in the resources directory. The configuration file of mybatisConfig.xml is as follows:
<?xml version="1.0" encoding="UTF-8" ?> <!--mybatis Global profile--> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--<properties resource="db.properties"/>--> <settings> <!--Turn on automatic conversion of hump naming rules--> <setting name="mapUnderscoreToCamelCase" value="true" /> <!--Enable L2 cache--> <setting name="cacheEnabled" value="false"/> </settings> <typeAliases> <!--Corresponding to database table Entity Package of class--> <package name="com.junehua.pojo"></package> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <!--Data source information--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!--mapper Directory where the file is located--> <mappers> <mapper resource="mapper/UserMapper.xml"></mapper> </mappers> </configuration>
4. Write Mapper's mapping file UserMapper.xml file
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.junehua.mapper.UserMapper"> <select id="listAll" resultType="com.junehua.pojo.User" > select * from user; </select> <insert id="insert" parameterType="com.junehua.pojo.User"> insert into user(id, name, age, create_time) value(#{id}, #{name}, #{age}, now()) </insert> </mapper>
5. Write and run the program
public class MybatisDemo { @Test public void mybatisDemoTest() throws IOException { // Mybatis global profile InputStream mybatisConfig = Resources.getResourceAsStream("mybatisConfig.xml"); // Create SqlSession factory class SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfig); // Create SqlSession class from factory class SqlSession sqlSession = sqlSessionFactory.openSession(); // Get Mapper file UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // Query and print data List<User> users = userMapper.listAll(); users.forEach(System.out::println); } }
Create a SqlSession using the SqlSessionFactory factory class, and obtain the UserMapper interface from the SqlSession. Note that UserMapper is an interface, and the implementation class of this interface is the MapperProxy proxy class created by MapperProxyFactory.
6. Operation results
We use Navicat to view the data information in the database table, as follows:
There are two pieces of data. View the information output from the IDEA console as follows:
The data was successfully printed.
6, Mybatis source code
Through the above Mybatis Demo, we have a basic understanding of Mybatis. Next, we will study the implementation and core source code of Mybatis according to the Demo given above.
public class MybatisDemo { @Test public void mybatisDemoTest() throws IOException { // Mybatis global profile InputStream mybatisConfig = Resources.getResourceAsStream("mybatisConfig.xml"); // Create SqlSession factory class SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfig); // Create SqlSession class from factory class SqlSession sqlSession = sqlSessionFactory.openSession(); // Get Mapper file UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // Query and print data List<User> users = userMapper.listAll(); users.forEach(System.out::println); } }
1. SqlSessionFactory and Configuration
First of all, we see that through new SqlSessionFactoryBuilder().build(mybatisConfig); Created a SqlSessionFactory
Our Mybatis Configuration file is passed in here, so it is not difficult to guess that mybatisConfiguration.xml will be parsed into Configuration class here.
We enter SqlSessionFactoryBuilder.build(), which returns the SqlSessionFactory object
// SqlSessionFactoryBuilder public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } // We then go to the build method // SqlSessionFactoryBuilder public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties) returns an XMLConfigBuilder parser. We enter the parse () method of XMLConfigBuilder
// XMLConfigBuilder public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
Sure enough, a Configuration is returned here. We enter the Configuration class as follows:
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 final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); .... }
Only part of the code is intercepted here. Let's observe the protected boolean mapUnderscoreToCamelCase above;
Isn't this the Configuration of whether underline to hump naming is enabled in the Mybatis global Configuration file. The same is true for other attributes. Map the global Configuration file and Mapper's xml to the Configuration class.
<settings> <!--Turn on automatic conversion of hump naming rules--> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings>
There is also a key attribute map < string, mappedstatement > mappedStatements in the above Configuration class. This mappedStatements is the result stored after we convert xxmapper.xml.
Among them,
key: full class name and method name, such as com.junehua.mapper.UserMapper.listAll
value: it is a MappedStatement object that holds the relevant information of the method, such as parameter type, resultSet type, etc.
Let's go back to the build method code in the SqlSessionFactoryBuilder class
// SqlSessionFactoryBuilder public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
Enter build(parser.parse())
// SqlSessionFactoryBuilder public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
DefaultSqlSessionFactory returns an implementation class of SqlSessionFactory.
Next, we enter the SqlSessionFactory source code as follows:
public interface SqlSessionFactory { SqlSession openSession(); SqlSession openSession(boolean autoCommit); SqlSession openSession(Connection connection); SqlSession openSession(TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType); SqlSession openSession(ExecutorType execType, boolean autoCommit); SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType, Connection connection); Configuration getConfiguration(); }
You can see that SqlSessionFactory is used to obtain the Configuration and create the factory of SqlSession.
OK, we have analyzed the source code of SqlSessionFactory and Configuration in the first part. Next, let's summarize:
Our global Configuration file and Mapper's xml will be stored in the Configuration class. A Configuration object is saved in SqlSessionFactory, as shown in the following figure:
2. SqlSessionFactory.openSession();
We enter SqlSession sqlSession = sqlSessionFactory.openSession();
// DefaultSqlSessionFactory public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
Continue to openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
// DefaultSqlSessionFactory private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // Returns an Executor final Executor executor = configuration.newExecutor(tx, execType); // Create a DefaultSqlSession 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(); } }
Here we focus on configuration.newExecutor(tx, execType); An Executor object is returned. Let's revisit the inheritance diagram of the Executor.
Here we enter the newExecutor method of Configuration
// Configuration public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
The executorType we passed in here is ExecutorType.SIMPLE, so a simpleexecution will be created. Let's look at the key line of code
if (cacheEnabled) { executor = new CachingExecutor(executor); }
Here is the level-1 cache of Mybatis. If cacheEnabled is true (configured in mybatisConfig.xml, the global configuration file of Mybatis), create a cacheingexecution, and take the newly created Executor as the property of cacheingexecution.
We return to the following code:
// DefaultSqlSessionFactory private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // Returns an Executor final Executor executor = configuration.newExecutor(tx, execType); // Create a DefaultSqlSession 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(); } }
Here, a DefaultSqlSession will be created and the executor just created will be passed in.
Next, let's summarize the role of SqlSessionFactory.openSession()
- Create an Executor. Here we create a SimpleExecutor. If Mybatis enables the L1 cache cacheEnabled (enabled by default), create a cacheingexecution and pass in simpleexecution.
- Create a DefaultSqlSession and return
3. SqlSession.getMapper(UserMapper.class);
We enter sqlSession.getMapper(UserMapper.class); The implementation class of SqlSession here is DefaultSqlSession
// DefaultSqlSession @Override public <T> T getMapper(Class<T> type) { return configuration.getMapper(type, this); }
Continue to getMapper of Configuration
// Configuration public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
Enter the following code:
// MapperRegistry public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // Get MapperProxyFactory factory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // Create a UserMapper proxy class return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
Here, we see the MapperProxyFactory we are familiar with. Yes, MapperProxyFactory is the factory class we use to create MapperProxyFactory. Sure enough, return mapperProxyFactory.newInstance(sqlSession) appears below;
We enter the code
// MapperProxyFactory public T newInstance(SqlSession sqlSession) { // Create MapperProxy final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
This is the method Proxy.newProxyInstance (class loader, proxy interface, interception mode) that we use JDK dynamic proxy. Later, we will see how MapperProxy intercepts. So far, we have passed UserMapper userMapper = sqlSession.getMapper(UserMapper.class); After getting the proxy class of UserMapper, we will enter the MapperProxy proxy class of UserMapper to see how the proxy class completes the proxy operation.
4. MapperProxy
MapperProxy class is relatively simple. Here we pull out all the code as follows
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); if (!constructor.isAccessible()) { constructor.setAccessible(true); } final Class<?> declaringClass = method.getDeclaringClass(); return constructor .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } /** * Backport of java.lang.reflect.Method#isDefault() */ private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } }
Here, we see the familiar InvocationHandler. Yes, this is the interception class of our dynamic agent. We focus on the abstract method invoke of InvocationHandler.
// MapperProxy @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // If the current method is the Object method, the method will be executed directly, such as toString, equal, etc if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { // Whether it is a method modified by default return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // Our demo will enter here final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
We see the MapperMethod class. The class name roughly means Mapper's method. Here is the MapperMethod corresponding to UserMapper's listAll(). The code is as follows:
public class MapperMethod { // sql information, corresponding to that method. Type of sql, such as select, update, etc private final SqlCommand command; // Method description, such as method parameters, parameter types, and returned results private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { 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 { Object param = method.convertArgsToSqlCommandParam(args); 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; } .... }
MapperMethod has two properties, SqlCommand and MethodSignature
public static class SqlCommand { // The name here is our method name com.junehua.mapper.UserMapper.listAll private final String name; // The type here is the type of our method, such as INSERT, UPDATE, etc // listAll here uses SELECT private final SqlCommandType type; .... } // We provide information about the method, such as the return value of the method, parameter name mapping rules, etc public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; private final boolean returnsOptional; private final Class<?> returnType; private final String mapKey; private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; .... }
After understanding SqlCommand and MethodSignature, we return to the execute method of MapperMethod
// MapperMethod public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { 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()) { // The demo will enter here result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); 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; }
Here, we use the SELECT type and return list < user >. The code will enter result = executeForMany(sqlSession, args), and we will continue to executeForMany
// MethodMapper private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; // Conversion parameters Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.selectList(command.getName(), param, rowBounds); } else { result = sqlSession.selectList(command.getName(), param); } // issue #510 Collections & arrays support if (!method.getReturnType().isAssignableFrom(result.getClass())) { if (method.getReturnType().isArray()) { return convertToArray(result); } else { return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result; }
Here, we finally come to our SqlSession and execute the selectList method. Next, we will analyze the source code of SqlSession.
5. SqlSession
We enter the implementation class DefaultSqlSession of SqlSession.
public class DefaultSqlSession implements SqlSession { // The Configuration described earlier stores the xml file of mybatis private final Configuration configuration; // The implementation class we use here is simpleexecution. If the L1 cache is enabled, the cacheingexecution is used here private final Executor executor; // Auto submit private final boolean autoCommit; private boolean dirty; private List<Cursor<?>> cursorList; public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; }
Again, there are no Connection and Statement processing in SqlSession. It is not SqlSession that actually executes JDBC Connection, query and encapsulation results, but the Executor of SqlSession.
Next, we go to sqlSession.selectList(command.getName(), param) of DefaultSqlSession;
// DefaultSqlSession @Override public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // Remember the statement value here? // Get the MappedStatement of key: com.junehua.mapper.UserMapper.listAll in the Map MappedStatement ms = configuration.getMappedStatement(statement); // The real executor return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
DefaultSqlSession is the Executor that actually executes queries. Next, we will analyze the source code of simpleexecution.
6. SimpleExecutor
First, let's review the inheritance diagram of Executor execution
We use simpleexecution. We enter executor. Query (MS, wrapcollection (parameter), rowboundaries, executor. No_result_handler);
// BaseExecutor @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // A BoundSql object is obtained here. There is a String sql attribute in the BoundSql object, which is the SQL statement we execute in the database // The sql value of BoundSql here is "select * from user;", that is, all data in the query table user // If we need to know the executed sql statement in the program, we can break the point and come here to view the sql statement BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } // BaseExecutor @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++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // Let's look at the queryFromDatabase, query from the database, and we enter the corresponding code block list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { 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; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // Execute doQuery list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
Next, we go to the doquery of simpleexecution (MS, parameter, rowbounds, resulthandler, boundsql);
// SimpleExecutor @Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // Here we see the familiar Statement in JDBC Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // Get an implementation class routingstatementhandler of StatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); // Execute query(stmt, resultHandler) of RoutingStatementHandler return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } // Configuration // This method will return a RoutingStatementHandler public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // RoutingStatementHandler aggregates a StatementHandler object according to the type of StatementType StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // If Mybatis has a plug-in, add it here statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } // Enter prepareStatement(handler, ms.getStatementLog()); private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // Here we see the Connection we are familiar with in JDBC Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
We enter the query(stmt, resultHandler) of the routing statementhandler;
// Enter handler.query(stmt, resultHandler); @Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { // The delegate here is PreparedStatementHandler return delegate.query(statement, resultHandler); } // delegate.query(statement, resultHandler); @Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { // Here we see the Preparedstatement that we are familiar with in JDBC PreparedStatement ps = (PreparedStatement) statement; // Execute our sql statement ps.execute(); // Use DefaultResultSetHandler to complete our result set processing // Encapsulate the queried data into list < user > return resultSetHandler.handleResultSets(ps); }
To summarize the execution process of simpleexecution:
- Get the executed SQL statement: BoundSql boundSql
- Create a RoutingStatementHandler based on the StatementType of the Statement
- Execute the doQuery method of simpleexecution ()
- Query using StatementHandler's query
- Encapsulate the result set with ResultSetHandler
So far, we have a certain understanding of the core source code of Mybatis. You can debug through the insert(User user) method in UserMapper and go through the source code implementation of Mybatis.
If there are any mistakes in this article, please leave a message and send me a private letter.