超详细的手把手撸代码---教你你⾃定义持久层框架设计--Mybatis(强烈建议阅读Mybatis源码前自己实现一个类似的框架)

目录

概述

为什么要Mybatis,之前的jdbc不香吗??

JDBC的问题

问题解决思路

⾃定义框架设计

使用方:

提供核⼼配置⽂件:

下面是具体的实现:

框架方:

1.读取配置⽂件

实现方法:

首先编写一个Resources类,用于加载配置文件

2.解析配置文件:dom4j(解析xml)

第二步:封装两个javaBean

3.创建sqlSessionFactory

4.创建sqlSession接口及实现类:DefaultSqlSession

5.创建Executor接口及实现类 SimpleExecutor实现类

存在问题:



概述

为什么要Mybatis,之前的jdbc不香吗??

带着这个问题,我们看一下原生的jdbc是如何进行数据库交互的,下面是一个简单的查询语句。

 public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            // 加载数据库驱动 
            Class.forName("com.mysql.jdbc.Driver");
            // 通过驱动管理类获取数据库链接 
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding = utf - 8 ", " root ", " root ");

            // 定义sql语句?表示占位符 
            String sql = "select * from user where username = ?";
            // 获取预处理statement 
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值 
            preparedStatement.setString(1, "tom");
            // 向数据库发出sql执⾏查询,查询出结果集 
            resultSet = preparedStatement.executeQuery();
            // 遍历查询结果集 
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                // 封装User 
                user.setId(id);
                user.setUsername(username);
            }
            System.out.println(user);
        }
    } catch(
    Exception e)

    {
        e.printStackTrace();
    } finally

    {
        // 释放资源 
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }

        }

        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }

        }

        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }

 what???一个简单的查询就做了这么多的操作,真实让人头疼。

JDBC的问题

下面我们再聊一聊其他问题JDBC问题:

原始jdbc开发存在的问题如下:

1、 数据库连接创建、释放频繁造成系统资源浪费,从⽽影响系统性能。(需要创建链接,三次握手等操作耗时)

2、 Sql语句在代码中硬编码,造成代码不易维护,实际应⽤中sql变化的可能较⼤,sql变动需要改变 java代码。

3、 使⽤preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可能 多也可能少,修改sql还要修改代码,系统不易维护。

4、 对结果集解析存在硬编码(查询列名,),手动设计结果集,如果结果很多,实现起来很麻烦,sql变化导致解析代码变化,系统不易维护,如果能将数据 库 记录封装成pojo对象解析⽐较⽅便

5、数据库配置信息 驱动等硬编码

 带着上面的问题我们能不能优化一下,改吧jdbc的不足呢?

问题解决思路

提到硬编码--->配置文件解决

创建连接------>连接池

①使⽤数据库连接池初始化连接资源

②将sql语句抽取到xml配置⽂件中

③使⽤反射、内省等底层技术,⾃动将实体与表进⾏属性与字段的⾃动映射

 根据上面的思路,我们来实现一个自己自定义的jdbc框架,实现对数据库的操作。

⾃定义框架设计

不可少的部分:

数据库配置信息、sql的配置信息(sql语句+参数类型+返回值类型) ------->由使用方进行提供

使用配置文件提供这些,具体见下面

使用方:

提供核⼼配置⽂件:

1)sqlMapConfig.xml : 存放数据源信息,引⼊mapper.xml

2)Mapper.xml : sql语句的配置⽂件信息

下面是具体的实现:

首先创建一个工程

我这里叫my_persistence_demo

主要进行了配置文件的编写

 sqlM aMapConfig.xml内容如下:

主要是数据库的一些配置信息,个标签都是自定义的(可以根据喜好命名,下面的也是如此)

未来能够减少读取xml文件的次数,将userMapper.xml文件路径也引入了

<configuration>
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///custom_mybatis"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSource>

    <!--存放mapper.xml的全路径-->
    <mapper resource="userMapper.xml"></mapper>
</configuration>

我这里使用的是本地数据库 库名是custom_mybatis

建表语句如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

SET FOREIGN_KEY_CHECKS = 1;

userMapper.xml内容如下

只要是一些sql语句的配置

<mapper namespace="user">
<!--如果有其他模块也使用对这个id,所以需要有其他区分-->
<!--    sql的唯一标示  namespace.id组成:这个唯一的id现在叫  statementId
    resultType是实现自动映射的目的 需要拿到类全路径,才能够通过反射实现属性的注入 -->
   <select id="selectList" resultType="cn.mystylefree.my_persistence_demo.domain.User">
       select * from user
   </select>

    <!--
        传递参数
        User u=new User();
        u.setId(2);
        u.serName("lee");
        这里就是把id和name赋值到下面的问号

        问号占位符就不行了 我们需要知道具体的哪个参数值  所以jdbc这里的?就不行了,使用 #{}
        里面的属性与对象的名称一致
    -->
    <select id="selectOne" resultType="cn.mystylefree.my_persistence_demo.domain.User" paramterType="cn.mystylefree.my_persistence_demo.domain.User">
        select * from user where id= #{id} and name= #{name}
    </select>

User实体类如下 

@Data
public class User {
    private String id;
    private String name;
}

这是使用端的代码就基本写完了,下面是对服务端的编写


框架方:

其实就是对jdbc代码进行封装优化

项使用必须有上面的配置信息,将信息解析出来就能连接数据库了

  • 读取配置⽂件
  • 解析配置文件
  • 创建sqlSessionFactory
  • 创建sqlSession接口及实现类:主要封装crud操作        

涉及到的设计模式:

Builder构建者设计模式、⼯⼚模式、代理模式


1.读取配置⽂件

创建Resources类。 方法。InputStream  getResourceAsStream(String path)

读取完成以后以流的形式存在,

我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可以创建javaBean来存储

封装成两个实体 javaBean,存放的就是上面两个配置文件的信息

  • (1)Configuration 核心配置类:

                存放数据库基本信息存放的就是sqlMapConfig.xml配置信息的内容:

                Map<唯⼀标识,Mapper> 唯⼀标识:namespace + "." + id

  • (2)MappedStatement映射配置类:

                存Mapper.xml的配置

                sql语句、statement类型、输⼊参数java类型、输出参数java类型

实现方法:

首先编写一个Resources类,用于加载配置文件

public class Resources {
    /**
     * 根据指定的配置文件路径,将配置文件加载成字节输入流,存储到内参中
     * @param path
     * @return
     */
    public static InputStream getResourceAsStream(String path){
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

下面对这歌类进行测试

首先要将框架端端项目进行打包,这样使用方才能够引入该依赖

        <dependency>
            <groupId>cn.mystylefree</groupId>
            <artifactId>my_persistence</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

说明一如成功了

创建测试类

2.解析配置文件:dom4j(解析xml)

        创建sqlSessionFactoryBuilder类:

        ⽅法:sqlSessionFactory build(inputStream):

  • 第⼀:使⽤dom4j解析配置⽂件,将解析出来内容封装到Configuration和MappedStatement中
  • 第⼆:创建SqlSessionFactory的实现类DefaultSqlSession 产生sqlSession(会话对象)工厂模式

                        为啥不直接new 用工厂模式

第二步:封装两个javaBean

这里的MappedStatement的属性是根据 userMapper.xml中的配置标签来的

@Data
public class MappedStatement {
    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数类型
    private String parameterType;
    //sql语句
    private String sql;
}

 未了方便进行对象的传递,这里将这两个配置Bean封装成一个对象

DataSource就是数据库的配置信息

@Data
public class Configuration {

    private DataSource dataSource;
    /**
     * 将解析的Configuration 和MappedStatement同时封装到一起
     * 每一个sql语句就是一个MappedStatement
     * k 唯一标示 statementId namespace.id组成:这个唯一的id现在叫  statementId
     * v 分装好的MappedStatement对象
     */
    Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
}

使用dom4j解析配置,要引入相关的依赖

下面是框架端的全部依赖

<dependencies>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.17</version>
        </dependency>

        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>

        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

        <dependency>

            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
public class SqlSessionFactoryBuilder {
    /**
     * 创建sqlSessionFactoryBuilder类:
     * <p>
     * ⽅法:sqlSessionFactory build(inputStream):
     * <p>
     * 第⼀:使⽤dom4j解析配置⽂件,将解析出来内容封装到Configuration和MappedStatement中
     * 第⼆:创建SqlSessionFactory的实现类DefaultSqlSession 产生sqlSession(会话对象)工厂模式
     *
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        //第⼀:使⽤dom4j解析配置⽂件,将解析出来内容封装到Configuration和MappedStatement中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        //第⼆:创建SqlSessionFactory
        return null;
    }

}

通过parseConfig方法实现解析 

对数据源信息解析

public class XMLConfigBuilder {
    private Configuration configuration;

    /**
     * 在调用到时候就会执行无参构造
     */
    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 对将配置文件使用dom4j进行解析,封装Configuration的方法
     *
     * @param inputStream
     * @return
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document read = new SAXReader().read(inputStream);
        /*<configuration>根对象*/
        //数据源的解析
        Element rootElement = read.getRootElement();
        //表示 property 在任意位置都能获取到 一个property就是一个元素
        List<Element> list = rootElement.selectNodes("//property");
        //<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        Properties properties = new Properties();
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        }

        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));

        configuration.setDataSource(comboPooledDataSource);

        //mapper.xml解析
        /*@step1 获取路径 拿到路径 字节流 dom4j*/
        //<mapper resource="userMapper.xml"></mapper>
        List<Element> elements = rootElement.selectNodes("//mapper");
        for (Element element : elements) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsStream);
        }
        return configuration;
    }
}

 对sql配置xml的解析

public class XMLMapperBuilder {

    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * <select id="selectList" resultType="cn.mystylefree.my_persistence_demo.domain.User">
     * select * from user
     * </select>
     *
     * @param inputStream
     * @throws DocumentException
     */
    public void parse(InputStream inputStream) throws DocumentException {
        Document read = new SAXReader().read(inputStream);
        //获取到了mapper节点 接下来获取select 标签
        Element rootElement = read.getRootElement();
        List<Element> selectNodes = rootElement.selectNodes("//select");
        /*没过element就是
        <select id="selectList" resultType="cn.mystylefree.my_persistence_demo.domain.User">
            select * from user
        </select>*/
        String namespace = rootElement.attributeValue("namespace");

        for (Element node : selectNodes) {
            String id = node.attributeValue("id");
            String resultType = node.attributeValue("resultType");
            String parameterType = node.attributeValue("parameterType");
            //sql语句 文本信息
            String sqlText = node.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sqlText);
            //唯一标识
            String key = namespace + "." + id;
            configuration.getMappedStatementMap().put(key, mappedStatement);
        }
    }
}

 到此 Configuration配置就解析完成了

3.创建sqlSessionFactory

        DefaultSqlSessionFactory 生产sqlSession

        ⽅法:openSession() : 获取sqlSession接⼝的实现类实例对象

public class SqlSessionFactoryBuilder {
    /**
     * 创建sqlSessionFactoryBuilder类:
     * <p>
     * ⽅法:sqlSessionFactory build(inputStream):
     * <p>
     * 第⼀:使⽤dom4j解析配置⽂件,将解析出来内容封装到Configuration和MappedStatement中
     * 第⼆:创建SqlSessionFactory的实现类DefaultSqlSession 产生sqlSession(会话对象)工厂模式
     *
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        //第⼀:使⽤dom4j解析配置⽂件,将解析出来内容封装到Configuration和MappedStatement中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);


        /*
         * 第⼆:创建SqlSessionFactory
         * 是一个工厂类:生产sqlSession 会话对象
         * 数据库交互的增、删、改、查方法都封装在sqlSession对象
         * 以有参构造的方式将 配置传递下去
         * */
        SqlSessionFactory sqlSessionFactory=new DefaultSqlSessionFactory(configuration);

        return sqlSessionFactory;
    }

}
public interface SqlSessionFactory {

    public SqlSession openSession();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }


    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession();
    }
}
public interface SqlSession {

}

public class DefaultSqlSession implements SqlSession {
}

 此时创建sqlSessionFactory就完成了

4.创建sqlSession接口及实现类:DefaultSqlSession

主要封装crud操作

⽅法:

        selectList(String statementId,Object param):查询所有

        selectOne(String statementId,Object param):查询单个

        update

        delete

具体实现:封装JDBC完成对数据库表的查询操作

实现SqlSession接口

public interface SqlSession {

    /**
     * 查询所有
     *
     * @param <E>         这里会有范型
     *                    user模块就是user Product就是product 所以不确定类型
     * @param statementId 这里是需要将查询的唯一标识传进来,
     *                    只有传递这个id了才知道执行的sql语句(Configuration中查出来)
     * @param params      查询条件
     * @return
     */
    public <E> List<E> selectList(String statementId, Object... params);

    /**
     * 根据条件查询单个
     * @param statementId
     * @param params
     * @param <T>
     * @return
     */
    public <T> T selectOne(String statementId, Object... params);


}

实现

这里其实就能够直接写jdbc了,但是为了更优雅继续进行封装
public class DefaultSqlSession implements SqlSession {
    /**
     * 这里其实就能够直接写jdbc了,但是为了更优雅继续进行封装
     * @param statementId 这里是需要将查询的唯一标识传进来,
     *                    只有传递这个id了才知道执行的sql语句(Configuration中查出来)
     * @param params      查询条件
     * @param <E>
     * @return
     */
    @Override
    public <E> List<E> selectList(String statementId, Object... params) {
        return null;
    }

   @Override
    public <T> T selectOne(String statementId, Object... params) {
        List<Object> objects = this.selectList(statementId, params);
        if (objects.size() == 1) {
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("查询结果为空或者查询结果过多");
            
        }
    }
}

5.创建Executor接口及实现类 SimpleExecutor实现类

        query(Configuration,  MappedStatement, Object ...params):执行的就是JDBC代码   

        参数信息就是步骤1中的对象信息

        Object       操作数据库查询条件的具体参数值,不确定查询对象参数个数

                        如id=1 code=xsd

将jdbc的代码添加到这里

下面是用于解析的工具类

这是标记的解析器

public class GenericTokenParser {

  private final String openToken; //开始标记
  private final String closeToken; //结束标记
  private final TokenHandler handler; //标记处理器

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
   * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
   */
  public String parse(String text) {
    // 验证参数问题,如果是null,就返回空字符串。
    if (text == null || text.isEmpty()) {
      return "";
    }

    // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
    // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression变量,避免空指针或者老数据干扰。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {存在结束标记时
          if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在转义字符,即需要作为参数进行处理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根据参数的key(即expression)进行参数处理,返回?作为占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

这里存的是context是参数名称 #{id} #{username} 中的id和username

就是#{}中的内容

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}
public interface TokenHandler {
  String handleToken(String content);
}

public class ParameterMappingTokenHandler implements TokenHandler {
	private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

	// context是参数名称 #{id} #{username} 中的id和username

	public String handleToken(String content) {
		parameterMappings.add(buildParameterMapping(content));
		return "?";
	}

	private ParameterMapping buildParameterMapping(String content) {
		ParameterMapping parameterMapping = new ParameterMapping(content);
		return parameterMapping;
	}

	public List<ParameterMapping> getParameterMappings() {
		return parameterMappings;
	}

	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
		this.parameterMappings = parameterMappings;
	}

}

下面的是编写jdbc代码

//step1 注册驱动,获取连接

        在配置中有

//step2 获取sql语句  select * from user where id= #{id} and name= #{name}

        mappedStatement中存储

        //id标识
        private String id;
        //返回值类型
        private String resultType;
        //参数类型
        private String parameterType;
        //sql语句
        private String sql;
//step3 转换sql语句

        对sql进行解析 #{}的解析工作

    将sql和参数名封装到BoundSql中
//step4 获取预处理对象 preparedStatement

        

        ParameterMapping中存的是 #{}中的参数

        

//step5 设置参数,进行替换 占位符
//step6 执行sql
//step7 封装返回结果集

public class SimpleExecutor implements Executor {
    /**
     * 编写jdbc代码
     *
     * @param configuration
     * @param mappedStatement
     * @param params
     * @param <E>
     * @return
     */
    @SneakyThrows
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) {
        //step1 注册驱动,获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //step2 获取sql语句  select * from user where id= #{id} and name= #{name}
        //jdbc不能识别
        // select * from user where id= ? and name= ? 转换过程中对 #{} 中对值进行解析存储
        //为啥不直接在写sql时就用? 因为是一种需求 传入对是对象,根据占位符中对值,到对象属性中查找
        String sql = mappedStatement.getSql();

        //step3 转换sql语句
        BoundSql boundSql = getBoundSql(sql);
        //step4 获取预处理对象 preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

        //step5 设置参数,如何进行替换呢??
        //替换中的数据是什么变量怎么进行设计的呢???
        //这里的数据是保存在   BoundSql中的
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        //需要这个下标
        //获取到参数的全地址
        String parameterType = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);

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

            ParameterMapping parameterMapping = parameterMappingList.get(i);
            //id 和 username
            String content = parameterMapping.getContent();
            //通过反射 根据 content 获取到对象中的属性值--在获取到user参数中传递的具体值

            //(1)获取实体的全路径 String resultType = node.attributeValue("resultType");
            //parameterType="cn.mystylefree.my_persistence_demo.domain.User
            Field declaredField = parameterTypeClass.getDeclaredField(content);
            //获取到属性对象之后,下面是获取属性对象的值的方法
            declaredField.setAccessible(true);//暴力访问
            //这里获取的就是属性的值 从入参获取
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i + 1, o);//sql设置参数的下标是从1开始
        }

        //step6 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();

        //step7 封装返回结果集
        //获取返回对象class
        Class<?> resultTypeClass = Class.forName(mappedStatement.getResultType());
        /*这就是封装好的对象了*/
        Object instance = resultTypeClass.getConstructor().newInstance();
        List<Object> objects = new ArrayList<>();
        while (resultSet.next()) {
            //元数据  这里包含了查询字段的名称
            ResultSetMetaData metaData = resultSet.getMetaData();
            //为啥从1开始?
            for (int i = 1; i < metaData.getColumnCount(); i++) {//查询结果的列数(表中的字段个数)
                //字段名
                String columnName = metaData.getColumnName(i);//这个ColumnName的下标从1开始
                //字段的值
                Object value = resultSet.getObject(columnName);
                //使用反射或者内省,根据数据库表和实体的对应关系,完成封装
                //利用有参数构造,在创建后 就会从 resultType中获取 columnName (这个属性)生成读写方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                //写方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(instance, value);//这里就把value值封装到了instance对象中了
            }
            objects.add(instance);
        }

        return (List<E>) objects;
    }

    private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
        if (parameterType != null) {
            Class<?> aClass = Class.forName(parameterType);
            return aClass;
        }
        return null;
    }

    /**
     * 完成对 #{}的解析工作:
     * <p>
     * 1.将#{}使用 ?进行代替
     * 2.解析成#{} 中的值进行存储
     *
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配置标记解析器 来完成对占位符对解析处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //解析出来的sql
        String parseSql = genericTokenParser.parse(sql);
        //2.解析成#{} 中的值进行存储
        //#{}中解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();

        //面向对象思想,将sql和参数名封装到BoundSql中
        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);

        return boundSql;
    }
}

 这是进行解析时调试的信息,将sql进行了替换,具体的替换方法在代码中有详细的标记,这里就不做过多的冗余介绍了。

数据库 数据如下: 

这时就将数据库中的数据查到了,是不是感到很有成就感,给自己点赞👍

接下来我们把测试代码封装到dao中:

public interface IUserDao {
    //查询所有用户
    public List<User1> findAll() throws PropertyVetoException, DocumentException;
    //根据条件查询
    public User1 findByCondition(User1 user) throws PropertyVetoException, DocumentException;


}
public class UserDao implements IUserDao {

    @Override
    public List<User1> findAll() throws PropertyVetoException, DocumentException {
//        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
//        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//
//        SqlSession sqlSession = sessionFactory.openSession();
        SqlSession sqlSession = getSqlSession();
        List<Object> list = sqlSession.selectList("user.selectList");
        List<User1> collect = list.stream()
                .filter(e -> e instanceof User1)
                .map(e -> (User1) (e))
                .collect(Collectors.toList());
        return collect;
    }

    /*优化
     * 1.代码重复
     * 2.硬编码问题*/
    //解决方法,把实现类干掉???
    //解决思路
    //不要dao的实现类

    //使用代理类生成Dao层接口的代理实现类(底层的调用都交给代理实现类实现)


    private SqlSession getSqlSession() throws PropertyVetoException, DocumentException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sessionFactory.openSession();
        return sqlSession;
    }

    @Override
    public User1 findByCondition(User1 user) throws PropertyVetoException, DocumentException {
//        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
//        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//
//        SqlSession sqlSession = sessionFactory.openSession();
        SqlSession sqlSession = getSqlSession();
        Object o = sqlSession.selectOne("user.selectOne", user);
        return o instanceof User1 ? (User1) o : null;
    }


}

我们现在暂时已经实现了一个持久层框架,也能够从数据库中获取到需要查询的数据了,但是现在还是有些问题需要进行优化,下面我们根据问题,对代码进行优化。

存在问题:

1.代码中有硬编码

2.代码有重复

/*优化
     * 1.代码重复
     * 2.硬编码问题*/
    //解决方法,把实现类干掉???
    //解决思路
    //不要dao的实现类

    //使用代理类生成Dao层接口的代理实现类(底层的调用都交给代理实现类实现)

下面是实现的代码

在SqlSession接口中增加一个方法 getMapper,参数是需要代理的接口类,利用反射实现类的加载,需要对之前的一些参数进修改,例如statementId. 之前设置的是 模块名.id 

由于这里获取不到配置的namespace和id值,所以只能自己设置一个约定,之后修改userMapperConfig.Xml的值。 这里设置成包的权限定名+方法名,由于约定好了,执需要把之前的配置信息修改一下即可。之后根据返回值的类型决定调用那个方法实现查询

//准备参数 1 statementId :sql语句的唯一标识 namespace.id :接口权限定名.方法名

@Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //底层还是调用JDBC
                //实现:根据不同的情况,来调用selectOne或者selectList

                //准备参数 1 statementId :sql语句的唯一标识 namespace.id :接口权限定名.方法名
                // 因为这里获取不到配置文件中的配置,所以在配置的时候需要遵循特定的限制
                //namespace 接口的 cn.mystylefree.my_persistence_demo.dao.IUserDao
                //id是方法名  findAll  findByCondition
                //2 params

                /*参数准备*/
                //方法名 findAll
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String statementId = className + "." + methodName;
                //params 实际传递的参数 args传递


                //有的返回值并不是List只是一个对象
                //所以对返回结果的类型进行判断 是List还是对象
                /*获取被调用方法的返回值类型*/

                Type genericReturnType = method.getGenericReturnType();
                //判断是否进行了 泛型类型的参数化
                if (genericReturnType instanceof ParameterizedType) {
                    //如果是泛型化 就是List集合
                    List<Object> selectList = selectList(statementId, args);
                    return selectList;
                }
                //返回值类型是 对象
                return selectOne(statementId, args);
            }
        });
        return (T) proxyInstance;
    }

下面是对方法执行流程的一些总结:
 

总结: 

最后的运行结果如下:

 是不是感觉我们做了很🐂🍺的封装,还对自己有一丝的崇拜哈哈哈。

好了经过优化代码,现在我们能够优雅的实现一个自己定义的数据持久化代码框架了。为自己点赞

下面我把源码链接放到这里供大家参考(代码中难免有bug,如有发现请大佬指出,我会第一时间改正)

https://gitee.com/li_gang_x/my-project-model

https://gitee.com/li_gang_x/my-project-model.githttps://gitee.com/li_gang_x/my-project-model.gitdemo开源还在审核中,如果想要源码请私信或发评论给我

如果觉得本文对你有帮助,欢迎点赞,欢迎关注我,如果有补充欢迎评论交流,我将努力创作更多更好的文章。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智达教育‍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值