Mybatis是目前开发中最常用的一款基于ORM思想的半自动持久层框架,平时我们都仅仅停留在使用阶段,对mybatis是怎样运行的并不清楚,今天抽空找到一些资料自学了一波,自己写了一个mybatis的雏形,在此对学习过程做一个记录
首先,我们新建一个提供mybatis框架功能的工程IMybatis,这个工程中主要完成mybatis整个初始化和执行过程的功能开发。
该工程中用到的依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.my</groupId> <artifactId>IMybatis</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compile.encoding>UTF-8</maven.compile.encoding> <java.version>1.8</java.version> <maven.compile.source>1.8</maven.compile.source> <maven.compile.target>1.8</maven.compile.target> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.1.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.26</version> </dependency> </dependencies> </project>
我们在完成上面第一步中框架的编写后会进行打包发布到本地仓库,再新建一个测试工程IMybatis-test,这个工程的pom文件中会引入IMybatis工程的依赖,完成测试
该工程的依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.my</groupId> <artifactId>IMybatis-test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.my</groupId> <artifactId>IMybatis</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
mybatis要完成对数据库的连接,增删改查功能,需要有两个配置文件(这里先不管以注解的形式在mapper接口中编写的sql),一个是配置的数据库的连接信息,我这里是datasourceConfig.xml,
<configuration> <!-- 数据库配置信息 --> <dataSource> <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" value="jdbc:mysql:///test?serverTimezone=Asia/Shanghai"></property> <property name="username" value="root"></property> <property name="password" value="123456"></property> </dataSource> <mapper resource="UserMapper.xml"></mapper> </configuration>
另一个是提供sql的mapper文件,这里是UserMapper.xml,这两个文件都在IMybatis-test工程中提供
<mapper namespace="com.my.dao.UserMapper"> <!-- sql的唯一表示由 namespace.id 来组成statementId --> <select id="findAll" resultType="com.my.pojo.User"> select * from user </select> <select id="findOne" parameterType="com.my.pojo.User" resultType="com.my.pojo.User"> select * from user where id = #{id} </select> <select id="findById" parameterType="java.lang.Long" resultType="com.my.pojo.User"> select * from user where id = #{id} </select> <delete id="delete" parameterType="com.my.pojo.User"> delete from user where id = #{id} </delete> <delete id="deleteById" parameterType="java.lang.Long"> delete from user where id = #{id} </delete> <update id="update" parameterType="com.my.pojo.User"> update user set name = #{name} where id = #{id} </update> <insert id="insert" parameterType="com.my.pojo.User"> insert into user(id, name) VALUES(#{id}, #{name}) </insert> </mapper>
下面就要完成IMybatis的功能开发。
一、新建Resource类完成对datasourceConfig.xml文件的加载,将其以流的形式加载到内存中
package com.my.io; import java.io.InputStream; /** * @Description: 配置文件读取 * @Author lzh * @Date 2020/12/6 16:01 */ public class Resource { /** * 根据传递的路径path去读取到该路径下的配置文件datasourceConfig.xml,并将其读成字节流返回 * @param path * @return InputStream */ public static InputStream getResourceAsStream(String path){ InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
二、新建SqlSessionFactoryBuilder类,编写build()方法,一步一步构建SqlSessionFactory对象
package com.my.sqlSession; import com.my.config.XMLConfigBuilder; import com.my.pojo.Configuration; import java.io.InputStream; /** * @Description: 解析配置文件 * @Author lzh * @Date 2020/12/6 16:23 */ public class SqlSessionFactoryBuilder { /** * 根据字节流解析出配置文件中各个标签的值,并封装到Configuration中,创建DefaultSqlSessionFactory对象 * @param in * @return * @throws Exception */ public SqlSessionFactory build(InputStream in) throws Exception { //创建一个XMLConfigBuilder对象 XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(); //对配置文件进行解析 Configuration configuration = xmlConfigBuilder.parseConfig(in); //创建DefaultSqlSessionFactory对象 DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } }
三、在build方法中可以看到首先要创建一个XMLConfigBuilder 对象,在该对象中编写了一个parseConfig()方法完成对配置文件的解析,并完成对Configuration 对象的封装,Configuration 是我们这个工程中的一个非常核心的对象,里面存储了对配置文件解析后的结果,同样在真正的Mybatis框架中也有该对象,当然功能比我这里的更强大。
package com.my.config; import com.alibaba.druid.pool.DruidDataSource; import com.my.io.Resource; import com.my.pojo.Configuration; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; import java.util.Properties; /** * @Description: * @Author lzh * @Date 2020/12/6 16:26 */ public class XMLConfigBuilder { private Configuration configuration; public XMLConfigBuilder() { this.configuration = new Configuration(); } /** * 解析dataSourceConfig.xml * @param in * @return * @throws Exception */ public Configuration parseConfig(InputStream in) throws Exception { //利用dom4j技术对配置文件进行解析 Document document = new SAXReader().read(in); Element rootElement = document.getRootElement(); //查找dataSourceConfig.xml中的property标签 List<Element> list = rootElement.selectNodes("//property"); Properties properties = new Properties(); for (Element element : list) { //取出每个property标签中的值存到Properties对象中 String name = element.attributeValue("name"); String value = element.attributeValue("value"); properties.setProperty(name, value); } //从Properties中取出各个属性构建一个连接池,来提供对数据库连接的管理,避免资源浪费,提高性能 DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(properties.getProperty("driverClass")); druidDataSource.setUrl(properties.getProperty("url")); druidDataSource.setUsername(properties.getProperty("username")); druidDataSource.setPassword(properties.getProperty("password")); //将连接池对象放入Configuration对象中 configuration.setDataSource(druidDataSource); //解析dataSourceConfig.xml中的mapper标签,mapper标签中的resource属性值存放的就是UserMapper.xml的文件位置 List<Element> mapperList = rootElement.selectNodes("//mapper"); for (Element element : mapperList) { String mapperPath = element.attributeValue("resource"); InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath); //解析UserMapper.xml文件,进一步封装Configuration对象 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); xmlMapperBuilder.parse(resourceAsStream); } return configuration; } }
上图红色的地方创建了一个XMLMapperBuilder对象,该对象提供了一个parse()方法,就是完成对UserMapper.xml文件的解析,并完成对Configuration封装
package com.my.config; import com.my.config.eunm.SqlCommandType; import com.my.pojo.Configuration; import com.my.pojo.MappedStatement; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; /** * @Description: * @Author lzh * @Date 2020/12/6 17:03 */ public class XMLMapperBuilder { private Configuration configuration; public XMLMapperBuilder(Configuration configuration) { this.configuration = configuration; } /** * 解析UserMapper.xml配置文件中得内容,将每一个标签构建成一个MappedStatement,并赋值到Configuration中 * @param in * @throws DocumentException */ public void parse(InputStream in) throws DocumentException { Document document = new SAXReader().read(in); Element rootElement = document.getRootElement(); String namespace = rootElement.attributeValue("namespace"); //解析select标签 List<Element> selectList = rootElement.selectNodes("//select"); this.parseElement(selectList, namespace, SqlCommandType.SELECT); //解析insert标签 List<Element> insertList = rootElement.selectNodes("//insert"); this.parseElement(insertList, namespace, SqlCommandType.INSERT); //解析update标签 List<Element> updateList = rootElement.selectNodes("//update"); this.parseElement(updateList, namespace, SqlCommandType.UPDATE); //解析delete标签 List<Element> deleteList = rootElement.selectNodes("//delete"); this.parseElement(deleteList, namespace, SqlCommandType.DELETE); } /** * 解析mapper.xml文件中增删改查标签 * @param elementList * @param namespace * @param sqlCommandType */ private void parseElement(List<Element> elementList, String namespace, SqlCommandType sqlCommandType) { for (Element element : elementList) { String id = element.attributeValue("id"); String resultType = element.attributeValue("resultType"); String parameterType = element.attributeValue("parameterType"); String sql = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setSqlCommandType(sqlCommandType); mappedStatement.setId(id); mappedStatement.setParameterType(parameterType); mappedStatement.setResultType(resultType); mappedStatement.setSql(sql); configuration.getMappedStatementMap().put(namespace + "." + id, mappedStatement); } } }
该类中用到的SqlCommandType是一个枚举类,就是列举的UserMapper.xml中的几个主要的sql标签类型增删改查,也是借鉴的原Mybatis框架中的写法
package com.my.config.eunm; public enum SqlCommandType { INSERT, UPDATE, DELETE, SELECT; private SqlCommandType(){ } }
还有一个MappedStatement对象,这个对象中就是封装的每一个insert、update、delete、select标签中的信息(包括每个标签中的id、parameterType、resutType、sql语句等等),每个标签就是一个MappedStatement对象
package com.my.pojo; import com.my.config.eunm.SqlCommandType; /** * @Description: * @Author lzh * @Date 2020/12/6 16:17 */ public class MappedStatement { private SqlCommandType sqlCommandType; private String id; private String resultType; private String parameterType; private String sql; public SqlCommandType getSqlCommandType() { return sqlCommandType; } public void setSqlCommandType(SqlCommandType sqlCommandType) { this.sqlCommandType = sqlCommandType; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getResultType() { return resultType; } public void setResultType(String resultType) { this.resultType = resultType; } public String getParameterType() { return parameterType; } public void setParameterType(String parameterType) { this.parameterType = parameterType; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } }
封装好MappedStatement对象后,再将其放入Configuration对象的mappedStatementMap属性中,该属性就是一个Map集合,key就是UserMapper.xml文件中的namespace的值+ “.” +每一个标签的id值(例如我们这里的com.my.dao.UserMapper.findAll),因为一个Mapper接口对应一个Mapper.xml文件,而每个Mapper.xml文件中的namespace的值就是Mapper接口的全限定类名,每个标签的id值就是Mapper接口中对应的方法名,所以通过这个组合key就能和Mapper接口产生关联,当我们在调用Mapper接口中的方法时,就可以通过Mapper接口的全限定类名和调用的方法名在Configuration中的Map集合中找到对应的MappedStatement对象,也就是能拿到需要执行的sql、参数类型、返回值类型等等。
package com.my.pojo; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** 核心对象 * @Description: * @Author lzh * @Date 2020/12/6 16:18 */ public class Configuration { /** * 数据源 */ private DataSource dataSource; /** * key:statementId vlaue:封装好的MappedStatement */ private Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>(); public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Map<String, MappedStatement> getMappedStatementMap() { return mappedStatementMap; } public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) { this.mappedStatementMap = mappedStatementMap; } }
到这里我们的Configuration对象就封装完毕。
四、然后我们可以在第二步中的SqlSessionFactoryBuilder类的build()方法中看到,根据Configuration对象构造出了DefaultSqlSessionFactory工厂对象,整个构建DefaultSqlSessionFactory的过程就是一个构建者模式的体现(通过多个小的对象构建出一个大的对象)
package com.my.sqlSession; public interface SqlSessionFactory { SqlSession createSqlSession(); }
package com.my.sqlSession; import com.my.pojo.Configuration; /** * @Description: SqlSession的工厂对象,用于生产SqlSession * @Author lzh * @Date 2020/12/6 17:17 */ public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } /** * 创建SqlSession会话 * @return */ public SqlSession createSqlSession() { return new DefaultSqlSession(configuration); } }
五、利用DefaultSqlSessionFactory工厂对象 的createSqlSession()方法来获取一个SqlSession对象,就是一个我们所说的一个会话对象,该对象也是一个非常重要的对象
package com.my.sqlSession; import java.util.List; /** * @Description: SqlSession * @Author lzh * @Date 2020/12/6 17:18 */ public interface SqlSession { <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception; <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception; <T> T getMapper(Class<?> mapperClass); }
package com.my.sqlSession; import com.my.config.eunm.SqlCommandType; import com.my.pojo.Configuration; import com.my.pojo.MappedStatement; import java.lang.reflect.*; import java.util.List; /** * @Description: SqlSession会话的实现 * @Author lzh * @Date 2020/12/6 17:21 */ public class DefaultSqlSession implements SqlSession, InvocationHandler { private Configuration configuration; public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } /** * 多条查询 * @param statementId * @param param * @param <E> * @return * @throws Exception */ public <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception { SimpleExecutor simpleExecutor = new SimpleExecutor(); List<Object> query = simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param); return (List<E>) query; } /** * 单条查询 * @param statementId * @param param * @param <T> * @return * @throws Exception */ public <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception { List<Object> objects = selectList(statementId, methodParameterType, param); if (objects.size() == 1){ return (T) objects.get(0); }else if (objects.size() <= 0){ return null; }else { throw new RuntimeException("Result more than one"); } } /** * 新增 * @param statementId * @param param * @return */ public int insert(String statementId, Class<?> methodParameterType, Object... param) throws Exception { return update(statementId, methodParameterType, param); } /** * 修改 * @param statementId * @param param * @return */ public int update(String statementId, Class<?> methodParameterType, Object... param) throws Exception { SimpleExecutor simpleExecutor = new SimpleExecutor(); return simpleExecutor.update(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param); } /** * 删除 * @param statementId * @param param * @return */ public int delete(String statementId, Class<?> methodParameterType, Object... param) throws Exception { return update(statementId, methodParameterType, param); } /** * 创建代理对象 * @param mapperClass * @param <T> * @return */ @Override public <T> T getMapper(Class<?> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, this); return (T) proxyInstance; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); Class<?> methodParameterType = null; if (null != method.getParameterTypes() && 0 < method.getParameterTypes().length){ methodParameterType = method.getParameterTypes()[0]; } String className = method.getDeclaringClass().getName(); String statementId = className + "." + methodName; MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); if (SqlCommandType.SELECT == sqlCommandType){ Type genericReturnType = method.getGenericReturnType(); if (genericReturnType instanceof ParameterizedType){ return selectList(statementId, methodParameterType, args); } return selectOne(statementId, methodParameterType, args); }else if (SqlCommandType.INSERT == sqlCommandType){ return insert(statementId, methodParameterType, args); }else if (SqlCommandType.UPDATE == sqlCommandType){ return update(statementId, methodParameterType, args); }else if (SqlCommandType.DELETE == sqlCommandType){ return delete(statementId, methodParameterType, args); }else { throw new RuntimeException("Unknown SqlCommandType For: " + sqlCommandType); } } }
六、在SqlSession中,就提供了增删改查方法,用于操作数据库,我们另外还可以看到一个getMapper()方法,该方法需要传入一个Class参数,那么这个方法是干什么的呢?我们有过开发经验的朋友都知道很早以前在用spring+Mybatis框架开发的时候,对每一个Dao(也就是这里我们说的Mapper层)层的接口都会去写一个实现类DaoImpl,在实现类中通过JDBC来完成对数据库的操作,这样的编码方式会存在很多问题,比如:
- 每次执行一个方法都会区获取一个Connection对象,也就是创建一个数据库连接
- sql语句和业务代码融合在一起,增加代码耦合度,也不便于维护
- 封装返回结果麻烦,不够智能
所以针对第一个问题我们引入了连接池来管理数据库连接,每次都是从池子里面去获取,减少了资源消耗,提高了效率,针对后面两个问题,首先Mybatis去调了DaoImpl实现类,其次,通过Java反射技术完成对参数的赋值和对返回结果的动态封装(这一步后面代码中会有体现)。那么去掉了DaoImpl实现类,Dao接口中需要做的事总是需要有人来做的,否则无法完成对数据库的操作,因此Mybatis中会为每个Dao接口(也就是这里我们说的Mapper接口)生成一个代理对象,去完成之前DaoImpl做的事。这里的getMapper()方法就是去获取传入参数对象的代理对象,我们这里就是获取UserMapper接口的代理对象,创建代理对象时我们可以看到在getMapper()方法中的Proxy.newProxyInstance(),需要传递三个参数,第一个参数就是一个类加载器,第二参数就是我们需要为哪个对象产生代理对象,也就是getMapper()方法的参数,重点是第三个参数,需要传入一个InvocationHandler对象,而InvocationHandler是一个接口,我们这里的DefaultSqlSession实现了这个接口,所以第三个参数传的就是this,该类本身。实现了InvocationHandler接口就需要重写invoke()方法,而我们知道调用代理对象的方法,都会走到该invoke()方法中,所以我们这里调用UserMapper接口中的方法时,同样会执行这里的invoke方法,这样在invoke()方法中就可以完成我们以前在DaoImpl中需要完成的事。
七、下面我们具体来看下invoke()中做了什么,首先看下三个参数,第一个就是一个代理对象,第二个就是我们调用的方法Method,第三个就是调用方法时传入的参数args,那么我们根据Method对象就可以获取到该方法的全限定类名和该方法的名称,从而组合一个statemenId,而我们在上面第五步中通过createSqlSession()方法创建SqlSession对象时,是将我们封装的Configuration对象传入了,所有这里我们可以通过statementId在Configuration对象的mappedStatementMap这个Map集合中找到我们封装的MappedStatement对象,通过MappedStatement对象中的SqlCommandType的值我们可以判断出我们需要执行增删改查中的哪个方法,从而去调用该类具体的增删改查方法,在执行具体方法时,我们这里并没有在SqlSession对象中直接去操作数据库,而是将这些crud操作交给了一个SimpleExecutor执行器去完成真正对数据库的操作。
package com.my.sqlSession; import com.my.pojo.Configuration; import com.my.pojo.MappedStatement; import java.util.List; public interface Executor { <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object... param) throws Exception; int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception; }
package com.my.sqlSession; import com.my.config.BoundSql; import com.my.pojo.Configuration; import com.my.pojo.MappedStatement; import com.my.utils.GenericTokenParser; import com.my.utils.ParameterMapping; import com.my.utils.ParameterMappingTokenHandler; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @Description: Executor执行器 * @Author lzh * @Date 2020/12/6 17:32 */ public class SimpleExecutor implements Executor { /** * 真正的查询方法,负责完成JDBC的操作 * @param configuration * @param mappedStatement * @param param * @param <E> * @return * @throws Exception */ public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement,Class<?> methodParameterType, Object... param) throws Exception { PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param); //执行sql,返回结果集ResultSet ResultSet resultSet = preparedStatement.executeQuery(); //对结果封装,映射出对应得返回类型 String resultType = mappedStatement.getResultType(); Class<?> resultClass = getClassType(resultType); List<Object> result = new ArrayList<Object>(); while (resultSet.next()){ Object o = resultClass.newInstance(); ResultSetMetaData metaData = resultSet.getMetaData(); for (int i = 1; i <= metaData.getColumnCount(); i++) { String columnName = metaData.getColumnName(i); Object object = resultSet.getObject(columnName); PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o, object); } result.add(o); } return (List<E>) result; } @Override public int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception { PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param); preparedStatement.execute(); int row = preparedStatement.getUpdateCount(); return row; } /** * 获取PreparedStatement对象 * @param configuration * @param mappedStatement * @param param * @return * @throws Exception */ private PreparedStatement createPreparedStatement(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception { //获取数据库连接 Connection connection = configuration.getDataSource().getConnection(); //从MappedStatement中取出sql,现在的sql就是userMapper.xml中我们编写的带有#{}的sql语句 String sql = mappedStatement.getSql(); //处理sql语句,解析出sql语句中#{}中的属性值,并将#{}替换为?,封装到BoundSql对象中 BoundSql boundSql = getBoundSql(sql); //获取PreparedStatement对象 PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText()); //如果有参数,给参数赋值 String parameterType = mappedStatement.getParameterType(); if (null != parameterType){ Class<?> parameterClass = getClassType(parameterType); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0; i < parameterMappingList.size(); i++) { if (isObject(methodParameterType)){ preparedStatement.setObject(i + 1, param[0]); }else { ParameterMapping parameterMapping = parameterMappingList.get(i); //该content就是我们sql中#{id}中的id String content = parameterMapping.getContent(); //利用反射在parameterClass中取出content这个属性的值,并完成sql的赋值 Field declaredField = parameterClass.getDeclaredField(content); declaredField.setAccessible(true); Object o = declaredField.get(param[0]); preparedStatement.setObject(i + 1, o); } } } return preparedStatement; } /** * 根据参数类型或者返回值类型获取该对象的Class * @param parameterType * @return * @throws ClassNotFoundException */ private Class<?> getClassType(String parameterType) throws ClassNotFoundException { Class<?> aClass = Class.forName(parameterType); return aClass; } /** * 解析sql,封装成BoundSql * @param sql * @return */ private BoundSql getBoundSql(String sql) { ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); //解析出来的sql String parseSql = genericTokenParser.parse(sql); //解析出来的id和name List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(parseSql, parameterMappings); return boundSql; } private Boolean isObject(Class<?> methodParameterType){ if (null == methodParameterType){ return false; } if (Integer.class.getName().equals(methodParameterType.getName()) || Long.class.getName().equals(methodParameterType.getName()) || String.class.getName().equals(methodParameterType.getName()) || Double.class.getName().equals(methodParameterType.getName()) || Float.class.getName().equals(methodParameterType.getName()) || Byte.class.getName().equals(methodParameterType.getName()) || Short.class.getName().equals(methodParameterType.getName()) || Character.class.getName().equals(methodParameterType.getName()) || Boolean.class.getName().equals(methodParameterType.getName()) || Date.class.getName().equals(methodParameterType.getName())){ return true; } return false; } }
八、在这个执行器中就是真正完成对数据库的操作,从连接池中获取一个Connection连接,从MappedStatement中获取到要执行的sql,这里注意这时候的sql还是从UserMapper.xml中解析出来的sql(select * from user where id = #{id}),需要对其进行处理用?替换掉#{},并记录大括号中的参数,因为JDBC中参数的占位符是?,所以这里的getBoundSql()方法就是在做这些事情,最终封装成一个BoundSql对象。
package com.my.config; import com.my.utils.ParameterMapping; import java.util.ArrayList; import java.util.List; /** * @Description: sql * @Author lzh * @Date 2020/12/6 17:42 */ public class BoundSql { private String sqlText; private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>(); public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } public String getSqlText() { return sqlText; } public void setSqlText(String sqlText) { this.sqlText = sqlText; } public List<ParameterMapping> getParameterMappingList() { return parameterMappingList; } public void setParameterMappingList(List<ParameterMapping> parameterMappingList) { this.parameterMappingList = parameterMappingList; } }
这其中用到的几个工具类我也贴在这里,这也是从Mybatis源码中拿到的,就是对sql解析处理,这里不用过大关注。
package com.my.utils; /** * @author lzh */ public interface TokenHandler { String handleToken(String content); }
package com.my.utils; import java.util.ArrayList; import java.util.List; public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context是参数名称 #{id} #{username} 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; } }
package com.my.utils; /** * @author Clinton Begin */ public class GenericTokenParser { private final String openToken; //开始标记 private final String closeToken; //结束标记 private final TokenHandler handler; //标记处理器 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * 解析${}和#{} * @param text * @return * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。 * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现 */ public String parse(String text) { // 验证参数问题,如果是null,就返回空字符串。 if (text == null || text.isEmpty()) { return ""; } // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。 int start = text.indexOf(openToken, 0); if (start == -1) { return text; } // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder, // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码 char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; while (start > -1) { // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理 if (start > 0 && src[start - 1] == '\\') { builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { //重置expression变量,避免空指针或者老数据干扰。 if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) {////存在结束标记时 if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时 // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else {//不存在转义字符,即需要作为参数进行处理 expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { //首先根据参数的key(即expression)进行参数处理,返回?作为占位符 builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }
package com.my.utils; public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
解析完sql后就是创建PreparedStatement对象,并通过MappedStatement对象中记录的参数类型,利用java反射技术进行赋值,然后执行sql,最后再通过MappedStatement对象中记录的返回值类型对结果进行封装,同样是用java反射,这样就实现了参数的动态赋值和结果的动态封装。这就是整个Mybatis的执行流程,到这里也就完成了IMybatis框架的编写,下面我们进行测试。
九、将IMybatis打包到本地仓库,在IMybatis-test中引入依赖,编写一个用户Pojo类、UserMapper接口和一个测试类,UserMapper.xml在上面已经提供
package com.my.pojo; /** * @Description: * @Author lzh * @Date 2020/12/6 15:57 */ public class User { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
package com.my.dao; import com.my.pojo.User; import java.util.List; public interface UserMapper { /** * 查询所有 * @return */ List<User> findAll() ; /** * 查询单条 * @param user * @return */ User findOne(User user); /** * 根据id查询单条 * @param id * @return */ User findById(Long id); /** * 根据id删除用户 * @param id * @return */ int deleteById(Long id); /** * 删除用户 * @param user * @return */ int delete(User user); /** * 新增用户 * @param user * @return */ int insert(User user); /** * 修改用户 * @param user * @return */ int update(User user); }
package com.my.test; import com.my.dao.UserMapper; import com.my.io.Resource; import com.my.pojo.User; import com.my.sqlSession.SqlSession; import com.my.sqlSession.SqlSessionFactory; import com.my.sqlSession.SqlSessionFactoryBuilder; import org.junit.Before; import org.junit.Test; import java.io.InputStream; import java.util.List; /** * @Description: * @Author lzh * @Date 2020/12/6 16:05 */ public class IMybatisTest { private SqlSession sqlSession; @Before public void before() throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("dataSourceConfig.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream); sqlSession = sqlSessionFactory.createSqlSession(); } @Test public void test1() { User user = new User(); user.setId(2L); user.setName("王五"); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user1 = mapper.findOne(user); System.out.println(user1); } @Test public void test2() { User user = new User(); user.setId(1L); user.setName("王五"); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> all = mapper.findAll(); for (User user1 : all) { System.out.println(user1); } } @Test public void test3() { UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user1= mapper.findById(2L); System.out.println(user1); } @Test public void test4() { User user = new User(); user.setId(3L); UserMapper mapper = sqlSession.getMapper(UserMapper.class); int row = mapper.delete(user); System.out.println(row); } @Test public void test5() { User user = new User(); user.setId(3L); user.setName("王五"); UserMapper mapper = sqlSession.getMapper(UserMapper.class); int row = mapper.update(user); System.out.println(row); } @Test public void test6() { User user = new User(); user.setId(3L); user.setName("张三"); UserMapper mapper = sqlSession.getMapper(UserMapper.class); int row = mapper.insert(user); System.out.println(row); } }
这里就不把全部的测试结果贴出来了,贴一个看下效果就行,可以看到控制台正常输出,说明我们自己写的IMybatis没问题,可以成功执行。
总结:我们可以看到最后仍然是通过JDBC完成的数据库操作。所以到这里我们可以知道Mybatis最终仍然是调用的JDBC去操作数据库,它只不过在执行JDBC之前还多去做了这一系列解析配置文件,封装各个对象等等这些操作,Mybatis就是对JDBC的包装。