翻过这座山之自定义mybatis框架

前言:
不知不觉以及工作快两年.但是工作年限增加了,技术却没提升多少.之前一直想着自学来着.但是下班回到家毫无疑问都是被手机或者其他事情所吸引😥,所以决定报个班.
这次选择了拉勾教育.其实之前一直都有关注拉勾的.平时投简历也有用拉勾.所以权威这一块是毋庸置疑的.所以很快啊,就毫无犹豫的报了名.了解之后才发现原来不仅仅是学习到技术,拉勾还会提供大厂的内推机会,让你有更多的可能性.所以想到这里我的学习欲望就一下子迸发出来.
定一个小目标,之后的每个任务都会写一篇笔记,加油 翻过这座山 你们就是冠军
文章内容输出来源:拉勾教育Java高薪训练营;

一、为什么有持久层框架的存在

我们先来看一段代码

public static void main(String[] args) {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        // 加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");
        // 通过驱动管理类获取数据库连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8",
                "root", "root");
        // 定义sql语句 ?标识占位符
        String sql = "select * from user where id = ? and username = ?";
        // 获取预处理statement
        ps = conn.prepareStatement(sql);
        // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为 设置的参数值
        ps.setInt(1,  1);
        ps.setString(2,  "张三");
        // 向数据库发出sql执行查询,查询出结果集
        rs = ps.executeQuery();
        // 遍历结果集
        while (rs.next()) {
        	User user = new User();
            int id = rs.getInt("id");
            String username = rs.getString("username");
            // 封装User
            user.setId(id);
            user.setUsername(username);
            System.out.println(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 释放资源
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

相信写过jdbc的人都被这段代码折磨过,这段又长又硬的代码写起来相当难受,还不能不写,所以我们就对这些代码问题进行研究并提出解决方法

1.1 jdbc问题分析

  1. 对数据库连接信息的硬核编码,数据库连接创建、释放频繁造成系统资源浪费
        // 加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");
        // 通过驱动管理类获取数据库连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
  1. sql语句在代码中硬编码,sql在实际应用变动频繁,代码维护起来及其麻烦
        // 定义sql语句 ?标识占位符
        String sql = "select * from user where id = ? and username = ?";
  1. 使用preparedStatement预处理对象向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护
        // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为 设置的参数值
        ps.setInt(1,  1);
        ps.setString(2,  "张三");
  1. 对结果集解析存在硬编码,sql变化导致解析代码变化,系统不易维护
        // 向数据库发出sql执行查询,查询出结果集
        rs = ps.executeQuery();
        // 遍历结果集
        while (rs.next()) {
        	User user = new User();
            int id = rs.getInt("id");
            String username = rs.getString("username");
            // 封装User
            user.setId(id);
            user.setUsername(username);
            System.out.println(user);
        }

1.2 jdbc问题解决方案

思考:代码硬编码问题我们可以将信息提取到配置文件中,对于sql这种经常发生改变的信息我们应该单独存放配置文件。数据库连接创建、释放频繁可以采用数据库连接技术来解决。参数设置和返回结果集解析麻烦,我们通过反射等技术,使其数据库字段对实体类产生映射关系来进行封装

  • 使用数据库连接池初始化连接资源
  • 将sql语句抽取到xml配置文件中
  • 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射

二、自定义持久层框架设计

2.1 为什么我们要自定义持久层框架

通过手写自定义持久层框架,使得我们对框架设计原理有更深的印象和更深的理解

2.2 框架设计思路

(1)使用端

  • 提供核心配置文件,引入框架端jar包进行测试
  1. SqlMapperCnfig.xml: 存放数据库连接配置信息,以及其他存放sql配置文件的路径信息
  2. UserMapper.xml: 存放sql相关信息配置文件

(2)框架端

  1. 读取配置文件
    Resources类: 根据配置文件路径获取对应的字节输入流,存储到内存中
    方法: InputStream getResourcesAsSteam(String path);
    Configuration类: 存放SqlMapperCnfig.xml解析出来的内容
    MapperStatement类: 存放UserMapper.xml解析出来的内容
  2. 解析配置文件
    SqlSessionFactoryBuilder类:
    方法: build(Inputstream in);
    XMLConfigBuilder类: 解析SqlMapperCnfig.xml
    XMLMapperBuilder: 解析UserMapper.xml
    第一:使用dom4j解析配置文件,将信息存储到对应的javaBean中
    第二:创建SqlSessionFactory对象
  3. 创建SqlSessionFactory接口以及实现类
    方法: openSqlSession 生成SqlSession
  4. 创建SqlSession接口以及实现类
    定义对数据的crud操作:
    selectList();
    slectOne();
    update();
    insert();
    delete();
  5. 创建Executor接口以及实现类
    query(Configuration configuration, Mapperstatement mapperStatement,Objects… params);
    执行的jdbc代码

三、使用端IPersistence_test的代码编写

3.1 UserMapper.xml

<mapper namespace="user">

    <!--mapper.xml 可能存在多个, select 标签也需要多个,
            所以我们需要一个sql的唯一标识 statementId: namespace.id-->
    <select id="selectOne" parameterType="com.lagou.pojo.User" resultType="com.lagou.pojo.User">
        select * from user where id = #{id} and username = #{username};
    </select>

    <!--查询所有-->
    <select id="selectList" resultType="com.lagou.pojo.User">
        select * from user;
    </select>

</mapper>

这里我们发现如果有另一个个xml.里面标签id重复怎么办?

<mapper namespace="role">

    <select id="selectOne">
        select * from role where id = #{id};
    </select>

    <select id="selectList">
        select * from role;
    </select>
</mapper>

我们定义一个唯一标识statementId用namespace属性加上 '.'加上标签id来组成.即 statementId = namespace+id

3.2 SqlMapperConfig.xml

<configuration>

    <!--数据库配置信息-->
    <dataSrouce>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSrouce>

    <!--mapper的全路径-->
    <mapper resources="UserMapper.xml"></mapper>
    <mapper resources="RoleMapper.xml"></mapper>

</configuration>

这个xml不仅存放了数据库配置信息,还存放了其他mapper的全路径名,这样我们可以解析这个配置文件,找到mapper.xml一起解析

3.3 pojo类User

public class User {

    private Integer id;

    private String username;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                '}';
    }
}

值得注意的是这里的属性名需和数据库表字段保持一致,方便我们后面根据实体类字段进行数据库映射
在这里插入图片描述

四、框架端IPersistence代码编写

4.0 引入需要使用的jar依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lagou</groupId>
    <artifactId>IPersistence</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--设置maven编译版本-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <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>
    </dependencies>
</project>

4.1 Resources类

public class Resources {


    /**
     * 通过路径获取配置文件的字节输入流,存储在内存中
     *
     * @param path IPersistence_test中的配置文件路径
     * @return InputStream 配置文件的字节输入流
     */
    public static InputStream getResourcesAsStream(String path) {
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}

接下来我们定义容器对象

4.2 Configuration类

/**
 * 核心配置类
 * 存放数据库配置文件中的信息
 */
public class Configuration {

    /**
     * 数据库配置信息
     */
    private DataSource dataSource;

    /**
     * 一个xml中可能有多个sql,使用map存储
     * key:statementId唯一标识 namespace + 标签中的id
     * value: MapperStatement 标签中的信息
     * 这样就可以通过唯一标识获取xml中的对应标签的信息
     */
    private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Map<String, MapperStatement> getMapperStatementMap() {
        return mapperStatementMap;
    }

    public void setMapperStatementMap(Map<String, MapperStatement> mapperStatementMap) {
        this.mapperStatementMap = mapperStatementMap;
    }
}

这里我们使用map存储标签信息,方便通过statementId查找

4.3 MapperStatement类

package com.lagou.pojo;
/**
 * 映射配置类
 * 存放sql信息的xml中select标签中的信息
 */
public class MapperStatement {

    /**
     * 标签里的sql文
     */
    private String sql;

    /**
     * 标签里的id属性
     */
    private String id;

    /**
     * 返回值类型的全路径名
     */
    private String parameterType;

    /**
     * 参数类型的全路径名
     */
    private String resultType;

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getParameterType() {
        return parameterType;
    }

    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }
}

五、解析配置文件

5.1 SqlSessionFactoryBuilder类

public class SqlSessionFactoryBuilder {

    /**
     * 解析配置文件并创建sqlSession工厂类
     *
     * @param inputStream 配置文件字节输入流
     * @return {@link SqlSessionFactory} SqlSessionFactory 工厂类
     */
    public SqlSessionFactory builder(InputStream inputStream) throws PropertyVetoException, DocumentException {
        //1.使用dom4j解析配置文件,将解析出来的信息封装到configuration中;
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        //2.创建sqlSessionFactory对象
        return new DefaultSqlSessionFactory(configuration);
    }
}

这里我们使用XMLConfigBuilder类封装起来解析SqlMapperConfig.xml配置文件的代码

public class XMLConfigBuilder {

    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 使用dom4j对配置文件进行解析,封装成Configuration返回
     *
     * @param inputStream 配置文件字节输入流
     * @return {@link Configuration} SqlMapperConfig.xml中的信息
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        //根对象即:<configuration>标签
        Element rootElement = document.getRootElement();
        //查找标签名为property所有节点
        List<Element> propertyList = rootElement.selectNodes("//property");

        Properties properties = new Properties();
        for (Element element : propertyList) {
            //<property>标签中的 name属性和value属性
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            //将标签中的name,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中
        configuration.setDataSource(comboPooledDataSource);

        //获取mapper标签中的全路径,根据全路径获取对于的mapper.xml,解析并封装成MapperStatement
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);

        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            //获取xml的路径
            String mapperPath = element.attributeValue("resources");
            //获取字节流并解析封装
            xmlMapperBuilder.parseXml(Resources.getResourcesAsStream(mapperPath));
        }

        return configuration;
    }
}

解析出来的数据库连接配置放入Configuration类中,之前我们将其他mapper.xml的数据放入SqlMapperConfig.xml中,这里我们也一起解析出来

/**
 * 解析mapper.xml信息builder类
 */
public class XMLMapperBuilder {

    private Configuration configuration;

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

    /**
     * 解析mapper.xml获取对于sql信息并封装进MapperStatement
     *
     * @param inputStream mapper.xml文件的字节输入流
     */
    public void parseXml(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        //<mapper>标签
        Element rootElement = document.getRootElement();

        //获取到namespace
        String namespace = rootElement.attributeValue("namespace");

        //获取select标签
        List<Element> selectList = rootElement.selectNodes("//select");

        for (Element element : selectList) {
            //封装MapperStatement
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setId(element.attributeValue("id"));
            mapperStatement.setSql(element.getTextTrim());
            mapperStatement.setParameterType(element.attributeValue("parameterType"));
            mapperStatement.setResultType(element.attributeValue("resultType"));
            //之前定义的statementId
            String statementId = namespace + "." + mapperStatement.getId();
            configuration.getMapperStatementMap().put(statementId, mapperStatement);
        }
    }
}

同样的我们也使用一个builder类封装这段代码

5.2 SqlSessionFactory接口及其实现类

/**
 * SqlSession工厂接口
 */
public interface SqlSessionFactory {

    /**
     * 生成sqlSession方法
     *
     * @return {@link SqlSession}
     */
    SqlSession opSqlSession();
}
/**
 * SqlSessionFactory实现类
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

    @Override
    public SqlSession opSqlSession() {
        return new DefaultSqlSession(configuration);
    }
}

这里使用有参构造使其configuration配置信息可以往下传递

5.3 SqlSession接口及其实现类


/**
 * 操作数据库的接口类
 */
public interface SqlSession {

    /**
     * 查询所有
     *
     * @param statementId 获取MapperStatement唯一标识
     * @param params      sql的参数
     * @param <E>         泛型 不确定是返回什么类
     * @return
     */
    <E> List<E> selectList(String statementId, Object... params) throws Exception;


    /**
     * 根据条件查询单条
     *
     * @param statementId 获取MapperStatement唯一标识
     * @param params      sql的参数
     * @param <T>         泛型 不确定是返回什么类
     * @return
     */
    <T> T selectOne(String statementId, Object... params) throws Exception;
}

提供了两个简单查询接口


/**
 * SqlSession接口实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

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

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        //创建执行器
        Executor executor = new SimpleExecutor();
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);

        //执行sql并返回结果集
        return executor.query(configuration, mapperStatement, params);
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
    	//两个方法差不多,根据返回数量决定返回集合还是实体类
        List<Object> objects = selectList(statementId, params);
        if (objects.size() == 1) {
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("查询结果为空或返回结果过多");
        }
    }
}

到这里还没有看到jdbc的代码,因为我们将其封装到了executor执行器中

5.4 Executor接口及其实现类

/**
 * sql执行器
 */
public interface Executor {

    /**
     * 对数据库进行查询的方法
     *
     * @param configuration   数据库信息
     * @param mapperStatement sql信息
     * @param params          sql参数
     * @param <E>
     * @return
     */
    <E> List<E> query(Configuration configuration,
                      MapperStatement mapperStatement,
                      Object... params) throws Exception;
}
/**
 * Executor接口实现类
 */
public class SimpleExecutor implements Executor {


    @Override
    public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception {
        //1.注册驱动,获取连接
        Connection connection = configuration.getDataSource().getConnection();

        //2.获取sql语句
        //select * from user where id = #{id} and username = #{username};
        //数据库无法识别#{},需进行转换
        String sql = mapperStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);

        //3.获取预处理对象
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getParseSql());

        //4.参数设置
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        //根据参数类型的全路径获取对应的参数类型的class
        String parameterType = mapperStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);

        int size = parameterMappingList.size();
        for (int i = 0; i < size; i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            //暴力反射
            Field field = parameterTypeClass.getDeclaredField(content);
            field.setAccessible(true);
            // params[0] = user
            Object value = field.get(params[0]);
            //设置参数
            preparedStatement.setObject(i + 1, value);
        }

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

        //6.封装返回值
        List<Object> objects = new ArrayList<>();
        Class<?> resultTypeClass = getClassType(mapperStatement.getResultType());
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            Object o = resultTypeClass.newInstance();
            for (int i = 1; i <= columnCount; i++) {
                //获取字段名,getColumnName()方法下标从1开始
                String columnName = metaData.getColumnName(i);
                //获取字段值
                Object columnValue = resultSet.getObject(columnName);
                //使用反射或者内省,根据数据库表和实体的对应关系,完成封装
                //PropertyDescriptor 内省库中的一个类,根据类的class对象和字段名生成读写方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                //将属性值设置到对象中
                writeMethod.invoke(o, columnValue);
            }
            objects.add(o);
        }

        return (List<E>) objects;
    }

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

    /**
     * 1.解析sql将占位符替换成?
     * 2.获取到占位符中的数据
     *
     * @param sql 原sql
     * @return 解析后的数据
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理器
        ParameterMappingTokenHandler mappingTokenHandler = new ParameterMappingTokenHandler();
        //GenericTokenParser 有参构造有三个参数 分别是开始标记,结束标记,标记处理器
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", mappingTokenHandler);
        //解析后的sql
        String parseSql = genericTokenParser.parse(sql);
        //占位符中的数据
        List<ParameterMapping> parameterMappings = mappingTokenHandler.getParameterMappings();
        //封装返回
        return new BoundSql(parseSql, parameterMappings);
    }

这里我们遇到一个问题,xml中sql是使用占位符代替了?的,所以我们需要做两个操作.第一解析sql替换占位符,第二获取占位符的数据并进行封装,后面设置参数使用

GenericTokenParser 类:
下面几个工具类是mybatis源码中,我们简单了解即可

/**
 * @author Clinton Begin
 */
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();
  }
}

ParameterMapping 类

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;
    }
}

ParameterMappingTokenHandler 接口实现类

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

	// context是参数名称 #{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;
	}

}

TokenHandler 接口

/**
 * @author Clinton Begin
 */
public interface TokenHandler {

    String handleToken(String content);
}

BoundSql 类

/**
 * 存储解析后的sql信息,以及占位符中的信息
 */
public class BoundSql {

    public BoundSql(String parseSql, List<ParameterMapping> parameterMappingList) {
        this.parseSql = parseSql;
        this.parameterMappingList = parameterMappingList;
    }

    /**
     * 解析后的sql
     */
    private String parseSql;

    /**
     * 占位符中的信息
     */
    private List<ParameterMapping> parameterMappingList;

    public String getParseSql() {
        return parseSql;
    }

    public void setParseSql(String parseSql) {
        this.parseSql = parseSql;
    }

    public List<ParameterMapping> getParameterMappingList() {
        return parameterMappingList;
    }

    public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
        this.parameterMappingList = parameterMappingList;
    }
}

六、编写测试类进行测试

6.0 需要先引入框架端的jar

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lagou</groupId>
    <artifactId>IPersistence_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--设置maven编译版本-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!--引入自定义持久层框架的依赖-->
    <dependencies>
        <dependency>
            <groupId>com.lagou</groupId>
            <artifactId>IPersistence</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

6.1IPersistenceTest类

/**
 * 测试类
 */
public class IPersistenceTest {

    @Test
    public void test() throws Exception {
        InputStream resourcesAsStream = Resources.getResourcesAsStream("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().builder(resourcesAsStream).opSqlSession();
        User user = new User();
        user.setId(1);
        user.setUsername("张三");
        User one = sqlSession.selectOne("user.selectOne", user);
        System.out.println(one);

        System.out.println("=============================");

        List<User> userList = sqlSession.selectList("user.selectList");
        System.out.println(userList);
    }
}

运行结果如下,完美
在这里插入图片描述
但是我们现在看看这段代码,在实际应用中我们会这么去使用吗?我们一般将数据库交互的代码放在dao层中.所以接下来我们要优化一下我们自定义框架

七、自定义持久层框架优化

7.1 创建dao层

首先我们先建一个IUserDao接口

public interface IUserDao {

    /**
     * 查询所有
     *
     * @return
     */
    List<User> findAll();

    /**
     * 根据条件查询
     *
     * @param user
     * @return
     */
    User findByCondition(User user);
}

再来一个实现类

/**
 * 实现类
 */
public class IUserDaoImpl implements IUserDao {

    @Override
    public List<User> findAll() throws Exception {
        InputStream resourcesAsStream = Resources.getResourcesAsStream("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().builder(resourcesAsStream).opSqlSession();
        List<User> userList = sqlSession.selectList("user.selectList");
        
        for (User user : userList) {
            System.out.println(user);
        }
        return userList;
    }

    @Override
    public User findByCondition(User user) throws Exception {
        InputStream resourcesAsStream = Resources.getResourcesAsStream("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().builder(resourcesAsStream).opSqlSession();
        User one = sqlSession.selectOne("user.selectOne", user);
        
        System.out.println(one);
        return one;
    }
}

发现上面这段代码存在两个问题
在这里插入图片描述
一个代码有明显的重复,可进行进一步的抽取封装.另一个是statementId有硬编码问题

7.2 通过动态代理方式进行优化

7.2.1 为dao接口生成代理实现类

SqlSession接口添加方法

    /**
     * 生成动态代理对象
     *
     * @param mapperClass
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<?> mapperClass);

DefaultSqlSession实现类进行实现

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        //使用jdk动态代理生成dao接口代理对象
        //Proxy.newProxyInstance()方法需要三个参数,类加载器,字节码对象数组,实现的一个接口
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

				return null;
            }
        });
        return (T) proxyInstance;
    }

现在我们编写测试类

/**
 * 测试类
 */
public class IPersistenceTest {

    @Test
    public void test() throws Exception {
        InputStream resourcesAsStream = Resources.getResourcesAsStream("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().builder(resourcesAsStream).opSqlSession();
        User user = new User();
        user.setId(1);
        user.setUsername("张三");
        IUserDao iUserDao = sqlSession.getMapper(IUserDao.class);
        User one = iUserDao.findByCondition(user);
        List<User> all = iUserDao.findAll();
    }
}

在这里插入图片描述
接下来我们在invoke方法实现我们的逻辑

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        //使用jdk动态代理生成dao接口代理对象
        //Proxy.newProxyInstance()方法需要三个参数,类加载器,字节码对象数组,实现的一个接口
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                //底层还是执行jdbc代码,根据返回值的类型判断使用selectList还是selectOne
                //需要参数statementId,修改xml将namespace改成IUserDao的全限定名,id改为IUserDao的方法名
                String methodName = method.getName();
                String classPathName = method.getDeclaringClass().getName();
                String statementId = classPathName + "." + methodName;

                //判断方法的返回值是不是泛型参数化
                if (method.getGenericReturnType() instanceof ParameterizedType) {
                    //是泛型,代表多条数据,使用selectList
                    return selectList(statementId, objects);
                }
                //返回单条数据
                return selectOne(statementId, objects);
            }
        });
        return (T) proxyInstance;
    }

其实底层还是走的我们之前的selectList和selectOne但是在使用端代码更加简洁,方便.这里通过文件全路径和方法名组成statementId相应的我们得修改xml

7.2.2 修改UserMapper.xml

在这里插入图片描述

<mapper namespace="com.lagou.dao.IUserDao">

    <!--mapper.xml 可能存在多个, select 标签也需要多个,
            所以我们需要一个sql的唯一标识 statementId: namespace.id-->
    <select id="findByCondition" parameterType="com.lagou.pojo.User" resultType="com.lagou.pojo.User">
        select * from user where id = #{id} and username = #{username};
    </select>

    <!--查询所有-->
    <select id="findAll" resultType="com.lagou.pojo.User">
        select * from user;
    </select>

</mapper>

7.2.3 代码测试

修改一下执行我们的代码
在这里插入图片描述
结果如下
在这里插入图片描述

八、总结

  1. 设计模式中使用builder 建造者 将一个复杂对象的构建与表示相分离,factory工厂模式,对需要创建的对象进行了封装.动态代理模式
  2. 大致了解了mybatis框架对于jdbc存在各种问题的解决方案.更好的理解框架思想
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值