深入浅出读懂mybatis框架!

目录

前言

原始 JDBC 存在的问题

我们知道最原始的数据库操作。分为以下几步

设计思路

使用端

框架端

那在 SqlSession 中 进行了哪些操作呢?

使用端实现

框架端实现

XmlConfigBuilder

创建 SqlSessionFactory

SqlSession 具体实现

{username} 这样的 sql 我们该怎么解析呢?

将参数注入到 preparedStatement 中

通过反射将结果集封装成对象

创建 SqlSessionFactoryBuilder

测试

优化

接下来我们在写一个测试方法

番外

最后来自小编的福利


前言

mybaits 在 ORM 框架中,可算是半壁江山了,由于它是轻量级,半自动加载,灵活性和易拓展性。深受广大公司的喜爱,所以我们程序开发也离不开 mybatis 。但是我们有对 mabtis 源码进行研究吗?或者想看但是不知道怎么看的苦恼吗?  归根结底,我们还是需要知道为什么会有 mybatis ,mybatis 解决了什么问题? 想要知道 mybatis 解决了什么问题,就要知道传统的 JDBC 操作存在哪些痛点才促使 mybatis 的诞生。 我们带着这些疑问,再来一步步学习吧。

上面的一份详细的mybatis的学习思维导图已整理好,还有一些mybatis的技术资料文档,需要领取可以 点我领取 。

原始 JDBC 存在的问题

所以我们先来来看下原始 JDBC 的操作

我们知道最原始的数据库操作。分为以下几步

1、获取 connection 连接

2、获取 preparedStatement

3、参数替代占位符

4、获取执行结果 resultSet

5、解析封装 resultSet 到对象中返回。

如下是原始 JDBC 的查询代码,存在哪些问题?

 
  1. public static void main(String[] args) {

  2. String dirver="com.mysql.jdbc.Driver";

  3. String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";

  4. String userName="root";

  5. String password="123456";

  6.  
  7. Connection connection=null;

  8. List<User> userList=new ArrayList<>();

  9. try {

  10. Class.forName(dirver);

  11. connection= DriverManager.getConnection(url,userName,password);

  12.  
  13. String sql="select * from user where username=?";

  14. PreparedStatement preparedStatement=connection.prepareStatement(sql);

  15. preparedStatement.setString(1,"张三");

  16. System.out.println(sql);

  17. ResultSet resultSet=preparedStatement.executeQuery();

  18.  
  19. User user=null;

  20. while(resultSet.next()){

  21. user=new User();

  22. user.setId(resultSet.getInt("id"));

  23. user.setUsername(resultSet.getString("username"));

  24. user.setPassword(resultSet.getString("password"));

  25. userList.add(user);

  26. }

  27. } catch (Exception e) {

  28. e.printStackTrace();

  29. }finally {

  30. try {

  31. connection.close();

  32. } catch (SQLException e) {

  33. e.printStackTrace();

  34. }

  35. }

  36.  
  37. if (!userList.isEmpty()) {

  38. for (User user : userList) {

  39. System.out.println(user.toString());

  40. }

  41. }

  42.  
  43. }

小伙伴们发现了上面有哪些不友好的地方?

我这里总结了以下几点:

1、数据库的连接信息存在硬编码,即是写死在代码中的。 2、每次操作都会建立和释放 connection 连接,操作资源的不必要的浪费。 3、sql 和参数存在硬编码。 4、将返回结果集封装成实体类麻烦,要创建不同的实体类,并通过 set 方法一个个的注入。

存在上面的问题,所以 mybatis 就对上述问题进行了改进。 对于硬编码,我们很容易就想到配置文件来解决。mybatis 也是这么解决的。 对于资源浪费,我们想到使用连接池,mybatis 也是这个解决的。 对于封装结果集麻烦,我们想到是用 JDK 的反射机制,好巧,mybatis 也是这么解决的。

设计思路

既然如此,我们就来写一个自定义持久层框架,来解决上述问题,当然是参照 mybatis 的设计思路,这样我们在写完之后,再来看 mybatis 的源码就恍然大悟,这个地方这样配置原来是因为这样啊。

我们分为使用端和框架端两部分。

使用端

我们在使用 mybatis 的时候是不是需要使用 SqlMapConfig.xml 配置文件,用来存放数据库的连接信息,以及 mapper.xml 的指向信息。mapper.xml 配置文件用来存放 sql 信息。

所以我们在使用端来创建两个文件 SqlMapConfig.xml 和 mapper.xml。

框架端

框架端要做哪些事情呢?

如下:

1、获取配置文件。也就是获取到使用端的 SqlMapConfig.xml 以及 mapper.xml 的文件

2、解析配置文件。对获取到的文件进行解析,获取到连接信息,sql,参数,返回类型等等。这些信息都会保存在 configuration 这个对象中。

3、创建 SqlSessionFactory,目的是创建 SqlSession 的一个实例。

4、创建 SqlSession ,用来完成上面原始 JDBC 的那些操作。

那在 SqlSession 中 进行了哪些操作呢?

1、获取数据库连接

2、获取 sql ,并对 sql 进行解析

3、通过内省,将参数注入到 preparedStatement 中

4、执行 sql

5、通过反射将结果集封装成对象

使用端实现

好了,上面说了一下,大概的设计思路,主要也是仿照 mybatis 主要的类实现的,保证类名一致,方便我们后面阅读源码。我们先来配置好使用端吧,我们创建一个 maven 项目。

在项目中,我们创建一个 User 实体类

 
  1. public class User {

  2. private Integer id;

  3. private String username;

  4. private String password;

  5. private String birthday;

  6. //getter()和 setter()方法

  7. }

创建 SqlMapConfig.xml 和 Mapper.xml SqlMapConfig.xml

 
  1. <?xml version="1.0" encoding="UTF-8" ?>

  2. <configuration>

  3. <property name="driverClass" value="com.mysql.jdbc.Driver"></property>

  4. <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>

  5. <property name="userName" value="root"></property>

  6. <property name="password" value="123456"></property>

  7.  
  8. <mapper resource="UserMapper.xml">

  9. </mapper>

  10. </configuration>

可以看到我们 xml 中就配置了数据库的连接信息,以及 mapper 一个索引。mybatis 中的 SqlMapConfig.xml 中还包含其他的标签,只是丰富了功能而已,所以我们只用最主要的。

mapper.xml 是每个类的 sql 都会生成一个对应的 mapper.xml 。我们这里就用 User 类来说吧,所以我们就创建一个 UserMapper.xml

 
  1. <?xml version="1.0" encoding="UTF-8" ?>

  2. <mapper namespace="cn.quellanan.dao.UserDao">

  3. <select id="selectAll" resultType="cn.quellanan.pojo.User">

  4. select * from user

  5. </select>

  6. <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">

  7. select * from user where username=#{username}

  8. </select>

  9. </mapper>

可以看到有点 mybatis 里面文件的味道,有 namespace 表示命名空间,id 唯一标识,resultType 返回结果集的类型,paramType 参数的类型。 我们使用端先创建到这,主要是两个配置文件,我们接下来看看框架端是怎么实现的。

加油哈哈。 

框架端实现

框架端,我们按照上面的设计思路一步一步来。

获取配置

怎么样获取配置文件呢?我们可以使用 JDK 自带自带的类 Resources 加载器来获取文件。我们创建一个自定义 Resource 类来封装一下:

 
  1. import java.io.InputStream;

  2. public class Resources {

  3. public static InputStream getResources(String path){

  4. //使用系统自带的类 Resources 加载器来获取文件。

  5. return Resources.class.getClassLoader().getResourceAsStream(path);

  6. }

  7. }

这样通过传入路径,就可以获取到对应的文件流啦。

解析配置文件

上面获取到了 SqlMapConfig.xml 配置文件,我们现在来解析它。 不过在此之前,我们需要做一点准备工作,就是解析的内存放到什么地方?

所以我们来创建两个实体类 Mapper 和 Configuration 。

Mapper Mapper 实体类用来存放使用端写的 mapper.xml 文件的内容,我们前面说了里面有 id、sql、resultType 和 paramType .所以我们创建的 Mapper 实体如下:

 
  1. public class Mapper {

  2. private String id;

  3. private Class<?> resultType;

  4. private Class<?> parmType;

  5. private String sql;

  6. //getter()和 setter()方法

  7. }

这里我们为什么不添加 namespace 的值呢? 聪明的你肯定发现了,因为 mapper 里面这些属性表明每个 sql 都对应一个 mapper , 而 namespace 是一个命名空间,算是 sql 的上一层,所以在 mapper 中暂时使用不到,就没有添加了。

Configuration Configuration 实体用来保存 SqlMapConfig 中的信息。所以需要保存数据库连接,我们这里直接用 JDK 提供的 DataSource 。还有一个就是 mapper 的信息。每个 mapper 有自己的标识,所以这里采用 hashMap 来存储。如下:

 
  1. public class Configuration {

  2.  
  3. private DataSource dataSource;

  4. HashMap <String,Mapper> mapperMap=new HashMap<>();

  5. //getter()和 setter 方法

  6. }

XmlMapperBuilder

做好了上面的准备工作,我们先来解析 mapper 吧。我们创建一个 XmlMapperBuilder 类来解析。通过 dom4j 的工具类来解析 XML 文件。我这里用的 dom4j 依赖为:

 
  1. <dependency>

  2. <groupId>org.dom4j</groupId>

  3. <artifactId>dom4j</artifactId>

  4. <version>2.1.3</version>

  5. </dependency>

思路:

1、获取文件流,转成 document。

2、获取根节点,也就是 mapper。获取根节点的 namespace 属性值

3、获取 select 节点,获取其 id,sql , resultType ,paramType

4、将 select 节点的属性封装到 Mapper 实体类中。

5、同理获取 update/insert/delete 节点的属性值封装到 Mapper 中

6、通过 namespace.id 生成 key 值将 mapper 对象保存到 Configuration 实体中的 HashMap 中。

7、返回 Configuration 实体

代码如下:

 
  1. public class XmlMapperBuilder {

  2. private Configuration configuration;

  3. public XmlMapperBuilder(Configuration configuration){

  4. this.configuration=configuration;

  5. }

  6.  
  7. public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {

  8. Document document=new SAXReader().read(in);

  9.  
  10. Element rootElement=document.getRootElement();

  11. String namespace=rootElement.attributeValue("namespace");

  12.  
  13. List<Node> list=rootElement.selectNodes("//select");

  14.  
  15. for (int i = 0; i < list.size(); i++) {

  16. Mapper mapper=new Mapper();

  17. Element element= (Element) list.get(i);

  18. String id=element.attributeValue("id");

  19. mapper.setId(id);

  20. String paramType = element.attributeValue("paramType");

  21. if(paramType!=null && !paramType.isEmpty()){

  22. mapper.setParmType(Class.forName(paramType));

  23. }

  24. String resultType = element.attributeValue("resultType");

  25. if (resultType != null && !resultType.isEmpty()) {

  26. mapper.setResultType(Class.forName(resultType));

  27. }

  28. mapper.setSql(element.getTextTrim());

  29. String key=namespace+"."+id;

  30. configuration.getMapperMap().put(key,mapper);

  31. }

  32. return configuration;

  33. }

  34.  
  35. }

上面我只解析了 select 标签。大家可以解析对应 insert/delete/uupdate 标签,操作都是一样的。

XmlConfigBuilder

我们再来解析一下 SqlMapConfig.xml 配置信息思路是一样的

 1、获取文件流,转成 document。

2、获取根节点,也就是 configuration。

3、获取根节点中所有的 property 节点,并获取值,也就是获取数据库连接信息

4、创建一个 dataSource 连接池

5、将连接池信息保存到 Configuration 实体中

6、获取根节点的所有 mapper 节点

7、调用 XmlMapperBuilder 类解析对应 mapper 并封装到 Configuration 实体中

8、代码如下:

 
  1. public class XmlConfigBuilder {

  2. private Configuration configuration;

  3. public XmlConfigBuilder(Configuration configuration){

  4. this.configuration=configuration;

  5. }

  6.  
  7. public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {

  8.  
  9. Document document=new SAXReader().read(in);

  10.  
  11. Element rootElement=document.getRootElement();

  12.  
  13. //获取连接信息

  14. List<Node> propertyList=rootElement.selectNodes("//property");

  15. Properties properties=new Properties();

  16.  
  17. for (int i = 0; i < propertyList.size(); i++) {

  18. Element element = (Element) propertyList.get(i);

  19. properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));

  20. }

  21. //是用连接池

  22. ComboPooledDataSource dataSource = new ComboPooledDataSource();

  23. dataSource.setDriverClass(properties.getProperty("driverClass"));

  24. dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));

  25. dataSource.setUser(properties.getProperty("userName"));

  26. dataSource.setPassword(properties.getProperty("password"));

  27. configuration.setDataSource(dataSource);

  28.  
  29. //获取 mapper 信息

  30. List<Node> mapperList=rootElement.selectNodes("//mapper");

  31. for (int i = 0; i < mapperList.size(); i++) {

  32. Element element= (Element) mapperList.get(i);

  33. String mapperPath=element.attributeValue("resource");

  34. XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);

  35. configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));

  36. }

  37. return configuration;

  38. }

  39. }

创建 SqlSessionFactory

完成解析后我们创建 SqlSessionFactory 用来创建 Sqlseesion 的实体,这里为了尽量还原 mybatis 设计思路,也也采用的工厂设计模式。 SqlSessionFactory 是一个接口,里面就一个用来创建 SqlSessionf 的方法。 如下:

 
  1. public interface SqlSessionFactory {

  2. public SqlSession openSqlSession();

  3. }

单单这个接口是不够的,我们还得写一个接口的实现类,所以我们创建一个 DefaultSqlSessionFactory。 如下:

 
  1. public class DefaultSqlSessionFactory implements SqlSessionFactory {

  2.  
  3. private Configuration configuration;

  4.  
  5. public DefaultSqlSessionFactory(Configuration configuration) {

  6. this.configuration = configuration;

  7. }

  8. public SqlSession openSqlSession() {

  9. return new DefaultSqlSeeion(configuration);

  10. }

  11. }

可以看到就是创建一个 DefaultSqlSeeion 并将包含配置信息的 configuration 传递下去。DefaultSqlSeeion 就是 SqlSession 的一个实现类。

创建 SqlSession

在 SqlSession 中我们就要来处理各种操作了,比如 selectList,selectOne,insert, update , delete 等等。 如下:

 
  1. public interface SqlSession {

  2.  
  3. /**

  4. * 条件查找

  5. * @param statementid 唯一标识,namespace.selectid

  6. * @param parm 传参,可以不传也可以一个,也可以多个

  7. * @param <E>

  8. * @return

  9. */

  10. public <E> List<E> selectList(String statementid,Object...parm) throws Exception;

  11.  
  12. public <T> T selectOne(String statementid, Object...parm) throws Exception;

  13.  
  14. public int insert(String statementid, Object...parm) throws Exception;

  15. public int update(String statementid, Object...parm) throws Exception;

  16. public int delete(String statementid, Object...parm) throws Exception;

  17. public void commit() throws Exception;

  18.  
  19.  
  20. /**

  21. * 使用代理模式来创建接口的代理对象

  22. * @param mapperClass

  23. * @param <T>

  24. * @return

  25. */

  26. public <T> T getMapper(Class<T> mapperClass);

  27.  

然后我们创建 DefaultSqlSeeion 来实现 SqlSeesion 。

 
  1. public class DefaultSqlSeeion implements SqlSession {

  2.  
  3. private Configuration configuration;

  4.  
  5. private Executer executer=new SimpleExecuter();

  6.  
  7. public DefaultSqlSeeion(Configuration configuration) {

  8. this.configuration = configuration;

  9. }

  10.  
  11. @Override

  12. public <E> List<E> selectList(String statementid, Object... parm) throws Exception {

  13. Mapper mapper=configuration.getMapperMap().get(statementid);

  14. List<E> query = executer.query(configuration, mapper, parm);

  15. return query;

  16. }

  17.  
  18. @Override

  19. public <T> T selectOne(String statementid, Object... parm) throws Exception {

  20. List<Object> list =selectList(statementid, parm);

  21. if(list.size()==1){

  22. return (T) list.get(0);

  23. }else{

  24. throw new RuntimeException("返回结果过多");

  25. }

  26. }

  27.  
  28. @Override

  29. public int insert(String statementid, Object... parm) throws Exception {

  30. return update(statementid,parm);

  31. }

  32.  
  33. @Override

  34. public int update(String statementid, Object... parm) throws Exception {

  35. Mapper mapper=configuration.getMapperMap().get(statementid);

  36. int update = executer.update(configuration, mapper, parm);

  37. return update;

  38. }

  39.  
  40. @Override

  41. public int delete(String statementid, Object... parm) throws Exception {

  42. return update(statementid,parm);

  43. }

  44.  
  45. @Override

  46. public void commit() throws Exception {

  47. executer.commit();

  48. }

  49.  
  50.  
  51.  
  52. @Override

  53. public <T> T getMapper(Class<T> mapperClass) {

  54.  
  55. Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {

  56. @Override

  57. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

  58.  
  59. //获取到方法名

  60. String name = method.getName();

  61. //类型

  62. String className = method.getDeclaringClass().getName();

  63. String statementid=className+"."+name;

  64.  
  65. Mapper mapper = configuration.getMapperMap().get(statementid);

  66. SqlCommandType sqlType = mapper.getSqlCommandType();

  67. Type genericReturnType = method.getGenericReturnType();

  68.  
  69.  
  70. switch (sqlType){

  71. case SELECT:

  72. //判断是否实现泛型类型参数化

  73. if(genericReturnType instanceof ParameterizedType){

  74. return selectList(statementid,args);

  75. }else {

  76. return selectOne(statementid,args);

  77. }

  78. case INSERT:return insert(statementid,args);

  79. case DELETE:return delete(statementid,args);

  80. case UPDATE:return update(statementid,args);

  81. default:break;

  82. }

  83. return null;

  84. }

  85. });

  86.  
  87.  
  88. return (T) proxyInstance;

  89. }

  90.  
  91. }

我们可以看到 DefaultSqlSeeion 获取到了 configuration,并通过 statementid 从 configuration 中获取 mapper。 然后具体实现交给了 Executer 类来实现。我们这里先不管 Executer 是怎么实现

的,就假装已经实现了。那么整个框架端就完成了。通过调用 Sqlsession.selectList() 方法,来获取结果。 

感觉我们都还没有处理,就框架搭建好了?骗鬼呢,确实前面我们从获取文件解析文件,然后创建工厂。都是做好准备工作。下面开始我们 JDBC 的实现。

SqlSession 具体实现

我们前面说 SqlSeesion 的具体实现有下面 5 步

1、获取数据库连接

2、获取 sql,并对 sql 进行解析

3、通过内省,将参数注入到 preparedStatement 中

4、执行 sql

5、通过反射将结果集封装成对象

但是我们在 DefaultSqlSeeion 中将实现交给了 Executer 来执行。所以我们就要在 Executer 中来实现这些操作。

我们首先来创建一个 Executer 接口,并写一个 DefaultSqlSeeion 中调用的 query 方法。

 
  1. public interface Executer {

  2.  
  3. <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;

  4.  
  5. }

接着我们写一个 SimpleExecuter 类来实现 Executer 。 然后 SimpleExecuter.query() 方法中,我们一步一步的实现。

获取数据库连接

因为数据库连接信息保存在 configuration,所以直接获取就好了。

 
  1. //获取连接

  2. connection=configuration.getDataSource().getConnection();

  3.  

获取 sql,并对 sql 进行解析

我们这里想一下,我们在 Usermapper.xml 写的 sql 是什么样子?

select * from user where username=#{username}

{username} 这样的 sql 我们该怎么解析呢?

分两步 1、将 sql 找到 #{***} ,并将这部分替换成 ?号

2、对 #{***} 进行解析获取到里面的参数对应的 paramType 中的值。

具体实现用到下面几个类。 GenericTokenParser 类,可以看到有三个参数,开始标记,就是我们的 “#{” ,结束标记就是 “}” , 标记处理器就是处理标记里面的内容也就是 username。

 
  1. public class GenericTokenParser {

  2.  
  3. private final String openToken; //开始标记

  4. private final String closeToken; //结束标记

  5. private final TokenHandler handler; //标记处理器

  6.  
  7. public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {

  8. this.openToken = openToken;

  9. this.closeToken = closeToken;

  10. this.handler = handler;

  11. }

  12.  
  13. /**

  14. * 解析${}和#{}

  15. * @param text

  16. * @return

  17. * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。

  18. * 其中,解析工作由该方法完成,处理工作是由处理器 handler 的 handleToken()方法来实现

  19. */

  20. public String parse(String text) {

  21. //具体实现

  22. }

主要的就是 parse() 方法,用来获取操作 1 的 sql。获取结果例如:

select * from user where username=?

那上面用到 TokenHandler 来处理参数。 ParameterMappingTokenHandler 实现 TokenHandler 的类

 
  1. public class ParameterMappingTokenHandler implements TokenHandler {

  2. private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

  3.  
  4. // context 是参数名称 #{id} #{username}

  5.  
  6. @Override

  7. public String handleToken(String content) {

  8. parameterMappings.add(buildParameterMapping(content));

  9. return "?";

  10. }

  11.  
  12. private ParameterMapping buildParameterMapping(String content) {

  13. ParameterMapping parameterMapping = new ParameterMapping(content);

  14. return parameterMapping;

  15. }

  16.  
  17. public List<ParameterMapping> getParameterMappings() {

  18. return parameterMappings;

  19. }

  20.  
  21. public void setParameterMappings(List<ParameterMapping> parameterMappings) {

  22. this.parameterMappings = parameterMappings;

  23. }

  24.  
  25. }

可以看到将参数名称存放 ParameterMapping 的集合中了。 ParameterMapping 类就是一个实体,用来保存参数名称的。

 
  1. public class ParameterMapping {

  2.  
  3. private String content;

  4.  
  5. public ParameterMapping(String content) {

  6. this.content = content;

  7. }

  8. //getter()和 setter() 方法。

  9. }

所以我们在我们通过 GenericTokenParser 类,就可以获取到解析后的 sql,以及参数名称。我们将这些信息封装到 BoundSql 实体类中。

 
  1. public class BoundSql {

  2.  
  3. private String sqlText;

  4. private List<ParameterMapping> parameterMappingList=new ArrayList<>();

  5. public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {

  6. this.sqlText = sqlText;

  7. this.parameterMappingList = parameterMappingList;

  8. }

  9. getter()和 setter() 方法。

  10. }

好了,那么分两步走,先获取,后解析 获取 获取原始 sql 很简单,sql 信息就存在 mapper 对象中,直接获取就好了。

String sql=mapper.getSql()

解析 1、创建一个 ParameterMappingTokenHandler 处理器 2、创建一个 GenericTokenParser 类,并初始化开始标记,结束标记,处理器 3、执行 genericTokenParser.parse(sql) ;获取解析后的 sql‘’,以及在 parameterMappingTokenHandler 中存放了参数名称的集合。 4、将解析后的 sql 和参数封装到 BoundSql 实体类中。

 
  1. /**

  2. * 解析自定义占位符

  3. * @param sql

  4. * @return

  5. */

  6. private BoundSql getBoundSql(String sql){

  7. ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();

  8. GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);

  9. String parse = genericTokenParser.parse(sql);

  10. return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());

  11.  
  12. }

将参数注入到 preparedStatement 中

上面的就完成了 sql 的解析,但是我们知道上面得到的 sql 还是包含 JDBC 的 占位符,所以我们需要将参数注入到 preparedStatement 中。

1、通过 boundSql.getSqlText() 获取带有占位符的 sql .

2、接收参数名称集合 parameterMappingList

3、通过 mapper.getParmType() 获取到参数的类。

4、通过 getDeclaredField(content) 方法获取到参数类的 Field。

5、通过 Field.get() 从参数类中获取对应的值

6、注入到 preparedStatement 中

 
  1. BoundSql boundSql=getBoundSql(mapper.getSql());

  2. String sql=boundSql.getSqlText();

  3. List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

  4.  
  5. //获取 preparedStatement,并传递参数值

  6. PreparedStatement preparedStatement=connection.prepareStatement(sql);

  7. Class<?> parmType = mapper.getParmType();

  8.  
  9. for (int i = 0; i < parameterMappingList.size(); i++) {

  10. ParameterMapping parameterMapping = parameterMappingList.get(i);

  11. String content = parameterMapping.getContent();

  12. Field declaredField = parmType.getDeclaredField(content);

  13. declaredField.setAccessible(true);

  14. Object o = declaredField.get(parm[0]);

  15. preparedStatement.setObject(i+1,o);

  16. }

  17. System.out.println(sql);

  18. return preparedStatement;

执行 sql

其实还是调用 JDBC 的 executeQuery() 方法或者 execute() 方法

 
  1. //执行 sql

  2. ResultSet resultSet = preparedStatement.executeQuery();

通过反射将结果集封装成对象

在获取到 resultSet 后,我们进行封装处理,和参数处理是类似的。

1、创建一个 ArrayList

2、获取返回类型的类

3、循环从 resultSet 中取数据

4、获取属性名和属性值

5、创建属性生成器

6、为属性生成写方法,并将属性值写入到属性中

7、将这条记录添加到 list 中

8、返回 list

 
  1. /**

  2. * 封装结果集

  3. * @param mapper

  4. * @param resultSet

  5. * @param <E>

  6. * @return

  7. * @throws Exception

  8. */

  9. private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{

  10. ArrayList<E> list=new ArrayList<>();

  11. //封装结果集

  12. Class<?> resultType = mapper.getResultType();

  13. while (resultSet.next()) {

  14. ResultSetMetaData metaData = resultSet.getMetaData();

  15. Object o = resultType.newInstance();

  16. int columnCount = metaData.getColumnCount();

  17. for (int i = 1; i <= columnCount; i++) {

  18. //属性名

  19. String columnName = metaData.getColumnName(i);

  20. //属性值

  21. Object value = resultSet.getObject(columnName);

  22. //创建属性描述器,为属性生成读写方法

  23. PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);

  24. Method writeMethod = propertyDescriptor.getWriteMethod();

  25. writeMethod.invoke(o,value);

  26. }

  27. list.add((E) o);

  28. }

  29. return list;

  30. }

  31.  

创建 SqlSessionFactoryBuilder

我们现在来创建一个 SqlSessionFactoryBuilder 类,来为使用端提供一个人口。

 
  1. public class SqlSessionFactoryBuilder {

  2.  
  3. private Configuration configuration;

  4.  
  5. public SqlSessionFactoryBuilder(){

  6. configuration=new Configuration();

  7. }

  8.  
  9. public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {

  10. XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);

  11. configuration=xmlConfigBuilder.loadXmlConfig(in);

  12.  
  13. SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);

  14. return sqlSessionFactory;

  15. }

  16. }

可以看到就一个 build 方法,通过 SqlMapConfig 的文件流将信息解析到 configuration ,创建并返回一个 sqlSessionFactory 。

到此,整个框架端已经搭建完成了,但是我们可以看到,只实现了 select 的操作, update 、inster 、delete 的操作我们在我后面提供的源码中会有实现,这里只是将整体的设计思路和流程。

在这里插入图片描述

测试

终于到了测试的环节啦。我们前面写了自定义的持久层,我们现在来测试一下能不能正常的使用吧。 见证奇迹的时刻到啦 

我们先引入我们自定义的框架依赖。以及数据库和单元测试

 
  1. <dependency>

  2. <groupId>mysql</groupId>

  3. <artifactId>mysql-connector-java</artifactId>

  4. <version>8.0.11</version>

  5. </dependency>

  6. <dependency>

  7. <groupId>cn.quellanan</groupId>

  8. <artifactId>myself-mybatis</artifactId>

  9. <version>1.0.0</version>

  10. </dependency>

  11.  
  12. <dependency>

  13. <groupId>junit</groupId>

  14. <artifactId>junit</artifactId>

  15. <version>4.10</version>

  16. </dependency>

然后我们写一个测试类

1、获取 SqlMapperConfig.xml 的文件流

2、获取 Sqlsession

3、执行查找操作

 
  1. @org.junit.Test

  2. public void test() throws Exception{

  3. InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");

  4. SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();

  5. List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");

  6.  
  7. for (User parm : list) {

  8. System.out.println(parm.toString());

  9. }

  10. System.out.println();

  11.  
  12. User user=new User();

  13. user.setUsername("张三");

  14. List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);

  15. for (User user1 : list1) {

  16. System.out.println(user1);

  17. }

  18.  
  19. }

在这里插入图片描述

可以看到已经可以了,看来我们自定义的持久层框架生效啦。 

优化

但是不要高兴的太早哈哈,我们看上面的测试方法,是不是感觉和平时用的不一样,每次都都写死 statementId ,这样不太友好,所以我们接下来来点骚操作,通用 mapper 配置。 我们在 SqlSession 中增加一个 getMapper 方法,接收的参数是一个类。我们通过这个类就可以知道 statementId .

 
  1. /**

  2. * 使用代理模式来创建接口的代理对象

  3. * @param mapperClass

  4. * @param <T>

  5. * @return

  6. */

  7. public <T> T getMapper(Class<T> mapperClass);

  8.  

具体实现就是利用 JDK 的动态代理机制。 1、通过 Proxy.newProxyInstance() 获取一个代理对象 2、返回代理对象 那代理对象执行了哪些操作呢? 创建代理对象的时候,会实现一个 InvocationHandler 接口,重写 invoke() 方法,让所有走这个代理的方法都会执行这个 i nvoke() 方法。那这个方法做了什么操作? 这个方法就是通过传入的类对象,获取到对象的类名和方法名。用来生成 statementid 。所以我们在 mapper.xml 配置文件中的 namespace 就需要制定为类路径,以及 id 为方法名。 实现方法:

 
  1. @Override

  2. public <T> T getMapper(Class<T> mapperClass) {

  3.  
  4. Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {

  5. @Override

  6. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

  7.  
  8. //获取到方法名

  9. String name = method.getName();

  10. //类型

  11. String className = method.getDeclaringClass().getName();

  12. String statementid=className+"."+name;

  13.  
  14. return selectList(statementid,args);

  15. }

  16. });

  17.  
  18.  
  19. return (T) proxyInstance;

  20. }

我们写一个 UserDao

 
  1. public interface UserDao {

  2. List<User> selectAll();

  3.  
  4. List<User> selectByName(User user);

  5. }

这个是不是我们熟悉的味道哈哈,就是 mapper 层的接口。 然后我们在 mapper.xml 中指定 namespace 和 id 在这里插入图片描述 

接下来我们在写一个测试方法

 
  1. @org.junit.Test

  2. public void test2() throws Exception{

  3. InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");

  4. SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();

  5.  
  6. UserDao mapper = sqlSession.getMapper(UserDao.class);

  7. List<User> users = mapper.selectAll();

  8. for (User user1 : users) {

  9. System.out.println(user1);

  10. }

  11.  
  12. User user=new User();

  13. user.setUsername("张三");

  14. List<User> users1 = mapper.selectByName(user);

  15. for (User user1 : users1) {

  16. System.out.println(user1);

  17. }

  18.  
  19. }

在这里插入图片描述

番外

自定义的持久层框架,我们就写完了。这个实际上就是 mybatis 的雏形,我们通过自己手动写一个持久层框架,然后在来看 mybatis 的源码,就会清晰很多。下面这些类名在 mybatis 中都有体现。  

在这里插入图片描述

文章到此就结束了!

最后来自小编的福利

以下是小编整理的一份大厂真题的面试资料,以及整理好的MyBatis源码分析300多页的技术文档集锦,深入浅出MyBatis技术原理与实战资料集锦200多页,Mybatis:入门+配置信息+印射+缓存+整合Spring+面试,这些都是整理好的资料 需要领取可以 点我 了解详情

部分资料图片:

喜欢小编的分享可以点赞关注哦,小编持续为你分享最新文章 和 福利领取哦

猜你喜欢

转载自blog.csdn.net/QLCZ0809/article/details/111600353