目录

  • 前言
  • JDBC
  • MyBatis
  • 源码分析
  • 前置知识
  • 原理分析
  • 自己实现一个 MyBatis 框架

前言

MyBatis是一个非常优秀的持久层应用框架,目前几乎已经一统天下。既然是持久层框架,那么一定是对于数据库的操作,Java 中谈到数据库操作,一定少不了 JDBC。那么 ,MyBatis 比传统的 JDBC 好在哪那?MyBatis 又在哪方面做了优化那?

JDBC

如果我们需要查询所有用户,传统的 JDBC 会这样写。

public static void main(String[] args) {//声明Connection对象Connection con = null;try {//加载驱动程序Class.forName("com.mysql.jdbc.Driver");//创建 connection 对象con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password");//使用 connection 对象创建statement 或者 PreparedStatement 类对象,用来执行SQL语句Statement statement = con.createStatement();//要执行的SQL语句String sql = "select * from user";//3.ResultSet类,用来存放获取的结果集!!ResultSet rs = statement.executeQuery(sql);String job = "";String id = "";while(rs.next()){//获取job这列数据job = rs.getString("job");//获取userId这列数据id = rs.getString("userId");//输出结果System.out.println(id + "t" + job);}} catch(ClassNotFoundException e) {e.printStackTrace();} catch(SQLException e) {//数据库连接失败异常处理e.printStackTrace();}catch (Exception e) {e.printStackTrace();}finally{rs.close();con.close();}
}

通过上面的代码,我们可以将 JDBC 对于数据库的操作总结为以下几个步骤:

  1. 加载驱动
  2. 创建连接,Connection 对象
  3. 根据 Connection 创建 Statement 或者 PreparedStatement 来执行 SQL 语句
  4. 返回结果集到 ResultSet 中
  5. 手动将 ResultSet 映射到 JavaBean 中

传统的 JDBC 操作的问题也一目了然,整体非常繁琐,也不够灵活,执行一个 SQL 查询就要写一堆代码。

# MyBatis

来看看 MyBatis 代码如何查询数据库。几行代码就完成了数据库查询操作,并且将数据库查询出来的结果映射到了 JavaBean 中了。我们的代码没有加入 Spring Mybatis,加入 Spring 后整体流程会复杂很多,不方便我们理解。

//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection
public static SqlSession getSqlSession(){InputStream configFile = new FileInputStream(filePath);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);return sqlSessionFactory.openSession();
}//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。
public static User get(SqlSession sqlSession, int id){UserMapper userMapper = sqlSession.getMapper(UserMapper.class);return userMapper.selectByPrimaryKey(id);
}

我们来对 MyBatis 操作数据库做一个总结:

  1. 使用配置文件构建 SqlSessionFactory
  2. 使用 SqlSessionFactory 获得 SqlSession,SqlSession 相当于传统 JDBC 的 Conection
  3. 使用 SqlSession 得到 Mapper
  4. 用 Mapper 来执行 SQL 语句,并返回结果直接封装到 JavaBean 中

源码分析

大家平时应该经常使用 MyBatis 框架,对于 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我们从源码来分析怎么实现这些概念。

前置知识

先给出一个大部分框架的代码流程,方便大家理解框架。下面的图片就说明了接口、抽象类和实现类的关系,我们自己写代码时也要多学习这种思想。

带着结果看过程

看源码对于很多人来说都是一个比较枯燥和乏味的过程,如果不做抽象和总结,会觉得非常乱。另外,看源码不要去扣某个细节,尽量从宏观上理解它。这样带着结果看过程你就会知道设计者为什么这么做。

先给出整个 MyBatis 框架的架构图,大家先有一个印象:

原理分析

说明,我们讲解的是原生的 MyBatis 框架,并不是与 Spring 结合的 MyBatis 框架。

还是把上面 MyBatis 操作数据库的代码拿过来,方便我们与源码对照。

//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conectionpublic static SqlSession getSqlSession(){//步骤一InputStream configFile = new FileInputStream(filePath);//步骤二SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);return sqlSessionFactory.openSession();}//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。public static User get(SqlSession sqlSession, int id){//步骤三UserMapper userMapper = sqlSession.getMapper(UserMapper.class);return userMapper.selectByPrimaryKey(id);}

MyBatis 框架的第一步就是加载我们数据库的相关信息,比如用户名、密码等。以及我们在 XML 文件中写的 SQL 语句。

//配置文件中指定了数据库相关的信息和写 sql 语句的 mapper 相关信息,稍后我们需要读取并加载到我们的配置类中。<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://localhost:3306/db"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></transactionManager></environment></environments></configuration><mappers><mapper resource="xml/UserMapper.xml"/></mappers>

第二步就是通过读取到的配置文件信息,构建一个 SqlSessionFactory。

通过 openSession 方法返回了一个 sqlSession,我们来看看 openSession 方法做了什么。

//我们来重点看看 openSession 做了什么操作, DefaultSqlSessionFactory.java@Overridepublic SqlSession openSession() {return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);}public Configuration getConfiguration() {return this.configuration;}//这个函数里面有着事务控制相关的代码。private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;DefaultSqlSession var8;try {Environment environment = this.configuration.getEnvironment();TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);//根据上面的参数得到 TransactionFactory,通过 TransactionFactory 生成一个 Transaction,可以理解为这个 SqlSession 的事务控制器tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 将这个事务控制器封装在 Executor 里Executor executor = this.configuration.newExecutor(tx, execType);// 使用 configuration 配置类,Executor,和 configuration(是否自动提交) 来构建一个 DefaultSqlSession。var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);} catch (Exception var12) {this.closeTransaction(tx);throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);} finally {ErrorContext.instance().reset();}return var8;}

看了上面的一大段代码你可能会觉得蒙,没关系,我们来划重点,最终结果返回了一个
DefaultSqlsession。

// 使用 configuration 配置类(我们上面读取的配置文件就需要加载到这个类中),Executor(包含了数据事务控制相关信息),和 autoCommit(是否自动提交) 来构建一个 DefaultSqlSession。var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

有了这个 sqlSession 之后,我们就可以实现所有对数据库的操作了,因为我们已经把所有的信息加载到这里面了。数据库信息、SQL 信息、SQL 语句执行器等。

当然我们一般使用这个 sqlSession 获得对应的 mapper 接口类,然后用这个接口类查询数据库。

既然所有东西都封装在 sqlSession 中,先来看看 sqlSession 的组成部分。

SqlSession 的接口定义:里面定义了增删改查和提交回滚等方法。

public interface SqlSession extends Closeable {<T> T selectOne(String var1);<T> T selectOne(String var1, Object var2);<E> List<E> selectList(String var1);<E> List<E> selectList(String var1, Object var2);<E> List<E> selectList(String var1, Object var2, RowBounds var3);<K, V> Map<K, V> selectMap(String var1, String var2);<K, V> Map<K, V> selectMap(String var1, Object var2, String var3);<K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);<T> Cursor<T> selectCursor(String var1);<T> Cursor<T> selectCursor(String var1, Object var2);<T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);void select(String var1, Object var2, ResultHandler var3);void select(String var1, ResultHandler var2);void select(String var1, Object var2, RowBounds var3, ResultHandler var4);int insert(String var1);int insert(String var1, Object var2);int update(String var1);int update(String var1, Object var2);int delete(String var1);int delete(String var1, Object var2);void commit();void commit(boolean var1);void rollback();void rollback(boolean var1);List<BatchResult> flushStatements();void close();void clearCache();Configuration getConfiguration();<T> T getMapper(Class<T> var1);Connection getConnection();}

接下来用 sqlSession 获取对应的 Mapper。

DefaultSqlSession 的 getMapper 实现:

public <T> T getMapper(Class<T> type) {return this.configuration.getMapper(type, this);}//从 configuration 里面 getMapper,Mapper 就在 Configuration 里public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return this.mapperRegistry.getMapper(type, sqlSession);}

MapperRegistry 里 getMapper 的最终实现,同时我们需要思考一个问题,我们的 sqlSession 接口里面只定义了抽象的增删改查,而这个接口并没有任何实现类,那么这个 XML 到底是如何与接口关联起来并生成实现类那?通过 MapperRegistry 可以得出答案,那就是动态代理。

public class MapperRegistry {private final Configuration config;// 用一个 Map 来存储接口和 xml 文件之间的映射关系,key 应该是接口,但是 value 是 MapperProxyFactoryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();public MapperRegistry(Configuration config) {this.config = config;}public <T> T getMapper(Class<T> type, SqlSession sqlSession) {//获取到这个接口对应的 MapperProxyFactory。MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");} else {try {//用上一步获取的 MapperProxyFactory 和 sqlSession 构建对应的 Classreturn mapperProxyFactory.newInstance(sqlSession);} catch (Exception var5) {throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);}}}}
最终的结果是生成一个 mapper 接口的动态代理

最终的结果是生成一个 mapper 接口的动态代理类,通过这个类,我们实现对数据库的增删改查。

接下来我们看看 newInstance 的具体实现:

public T newInstance(SqlSession sqlSession) {// mapperInterface 就是接口MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);return this.newInstance(mapperProxy);}protected T newInstance(MapperProxy<T> mapperProxy) {//动态代理,这里的动态代理有一些不一样return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);}

为什么说这里的动态代理有一些不一样那?我们先看看正常流程的动态代理,接口,和接口实现类是必须的。而我们的 Mapper 接口只有充满了 SQL 语句的 XML 文件,没有具体实现类。

与传统的动态代理相比,MyBatis 的 Mapper 接口是没有实现类的,那么它又是怎么实现动态代理的那?

我们来看一下 MapperProxy 的源码:

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;}// 正常的动态代理中 Object proxy 这个参数应该是接口的实现类// com.paul.pkg.UserMapper@5a123uf// 现在里面是 org.apache.ibatis.binding.MapperProxy@6y213kn, 这俩面public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);}if (this.isDefaultMethod(method)) {return this.invokeDefaultMethod(proxy, method, args);}} catch (Throwable var5) {throw ExceptionUtil.unwrapThrowable(var5);}// Mapper 走这个流程,先尝试在缓存里获取 methodMapperMethod mapperMethod = this.cachedMapperMethod(method);return mapperMethod.execute(this.sqlSession, args);}private MapperMethod cachedMapperMethod(Method method) {MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);if (mapperMethod == null) {// mapperMethod 的构建,通过接口名,方法,和 xml 配置(通过 sqlSession 的 Configuration 获得)mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());//通过 execute 执行方法,因为 sqlSession 封装了 Executor,所以还要传进来,execute 方法使用//sqlSession 里面的方法。this.methodCache.put(method, mapperMethod);}return mapperMethod;}}

来看 MapperMethod 的定义:

// command 里面包含了方法名,比如 com.paul.pkg.selectByPrimaryKey// type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE// method 是方法的签名public class MapperMethod {private final MapperMethod.SqlCommand command;private final MapperMethod.MethodSignature method;public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);}}
public Object execute(SqlSession sqlSession, Object[] args) {Object result;//返回结果//INSERT操作if (SqlCommandType.INSERT == command.getType()) {//处理参数Object param = method.convertArgsToSqlCommandParam(args);//调用sqlSession的insert方法 result = rowCountResult(sqlSession.insert(command.getName(), param));}..........}

通过 sqlSession 来执行我们的 SQL 语句,返回结果,动态代理的方法调用结束。

进入 DefaultSqlSession 执行对应的 SQL 语句。

public <T> T selectOne(String statement, Object parameter) {List<T> list = this.selectList(statement, parameter);if (list.size() == 1) {return list.get(0);} else if (list.size() > 1) {throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());} else {return null;}}public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {List var5;try {// 这里又需要 configuration 来获取对应的 statement// MappedStatement 里面有 xml 文件,和要执行的方法,就是 xml 里面的 id,statementType,以及 sql 语句。MappedStatement ms = this.configuration.getMappedStatement(statement);// 用 executor 执行 query,executor 里面应该是包装了 JDBC。var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception var9) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);} finally {ErrorContext.instance().reset();}return var5;}

Executor 的实现类里面执行 query 方法。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache();if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);this.tcm.putObject(cache, key, list);}return list;}}// 使用 delegate 去 query,delegate 是 SimpleExecutor。里面使用 JDBC 进行数据库操作。return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

# 自己实现一个 MyBatis 框架

整体流程

image

  1. 首先创建 SqlSessionFactory 实例,SqlSessionFactory 就是创建 SqlSession 的工厂类。
  2. 加载配置文件创建 Configuration 对象,配置文件包括数据库相关配置文件以及我们在 XML 文件中写的 SQL。
  3. 通过 SqlSessionFactory 创建 SqlSession。
  4. 通过 SqlSession 获取 mapper 接口动态代理。
  5. 动态代理回调 SqlSession 中某查询方法。
  6. SqlSession 将查询方法转发给 Executor。
  7. Executor 基于 JDBC 访问数据库获取数据,最后还是通过 JDBC 操作数据库。
  8. Executor 通过反射将数据转换成 POJO 并返回给 SqlSession。
  9. 将数据返回给调用者。

项目整体使用 Maven 构建,mybatis-demo 是脱离 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代码就不在讲解了。paul-mybatis 是我们自己实现的 MyBatis 框架。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。

首先按照我们以前的使用 MyBatis 代码时的流程,创建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,这几个文件我们和 mybatis-demo 创建一样的即可,方便我们比较结果。

Mapper 接口,这里面定义两个抽象方法,根据主键查找用户和查找所有用户:

package com.paul.mybatis.mapper;import com.paul.mybatis.entity.User;import java.util.List;public interface UserMapper {User selectByPrimaryKey(long userId);List<User> selectAll();}

XML 文件,里面是上面两个抽象方法的具体 SQL 实现,完全消防官方 XML 文件的写法,需要注意 namespace、id、resultType、SQL 语句这几个点,都是我们后面代码需要处理的。

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.paul.mybatis.mapper.UserMapper"><select id="selectByPrimaryKey" resultType="User">select *from t_userwhere userId = #{userId}</select><select id="selectAll" resultType="User">select *from t_user</select></mapper>

最后,是我们的实体类,它的属性与数据库的表相对应:

package com.paul.mybatis.entity;public class User {private long userId;private String userName;private int sex;private String role;public long getUserId() {return userId;}public void setUserId(long userId) {this.userId = userId;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public int getSex() {return sex;}public void setSex(int sex) {this.sex = sex;}public String getRole() {return role;}public void setRole(String role) {this.role = role;}}

最后一个配置文件,数据库连接配置文件 db.propreties:

jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8jdbc.username=rootjdbc.password=root

配置文件和一些测试的必须类已经写完了,首先我们需要把这些配置信息加载到 Configuration 配置类中。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。

先定义一个类来加载写 SQL 语句的 XML 文件,上面我们说过要注意四个点,namespace、id、resultType、SQL 语句,我们写对应的属性来保存它,代码很简单,就不多讲了。

package com.paul.mybatis.confiuration;/**** XML 中的 sql 配置信息加载到这个类中**/public class MappedStatement {private String namespace;private String id;private String resultType;private String sql;public String getNamespace() {return namespace;}public void setNamespace(String namespace) {this.namespace = namespace;}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 getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}}

接下来我们定义一个 Configuration 总配置类,来保存 db.propeties 里面的属性和 XML 文件的 SQL 信息,Configuration 类里面的文件对应我们配置文件中的属性。

package com.paul.mybatis.confiuration;import java.util.HashMap;import java.util.List;import java.util.Map;/**** 所有的配置信息**/public class Configuration {private String jdbcDriver;private String jdbcUrl;private String jdbcPassword;private String jdbcUsername;private Map<String,MappedStatement> mappedStatement = new HashMap<>();public Map<String, MappedStatement> getMappedStatement() {return mappedStatement;}public void setMappedStatement(Map<String, MappedStatement> mappedStatement) {this.mappedStatement = mappedStatement;}public String getJdbcDriver() {return jdbcDriver;}public void setJdbcDriver(String jdbcDriver) {this.jdbcDriver = jdbcDriver;}public String getJdbcUrl() {return jdbcUrl;}public void setJdbcUrl(String jdbcUrl) {this.jdbcUrl = jdbcUrl;}public String getJdbcPassword() {return jdbcPassword;}public void setJdbcPassword(String jdbcPassword) {this.jdbcPassword = jdbcPassword;}public String getJdbcUsername() {return jdbcUsername;}public void setJdbcUsername(String jdbcUsername) {this.jdbcUsername = jdbcUsername;}}

按照上面的流程图,我们来创建一个 SqlSessionFactory 工厂类,这个类有两个功能,一个是加载配置文件信息到 Configuration 类中,另一个是创建 SqlSession。

SqlSessionFactory 抽象模版:

package com.paul.mybatis.factory;import com.paul.mybatis.sqlsession.SqlSession;public interface SqlSessionFactory {SqlSession openSession();}

创建 SqlSessionFactory 的 Default 实现类,Default 实现类主要完成了两个功能,加载配置信息到 Configuration 对象里,实现创建 SqlSession 的功能。

package com.paul.mybatis.factory;import com.paul.mybatis.confiuration.Configuration;import com.paul.mybatis.confiuration.MappedStatement;import com.paul.mybatis.sqlsession.DefaultSqlSession;import com.paul.mybatis.sqlsession.SqlSession;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.util.ArrayList;import java.util.List;import java.util.Properties;/**** 1.初始化时就完成了 configuration 的实例化* 2.工厂类,生成 sqlSession**/public class DefaultSqlSessionFactory implements SqlSessionFactory{private final Configuration configuration = new Configuration();// xml 文件存放的位置private static final String MAPPER_CONFIG_LOCATION = "mappers";// 数据库信息存放的位置private static final String DB_CONFIG_FILE = "db.properties";public DefaultSqlSessionFactory() {loadDBInfo();loadMapperInfo();}private void loadDBInfo() {InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE);Properties p = new Properties();try {p.load(db);} catch (IOException e) {e.printStackTrace();}//将配置信息写入Configuration 对象configuration.setJdbcDriver(p.get("jdbc.driver").toString());configuration.setJdbcUrl(p.get("jdbc.url").toString());configuration.setJdbcUsername(p.get("jdbc.username").toString());configuration.setJdbcPassword(p.get("jdbc.password").toString());}//解析并加载xml文件private void loadMapperInfo(){URL resources = null;resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION);File mappers = new File(resources.getFile());//读取文件夹下面的文件信息if(mappers.isDirectory()){File[] files = mappers.listFiles();for(File file:files){loadMapperInfo(file);}}}private void loadMapperInfo(File file){SAXReader reader = new SAXReader();//通过read方法读取一个文件转换成Document 对象Document document = null;try {document = reader.read(file);} catch (DocumentException e) {e.printStackTrace();}//获取根结点元素对象<mapper>Element e = document.getRootElement();//获取命名空间namespaceString namespace = e.attribute("namespace").getData().toString();//获取select,insert,update,delete子节点列表List<Element> selects = e.elements("select");List<Element> inserts = e.elements("insert");List<Element> updates = e.elements("update");List<Element> deletes = e.elements("delete");List<Element> all = new ArrayList<>();all.addAll(selects);all.addAll(inserts);all.addAll(updates);all.addAll(deletes);//遍历节点,组装成 MappedStatement 然后放入到configuration 对象中for(Element ele:all){MappedStatement mappedStatement = new MappedStatement();String id = ele.attribute("id").getData().toString();String resultType = ele.attribute("resultType").getData().toString();String sql = ele.getData().toString();mappedStatement.setId(namespace+"."+id);mappedStatement.setResultType(resultType);mappedStatement.setNamespace(namespace);mappedStatement.setSql(sql);// xml 文件中的每个 sql 方法都组装成 mappedStatement 对象,以 namespace+"."+id 为 key, 放入// configuration 配置类中。configuration.getMappedStatement().put(namespace+"."+id,mappedStatement);}}@Overridepublic SqlSession openSession() {// openSession 方法创建一个 DefaultSqlSession,configuration 配置类作为 构造函数参数传入return new DefaultSqlSession(configuration);}}

在 SqlSessionFactory 里创建了 DefaultSqlSession,我们看看它的具体实现。SqlSession 里面应该封装了所有数据库的具体操作和一些获取 mapper 实现类的方法。

SqlSession 接口,定义模版方法package com.paul.mybatis.sqlsession;import java.util.List;/**** 封装了所有数据库的操作* 所有功能都是基于 Excutor 来实现的,Executor 封装了 JDBC 操作***/public interface SqlSession {/*** 根据传入的条件查询单一结果* @param statement  namespace+id,可以用做 key,去 configuration 里面获取 sql 语句,resultType* @param parameter  要传入 sql 语句中的查询参数* @param <T> 返回指定的结果对象* @return*/<T> T selectOne(String statement, Object parameter);<T> List<T> selectList(String statement, Object parameter);<T> T getMapper(Class<T> type);}

Default 的 SqlSession 实现类。里面需要传入 Executor,这个 Executor 里面封装了 JDBC 操作数据库的流程。我们重点关注 getMapper 方法,使用动态代理生成一个加强类。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。这里面最终还是把数据库的相关操作转给 SqlSession,使用 Mapper 能使编程更加优雅。

package com.paul.mybatis.sqlsession;import com.paul.mybatis.bind.MapperProxy;import com.paul.mybatis.confiuration.Configuration;import com.paul.mybatis.confiuration.MappedStatement;import com.paul.mybatis.executor.Executor;import com.paul.mybatis.executor.SimpleExecutor;import java.lang.reflect.Proxy;import java.util.List;public class DefaultSqlSession implements  SqlSession {private final Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration) {super();this.configuration = configuration;executor = new SimpleExecutor(configuration);}@Overridepublic <T> T selectOne(String statement, Object parameter) {List<T> selectList = this.selectList(statement,parameter);if(selectList == null || selectList.size() == 0){return null;}if(selectList.size() == 1){return (T) selectList.get(0);}else{throw new RuntimeException("too many result");}}@Overridepublic <T> List<T> selectList(String statement, Object parameter) {MappedStatement ms = configuration.getMappedStatement().get(statement);// 我们的查询方法最终还是交给了 Executor 去执行,Executor 里面封装了 JDBC 操作。传入参数包含了 sql 语句和 sql 语句需要的参数。return executor.query(ms,parameter);}@Overridepublic <T> T getMapper(Class<T> type) {//通过动态代理生成了一个实现类,我们重点关注,动态代理的实现,它是一个 InvocationHandler,传入参数是 this,就是 sqlSession 的一个实例。MapperProxy mp = new MapperProxy(this);//给我一个接口,还你一个实现类return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);}}

来看看我们的 InvocationHandler 如何实现 invoke 方法:

package com.paul.mybatis.bind;import com.paul.mybatis.sqlsession.SqlSession;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Collection;import java.util.Collections;/**** 将请求转发给 sqlSession**/public class MapperProxy implements InvocationHandler {private SqlSession sqlSession;public MapperProxy(SqlSession sqlSession) {this.sqlSession = sqlSession;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getDeclaringClass().getName()+"."+method.getName());//最终还是将执行方法转给 sqlSession,因为 sqlSession 里面封装了 Executor//根据调用方法的类名和方法名以及参数,传给 sqlSession 对应的方法if(Collection.class.isAssignableFrom(method.getReturnType())){return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);}else{return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);}}}

获取 Mapper 接口的实现类我们已经实现了,通过动态代理调用 sqlSession 的方法。那么就剩最后一个重要的工作了,那就是实现 Exectuor 类去操作数据库,封装 JDBC。

Executor 抽象模版,我们只实现了 query、update 等操作慢慢增加。

package com.paul.mybatis.executor;import com.paul.mybatis.confiuration.MappedStatement;import java.util.List;/**** mybatis 核心接口之一,定义了数据库操作的最基本的方法,JDBC,sqlSession的所有功能都是基于它来实现的**/public interface Executor {/**** 查询接口* @param ms 封装sql 语句的 mappedStatemnet 对象,里面包含了 sql 语句,resultType 等。* @param parameter 传入sql 参数* @param <E> 将数据对象转换成指定对象结果集返回* @return*/<E> List<E> query(MappedStatement ms, Object parameter);}

Executor 接口的实现类,主要是对 JDBC 的封装,和利用反射方法将结果映射到 resultType 对应的实体类中

java
package com.paul.mybatis.executor;import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.util.ReflectionUtil;import java.sql.*;
import java.util.ArrayList;
import java.util.List;public class SimpleExecutor implements Executor {private final Configuration configuration;public SimpleExecutor(Configuration configuration) {this.configuration = configuration;}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter) {System.out.println(ms.getSql().toString());List<E> ret = new ArrayList<>(); //返回结果集try {Class.forName(configuration.getJdbcDriver());} catch (ClassNotFoundException e) {e.printStackTrace();}Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;try {connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());String regex = "#{([^}])*}";// 将 sql 语句中的 #{userId} 替换为 ?String  sql = ms.getSql().replaceAll(regex,"");preparedStatement = connection.prepareStatement(sql);//处理占位符,把占位符用传入的参数替换parametersize(preparedStatement, parameter);resultSet = preparedStatement.executeQuery();handlerResultSet(resultSet, ret,ms.getResultType());}catch (SQLException e){e.printStackTrace();}finally {try {resultSet.close();preparedStatement.close();connection.close();}catch (Exception e){e.printStackTrace();}}return ret;}private void parametersize(PreparedStatement preparedStatement,Object parameter) throws SQLException{if(parameter instanceof Integer){preparedStatement.setInt(1,(int)parameter);}else if(parameter instanceof  Long){preparedStatement.setLong(1,(Long)parameter);}else if(parameter instanceof  String){preparedStatement.setString(1,(String)parameter);}}private <E> void handlerResultSet(ResultSet resultSet, List<E> ret,String className){Class<E> clazz = null;//通过反射获取类对象try {clazz = (Class<E>)Class.forName(className);} catch (ClassNotFoundException e) {e.printStackTrace();}try {while (resultSet.next()) {Object entity = clazz.newInstance();//通过反射工具 将 resultset 中的数据填充到 entity 中ReflectionUtil.setPropToBeanFromResultSet(entity, resultSet);ret.add((E) entity);}} catch (Exception e) {e.printStackTrace();}}
}

看一下测试的结果,整个 MyBatis 框架已经实现完成了,当然有很多地方需要完善,比如 XML 中的 SQL 语句处处理还缺很多功能,目前只支持 select 等,希望大家能通过源码解读和自己写的过程明白 MyBatis 的具体实现要点。

mybatis查询返回null的原因_可怕!你没看错,这次确实是纯手工实现一个MyBatis框架...相关推荐

  1. mybatis查询返回null解决方案

    mybatis查询返回null解决方案 参考文章: (1)mybatis查询返回null解决方案 (2)https://www.cnblogs.com/zipon/p/6361661.html 备忘一 ...

  2. mysql查询有数据但返回null_Mybatis查不到数据查询返回Null问题

    mybatis突然查不到数据,查询返回的都是Null,但是 select count(*) from xxx查询数量,返回却是正常的. Preparing: SELECT id,a9004,a9005 ...

  3. Mybatis 查询 返回值中只有id有值,其他都是null;

    最近在重温mybatis, 但是在做练习的时候发现一个问题; 查询,简单的查询,返回之后发现结果中,只有id被映射了值,其他属性都是null; 很纳闷,为什么一个简单的测试会出现这种问题; 一开始以为 ...

  4. mybatis查询返回空,SQL数据库执行有数据!

    我的数据库为Oracle,可以插入,更新,但在在Mybatis中执行SQL查询返回的数据发现返回NULL,但是生成SQL放在数据库中查询是有数据的,并且SQL是正确的! 这是没有改动的XML: < ...

  5. modelandview为null的原因_一千个不用 Null 的理由!

    ★★★建议星标我们★★★ 公众号改版后文章乱序推荐,希望你可以点击上方"Java进阶架构师",点击右上角,将我们设为★"星标"!这样才不会错过每日进阶架构文章呀 ...

  6. modelandview为null的原因_如何在Java代码中去掉烦人的“!=null”

    云栖号资讯:[点击查看更多行业资讯] 在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!问题 为了避免空指针调用,我们经常会看到这样的语句 if (someobject != null) ...

  7. mybatis查询返回map的问题

    文章目录 背景 1.mybatis只返回单个map 2.查询返回map的list 3.利用mybatis的@MapKey注解返回map 4.重写handler 背景 假设背景: 想获取某个省下各个市有 ...

  8. 消防管件做的机器人图片_报废消防器材变身“机器人” 由消防官兵纯手工打造(图)...

    原标题:报废消防器材变身"机器人" 由消防官兵纯手工打造(图) 由报废的消防零配件组成的机器人模型. 厦门网讯 (厦门日报记者林路然通讯员阙凤芳曾德猛)远看好似变形金刚,凑近还会说 ...

  9. MyBatis查询返回类型为int,查询结果为空NULL,报异常解决

    例如: <select id="getPersonRecordId" parameterType="java.lang.String" resultTyp ...

最新文章

  1. Window Server 2008 R2 安装 Share Point 2013
  2. android 输入法 suretype,没输入法的年代,黑莓都有哪些经典键盘?
  3. 终于要揭开神秘面纱?Magic Leap将要展示产品
  4. python的web抓取_python实现从web抓取文档的方法
  5. JAVA【long值与ip地址互转】 - ip的判别
  6. Maven警告:“java使用了未经检查或不安全的操作。java: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。“
  7. TensorFlow学习笔记(二十一) tensorflow机器学习模型
  8. mixin network_基于Mixin Network的Go语言比特币开发教程 : 用 Mixin Messenger 机器人接受和发送比特币...
  9. Vue+ElementUI项目使用webpack输出MPA【华为云分享】
  10. android+委托列表,在Android适配器中使用委托者模式
  11. css几个居中的方法
  12. linux mysql c语言 api_linux连接MySQL数据库(C语言 API 分析,动态插入修改数据)
  13. python抓取图片_Python3简单爬虫抓取网页图片
  14. 做朋友圈需先从做人开始
  15. JavaScript DOM 编程艺术(第2版)读书笔记 (7)
  16. linux yum安装驱动,centos8安装alsa驱动
  17. UE4-(蓝图)第四十七课过场动画之主序列新建镜头、镜头剪辑、部分功能简介
  18. metasploit的SET的Credential Harvester Attack Method
  19. windows如何截图
  20. Nexmo 短信平台接口 遇到的坑

热门文章

  1. JVM分代垃圾回收策略的基础概念
  2. c lambda表达式 select 改变字段名称_大神是如何学习 Go 语言之浅谈 select 的实现原理...
  3. mac os android连接wifi密码,Mac使用小技巧:找回WiFi密码
  4. java程序计时器_求完整简单java计时器小程序代码,急!!
  5. Python+pandas查找前5位成绩最高的同学与前5个最高成绩的同学
  6. matlab生成exe失败,求助,m文件生成exe遇到的错误
  7. 广工android嵌入式系统试卷_教育录播系统的选择
  8. .html()与.text()区别与辨析
  9. python判断对错题_可以在线答题,并且能判断对错,将错题保存起来
  10. 防仿百度图片背景色php,基于jQuery实现仿百度首页换肤背景图片切换代码_jquery...