preface
mybaits is half of the world in ORM framework, because it is lightweight, semi-automatic loading, flexibility and extensibility. Deeply loved by the majority of companies, so our program development is inseparable from mybatis. But have we studied the mabtis source code? Or want to see but don't know how to look?
In the final analysis, we still need to know why there is mybatis, and what problems has mybatis solved?
If you want to know what problems mybatis has solved, you need to know what pain points exist in traditional JDBC operations to promote the birth of mybatis.
With these questions, let's take another step to learn.
Problems with the original JDBC
So let's first look at the operation of the original JDBC:
We know the most primitive database operations. It is divided into the following steps:
1. Get connection connection
2. Get preparedStatement
3. Parameter override placeholder
4. Get execution result resultSet
5. Parsing encapsulates the resultSet to be returned in the object.
The following is the original JDBC query code. What are the problems?
public static void main(String[] args) { String dirver="com.mysql.jdbc.Driver"; String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"; String userName="root"; String password="123456"; Connection connection=null; List<User> userList=new ArrayList<>(); try { Class.forName(dirver); connection= DriverManager.getConnection(url,userName,password); String sql="select * from user where username=?"; PreparedStatement preparedStatement=connection.prepareStatement(sql); preparedStatement.setString(1,"Zhang San"); System.out.println(sql); ResultSet resultSet=preparedStatement.executeQuery(); User user=null; while(resultSet.next()){ user=new User(); user.setId(resultSet.getInt("id")); user.setUsername(resultSet.getString("username")); user.setPassword(resultSet.getString("password")); userList.add(user); } } catch (Exception e) { e.printStackTrace(); }finally { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } if (!userList.isEmpty()) { for (User user : userList) { System.out.println(user.toString()); } } }
What are the unfriendly places on it that my friends found?
Here I summarize the following points:
1. The connection information of the database is hard coded, that is, it is written in the code.
2. Each operation will establish and release the connection connection, which is unnecessary waste of operating resources.
3. There is hard coding for sql and parameters.
4. It is troublesome to encapsulate the returned result set as an entity class. To create different entity classes and inject them one by one through the set method.
There are the above problems, so mybatis has improved the above problems.
For hard coding, it's easy to think of configuration files. mybatis also solves this problem.
For resource waste, we think of using connection pool, which is also the solution of mybatis.
For the trouble of encapsulating result set, we think it's the reflection mechanism of JDK. It's so ingenious that mybatis also solves it.
Design ideas
In this case, we will write a custom eating persistence layer framework to solve the above problems. Of course, we will refer to the design idea of mybatis. After we finish writing, we will see the source code of mybatis and realize that this is the reason for this configuration.
We are divided into two parts: the user end and the frame end.
End of use
Do we need to use mybatis SqlMapConfig.xml Configuration files that hold connection information for the database, and mapper.xml Point information for. mapper.xml Configuration files are used to store sql information.
So we are creating two files on the user side SqlMapConfig.xml and mapper.xml .
Frame end
What should framework end do? As follows:
1. Get the profile. That is to get the user's SqlMapConfig.xml as well as mapper.xml Documents of
2. Resolve the configuration file. Analyze the obtained files, get the connection information, sql, parameters, return types, etc. This information will be saved in the configuration object.
3. Create SqlSessionFactory to create an instance of SqlSession.
4. Create SqlSession to complete the operations of the original JDBC above.
What are the operations in SqlSession?
1. Get database connection
2. Get sql and analyze it
3. Inject parameters into preparedStatement through introspection
4. Execute sql
5. Encapsulate the result set as an object by reflection
End of use implementation
Well, as mentioned above, the general design idea is mainly based on the main class implementation of mybatis to ensure that the class names are consistent, which is convenient for us to read the source code later. Let's configure the user first. Let's create a maven project.
In the project, we create a User entity class
public class User { private Integer id; private String username; private String password; private String birthday; //getter() and setter() methods }
establish SqlMapConfig.xml and Mapper.xml
SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false"></property> <property name="userName" value="root"></property> <property name="password" value="123456"></property> <mapper resource="UserMapper.xml"> </mapper> </configuration>
You can see that we have configured the connection information of the database and an index of mapper in XML. In mybatis SqlMapConfig.xml There are other tags in it, which only enrich the functions, so we only use the most important ones.
mapper.xml
The sql of each class will generate a corresponding mapper.xml . Let's use the User class here, so let's create a UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="cn.quellanan.dao.UserDao"> <select id="selectAll" resultType="cn.quellanan.pojo.User"> select * from user </select> <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User"> select * from user where username=#{username} </select> </mapper>
You can see that it has a bit of the flavor of files in mybatis, with namespace representing namespace, id unique identification, resultType returning the type of result set and paramType parameter.
Let's create two configuration files on the user side first. Let's see how the framework side is implemented.
Come on ha ha.
Frame end implementation
At the frame end, we follow the above design ideas step by step.
Get configuration
How to get the configuration file? We can use JDK's own class Resources loader to get files. We create a custom Resource class to encapsulate the following:
import java.io.InputStream; public class Resources { public static InputStream getResources(String path){ //Use the class Resources loader that comes with the system to get the files. return Resources.class.getClassLoader().getResourceAsStream(path); } }
In this way, you can get the corresponding file stream through the incoming path.
Resolve profile
We got it from above SqlMapConfig.xml Configuration file, let's parse it now.
But before that, we need to do a little preparatory work, that is, where is the memory to be parsed?
So let's create two entity classes Mapper and Configuration.
Mapper
Mapper entity class is used to store user written mapper.xml The contents of the file include. id, sql, resultType and paramType. So the mapper entity we created is as follows:
public class Mapper { private String id; private Class<?> resultType; private Class<?> parmType; private String sql; //getter() and setter() methods }
Why don't we add the value of namespace here?
Smart, you must have found that, because these attributes in mapper indicate that each sql corresponds to a mapper, and namespace is a namespace, which is the upper layer of sql, so if you can't use it in mapper temporarily, you won't add it.
Configuration
The Configuration entity is used to hold information in SqlMapConfig. So we need to save the database connection. Here we directly use the DataSource provided by JDK. Another is mapper information. Each mapper has its own identity, so hashMap is used for storage. As follows:
public class Configuration { private DataSource dataSource; HashMap <String,Mapper> mapperMap=new HashMap<>(); //getter() and setter methods }
XmlMapperBuilder
Having done the above preparations, let's analyze mapper first. We create an XmlMapperBuilder class to parse. Parse the XML file through dom4j's tool class. The dom4j dependency I use here is:
<dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency>
Ideas:
1. Get the file stream and convert it to document.
2. Get the root node, which is mapper. Get the value of the namespace property of the root node
3. Get the select node, get its id, sql,resultType, paramType
4. Encapsulate the attributes of the select node into the Mapper entity class.
5. Similarly, getting the attribute value of update/insert/delete node is encapsulated in Mapper
6. Through namespace.id Generate the key value to save the mapper object to the HashMap in the Configuration entity.
7. Return to Configuration entity
The code is as follows:
public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); String namespace=rootElement.attributeValue("namespace"); List<Node> list=rootElement.selectNodes("//select"); for (int i = 0; i < list.size(); i++) { Mapper mapper=new Mapper(); Element element= (Element) list.get(i); String id=element.attributeValue("id"); mapper.setId(id); String paramType = element.attributeValue("paramType"); if(paramType!=null && !paramType.isEmpty()){ mapper.setParmType(Class.forName(paramType)); } String resultType = element.attributeValue("resultType"); if (resultType != null && !resultType.isEmpty()) { mapper.setResultType(Class.forName(resultType)); } mapper.setSql(element.getTextTrim()); String key=namespace+"."+id; configuration.getMapperMap().put(key,mapper); } return configuration; } }
I only parse the select tag above. You can parse the corresponding insert/delete/uupdate tags. The operations are the same.
XmlConfigBuilder
Let's analyze it again SqlMapConfig.xml The idea of configuration information is the same,
1. Get the file stream and convert it to document.
2. Get the root node, which is configuration.
3. Get all the property nodes in the root node, and get the value, that is, get the database connection information
4. Create a dataSource connection pool
5. Save connection pool information to Configuration entity
6. Get all mapper nodes of the root node
7. Call the XmlMapperBuilder class to parse the corresponding mapper and encapsulate it into the Configuration entity
8. Over
The code is as follows:
public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); //Get connection information List<Node> propertyList=rootElement.selectNodes("//property"); Properties properties=new Properties(); for (int i = 0; i < propertyList.size(); i++) { Element element = (Element) propertyList.get(i); properties.setProperty(element.attributeValue("name"),element.attributeValue("value")); } //Connection pool is used ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setDriverClass(properties.getProperty("driverClass")); dataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); dataSource.setUser(properties.getProperty("userName")); dataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(dataSource); //Get mapper information List<Node> mapperList=rootElement.selectNodes("//mapper"); for (int i = 0; i < mapperList.size(); i++) { Element element= (Element) mapperList.get(i); String mapperPath=element.attributeValue("resource"); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath)); } return configuration; } }
Create SqlSessionFactory
After the analysis, we create SqlSessionFactory to create the entity of sqlsession. In order to restore mybatis design idea as much as possible, we also adopt the factory design mode.
SqlSessionFactory is an interface that contains a method to create SqlSessionf.
As follows:
public interface SqlSessionFactory { public SqlSession openSqlSession(); }
This interface alone is not enough. We have to write an interface implementation class, so we create a DefaultSqlSessionFactory.
As follows:
public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSeeion(configuration); } }
As you can see, it is to create a DefaultSqlSeeion and pass on the configuration containing configuration information. Defaultsqlsession is an implementation class of SqlSession.
Create SqlSession
In SqlSession, we will deal with various operations, such as selectList, selectOne, insert.update,delete, and so on.
Let's write a selectList method for SqlSession.
As follows:
public interface SqlSession { /** * Condition search * @param statementid Unique identification, namespace.selectid * @param parm You can pass parameters, one or more than one * @param <E> * @return */ public <E> List<E> selectList(String statementid,Object...parm) throws Exception;
Then we create DefaultSqlSeeion to implement sqlseeion.
public class DefaultSqlSeeion implements SqlSession { private Configuration configuration; private Executer executer=new SimpleExecuter(); public DefaultSqlSeeion(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); List<E> query = executer.query(configuration, mapper, parm); return query; } }
We can see that DefaultSqlSeeion obtains the configuration and obtains the mapper from the configuration through statement. Then the implementation is given to the executor class. No matter how executor is implemented, we pretend to have implemented it. Then the whole frame end is finished. By calling Sqlsession.selectList() method to get the result.
I don't think we've dealt with it yet, so we've set up the framework? To cheat, we need to get the file parsing file and create the factory. It's all about getting ready. Let's start our JDBC implementation.
Specific implementation of SqlSession
We said earlier that the specific implementation of sqlsession has the following five steps
1. Get database connection
2. Get sql and analyze it
3. Inject parameters into preparedStatement through introspection
4. Execute sql
5. Encapsulate the result set as an object by reflection
But we left the implementation to executor in DefaultSqlSeeion. So we need to implement these operations in executor.
We first create a Executer interface and write a query method called in DefaultSqlSeeion.
public interface Executer { <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception; }
Then we write a SimpleExecuter class to implement the Executer.
then SimpleExecuter.query() method, we implement step by step.
Get database connection
Because the database connection information is saved in the configuration, it is better to get it directly.
//Get connection connection=configuration.getDataSource().getConnection();
Get sql and analyze it
Let's think about it. We're here Usermapper.xml What does sql look like?
select * from user where username=#{username}
How can we parse sql like {username}?
Two steps
1. Find {* * *} in sql, and replace this part with {}? Number
2. Parse {***} to get the value of the paramType corresponding to the parameter.
The specific implementation uses the following classes.
GenericTokenParser class, you can see that there are three parameters, the start tag is our "{", the end tag is "}", and the tag processor is to process the content in the tag, that is, username.
public class GenericTokenParser { private final String openToken; //Start tag private final String closeToken; //End tag private final TokenHandler handler; //Tag processor public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * Parse ${} and {} * @param text * @return * This method mainly implements the analysis and processing of placeholders in configuration files, scripts and other fragments, and returns the final required data. * Among them, the parsing work is completed by this method, and the processing work is implemented by the handleToken() method of the processor handler */ public String parse(String text) { //Specific implementation } }
The main one is the parse() method, which is used to get the sql of operation 1. Get results such as:
select * from user where username=?
TokenHandler is used to process parameters.
ParameterMappingTokenHandler class to implement TokenHandler
public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context is the parameter name {ID} {username} @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<ParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; } }
You can see that the parameter name is stored in the collection of ParameterMapping.
The ParameterMapping class is an entity used to save parameter names.
public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } //getter() and setter() methods. }
So we can get the parsed sql and parameter name through GenericTokenParser class. We encapsulate this information into the BoundSql entity class.
public class BoundSql { private String sqlText; private List<ParameterMapping> parameterMappingList=new ArrayList<>(); public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } ////getter() and setter() methods. }
OK, so we can go in two steps: first obtain, then analyze
obtain
Getting the original sql is very simple. The sql information exists in the mapper object, so it's good to get it directly.
String sql=mapper.getSql()
analysis
1. Create a ParameterMappingTokenHandler processor
2. Create a GenericTokenParser class and initialize the start tag, end tag, and processor
3. Execution genericTokenParser.parse(sql); get the parsed sql '' and the set of parameter names stored in the parameterMappingTokenHandler.
4. Encapsulate the parsed sql and parameters into the BoundSql entity class.
/** * Resolve custom placeholders * @param sql * @return */ private BoundSql getBoundSql(String sql){ ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String parse = genericTokenParser.parse(sql); return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings()); }
Inject parameters into preparedStatement
The above completes the parsing of sql, but we know that the above sql still contains placeholders for JDBC, so we need to inject parameters into preparedStatement.
1. Through boundSql.getSqlText() get sql with placeholder
2. Receive parameter name set parameterMappingList
3. Through mapper.getParmType() gets the class of the parameter.
4. Get the Field of the parameter class through the getDeclaredField(content) method.
5. Through Field.get() get the corresponding value from the parameter class
6. Inject into preparedStatement
BoundSql boundSql=getBoundSql(mapper.getSql()); String sql=boundSql.getSqlText(); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); //Get preparedStatement and pass the parameter value PreparedStatement preparedStatement=connection.prepareStatement(sql); Class<?> parmType = mapper.getParmType(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); Field declaredField = parmType.getDeclaredField(content); declaredField.setAccessible(true); Object o = declaredField.get(parm[0]); preparedStatement.setObject(i+1,o); } System.out.println(sql); return preparedStatement;
Execute sql
In fact, it is still to call the executeQuery() method or the execute() method of JDBC
//Execute sql ResultSet resultSet = preparedStatement.executeQuery();
Encapsulate the result set as an object by reflection
After getting the resultSet, we perform encapsulation processing, which is similar to parameter processing.
1. Create an ArrayList
2. Get the class of return type
3. Loop to fetch data from resultSet
4. Get property name and property value
5. Create property builder
6. Generate a write method for the attribute and write the attribute value to the attribute
7. Add this record to the list
8. Return to list
/** * Encapsulate result set * @param mapper * @param resultSet * @param <E> * @return * @throws Exception */ private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{ ArrayList<E> list=new ArrayList<>(); //Encapsulate result set Class<?> resultType = mapper.getResultType(); while (resultSet.next()) { ResultSetMetaData metaData = resultSet.getMetaData(); Object o = resultType.newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { //Property name String columnName = metaData.getColumnName(i); //Property value Object value = resultSet.getObject(columnName); //Create a property descriptor to generate a read-write method for the property PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,value); } list.add((E) o); } return list; }
Create SqlSessionFactoryBuilder
Let's create a SqlSessionFactoryBuilder class to provide a population for the user.
public class SqlSessionFactoryBuilder { private Configuration configuration; public SqlSessionFactoryBuilder(){ configuration=new Configuration(); } public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration); configuration=xmlConfigBuilder.loadXmlConfig(in); SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; } }
As you can see, a build method parses the information to configuration through the file stream of SqlMapConfig, creates and returns an sqlSessionFactory.
At this point, the whole framework end has been built, but we can see that only select operation has been implemented, update, inster, delete operation will be implemented in the source code I provide later, here is just the overall design idea and process.
test
Finally, it's time to test. We wrote a custom persistence layer earlier. Now let's test whether it can be used normally.
It's time to witness the miracle
Let's first introduce our custom framework dependency. And database and unit testing
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>cn.quellanan</groupId> <artifactId>myself-mybatis</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency>
Then we write a test class
1. Get SqlMapperConfig.xml File stream for
2. Get Sqlsession
3. Perform a find operation
@org.junit.Test public void test() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll"); for (User parm : list) { System.out.println(parm.toString()); } System.out.println(); User user=new User(); user.setUsername("Zhang San"); List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user); for (User user1 : list1) { System.out.println(user1); } }
It can be seen that it is OK. It seems that our customized persistence layer framework has taken effect.
optimization
But don't be too happy too early. Let's see whether the above test method feels different from the usual one. Every time, the statementId is written down, which is not very friendly. So let's do some operation next, general mapper configuration.
We add a getMapper method to SqlSession, and the received parameter is a class. We can know statementId through this class
/** * Use proxy mode to create proxy objects for interfaces * @param mapperClass * @param <T> * @return */ public <T> T getMapper(Class<T> mapperClass);
The specific implementation is to use the dynamic proxy mechanism of JDK.
1. Through Proxy.newProxyInstance() get a proxy object
2. Return proxy object
What do proxy objects do?
When creating a proxy object, an InvocationHandler interface will be implemented, and the invoke() method will be overridden so that all methods that walk through the proxy will execute the invoke() method. What does this method do?
This method is to get the class name and method name of the object through the class object passed in. Used to generate statement D. So we are mapper.xml The namespace in the configuration file needs to be specified as the classpath and the id as the method name.
Implementation method:
@Override public <T> T getMapper(Class<T> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //Get method name String name = method.getName(); //type String className = method.getDeclaringClass().getName(); String statementid=className+"."+name; return selectList(statementid,args); } }); return (T) proxyInstance; }
Let's write a UserDao
public interface UserDao { List<User> selectAll(); List<User> selectByName(User user); }
This is the familiar flavor ha ha, which is the interface of the mapper layer.
And then we mapper.xml namespace and id specified in
Next, we are writing a test method
@org.junit.Test public void test2() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); List<User> users = mapper.selectAll(); for (User user1 : users) { System.out.println(user1); } User user=new User(); user.setUsername("Zhang San"); List<User> users1 = mapper.selectByName(user); for (User user1 : users1) { System.out.println(user1); } }
Fanwai
Custom persistence layer framework, we're done. This is actually the prototype of mybatis. We manually write a persistence layer framework by ourselves, and then look at the source code of mybatis, it will be much clearer. The following class names are reflected in mybatis.
Here, I wish you a happy reading of the source code.
Brothers who think it's useful remember to collect it.
Cheeky for wave point praise!!!
This article is based on the platform of blog one article multiple sending OpenWrite release!