目录

  • 前言

  • 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 接口的动态代理类,通过这个类,我们实现对数据库的增删改查。

接下来我们看看 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);}}``````javapublic 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 框架

整体流程

  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 框架。

首先按照我们以前的使用 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 配置类中。

先定义一个类来加载写 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 方法,使用动态代理生成一个加强类。这里面最终还是把数据库的相关操作转给 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 对应的实体类中**```javapackage 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 框架已经实现了,我们来写一个测试类测试一下。

  package com.paul.mybatis;import com.paul.mybatis.entity.User;import com.paul.mybatis.factory.DefaultSqlSessionFactory;import com.paul.mybatis.factory.SqlSessionFactory;import com.paul.mybatis.mapper.UserMapper;import com.paul.mybatis.sqlsession.SqlSession;public class TestDemo {public static void main(String[] args) {SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.selectByPrimaryKey(121312312313L);System.out.println(user.toString());}}

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

最后,本文如果对你来说,有收获,请点个在看或者评论区留个言,没收获,下周末我直播吃**,哈哈。

作者:当年明月

来源:https://www.cnblogs.com/paulwang92115/p/12130224.html


http://www.taodudu.cc/news/show-1750035.html

相关文章:

  • 再见,ELK!
  • 太火了,这些牛逼的Java代码技巧,肯定能让你目瞪口呆!
  • 支付宝架构到底有多牛逼?看完这篇你就明白了!
  • 同事线上埋的这个坑,我整整找了3天3夜
  • 同事1000行又臭又长 的类!被我用IDEA几分钟重构!真香!
  • 腾讯面试题:如何实现一个类似新浪微博的短链接服务!
  • 惊呆了!JDK1.8竟然打破了我对接口的一切认知...
  • Hibernate 与 Mybatis 如何共存?打破你的认知!
  • Oracle用户可要顶住了:准备好大规模补丁工作!以修补多达 433 个的新安全漏洞...
  • Mybatis trim 标签的 2 个妙用!
  • 震惊!Redis 的字符串居然是这样实现的…
  • 别在网上乱找代码了,找了一段代码突然爆了!!!
  • 看图说话,FastJson 并没有那么流行!
  • GitLab 内置了 CI/CD 工具,强大啊!!
  • 推荐:Windows平台上三款提高工作效率的免费神器!
  • 怒爬某 Hub 资源就为撸了一个鉴黄平台
  • 被Redis击穿的一次面试经历
  • 彻底搞懂“红黑树”......
  • 我们公司不会用分布式事务!
  • 看看华为 Java 编程的军规...
  • 我的天!你竟然没有在SpringBoot中使用过异步请求和异步调用...
  • Spring Cloud 微服务下的权限解决方案
  • DataGrip 上手体验,真香!
  • 又整理了一批可以拿去做副业的开源项目...
  • 为什么我劝你放弃了Restful API?
  • 因为一条SQL,我差点被祭天......
  • 40+ 张最全 Linux / C / C++ 思维导图,收藏!
  • 权威!盘点 100 个最受欢迎的 Java 库!绝对经典
  • 一起围观下我们CTO写的代码,巧妙使用枚举干掉if-else!
  • 要注意了!这样使用MyBatis框架,被攻击了!

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架!相关推荐

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

    目录 前言 JDBC MyBatis 源码分析 前置知识 原理分析 自己实现一个 MyBatis 框架 前言 MyBatis是一个非常优秀的持久层应用框架,目前几乎已经一统天下.既然是持久层框架,那么 ...

  2. 大数据分析苏轼,你没看错,这些都是小学生完成的

    适逢苏轼先生诞辰980周年,清华附小开展了一系列致敬苏轼的活动.同学们利用假期时间共完成课题研究报告23份:<大数据帮你进一步认识苏轼>.<苏轼的朋友圈>.<苏轼的旅游品 ...

  3. 32岁了 学python 来的及吗-32岁的程序员去了国企工作, 晒出年薪, 网友: 我没看错吧...

    原标题:32岁的程序员去了国企工作, 晒出年薪, 网友: 我没看错吧 现如今,相信很多朋友都非常羡慕那些互联网大公司的从业人员,因为他们动不动就年薪三四十万以上,但是这些互联网行业的从业人员也有自己的 ...

  4. android手机怎么拍月亮,用手机拍月亮!对,你没看错

    用手机拍月亮!对,你没看错 2020-01-12 20:54:00 0点赞 3收藏 4评论 12月16日,vivo在桂林发布了全新的X系列影像旗舰手机,vivo X30 Pro .拍照硬件方面,这台手 ...

  5. 你没看错,浩辰3D软件中CAD图纸与3D模型高效转化这么好用!

    3D模型精度低,无法有效利用? 零件设计很复杂,手忙脚乱.效率低? 各类CAD图纸,都要一笔一画来绘制? 每次设计修改,都仿佛是渡劫修仙, 熬夜加班,咖啡续命? 是时候,改变这一切了! 你没看错,浩辰 ...

  6. 主存地址位数怎么算_两位数乘一位数也能口算?对!你没看错,不是特殊情况也行...

    什么叫四则运算?加.减.乘.除呗.在四则运算中,如果没有括号,运算规则是先算乘.除,后算加.减. 乘法也是算术中最常见的一种运算.它是将相同的数加起来的一种快捷方式.比如说16×8=128.表示:8个 ...

  7. [惊!] IE 10.0,你没看错! IE10 Platform Preview 1出来啰~

    你还没安装上 IE 9.0吗?那真的逊! 因为..... IE 10.0已经有 Preview版本给人下载啰!!!!! 下载 IE10 Platform Preview 1    (2011/4/12 ...

  8. 1秒把 FLV MOV AVI MKV 3GP WEBM 转去 MP4 完全免费 - 完美教程 超级简单 你没看错

    1秒把 FLV MOV AVI MKV 3GP WEBM 转去 MP4 A. 前言 - 点赞吖,点赞是免费滴~ B. FFmpeg + Medlexo = 无敌搭配 C. 结论 A. 前言 - 点赞吖 ...

  9. C语言代码注释必须用/**/ , 你没看错~

    事情是这样的,有人离职,公司调我补缺.那个系统一直有个工程师在维护,参与该系统的新人来了又走,他始终泰然自若.刚过去一个礼拜,我就心下窃吼:"坑爹啊!",也彻底体会到什么叫---绝 ...

  10. HTML6 初探 — 你没看错,是6不是5

    前言: HTML5还没玩出个所以然,刚刚在网上看到关于HTML6的信息. 程序员这工作.我只想说...头儿!我要转行!!! 原文如下: HTML5 概述 HTML5 是 HTML 语言最受欢迎的版本之 ...

最新文章

  1. Linux 磁盘I/O读写速度检测
  2. UIApplication,UIWindow,UIViewController,UIView(layer)
  3. 面试题:聊聊Unix与Java的IO模型?
  4. php中var_dump()函数
  5. 被忽视的代码审查,往往可以事半功倍?
  6. 303.区域和检索-数组不可变
  7. 日期时间公式计算机,‎App Store 上的“多少天-重要日期时间计算器”
  8. mysql 嵌套查询优化
  9. C#软件注册与注册机
  10. 个人独资有限公司章程模板
  11. 打印1000张大概多少钱,打印费多少钱一张
  12. 浏览器在线视频播放加速方法——直接修改网页代码实现加速
  13. 使用Cdn加速静态资源
  14. swiper滑动时每页都有动画
  15. 卷积神经网络学习项目--Kaggle仙人掌识别--基于TensorFlow(未完成)
  16. 头像照片汇聚logo视频片头ae竖屏模板
  17. JavaWeb是什么?如何学习JavaWeb的体系
  18. 胡润区块链富豪榜公布,卖铲子卖水的最赚
  19. scikit-learn线性回归实践 - 波斯顿房价预测
  20. Jedis远程链接阿里云Redis

热门文章

  1. 网络电话坐享iOS10红利 iphone7免费通话
  2. Android通过Alpha实现渐变效果的几个方法
  3. Linux下的通用打开命令
  4. bash之sed与awk初步
  5. 未知账户(S-1-5-21)无法删除的问题
  6. hdu 4302 Holedox Eating 线段树去维护蛋糕!! 多校联合赛第三题
  7. Patternodes 3 for Mac(创建图形矢量模式工具)
  8. MimoLive for Mac(视频直播制作软件) 支持big sur
  9. 如何在 Mac 上将 WebP 图像批量转换为 JPG?
  10. iOS内存管理学习笔记二