• 深度自定义mybatis


    > 回顾mybatis的操作的核心步骤

      >

      > 编写核心类SqlSessionFacotryBuild进行解析配置文件

      > 深度分析解析SqlSessionFacotryBuild干的核心工作

      >

      > 编写核心类SqlSessionFacotry

      > 深度分析解析SqlSessionFacotry干的核心工作

      > 编写核心类SqlSession

      > 深度分析解析SqlSession干的核心工作

      > 总结自定义mybatis用的技术点
     

      一. 回顾mybatis的操作的核心步骤
     

      声明一点我们本篇主要探讨的是mybatis的注解方式的操作, 完全从头开始都是小编从头开搞的, 如果与其他大神的代码思维有出入请多指教。

      我们首先需要准备mybatis的核心配置文件(当然导入相关的坐标这里不在啰嗦)

    1. "1.0" encoding="UTF-8" ?>
    2. configuration
    3. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    4. "http://mybatis.org/dtd/mybatis-3-config.dtd">
    5. <configuration>
    6. <environments default="development">
    7. <environment id="development">
    8. <transactionManager type="JDBC"/>
    9. <dataSource type="POOLED">
    10. <property name="driver" value="com.mysql.jdbc.Driver"/>
    11. <property name="url" value="jdbc:mysql:///db6?useSSL=false"/>
    12. <property name="username" value="root"/>
    13. <property name="password" value="root"/>
    14. dataSource>
    15. environment>
    16. environments>
    17. <mappers>
    18. <package name="cn.itcast.mapper"/>
    19. mappers>
    20. configuration>

      准备好结果的实体类以及在mapper接口上编写需要执行的sql语句

    1. public class User {
    2. private Integer uid;
    3. private String username;
    4. private String password;
    5. private String nickname;
    6. }
    1. package cn.itcast.mapper;
    2. import cn.itcast.pojo.User;
    3. import org.apache.ibatis.annotations.Select;
    4. import java.util.List;
    5. public interface UserMapper {
    6. @Select("select * from users")
    7. List findAll();
    8. }

      使用mybatis的api来帮助我们完成sql语句的执行以及结果集的封装

    1. //1.关联主配置文件
    2. InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    3. //2.解析配置文件
    4. SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    5. SqlSessionFactory sqlSessionFactory = builder.build(in);
    6. //3.创建会话对象
    7. SqlSession sqlSession = sqlSessionFactory.openSession();
    8. //4.可以采用接口代理的方式
    9. UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    10. List<User> all = mapper.findAll();
    11. System.out.println(all);
    12. //5.释放资源
    13. sqlSession.close();

      思考: mybatis大致是如何帮我们完成相关操作的 ?
     

      我们通过Resources的getResourceAsStream告诉了mybatis我们编写的核心配置文件的位置, mybatis就可以找到我们数据库的连接信息, 也同时找到我们编写的sql语句的地方, 然后可以将其解析按照某种规则存放起来, 我们通过调用接口代理的方式执行方法时, 可以找到对应方法上的sql语句然后执行将结果封装返回给我们
     

      二. 编写核心类SqlSessionFacotryBuild进行解析配置文件
     

      那么我们废话不多说开始我们自定义mybatis的旅程,
     

      1.首先我们需要用户编写配置文件, 然后通过我们自己的Resources来告诉我们配置文件所在位置。

    1. package com.itheima.ibatis.configuration;
    2. import java.io.InputStream;
    3. public class Resources {
    4. public static InputStream getResourceAsStream(String path) {
    5. return ClassLoader.getSystemClassLoader().getResourceAsStream(path);
    6. }
    7. }

      2. 然后需要定义SqlSessionFacotryBuild来对配置文件进行解析分发

    1. package com.itheima.ibatis.configuration;
    2. import com.itheima.ibatis.core.session.SqlSessionFactory;
    3. import com.itheima.ibatis.core.session.impl.DefaultSqlSessionFactory;
    4. import org.dom4j.Document;
    5. import org.dom4j.DocumentException;
    6. import org.dom4j.Element;
    7. import org.dom4j.Node;
    8. import org.dom4j.io.SAXReader;
    9. import javax.sql.DataSource;
    10. import java.io.File;
    11. import java.io.InputStream;
    12. import java.lang.reflect.Method;
    13. import java.util.List;
    14. import java.util.Properties;
    15. public class SqlSessionFactoryBuilder {
    16. private Configuration configuration = new Configuration();
    17. public SqlSessionFactory build(InputStream in) {
    18. SAXReader saxReader = new SAXReader();
    19. Document document = null;
    20. try {
    21. document = saxReader.read(in);
    22. } catch (DocumentException e) {
    23. e.printStackTrace();
    24. }
    25. Element rootElement = document.getRootElement();
    26. parseEnvironment(rootElement.element("environments"));
    27. parseMapper(rootElement.element("mappers"));
    28. return new DefaultSqlSessionFactory(configuration);
    29. }
    30. private void parseMapper(Element mapper) {
    31. String pack = mapper.element("package").attributeValue("name");
    32. String directory = pack.replace(".", "/");
    33. String path = ClassLoader.getSystemClassLoader().getResource("").getPath();
    34. File mapperDir = new File(path, directory);
    35. if (!mapperDir.exists()) {
    36. throw new RuntimeException("找不到mapper映射");
    37. }
    38. findMapper(mapperDir, pack);
    39. // System.out.println(configuration.getSql());
    40. }
    41. private void findMapper(File mapperDir, String base) {
    42. File[] files = mapperDir.listFiles();
    43. if (files != null) {
    44. for (File file : files) {
    45. if (file.isFile()) {
    46. if (file.getName().endsWith(".class")) {
    47. String name = file.getName();
    48. name = name.substring(0, name.lastIndexOf("."));
    49. String className = base + "." + name;
    50. initMapper(className);
    51. }
    52. } else {
    53. findMapper(file, base + "." + file.getName());
    54. }
    55. }
    56. }
    57. }
    58. private void initMapper(String className) {
    59. try {
    60. Class<?> clazz = Class.forName(className);
    61. Method[] methods = clazz.getMethods();
    62. for (Method method : methods) {
    63. if(method.getAnnotations().length>0){
    64. Mapper mapper = ParseMapper.parse(method);
    65. this.configuration.getMappers().put(className + "." + method.getName(), mapper);
    66. }
    67. }
    68. } catch (Exception e) {
    69. e.printStackTrace();
    70. }
    71. }
    72. private void parseEnvironment(Element environments) {
    73. String defEnv = environments.attributeValue("default");
    74. Node node = environments.selectSingleNode("//environment[@id='" + defEnv + "']");
    75. List<Element> list = node.selectNodes("//property");
    76. Properties properties = new Properties();
    77. for (Element element : list) {
    78. String name = element.attributeValue("name");
    79. String value = element.attributeValue("value");
    80. properties.put(name, value);
    81. }
    82. DataSource dataSource = new DefaultDataSource().getDataSource(properties);
    83. configuration.setDataSource(dataSource);
    84. }
    85. }

      三. 深度分析解析SqlSessionFacotryBuild干的核心工作
     

          1.build(InputStream in) 方法做的工作
     

      ①借助Dom4j的来解析了xml文件, 将environments解析工作分发给了parseEnvironment(Element environments)
     

      ②将mappers的解析工作分发给了parseMapper(Element mapper)
     

      2. parseEnvironment(Element environments)方法做的工作
     

      ①主要解析了连接数据库的参数们, 并且创建了数据库连接池
     

      自定义连接池非本章节的重点,所以这里内部本质采用的Druid连接池来做了简化

    1. package com.itheima.ibatis.configuration;
    2. import com.alibaba.druid.pool.DruidDataSourceFactory;
    3. import javax.sql.DataSource;
    4. import java.util.Properties;
    5. public class DefaultDataSource {
    6. public DataSource getDataSource(Properties properties) {
    7. try {
    8. return DruidDataSourceFactory.createDataSource(properties);
    9. } catch (Exception e) {
    10. e.printStackTrace();
    11. }
    12. return null;
    13. }
    14. }

      
           ②将解析好的连接池放入configuration对象中,mappers成员变量先别纠结下一章节会讲解

    1. package com.itheima.ibatis.configuration;
    2. import lombok.Data;
    3. import javax.sql.DataSource;
    4. import java.util.HashMap;
    5. import java.util.Map;
    6. @Data
    7. public class Configuration {
    8. private Map mappers = new HashMap<>();
    9. private DataSource dataSource;
    10. }

      详细图解如下图

      

      3.parseMapper(Element mapper) 方法做的工作
     

      ①解析出用户配置的package找到sql语句所在接口的文件夹, 交给initMapper来处理

      

      ②递归找到这个包下所有的.class文件,并且获取到接口的全类名, 然后交给initMapper来处理

      

      ③initMapper通过反射获取类中的每一个方法,将方法交给一个专门解析方法上的注解的工具类ParseMapper的parse方法处理,处理完后将其放到configuration中的mappers的集合中

      

      ④ParseMapper的parse方法做的工作, 这是解析配置的核心地方

    1. package com.itheima.ibatis.configuration;
    2. import java.lang.annotation.Annotation;
    3. import java.lang.reflect.InvocationTargetException;
    4. import java.lang.reflect.Method;
    5. import java.lang.reflect.ParameterizedType;
    6. import java.lang.reflect.Type;
    7. import java.util.ArrayList;
    8. import java.util.List;
    9. import java.util.regex.Matcher;
    10. import java.util.regex.Pattern;
    11. public class ParseMapper {
    12. public static Mapper parse(Method method) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    13. Annotation[] annotations = method.getAnnotations();
    14. Object value = annotations[0].getClass().getMethod("value").invoke(annotations[0]);
    15. Mapper mapper = new Mapper();
    16. Class<?> resultType = method.getReturnType();
    17. String val = (String) value;
    18. Pattern pattern = Pattern.compile("\\#\\{\\s*\\w+\\s*\\}");
    19. Matcher matcher = pattern.matcher(val);
    20. List<String> paramNames = new ArrayList<>();
    21. while (matcher.find()) {
    22. String group = matcher.group();
    23. String fieldName = group.substring(2, group.length() - 1).trim();
    24. paramNames.add(fieldName);
    25. }
    26. String sql = val.replaceAll("\\#\\{\\s*\\w+\\s*\\}", "?");
    27. mapper.setSql(sql);
    28. mapper.setParameterNames(paramNames);
    29. mapper.setSql(sql);
    30. if (resultType == List.class) {
    31. mapper.setSelectList(true);
    32. Type genericReturnType = method.getGenericReturnType();
    33. ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
    34. Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
    35. mapper.setResultType(actualTypeArgument.getTypeName());
    36. mapper.setType("SELECT");
    37. } else if (resultType == Integer.class || resultType == int.class) {
    38. mapper.setType("UPDATE");
    39. } else {
    40. mapper.setType("SELECT");
    41. mapper.setResultType(resultType.getName());
    42. }
    43. return mapper;
    44. }
    45. }

      首先拿到方法上的注解,得到用户填入的sql语句

      

      然后处理sql语句#{参数}的这些数据, 然后将参数的顺序保存起来, 用来后期设置参数的数据做准备, 一个方法对应一个Mapper对象

      

      然后再根据结果类型, 判断是什么类型相关的操作,方便后期执行对应的sql语句

      

      四. 编写核心类SqlSessionFacotry
     

            1.回顾那个地方创建的SqlSessionFacotry对象
     

      经过SqlSessionFacotryBuilder的努力, 我们成功的将配置文件中核心的信息解析出来并放入了configuration对象中了, 然后我们此时将解析好的configuration传入到SqlSessionFacotry中

      

      SqlSessionFactory的实现类如下:

    1. public class DefaultSqlSessionFactory implements SqlSessionFactory {
    2. private final Configuration configuration;
    3. private TransactionManagement defaultTransactionManagement;
    4. public DefaultSqlSessionFactory(Configuration configuration) {
    5. this.configuration =configuration;
    6. defaultTransactionManagement = new DefaultTransactionManagement(configuration.getDataSource());
    7. }
    8. @Override
    9. public SqlSession openSession() {
    10. return new DefaultSqlSession(configuration,defaultTransactionManagement,false);
    11. }
    12. }

      2.添加事务管理器
     

      事务管理是一个小的功能, 里面希望使用ThreadLocal集合来保证一个用户拿到的链接是同一个

      

      事务管理的代码如下:

    1. public class DefaultTransactionManagement implements TransactionManagement {
    2. private ThreadLocal threadLocal = new ThreadLocal<>();
    3. private DataSource dataSource;
    4. public DefaultTransactionManagement(DataSource dataSource) {
    5. this.dataSource = dataSource;
    6. }
    7. public Connection getConnection() {
    8. Connection connection = threadLocal.get();
    9. if (connection == null) {
    10. try {
    11. connection = dataSource.getConnection();
    12. } catch (SQLException e) {
    13. e.printStackTrace();
    14. }
    15. threadLocal.set(connection);
    16. }
    17. return connection;
    18. }
    19. @Override
    20. public void commit() {
    21. Connection connection = threadLocal.get();
    22. if (connection != null ) {
    23. try {
    24. connection.commit();
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. }
    30. @Override
    31. public void rollback() {
    32. Connection connection = threadLocal.get();
    33. if (connection != null) {
    34. try {
    35. connection.rollback();
    36. } catch (SQLException e) {
    37. e.printStackTrace();
    38. }
    39. }
    40. }
    41. public void close() {
    42. Connection connection = threadLocal.get();
    43. if (connection != null) {
    44. try {
    45. connection.close();
    46. threadLocal.remove();
    47. } catch (SQLException e) {
    48. e.printStackTrace();
    49. }
    50. }
    51. }
    52. @Override
    53. public void begin() {
    54. Connection connection = threadLocal.get();
    55. if (connection != null) {
    56. try {
    57. connection.setAutoCommit(false);
    58. } catch (SQLException e) {
    59. e.printStackTrace();
    60. }
    61. }
    62. }
    63. }

      五. 深度分析解析SqlSessionFacotry干的核心工作
     

          1.SqlSession openSession() 方法做的工作
     

      可以看的出来我们在这个方法创建了DefaultSqlSession对象,并传入封装好的configuration,默认的事务管理器
     

      默认通过openSession事务是开启的等等相关的参数

      

      六.编写核心类SqlSession
     

      其实有SqlSession的接口,我们使用的实现类是DefaultSession, 这里记录了解析的配置对象configuration
     

      默认事务管理器对象transactionManagement, 默认事务开启的状态tx标记

    1. package com.itheima.ibatis.core.session.impl;
    2. import com.itheima.ibatis.configuration.Configuration;
    3. import com.itheima.ibatis.configuration.Mapper;
    4. import com.itheima.ibatis.core.BaseExecutor;
    5. import com.itheima.ibatis.core.annotation.Param;
    6. import com.itheima.ibatis.core.session.SqlSession;
    7. import com.itheima.ibatis.core.transaction.TransactionManagement;
    8. import java.lang.reflect.*;
    9. import java.util.HashMap;
    10. import java.util.List;
    11. import java.util.Map;
    12. public class DefaultSqlSession implements SqlSession {
    13. private final Configuration configuration;
    14. private final boolean tx;
    15. private TransactionManagement transactionManagement;
    16. public DefaultSqlSession(Configuration configuration, TransactionManagement transactionManagement, boolean tx) {
    17. this.configuration = configuration;
    18. this.transactionManagement = transactionManagement;
    19. this.tx = tx;
    20. }
    21. public void close() {
    22. transactionManagement.close();
    23. }
    24. @Override
    25. public void commit() {
    26. transactionManagement.commit();
    27. }
    28. @Override
    29. public void rollback() {
    30. transactionManagement.rollback();
    31. }
    32. @Override
    33. public <T> List<T> selectList(String sqlId) {
    34. return selectList(sqlId, null);
    35. }
    36. @Override
    37. public <T> List<T> selectList(String sqlId, Object param) {
    38. List<Object> list = new BaseExecutor(transactionManagement, tx).queryList(getMapper(sqlId), param);
    39. return (List<T>) list;
    40. }
    41. @Override
    42. public <T> T selectOne(String sqlId) {
    43. return selectOne(sqlId, null);
    44. }
    45. @Override
    46. public <T> T selectOne(String sqlId, Object param) {
    47. return new BaseExecutor(transactionManagement, tx).query(getMapper(sqlId), param);
    48. }
    49. @Override
    50. public int delete(String sqlId) {
    51. return update0(sqlId, null);
    52. }
    53. @Override
    54. public int delete(String sqlId, Object param) {
    55. return update0(sqlId, param);
    56. }
    57. @Override
    58. public int update(String sqlId) {
    59. return update0(sqlId, null);
    60. }
    61. @Override
    62. public int update(String sqlId, Object param) {
    63. return update0(sqlId, param);
    64. }
    65. @Override
    66. public int insert(String sqlId) {
    67. return update0(sqlId, null);
    68. }
    69. @Override
    70. public int insert(String sqlId, Object param) {
    71. return update0(sqlId, param);
    72. }
    73. @Override
    74. public <T> T getMapper(Class<T> clazz) {
    75. Object o = Proxy.newProxyInstance(
    76. clazz.getClassLoader(),
    77. new Class[]{clazz}, new InvocationHandler() {
    78. @Override
    79. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    80. String sqlId = clazz.getName() + "." + method.getName();
    81. Mapper mapper = configuration.getMappers().get(sqlId);
    82. String type = mapper.getType();
    83. Object findParam = null;
    84. if (args != null) {
    85. if (args.length == 1) {
    86. Object param = args[0];
    87. boolean isArray = param.getClass().isArray();
    88. if (!isArray) {
    89. findParam = param;
    90. }
    91. } else {
    92. Map<String, Object> map = new HashMap<>();
    93. Parameter[] parameters = method.getParameters();
    94. for (int i = 0; i < parameters.length; i++) {
    95. Param param = parameters[i].getAnnotation(Param.class);
    96. String key = "arg"+i;
    97. if(param !=null){
    98. key = param.value();
    99. }
    100. map.put(key, args[i]);
    101. }
    102. findParam = map;
    103. }
    104. }
    105. if (type.equals("SELECT")) {
    106. boolean selectList = mapper.isSelectList();
    107. if (selectList)
    108. return selectList(sqlId, findParam);
    109. else
    110. return selectOne(sqlId, findParam);
    111. } else {
    112. return update0(sqlId, findParam);
    113. }
    114. }
    115. });
    116. return (T) o;
    117. }
    118. private int update0(String sqlId, Object param) {
    119. return new BaseExecutor(transactionManagement, tx).update(getMapper(sqlId), param);
    120. }
    121. public Mapper getMapper(String sqlId) {
    122. Mapper mapper = configuration.getMappers().get(sqlId);
    123. if (mapper == null) {
    124. throw new RuntimeException("没有找到sql映射,请检查");
    125. }
    126. return mapper;
    127. }
    128. }

      七.深度分析解析SqlSession干的核心工作
     

           1.selectOne & selectList做的工作
     

      主要是分发了下功能, 执行sql语句避免不了有参数和无参数的, 都让调用有参数的方便管理

      

      在执行前, 考虑还有一种情况, 用户不是通过接口代理的方式来执行以上方法, 这样手动输入sqlId容易造成错误
     

      这里做一个健壮性判断

      

      BaseExecutor中的query以及queryList做的核心工作
     

      首先这两个方法的特点都是查询, 其步骤基本类似, 所以这里可以合并一起转调query0功能

      

      这里需要对参数进行设定, 还根据最后isOne的参数决定返回值是否是单个

      

      参数设置这里比较复杂我们通过图解的方式来解释, (注: 参数是List集合类型的和数组类型的没有做!!!)

      

      对结果的封装主要用到内省技术和数据库元数据等等知识点

      

      2.update&delete&insert做的工作

      

      BaseExecutor中的update做的核心工作
     

      还是和query&queryList一样需要设置参数, 不管是增删改其本质其结果都是一致

      

      3.getMapper代理模式开发的原理
     

      主要使用的动态代理的技术创建接口的实现类, 内部主要整合了sqlId和参数, 省去用户自己拼sqlId拼错的风险
     

      也同时解决用户手动合参数的麻烦, 但是最终工作的还是selectOne,selectList以及update0这些方法

      

      

      

      总结自定义mybatis用的技术点

      一款框架的诞生肯定不是一蹴而就的, 随着时间慢慢推进逐步更新出来, 所以一款好的框架肯定要经过很多考验才能够稳定靠谱, 但是纵观整篇用的技术点, 不难发现框架也是由基础代码编写而来,解决大量重复的工作, 提供扩展性等等机制,比如本篇用核心的技术点有。

      ① 反射

      ② 内省

      ③ 解析xml

      ④ 动态代理

      ⑤ 工厂设计模式
     

  • 相关阅读:
    Hadoop 2.0:主流开源云架构(四)
    spring的一些了解和使用maven确定目录结构
    【完整解析】2024电工杯数学建模A题论文与代码
    【Mysql】Lock wait timeout exceeded; try restarting transaction
    【Seata】分布式事务框架原理解析
    Docker Dockerfile 文件中设置时区的命令解析
    【二叉树-困难】124. 二叉树中的最大路径和
    【iOS-知乎日报第四周总结】
    怎么设置代理IP进行网络爬取呢?代理访问网络如何设置?
    进阶JAVA篇-如何理解作为参数使用的匿名内部类与 Arrays 类的常用API(九)
  • 原文地址:https://blog.csdn.net/Blue92120/article/details/127801641