Author: Xiao Fu Ge
Blog: https://bugstack.cn
Precipitate, share and grow, so that you and others can gain something! 😄
1, Foreword
The core principle of Mybatis is also the embodiment of its most convenient use. Why?
Because when we use Mybatis, we only need to define an interface that does not need to write implementation classes, and we can CRUD the database by annotation or configuring SQL statements.
So how do you do this? One very important point is that in Spring, you can hand over your proxy object to the Spring container. This proxy object can be regarded as a specific implementation class of DAO interface, and the proxy implementation class can complete an operation on the database, that is, this encapsulation process is called ORM framework.
After talking about the basic process, let's do some tests so that everyone can operate it! Learning knowledge must be first-hand, in order to get it! You can practice through the following source code repository
Source code: https://github.com/fuzhengwei/CodeGuide/wiki
2, Insert the Bean into the Spring container in several steps
- As for the technical scenario of Bean registration, MyBatis is the most common technical framework we use every day. When using MyBatis, you only define an interface without writing an implementation class, but this interface can be associated with the configured SQL statement, and the corresponding results can be returned when performing corresponding database operations. Then the proxy and registration of the Bean are used for the operation of this interface and the database.
- We all know that a class call cannot directly call an interface that is not implemented, so we need to generate a corresponding implementation class for the interface through a proxy. Next, put the proxy class into the FactoryBean implementation of Spring, and finally register the FactoryBean implementation class with the Spring container. Now that your proxy class has been registered in the Spring container, you can inject it into the attribute by annotation.
According to this implementation method, let's operate to see how the registration process of a Bean is implemented in the code.
1. Define interface
public interface IUserDao { String queryUserInfo(); }
- First define an interface similar to DAO, which is still very common when using MyBatis. We will proxy and register this interface later.
2. Class proxy implementation
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Class<?>[] classes = {IUserDao.class}; InvocationHandler handler = (proxy, method, args) -> "You're represented " + method.getName(); IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler); String res = userDao.queryUserInfo(); logger.info("Test results:{}", res);
- The proxy method of Java itself is relatively simple to use, and the usage is also very fixed.
- InvocationHandler is an interface class, and its corresponding implementation content is the specific implementation of the proxy object.
- Finally, give Proxy to create Proxy object, Proxy newProxyInstance.
3. Implement Bean factory
public class ProxyBeanFactory implements FactoryBean { @Override public Object getObject() throws Exception { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Class[] classes = {IUserDao.class}; InvocationHandler handler = (proxy, method, args) -> "You're represented " + method.getName(); return Proxy.newProxyInstance(classLoader, classes, handler); } @Override public Class<?> getObjectType() { return IUserDao.class; } }
- FactoryBean plays a second leading role in spring. It has nearly 70 younger brothers (implementing its interface definition), so it has three methods;
- T getObject() throws Exception; Return bean instance object
- Class<?> getObjectType(); Returns the instance class type
- boolean isSingleton(); Judge whether the singleton is. The singleton will be placed in the singleton cache pool in the Spring container
- Here, we put the above object using Java proxy into the getObject() method, and now the object obtained from Spring is our proxy object.
4. Bean registration
public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(ProxyBeanFactory.class); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao"); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); } }
In Spring's Bean management, all beans will eventually be registered in the class DefaultListableBeanFactory. The main contents of the above code include:
- Implement beandefinitionregistrypostprocessor Postprocessbeandefinitionregistry method to obtain the Bean registration object.
- Define Bean, GenericBeanDefinition. Here we mainly set up our proxy class factory.
- Create a Bean definition processing class, BeanDefinitionHolder, and the main parameters required here; Define the Bean and name setBeanClass(ProxyBeanFactory.class).
- Finally, register our own bean in the spring container, registry registerBeanDefinition()
5. Test verification
Above, we have registered the Bean of the custom proxy into the Spring container. Next, let's test how the Bean of the proxy is called.
1. Define spring config xml
<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/>
- Here, we configure RegisterBeanFactory into the xml configuration of spring to facilitate loading at startup.
2. Unit test
@Test public void test_IUserDao() { BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml"); IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class); String res = userDao.queryUserInfo(); logger.info("Test results:{}", res); }
test result
22:53:14.759 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source 22:53:14.760 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao' 22:53:14.796 [main] INFO org.itstack.interview.test.ApiTest - Test result: you are represented queryUserInfo Process finished with exit code 0
- From the test results, we can see that we can achieve our expected results by injecting the proxy Bean object into Spring.
- In fact, this process is also used in many frameworks, especially in some middleware development, similar ORM frameworks need to be used.
3, Write a Mybatis
Expand the previous source code analysis project; Itstack demo Mybatis, add like package to imitate Mybatis project. Complete procedure Download https://github.com/fuzhengwei/CodeGuide/wiki
itstack-demo-mybatis └── src ├── main │ ├── java │ │ └── org.itstack.demo │ │ ├── dao │ │ │ ├── ISchool.java │ │ │ └── IUserDao.java │ │ ├── like │ │ │ ├── Configuration.java │ │ │ ├── DefaultSqlSession.java │ │ │ ├── DefaultSqlSessionFactory.java │ │ │ ├── Resources.java │ │ │ ├── SqlSession.java │ │ │ ├── SqlSessionFactory.java │ │ │ ├── SqlSessionFactoryBuilder.java │ │ │ └── SqlSessionFactoryBuilder.java │ │ └── interfaces │ │ ├── School.java │ │ └── User.java │ ├── resources │ │ ├── mapper │ │ │ ├── School_Mapper.xml │ │ │ └── User_Mapper.xml │ │ ├── props │ │ │ └── jdbc.properties │ │ ├── spring │ │ │ ├── mybatis-config-datasource.xml │ │ │ └── spring-config-datasource.xml │ │ ├── logback.xml │ │ ├── mybatis-config.xml │ │ └── spring-config.xml │ └── webapp │ └── WEB-INF └── test └── java └── org.itstack.demo.test ├── ApiLikeTest.java ├── MybatisApiTest.java └── SpringApiTest.java
The whole Demo version as like as two peas is not the same as all Mybatis, but the core of the content is displayed to you. You will feel the same from the use, but the implementation class has been replaced.
- Configuration
- DefaultSqlSession
- DefaultSqlSessionFactory
- Resources
- SqlSession
- SqlSessionFactory
- SqlSessionFactoryBuilder
- XNode
1. Test the whole DemoJdbc framework first
ApiLikeTest.test_queryUserInfoById()
@Test public void test_queryUserInfoById() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L); System.out.println(JSON.toJSONString(user)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }
Everything goes well and the results are as follows (newcomers often encounter various problems);
{"age":18,"createTime":1576944000000,"id":1,"name":"Water water","updateTime":1576944000000} Process finished with exit code 0
Perhaps at first glance, this test class is completely different from mybatisapitest The code of Java as like as two peas is not the same. In fact, their introduced packages are different;
MybatisApiTest. Package introduced in Java
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder;
ApiLikeTest. Package introduced in Java
import org.itstack.demo.like.Resources; import org.itstack.demo.like.SqlSession; import org.itstack.demo.like.SqlSessionFactory; import org.itstack.demo.like.SqlSessionFactoryBuilder;
OK! Next, we start to analyze this part of the core code.
2. Load XML configuration file
Here, we use the configuration file structure of mybatis for parsing, which is as close to the source code as possible without destroying the original structure. When mybatis is used alone, two configuration files are used; Data source configuration and Mapper mapping configuration are as follows:;
mybatis-config-datasource. XML & data source configuration
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/User_Mapper.xml"/> <mapper resource="mapper/School_Mapper.xml"/> </mappers> </configuration>
User_ Mapper. XML & mapper mapping configuration
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.itstack.demo.dao.IUserDao"> <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select> <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where age = #{age} </select> </mapper>
The loading process here is different from mybaits. We use dom4j. In the case, you will see that the resources obtained at the beginning are as follows;
ApiLikeTest. test_ Queryuserinfo byid() & partial interception
String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); ...
It can be seen from the above that this is the process of obtaining the read stream through the address of the configuration file, so as to lay the foundation for subsequent parsing. First, let's look at the Resources class, which is our resource class.
Resources. Java & resource class
/** * Blog| https://bugstack.cn * Create by Little brother Fu @ 2020 */ public class Resources { public static Reader getResourceAsReader(String resource) throws IOException { return new InputStreamReader(getResourceAsStream(resource)); } private static InputStream getResourceAsStream(String resource) throws IOException { ClassLoader[] classLoaders = getClassLoaders(); for (ClassLoader classLoader : classLoaders) { InputStream inputStream = classLoader.getResourceAsStream(resource); if (null != inputStream) { return inputStream; } } throw new IOException("Could not find resource " + resource); } private static ClassLoader[] getClassLoaders() { return new ClassLoader[]{ ClassLoader.getSystemClassLoader(), Thread.currentThread().getContextClassLoader()}; } }
The entry of this code method is getResourceAsReader until it is done;
- Get the ClassLoader collection and search the configuration file to the maximum extent
- Through classloader Getresourceasstream reads the configuration resource and returns immediately after finding it. Otherwise, an exception is thrown
3. Parse XML configuration file
After the configuration file is loaded, start the parsing operation. Here we also follow the example of mybatis, but simplify it as follows;
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder. Build () & build class for entry
public DefaultSqlSessionFactory build(Reader reader) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new InputSource(reader)); Configuration configuration = parseConfiguration(document.getRootElement()); return new DefaultSqlSessionFactory(configuration); } catch (DocumentException e) { e.printStackTrace(); } return null; }
- Create an xml parsed Document class by reading the stream
- parseConfiguration parses the xml file and sets the result to the configuration class, including; Connection pool, data source, mapper relationship
SqlSessionFactoryBuilder. Parseconfiguration() & parse process
private Configuration parseConfiguration(Element root) { Configuration configuration = new Configuration(); configuration.setDataSource(dataSource(root.selectNodes("//dataSource"))); configuration.setConnection(connection(configuration.dataSource)); configuration.setMapperElement(mapperElement(root.selectNodes("mappers"))); return configuration; }
- As you can see from the previous xml content, we need to parse the database connection pool information datasource and the database statement mapping relationship mappers
SqlSessionFactoryBuilder. Datasource () & resolve the datasource
private Map<String, String> dataSource(List<Element> list) { Map<String, String> dataSource = new HashMap<>(4); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String name = e.attributeValue("name"); String value = e.attributeValue("value"); dataSource.put(name, value); } return dataSource; }
- This process is relatively simple. You only need to obtain the data source information
SqlSessionFactoryBuilder. Connection () & get database connection
private Connection connection(Map<String, String> dataSource) { try { Class.forName(dataSource.get("driver")); return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password")); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return null; }
- This is the original jdbc code, which obtains the database connection pool
SqlSessionFactoryBuilder. Mapperelement() & parse SQL statement
private Map<String, XNode> mapperElement(List<Element> list) { Map<String, XNode> map = new HashMap<>(); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String resource = e.attributeValue("resource"); try { Reader reader = Resources.getResourceAsReader(resource); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(new InputSource(reader)); Element root = document.getRootElement(); //Namespace String namespace = root.attributeValue("namespace"); // SELECT List<Element> selectNodes = root.selectNodes("select"); for (Element node : selectNodes) { String id = node.attributeValue("id"); String parameterType = node.attributeValue("parameterType"); String resultType = node.attributeValue("resultType"); String sql = node.getText(); // ? matching Map<Integer, String> parameter = new HashMap<>(); Pattern pattern = Pattern.compile("(#\\{(.*?)})"); Matcher matcher = pattern.matcher(sql); for (int i = 1; matcher.find(); i++) { String g1 = matcher.group(1); String g2 = matcher.group(2); parameter.put(i, g2); sql = sql.replace(g1, "?"); } XNode xNode = new XNode(); xNode.setNamespace(namespace); xNode.setId(id); xNode.setParameterType(parameterType); xNode.setResultType(resultType); xNode.setSql(sql); xNode.setParameter(parameter); map.put(namespace + "." + id, xNode); } } catch (Exception ex) { ex.printStackTrace(); } } return map; }
- This process first includes parsing all sql statements. At present, only select related statements are parsed for testing
- All sql statements are used to confirm uniqueness; The IDS in namespace + select are spliced as key s, and then stored in the map together with sql.
- In the sql statement configuration of mybaits, placeholders are used to pass parameters. where id = #{id} so we need to set the placeholder as a question mark. In addition, we need to store the order information and name of the placeholder in the map structure to facilitate the subsequent setting of input parameters during query.
4. Create DefaultSqlSessionFactory
Finally, use the initialized Configuration class Configuration as a parameter to create DefaultSqlSessionFactory, as follows:;
public DefaultSqlSessionFactory build(Reader reader) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new InputSource(reader)); Configuration configuration = parseConfiguration(document.getRootElement()); return new DefaultSqlSessionFactory(configuration); } catch (DocumentException e) { e.printStackTrace(); } return null; }
DefaultSqlSessionFactory. Implementation class of Java & sqlsessionfactory
public class DefaultSqlSessionFactory implements SqlSessionFactory { private final Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration.connection, configuration.mapperElement); } }
- This process is relatively simple. The constructor only provides configuration class input parameters
- openSession() of SqlSessionFactory is implemented to create DefaultSqlSession, which can also execute sql operations
5. Open SqlSession
SqlSession session = sqlMapper.openSession();
The above step is to create DefaultSqlSession, which is relatively simple. As follows;
@Override public SqlSession openSession() { return new DefaultSqlSession(configuration.connection, configuration.mapperElement); }
6. Execute SQL statement
User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
In DefaultSqlSession, SqlSession is implemented to provide database statement query and close connection pool, as follows;
SqlSession. Java & definition
public interface SqlSession { <T> T selectOne(String statement); <T> T selectOne(String statement, Object parameter); <T> List<T> selectList(String statement); <T> List<T> selectList(String statement, Object parameter); void close(); }
Next, let's look at the specific execution process, session selectOne
DefaultSqlSession. Selectone() & execute query
public <T> T selectOne(String statement, Object parameter) { XNode xNode = mapperElement.get(statement); Map<Integer, String> parameterMap = xNode.getParameter(); try { PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql()); buildParameter(preparedStatement, parameter, parameterMap); ResultSet resultSet = preparedStatement.executeQuery(); List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType())); return objects.get(0); } catch (Exception e) { e.printStackTrace(); } return null; }
-
selectOne objects get(0);, selectList returns all
-
Obtain the stored select tag information when parsing xml initially through the statement;
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select>
-
After obtaining the sql statement, give it to the PreparedStatement class of jdbc for execution
-
Here, we also need to set the input parameter. We extract the input parameter settings, as follows;
private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException { int size = parameterMap.size(); // Single parameter if (parameter instanceof Long) { for (int i = 1; i <= size; i++) { preparedStatement.setLong(i, Long.parseLong(parameter.toString())); } return; } if (parameter instanceof Integer) { for (int i = 1; i <= size; i++) { preparedStatement.setInt(i, Integer.parseInt(parameter.toString())); } return; } if (parameter instanceof String) { for (int i = 1; i <= size; i++) { preparedStatement.setString(i, parameter.toString()); } return; } Map<String, Object> fieldMap = new HashMap<>(); // Object parameters Field[] declaredFields = parameter.getClass().getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); field.setAccessible(true); Object obj = field.get(parameter); field.setAccessible(false); fieldMap.put(name, obj); } for (int i = 1; i <= size; i++) { String parameterDefine = parameterMap.get(i); Object obj = fieldMap.get(parameterDefine); if (obj instanceof Short) { preparedStatement.setShort(i, Short.parseShort(obj.toString())); continue; } if (obj instanceof Integer) { preparedStatement.setInt(i, Integer.parseInt(obj.toString())); continue; } if (obj instanceof Long) { preparedStatement.setLong(i, Long.parseLong(obj.toString())); continue; } if (obj instanceof String) { preparedStatement.setString(i, obj.toString()); continue; } if (obj instanceof Date) { preparedStatement.setDate(i, (java.sql.Date) obj); } } }
- A single parameter is relatively simple. You can set the value directly, such as Long, Integer, String
- If it is a class object, you need to get the Field property and set it to match the parameter Map
-
After setting parameters, execute the query Preparedstatement executeQuery()
-
Next, we need to convert the query result into our class (mainly the operation of reflection class), resultset2obj (resultset, class. Forname (xnode. Getresulttype());
private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { List<T> list = new ArrayList<>(); try { ResultSetMetaData metaData = resultSet.getMetaData(); int columnCount = metaData.getColumnCount(); // Value of each traversal row while (resultSet.next()) { T obj = (T) clazz.newInstance(); for (int i = 1; i <= columnCount; i++) { Object value = resultSet.getObject(i); String columnName = metaData.getColumnName(i); String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1); Method method; if (value instanceof Timestamp) { method = clazz.getMethod(setMethod, Date.class); } else { method = clazz.getMethod(setMethod, value.getClass()); } method.invoke(obj, value); } list.add(obj); } } catch (Exception e) { e.printStackTrace(); } return list; }
- Our class objects are generated mainly through reflection, and the type of this class is defined on the sql tag
- The time type needs post-processing. Timestamp is not the same type as java
7. Sql query supplementary description
sql queries have input parameters, whether they need input parameters, one query, and a query set. They only need to be properly packaged. For example, the following query sets, input parameters are object types;
ApiLikeTest.test_queryUserList()
@Test public void test_queryUserList() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User req = new User(); req.setAge(18); List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req); System.out.println(JSON.toJSONString(userList)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }
**Test results:
[{"age":18,"createTime":1576944000000,"id":1,"name":"Water water","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"peas","updateTime":1576944000000}] Process finished with exit code 0
4, Source code analysis (mybatis)
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency>
The whole source code of Mybatis is still large. The following mainly arranges and analyzes some core contents to facilitate the subsequent analysis of the source code part of the integration of Mybatis and Spring. Brief description includes:; Container initialization, configuration file parsing, Mapper loading and dynamic proxy.
1. Start with a simple case
To learn the source code of Mybatis, the best way must be to enter from a simple point, rather than analyze it from the integration of Spring. SqlSessionFactory is the core instance object of the whole Mybatis, and the instance of SqlSessionFactory object is obtained through SqlSessionFactoryBuilder object. The SqlSessionFactoryBuilder object can load configuration information from an XML configuration file and then create a SqlSessionFactory. Examples are as follows:
MybatisApiTest.java
public class MybatisApiTest { @Test public void test_queryUserInfoById() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L); System.out.println(JSON.toJSONString(user)); } finally { session.close(); reader.close(); } } catch (IOException e) { e.printStackTrace(); } } }
dao/IUserDao.java
public interface IUserDao { User queryUserInfoById(Long id); }
spring/mybatis-config-datasource.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/User_Mapper.xml"/> </mappers> </configuration>
If all goes well, there will be the following results:
{"age":18,"createTime":1571376957000,"id":1,"name":"tearful","updateTime":1571376957000}
From the code block above, you can see the core code; SqlSessionFactoryBuilder().build(reader) is responsible for loading, parsing and building the Mybatis configuration file until it can be executed and returned through SqlSession.
2. Container initialization
As can be seen from the above code, SqlSessionFactory is created through the SqlSessionFactoryBuilder factory class instead of using the constructor directly. The loading and initialization process of container configuration file is as follows:
- Process core class
- SqlSessionFactoryBuilder
- XMLConfigBuilder
- XPathParser
- Configuration
SqlSessionFactoryBuilder.java
public class SqlSessionFactoryBuilder { public SqlSessionFactory build(Reader reader) { return build(reader, null, null); } public SqlSessionFactory build(Reader reader, String environment) { return build(reader, environment, null); } public SqlSessionFactory build(Reader reader, Properties properties) { return build(reader, null, properties); } 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. } } } public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment) { return build(inputStream, environment, null); } public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); } 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. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } }
As can be seen from the above source code, SqlSessionFactory provides three ways to build objects;
- Byte stream: Java io. InputStream
- Character stream: Java io. Reader
- Configuration class: org apache. ibatis. session. Configuration
Then, the byte stream and character stream will create a configuration file parsing class: XMLConfigBuilder, and pass it through parser Parse () generates Configuration, and finally calls the configuration class construction method to generate SqlSessionFactory.
XMLConfigBuilder.java
public class XMLConfigBuilder extends BaseBuilder { private boolean parsed; private final XPathParser parser; private String environment; private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); ... public XMLConfigBuilder(Reader reader, String environment, Properties props) { this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); } ... }
- XMLConfigBuilder delegates the loading and parsing of XML files to XPathParser, and finally uses javax.xml that comes with JDK XML for XML parsing (XPath)
- XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)
- reader: use the character stream to create a new input source for reading XML files
- validation: whether to perform DTD verification
- variables: property configuration information
- entityResolver: Mybatis hardcoded new xmlmapterentityresolver() to provide the default XML parser
XMLMapperEntityResolver.java
public class XMLMapperEntityResolver implements EntityResolver { private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd"; private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd"; private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd"; private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd"; /* * Converts a public DTD into a local one * * @param publicId The public id that is what comes after "PUBLIC" * @param systemId The system id that is what comes after the public id. * @return The InputSource for the DTD * * @throws org.xml.sax.SAXException If anything goes wrong */ @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException { try { if (systemId != null) { String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH); if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) { return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId); } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) { return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId); } } return null; } catch (Exception e) { throw new SAXException(e.toString()); } } private InputSource getInputSource(String path, String publicId, String systemId) { InputSource source = null; if (path != null) { try { InputStream in = Resources.getResourceAsStream(path); source = new InputSource(in); source.setPublicId(publicId); source.setSystemId(systemId); } catch (IOException e) { // ignore, null is ok } } return source; } }
- Mybatis relies on the dtd file for parsing, in which ibatis-3-config DTDs are primarily used for compatibility purposes
- The call to getInputSource(String path, String publicId, String systemId) contains two parameters publicId (public identifier) and systemId (system identifier)
XPathParser.java
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { commonConstructor(validation, variables, entityResolver); this.document = createDocument(new InputSource(reader)); } private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); } private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException("Error creating document instance. Cause: " + e, e); } }
-
From top to bottom, you can see that it is mainly to create a Mybatis Document parser. Finally, according to builder Parse (inputsource) returns Document
-
After getting the XPathParser instance, next call the method: this (New XPathParser (reader, true, props, new xmlmapterentityresolver()), environment, props);
XMLConfigBuilder.this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
-
Which calls the constructor of the parent class.
public abstract class BaseBuilder { protected final Configuration configuration; protected final TypeAliasRegistry typeAliasRegistry; protected final TypeHandlerRegistry typeHandlerRegistry; public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } }
-
After XMLConfigBuilder is created, sqlSessionFactoryBuild calls parser Parse() create Configuration
public class XMLConfigBuilder extends BaseBuilder { public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } }
3. Configuration file analysis
This part is the core content of the whole XML file parsing and loading, including;
- Property resolution propertiesElement
- Load settings node settingsAsProperties
- Load custom VFS loadCustomVfs
- Resolve type alias typeAliasesElement
- Loading plugin pluginElement
- Load object factory objectFactoryElement
- Create an object wrapper factory objectWrapperFactoryElement
- Load reflection factory reflectorFactoryElement
- Element setting settingsElement
- Load environment configuration environmentsElement
- Database vendor ID load databaseIdProviderElement
- Load type processor typeHandlerElement
- (core) load mapper file mapperElement
parseConfiguration(parser.evalNode("/configuration")); private void parseConfiguration(XNode root) { try { //issue #117 read properties first //Property resolution propertiesElement propertiesElement(root.evalNode("properties")); //Load settings node settingsAsProperties Properties settings = settingsAsProperties(root.evalNode("settings")); //Load custom VFS loadCustomVfs loadCustomVfs(settings); //Resolve type alias typeAliasesElement typeAliasesElement(root.evalNode("typeAliases")); //Loading plugin pluginElement pluginElement(root.evalNode("plugins")); //Load object factory objectFactoryElement objectFactoryElement(root.evalNode("objectFactory")); //Create an object wrapper factory objectWrapperFactoryElement objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //Load reflection factory reflectorFactoryElement reflectorFactoryElement(root.evalNode("reflectorFactory")); //Element settings settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 //Load environment configuration environmentsElement environmentsElement(root.evalNode("environments")); //Database vendor ID load databaseIdProviderElement databaseIdProviderElement(root.evalNode("databaseIdProvider")); //Load type processor typeHandlerElement typeHandlerElement(root.evalNode("typeHandlers")); //Load mapper file mapperElement mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
All root The bottom layer of evalnode() is to call XML DOM methods: Object evaluate(String expression, Object item, QName returnType), expression parameter expression, and return the final node content through xobject resultobject = Eval (expression, item), which can be used for reference http://mybatis.org/dtd/mybatis-3-config.dtd , as follows;
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)> <!ELEMENT databaseIdProvider (property*)> <!ATTLIST databaseIdProvider type CDATA #REQUIRED > <!ELEMENT properties (property*)> <!ATTLIST properties resource CDATA #IMPLIED url CDATA #IMPLIED > <!ELEMENT property EMPTY> <!ATTLIST property name CDATA #REQUIRED value CDATA #REQUIRED > <!ELEMENT settings (setting+)> <!ELEMENT setting EMPTY> <!ATTLIST setting name CDATA #REQUIRED value CDATA #REQUIRED > <!ELEMENT typeAliases (typeAlias*,package*)> <!ELEMENT typeAlias EMPTY> <!ATTLIST typeAlias type CDATA #REQUIRED alias CDATA #IMPLIED > <!ELEMENT typeHandlers (typeHandler*,package*)> <!ELEMENT typeHandler EMPTY> <!ATTLIST typeHandler javaType CDATA #IMPLIED jdbcType CDATA #IMPLIED handler CDATA #REQUIRED > <!ELEMENT objectFactory (property*)> <!ATTLIST objectFactory type CDATA #REQUIRED > <!ELEMENT objectWrapperFactory EMPTY> <!ATTLIST objectWrapperFactory type CDATA #REQUIRED > <!ELEMENT reflectorFactory EMPTY> <!ATTLIST reflectorFactory type CDATA #REQUIRED > <!ELEMENT plugins (plugin+)> <!ELEMENT plugin (property*)> <!ATTLIST plugin interceptor CDATA #REQUIRED > <!ELEMENT environments (environment+)> <!ATTLIST environments default CDATA #REQUIRED > <!ELEMENT environment (transactionManager,dataSource)> <!ATTLIST environment id CDATA #REQUIRED > <!ELEMENT transactionManager (property*)> <!ATTLIST transactionManager type CDATA #REQUIRED > <!ELEMENT dataSource (property*)> <!ATTLIST dataSource type CDATA #REQUIRED > <!ELEMENT mappers (mapper*,package*)> <!ELEMENT mapper EMPTY> <!ATTLIST mapper resource CDATA #IMPLIED url CDATA #IMPLIED class CDATA #IMPLIED > <!ELEMENT package EMPTY> <!ATTLIST package name CDATA #REQUIRED >
mybatis-3-config. There are 11 configuration files in the DTD definition file, as follows:;
- properties?,
- settings?,
- typeAliases?,
- typeHandlers?,
- objectFactory?,
- objectWrapperFactory?,
- reflectorFactory?,
- plugins?,
- environments?,
- databaseIdProvider?,
- mappers?
Each of the above configurations is optional. The final configuration content will be saved to org apache. ibatis. session. Configuration, as follows;
public class Configuration { protected Environment environment; // Allow paging (rowboundaries) in nested statements. Set to false if allowed. The default is false protected boolean safeRowBoundsEnabled; // Allows paging (ResultHandler) in nested statements. Set to false if allowed. protected boolean safeResultHandlerEnabled = true; // Whether to enable automatic hump naming rule (camel case) mapping, that is, from the classic database column name a_ A similar mapping from column to the classic Java property name aColumn. Default false protected boolean mapUnderscoreToCamelCase; // When on, any method call will load all the properties of the object. Otherwise, each property is loaded on demand. The default value is false (true in ≤ 3.4.1) protected boolean aggressiveLazyLoading; // Whether a single statement is allowed to return multiple result sets (compatible drivers are required). protected boolean multipleResultSetsEnabled = true; // JDBC is allowed to support automatic generation of primary keys. Driver compatibility is required. This is the switch to obtain mysql auto incremented primary key / oracle sequence during insert. Note: Generally speaking, this is the desired result, and the default value should be true. protected boolean useGeneratedKeys; // Using column labels instead of column names is generally the desired result protected boolean useColumnLabel = true; // Whether to enable caching {it is enabled by default. Maybe this is also your interview question} protected boolean cacheEnabled = true; // Specifies whether to call the setter (put in case of map object) method of the mapping object when the value in the result set is null Keyset () is useful when initializing dependent or null values. protected boolean callSettersOnNulls; // It is allowed to use the name in the method signature as the statement parameter name. In order to use this feature, your project must be compiled in Java 8 with the - parameters option. (starting from 3.4.1) protected boolean useActualParamName = true; //When all columns of the returned row are empty, MyBatis returns null by default. When this setting is enabled, MyBatis will return an empty instance. Note that it also applies to nested result sets (i.e. collectioin and association). (starting from 3.4.2) Note: it is appropriate to split it into two parameters, one for result set and one for single record. Generally speaking, we want the result set not to be null, and the single record is still null protected boolean returnInstanceForEmptyRow; // Specifies the prefix that MyBatis adds to the log name. protected String logPrefix; // Specify the specific implementation of the log used by MyBatis. If it is not specified, it will be found automatically. It is generally recommended to specify slf4j or log4j protected Class <? extends Log> logImpl; // Specify the implementation of VFS, which is a simple interface provided by mybatis for accessing resources in AS protected Class <? extends VFS> vfsImpl; // MyBatis uses the Local Cache mechanism to prevent circular references and accelerate repeated nested queries. The default value is SESSION, in which case all queries executed in a SESSION are cached. If the value is set to state, the local SESSION is only used for STATEMENT execution, and different calls to the same SqlSession will not share data. protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; // Specify a JDBC type for a NULL value when no specific JDBC type is provided for the parameter. Some drivers need to specify the JDBC type of the column. In most cases, they can directly use the general type, such as NULL, VARCHAR or OTHER. protected JdbcType jdbcTypeForNull = JdbcType.OTHER; // Specifies which method of the object triggers a deferred load. protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" })); // Set the timeout, which determines the number of seconds the driver waits for a response from the database. Default no timeout protected Integer defaultStatementTimeout; // Sets the default fetch quantity for the driven result set. protected Integer defaultFetchSize; // SIMPLE is an ordinary actuator; REUSE executor will REUSE prepared statements; The BATCH executor reuses the statements and performs BATCH updates. protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; // Specify how MyBatis should automatically map columns to fields or properties. NONE means to cancel automatic mapping; PARTIAL automatically maps only result sets that do not have nested result set mappings defined. FULL automatically maps any complex result set, nested or not. protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; // Specifies the behavior of discovering unknown columns (or unknown attribute types) that are automatically mapped to. This value should be set to WARNING, which is more appropriate protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE; // properties property under settings protected Properties variables = new Properties(); // The default reflector factory is used to operate properties and constructors protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); // Object factory. All resultMap classes need to be instantiated by relying on the object factory protected ObjectFactory objectFactory = new DefaultObjectFactory(); // The object wrapper factory is mainly used to create non-native objects, such as proxy classes with some monitoring or special attributes protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); // Global switch for delayed loading. When on, all associated objects are loaded late. In a specific association, the switch state of the item can be overridden by setting the fetchType property. protected boolean lazyLoadingEnabled = false; // Specifies the proxy tool used by mybatis to create objects with deferred loading capability. MyBatis 3.3 + uses JAVASSIST protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL // MyBatis can execute different statements according to different database vendors. This multi vendor support is based on the databaseId attribute in the mapping statement. protected String databaseId; ... }
As can be seen from the above, Mybatis has all the configurations; resultMap, Sql statement, plug-in and cache are maintained in Configuration. Here is another trick. There is also a StrictMap internal class in Configuration, which inherits from HashMap and improves the anti duplication during put and the exception handling of unable to get a value during get, as follows;
protected static class StrictMap<V> extends HashMap<String, V> { private static final long serialVersionUID = -4950446264854982944L; private final String name; public StrictMap(String name, int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); this.name = name; } public StrictMap(String name, int initialCapacity) { super(initialCapacity); this.name = name; } public StrictMap(String name) { super(); this.name = name; } public StrictMap(String name, Map<String, ? extends V> m) { super(m); this.name = name; } }
(core) load mapper file mapperElement
Mapper file processing is the core service of Mybatis framework. All SQL statements are written in mapper, which is also the focus of our analysis. Other modules can be explained later.
XMLConfigBuilder.parseConfiguration()->mapperElement(root.evalNode("mappers"));
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // If you want to use package automatic scanning and explicitly specify the mapper to be loaded through mapper at the same time, you must ensure that the scope of package automatic scanning does not include the explicitly specified mapper. Otherwise, when you try to load the interface scanned through package, a duplicate judgment error occurs in the logic of loadXmlResource() of the corresponding xml file and is reported as org apache. ibatis. binding. Bindingexception exception. Even if the contents contained in the xml file and the statements contained in the mapper interface are not repeated, errors will occur, including the xml mapper automatically loaded when loading the mapper interface. if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { 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); 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); 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."); } } } } }
-
Mybatis provides two methods to configure mapper. The first is to use the automatic search mode of package, so that all interfaces under the specified package will be registered as mapper, which is also a common method in Spring, such as:
<mappers> <package name="org.itstack.demo"/> </mappers>
-
The other is to specify Mapper explicitly, which can be subdivided by resource, url or class, for example;
<mappers> <mapper resource="mapper/User_Mapper.xml"/> <mapper class=""/> <mapper url=""/> </mappers>
4. Mapper loading and dynamic proxy
Automatically search and load through package to generate the corresponding mapper agent class, code block and process, as follows;
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { ... } } } }
Mapper is loaded into the process of generating proxy objects. The main core classes include;
- XMLConfigBuilder
- Configuration
- MapperRegistry
- MapperAnnotationBuilder
- MapperProxyFactory
MapperRegistry.java
Parse and load Mapper
public void addMappers(String packageName, Class<?> superType) { // The mybatis framework provides a search for the specified package under the classpath and the classes in the child packages that meet the conditions (annotation or inheritance from a class / interface). Thread is used by default currentThread(). The loader returned by getcontextclassloader () is the same as the spring tool class. ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); // Load all classes unconditionally because the caller passed object Class as the parent class, which also leaves room for specifying the mapper interface in the future resolverUtil.find(new ResolverUtil.IsA(superType), packageName); // All matching calss are stored in resolverutil In the matches field Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { //Call the addMapper method to parse the mapper class / interface addMapper(mapperClass); } }
Generate proxy class: MapperProxyFactory
public <T> void addMapper(Class<T> type) { // The mybatis mapper interface file must be interface, not class if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // Create a MapperProxyFactory proxy for the mapper interface knownMappers.put(type, new MapperProxyFactory<T>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
The mapping relationship between interface classes and proxy projects is maintained in MapperRegistry, known mappers;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
MapperProxyFactory.java
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
The above is Mapper's proxy class project. mapperInterface in the constructor is the corresponding interface class. When instantiated, a specific MapperProxy proxy will be obtained, which mainly contains SqlSession.
5, Source code analysis (mybatis spring)
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency>
As an easy-to-use ORM framework, it must be Lori's face (simple), Royal sister's heart (powerful), paved bed (shielding direct contact with JDBC) and warm room (good speed performance)! In view of these advantages, Mybatis is used in almost most domestic Internet development frameworks, especially in some scenarios that require high performance. If sql needs to be optimized, it must be handwritten in xml. So, are you ready! Start analyzing its source code;
1. Start with a simple case
Like analyzing the source code of mybatis, first make a simple case; Define dao, write configuration file and junit unit test;
SpringApiTest.java
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring-config.xml") public class SpringApiTest { private Logger logger = LoggerFactory.getLogger(SpringApiTest.class); @Resource private ISchoolDao schoolDao; @Resource private IUserDao userDao; @Test public void test_queryRuleTreeByTreeId(){ School ruleTree = schoolDao.querySchoolInfoById(1L); logger.info(JSON.toJSONString(ruleTree)); User user = userDao.queryUserInfoById(1L); logger.info(JSON.toJSONString(user)); } }
spring-config-datasource.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 1.Database connection pool: DriverManagerDataSource You can also use DBCP2--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${db.jdbc.driverClassName}"/> <property name="url" value="${db.jdbc.url}"/> <property name="username" value="${db.jdbc.username}"/> <property name="password" value="${db.jdbc.password}"/> </bean> <!-- 2.to configure SqlSessionFactory object --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- Inject database connection pool --> <property name="dataSource" ref="dataSource"/> <!-- to configure MyBaties Global profile:mybatis-config.xml --> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- scanning entity Package use alias --> <property name="typeAliasesPackage" value="org.itstack.demo.po"/> <!-- scanning sql configuration file:mapper Needed xml file --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 3.Configure scan Dao Interface package, dynamic implementation Dao Interface, injection into spring In container --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- injection sqlSessionFactory --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- Give the information that needs to be scanned Dao Interface package, separated by multiple commas --> <property name="basePackage" value="org.itstack.demo.dao"/> </bean> </beans>
If all goes well, there will be the following results:
{"address":"No. 5, Yiheyuan Road, Haidian District, Beijing","createTime":1571376957000,"id":1,"name":"Peking University","updateTime":1571376957000} {"age":18,"createTime":1571376957000,"id":1,"name":"tearful","updateTime":1571376957000}
From the above unit test code, we can see that two annotations without method body magically execute the configuration statements in our xml and output the results. In fact, it mainly benefits from the following two categories;
- org.mybatis.spring.SqlSessionFactoryBean
- org.mybatis.spring.mapper.MapperScannerConfigurer
2. Scan assembly registration (mappercannerconfigurer)
Mappercannerconfigurer generates dynamic proxy class registration for the whole Dao interface layer, and starts to play a core role. This class implements the following interfaces to process the scanned Mapper:
- BeanDefinitionRegistryPostProcessor
- InitializingBean
- ApplicationContextAware
- BeanNameAware
The overall class diagram is as follows;
The implementation process is as follows:;
In fact, the above class diagram + flow chart has clearly described the mappercannerconfigurer initialization process, but it is still too difficult for me to see for the first time. OK, continue!
MapperScannerConfigurer. Java & partial interception
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
- Implemented beandefinitionregistrypostprocessor Postprocessbeandefinitionregistry is used to register beans into the Spring container
- Line 306: new ClassPathMapperScanner(registry); Hard coded classpath scanner for parsing Mapper files of Mybatis
- Line 317: scanner Scan to scan Mapper. This includes a call to the inheritance class implementation relationship, which is the test question at the beginning of this article.
ClassPathMapperScanner. Java & partial interception
@Override public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); } else { processBeanDefinitions(beanDefinitions); } return beanDefinitions; }
- Call super. Of the parent class first doScan(basePackages); Register Bean information
ClassPathBeanDefinitionScanner. Java & partial interception
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate) } if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.regi beanDefinitions.add(definitionHolder); registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
- The doScan method of the parent class is preferentially called for Mapper scanning, Bean definition and registration to the DefaultListableBeanFactory Defaultlistablebeanfactory is the ancestor of IOC container in Spring. All classes that need to be instantiated need to be registered and initialized
- Line 272: find candidate components (base package). Scan the package path. There is another way for annotation classes, which is similar
- Line 288: registerBeanDefinition(definitionHolder, this.registry); The process of registering Bean information will eventually call: org springframework. beans. factory. support. DefaultListableBeanFactory
ClassPathMapperScanner. Java & partial interception
**processBeanDefinitions(beanDefinitions);** private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
- Line 163: super doScan(basePackages);, After calling the parent class method, execute the internal method: processBeanDefinitions(beanDefinitions)
- Line 186: definition getConstructorArgumentValues(). addGenericArgumentValue(definition.getBeanClassName()); Set the BeanName parameter, that is, our ISchoolDao and IUserDao
- Line 187: definition setBeanClass(this.MapperFactoryBean.getClass());, When setting BeanClass, the interface itself does not have a class, so set the MapperFactoryBean class here. Finally, all dao layer interface classes are this MapperFactoryBean
MapperFactoryBean. Java & partial interception
This class has both inheritance and interface implementation. It's best to understand the overall class diagram, as follows;
This class is very important. Finally, all sql information execution will obtain getObject() through this class, that is, the proxy class of SqlSession to obtain mapper: mapperproxyfactory - > mapperproxy
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { private Class<T> mapperInterface; private boolean addToConfig = true; public MapperFactoryBean() { //intentionally empty } public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } /** * When the SpringBean container initializes, it will call checkDaoConfig(), which is an abstract method in the inherited class * {@inheritDoc} */ @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } } /** * {@inheritDoc} */ @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } ... }
-
Line 72: checkDaoConfig(), which is an abstract method in the inherited class, will be called when the SpringBean container is initialized
-
Line 95: getsqlsession() get Mapper( this.mapperInterface);, Get mapper (proxy class) through the interface. The calling process is as follows:;
-
DefaultSqlSession.getMapper(Class type), get Mapper
-
Configuration. Getmapper (class type, sqlsession, sqlsession), which is obtained from the configuration
-
MapperRegistry. Getmapper (class type, sqlsession, sqlsession), which is obtained from the registry and generated by instantiation
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); } }
-
mapperProxyFactory.newInstance(sqlSession);, Generate MapperProxy through reflection project
@SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
-
MapperProxy. Java & partial interception
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) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } @UsesJava7 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); } ... }
-
Line 58: final MapperMethod mappermethod = cachedmapppermethod (method);, Get MapperMethod from cache
-
Line 59: mappermethod execute(sqlSession, args);, Execute the SQL statement and return the results (about the query, the obtained results will go to the bone (dry) layer); INSERT,UPDATE,DELETE,SELECT
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); } 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; }
The above analysis of mappercannerconfigurer layer is completed. The information injected from scan definition to Bean preparation for Spring container, proxy, reflection and SQL execution basically include all core contents. Next, analyze SqlSessionFactoryBean
3. SqlSession container factory initialization (SqlSessionFactoryBean)
During the initialization of SqlSessionFactoryBean, some of its own contents need to be processed, so the following interfaces also need to be implemented;
- FactoryBean
- InitializingBean -> void afterPropertiesSet() throws Exception
- ApplicationListener
In fact, the above process has clearly described the whole core process, but there will be obstacles for novices on the road, so! OK, go on!
SqlSessionFactoryBean. Java & partial interception
public void afterPropertiesSet() throws Exception { notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); this.sqlSessionFactory = buildSqlSessionFactory(); }
- afterPropertiesSet(), the InitializingBean interface provides a way for beans to initialize methods. It only includes the afterPropertiesSet method. All classes that inherit this interface will execute this method when initializing beans.
- Line 380: buildSqlSessionFactory(); Internal method construction, core functions continue to look down.
SqlSessionFactoryBean. Java & partial interception
protected SqlSessionFactory buildSqlSessionFactory() throws IOException { Configuration configuration; XMLConfigBuilder xmlConfigBuilder = null; ... if (!isEmpty(this.mapperLocations)) { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found"); } } return this.sqlSessionFactoryBuilder.build(configuration); }
- Line 513: for (resource mapper location: this. Mapper locations) loop parses mapper content
- Line 519: XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(...) parse XMLMapperBuilder
- Line 521: xmlmapperbuilder Parse() performs parsing, as follows:;
XMLMapperBuilder. Java & partial interception
public class XMLMapperBuilder extends BaseBuilder { private final XPathParser parser; private final MapperBuilderAssistant builderAssistant; private final Map<String, XNode> sqlFragments; private final String resource; private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } } }
- Line 413 here is very important, configuration addMapper(boundType);, Really add Mapper to the configuration center
MapperRegistry. Java & partial interception
public class MapperRegistry { public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<T>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } } }
- Line 67: create the proxy project knownmappers put(type, new MapperProxyFactory(type));
Up to now, mappercannerconfigurer and SqlSessionFactoryBean have integrated the work done by the two classes;
-
The first one is used to scan the Dao interface, set the proxy class and register it in the IOC for subsequent generation of Bean entity class, MapperFactoryBean, and obtain Mapper from Configuration through mapperInterface
-
The other is used to generate SqlSession factory initialization, parse the XML Configuration in Mapper, conduct dynamic proxy mapperproxyfactory - > mapperproxy, and inject it into Mapper of Configuration
-
Finally, with the help of annotation classes, method injection can be carried out, and the dynamic proxy object can be obtained during operation, so as to execute the corresponding CRUD operation
@Resource private ISchoolDao schoolDao; schoolDao.querySchoolInfoById(1L);
6, To sum up
- The analysis process is long and lengthy. You may not be able to understand the whole process in one day, but you can still get a lot of gains when you are patient and study a little. In the future, such abnormalities can be easily solved, which is also helpful for interview and recruitment!
- The reason for analyzing Mybatis is to add custom annotations to Dao at first, and it is found that the section cannot be intercepted. Thinking that this is a class that is dynamically proxied, it is often raked down until mapperproxy invoke! Of course, Mybatis provides custom plug-in development.
- The above source code analysis only analyzes some core contents. If you want to know all of them, you can refer to resources; MyBatis 3 source code in-depth analysis, and debug the code. IDEA is still very convenient to see the source code, including class diagram, call sequence, etc.
- In mybatis and mybatis spring, the most important thing is to assemble Mapper configuration file parsing and interface classes into proxy classes for mapping, so as to facilitate the CRUD operation of the database. After analyzing the source code, you can get more programming experience (routines).
- Links to Mybatis;