持久层框架设计思路

1,337 阅读15分钟

ORM(对象关系映射):为了解决面向对象与关系数据库存在的互不匹配的现象的技术。 Java中持久层框架(ORM)主要是为了解决:JDCB操作SQL硬编码问题以及性能、难以维护的问题。

JDBC操作的问题:

  1. 数据库的配置信息存在硬编码问题
  2. 频繁的创建释放数据库连接
  3. SQL语句、设置参数、获取结果集存在硬编码问题
  4. 手动封装结果集,较为繁琐

而持久层框架设计就是为了解决上述的问题,将程序中的对象自动持久化到关系数据库中。

持久层框架设计思路

JDBC操作问题的解决方案:

  • 问题1:数据库的配置信息存在硬编码问题 -> 将数据库相关的配置信息放到配置文件中。
  • 问题2:频繁的创建释放数据库连接 -> 使用连接池技术
  • 问题3:SQL语句、设置参数、获取结果集存在硬编码问题 -> 配置文件
  • 问题4:手动封装结果集,较为繁琐 -> 反射、内省技术

按照上述的解决方案来设计持久层框架:

  1. 使用端:引入自定义持久层框架jar包,提供两部分配置信息:数据库配置文件(sqlMapConfig.xml)、SQL配置文件Mapper.xml(sql、参数、返回值等)
  2. 架构本身:对JDBC代码进行封装
    1. 加载配置文件:Resource类,定义InputSteam getResourceAsSteam(path)方法,根据配置文件路径,加载配置文件,返回字节输入流存储在内存中。在1中我们设计了两个配置文件其中核心配置文件sqlMapConfig.xml以及SQL映射配置文件Mapper.xml 映射配置文件在实际开发中我们不可能都写在一个配置文件中,而是根据功能划分有多个映射配置文件,所以需要将所有的映射配置文件以XML标签的形式添加到核心配置文件sqlMapConfig.xml中,这样Resource类只加载sqlMapConfig.xml即可,而不用添加很多的映射配置文件。
    2. 创建两个JavaBean对象:这两个Java对象主要用来存在配置文件解析出来的内容。Configuration 核心配置类、MappedStatement映射配置类。
    3. 解析配置文件:使用dom4j进行解析,创建类SqlSessionFactoryBuilder定义方法build(InputSteam in), build的方法作用如下:
      1. 使用dom4j解析在步骤a返回的InputSteam,将内容放在步骤b创建的两个JavaBean对象中;
      2. 创建SqlSessionFactory对象,生产SqlSession会话对象(工厂模式)
    4. 创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory,主要方法openSession() 生产SqlSession
    5. 创建SqlSession接口及实现类DefaultSession,对数据库的CRUD操作方法:selectList() selectOne() update() delete()
    6. 创建Executor接口及实现类SimpleExecutor 方法query(Configuration,MappedStatement,Object... params)

具体的流程图如下: image.png OK,持久层的框架思路捋清了然后我们开始撸代码,创建一个工程打包方式为jar

 <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</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>

配置文件定义与解析实现

配置文件是在使用端创建的,先创建使用端的一个maven项目,创建配置文件:

  • 核心配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!-- 数据源配置 -->
    <datasource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis_test"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </datasource>

    <!-- mapper配置 -->
    <mapper resource="UserMapper.xml"/>
</configuration>
  • 映射配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.pbatis.dao.IUserDao">
    <select id="findAll" resultType="com.pbatis.pojo.User">
        select * from tb_user
    </select>

    <select id="findById" resultType="com.pbatis.pojo.User" parameterType="com.pbatis.pojo.User">
        select * from tb_user where id = #{id} and username=#{username}
    </select>

    <insert id="save" parameterType="com.pbatis.pojo.User">
        insert into tb_user(id,username,password,birthday,version,is_delete)
        values(null,#{username},#{password},#{birthday},#{version},#{is_delete})
    </insert>

    <update id="update" parameterType="com.pbatis.pojo.User">
        update tb_user set username=#{username} where id=#{id}
    </update>

    <delete id="delete" parameterType="com.pbatis.pojo.User">
        delete from tb_user where id=#{id}
    </delete>
</mapper>

解析配置文件,在解析配置文件我们需要将配置数据存储,所以首先创建存储的类。 配置信息类:Configuration 主要存储DataSource数据源以及mapper的映射信息

public class Configuration {
    /**
     * 数据源对象
     */
    private DataSource dataSource;

    /**
     * 存储mapper的映射
     * key: statementid = (namespace + id)
     * value: MappedStatement - 参数类型、返回值类型、SQL语句
     */
    private Map<String, MappedStatement> mapper = new HashMap<>();

    //get/set....
}

Mapper文件映射的信息类:MappedStatement 存储着每个映射文件中的每一条标签的映射信息,例如:<select> <insert> <update> <delete> 标签,对标签的设置的属性以及SQL语句存储。

public class MappedStatement implements Serializable {
    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String parameterType;
    //SQL语句
    private String sql;
    //mapped 的类型
    private MappedType mappedType;

    //get/set...
}
//mapper的类型对应着<select /> <insert/> <update/> <delete/> 标签
public enum MappedType {
    SELECT, INSERT, UPDATE, DELETE
}

解析配置文件:XmlConfigBuild 解析核心配置文件、XMLMapperBuilder解析mapper映射配置文件 首先我们看XmlConfigBuild类:使用dom4j进行解析,解析数据源,然后将数据源信息存入c3p0的连接池中,然后解析mapper映射文件,转为输入流inputStream交给XMLMapperBuilder解析。

public class XmlConfigBuilder {

    private Configuration configuration;

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

    /**
     * 使用dom4j解析配置,封装为Configuration对象
     *
     * @param inputStream
     * @return Configuration
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        //根标签 <configuration>
        Element rootElement = document.getRootElement();
        //查找property标签
        List<Element> list = rootElement.selectNodes("//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("url"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        //把数据源存入configuration
        configuration.setDataSource(comboPooledDataSource);

        //解析mapper.xml 拿到路径
        List<Element> mapperElement = rootElement.selectNodes("//mapper");
        for (Element element : mapperElement) {
            String resource = element.attributeValue("resource");
            System.out.println("resource:" + resource);
            //获取mapper.xml的输入流
            InputStream resourceAsSteam = Resources.getResourceAsSteam(resource);
            //解析mapper.xml
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parseMapper(resourceAsSteam);
        }
        return configuration;
    }
}

我们看XMLMapperBuilder类的实现 如下代码:解析namespeace配合标签的id属性组成唯一的标识statementid.将这些信息最后存储到了configurationmapper中。

public class XMLMapperBuilder {
    private Configuration configuration;

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

    public void parseMapper(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        //select标签
        List<Element> selectNodes = rootElement.selectNodes("//select");
        extracted(namespace, selectNodes, MappedType.SELECT);

        //insert标签
        List<Element> insertNodes = rootElement.selectNodes("//insert");
        extracted(namespace, insertNodes, MappedType.INSERT);

        //update标签
        List<Element> updateNodes = rootElement.selectNodes("//update");
        extracted(namespace, updateNodes, MappedType.UPDATE);

        //delete标签
        List<Element> deleteNodes = rootElement.selectNodes("//delete");
        extracted(namespace, deleteNodes, MappedType.DELETE);
    }

    private void extracted(String namespace, List<Element> selectNodes, MappedType mappedType) {
        for (Element selectNode : selectNodes) {
            String id = selectNode.attributeValue("id");
            //获取返回值类型
            String resultType = selectNode.attributeValue("resultType");
            //获取参数类型
            String parameterType = selectNode.attributeValue("parameterType");
            //获取SQL语句
            String sql = selectNode.getText();
            //statementId
            String statementid = namespace + "." + id;
            //存储到MappedStatement
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sql);
            mappedStatement.setMappedType(mappedType);
            //存储数据到configuration
            configuration.getMapper().put(statementid, mappedStatement);
        }
    }
}

OK,经过上述的步骤我们已经拿到了所有的配置信息,接下来拿到配置信息去进行SQL操作。

构建SqlSession

从上述的流程图中,SqlSession是由SqlSessionFactory类构建的,而SqlSessionFactory类是由SqlSessionFactoryBuild创建的,也就是说在SqlSessionFactoryBuild类,对配置文件进行了解析的入口类可以说是自定义持久层框架的入口类。

SqlSessionFactoryBuild

实现如下,调用了XmlConfigBuilder解析配置文件,然后创建了DefaultSqlSessionFactoryConfiguration传递进入

public class SqlSessionFactoryBuilder {
    /**
     * SqlSessionFactory
     *
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        //1. dom4j 解析配置文件,将解析出来的内存封装到Configuration
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        //2. 创建SqlSessionFactory : 生产sqlSession会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }
}

SqlSessionFactory

DefaultSqlSessionFactory实现了SqlSessionFactory接口,通过调用openSession方法生产SqlSession对象

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

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

SqlSession

SqlSession的接口如下:

public interface SqlSession {
    /**
     * 根据条件查询单个
     *
     * @param statementid = namespace.id sql 语句的唯一标识
     * @param params      参数
     */
    <E> E selectOne(String statementid, Object... params);

    /**
     * 查询所有
     *
     * @param statementid = namespace.id sql 语句的唯一标识
     * @param params      参数
     */
    <E> List<E> selectList(String statementid, Object... params);

    /**
     * 添加
     */
    int insert(String statement, Object params);

    /**
     * 修改
     */
    int update(String statement, Object params);
}

我们需要实现SqlSession的接口,这里需要注意,SqlSession的实现类,并不会执行SQL操作而是交给Executor去执行。会将Configuration以及mappedStatement和params传递给Executor执行真正的SQL操作

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

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

    @Override
    public <E> E selectOne(String statementid, Object... params) {
        //调用selectList方法
        List<Object> objects = selectList(statementid, params);
        //size等于1表示只有一条记录
        if (objects.size() == 1) {
            return (E) objects.get(0);
        } else {
            throw new RuntimeException("查询结果为空或者返回结果过多");
        }
    }

    @Override
    public <E> List<E> selectList(String statementid, Object... params) {
        //去完成Executor里的query方法的调用
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        //获取再configuration中存储的Mapper映射信息
        MappedStatement mappedStatement = configuration.getMapper().get(statementid);
        List<Object> list = null;
        try {
            list = simpleExecutor.query(configuration, mappedStatement, params);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (List<E>) list;
    }

    @Override
    public int insert(String statement, Object params) {
        return update(statement, params);
    }

    @Override
    public int update(String statement, Object params) {
        try {
            //在Executor中完成插入操作
            SimpleExecutor simpleExecutor = new SimpleExecutor();
            //获取再configuration中存储的Mapper映射信息
            MappedStatement mappedStatement = configuration.getMapper().get(statement);
            //执行更新操作
            int row = simpleExecutor.update(configuration, mappedStatement, params);
            return row;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    @Override
    public int delete(String statement, Object params) {
        return update(statement, params);
    }
}

Executor执行器实现

Executor执行器接口

public interface Executor {
    //查询操作
    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, Exception;
	//新增、更新、删除操作
    int update(Configuration configuration, MappedStatement mappedStatement, Object params);
}

创建SimpleExecutor去实现Executor接口,首先我们看query查询操作的实现。如何实现呢?其实底层就是调用JDBC进行实现,不知道大家还是否记得JDBC的实现,看如下代码:

		// 1. 获得连接
		Connection connection = configuration.getDataSource().getConnection();
		// 2. sql语句	
        String sql = select * from user where id = ? and username=?

        // 3. 获取预处理对象 preparedStatement,传递解析过后的sql
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
		// 4. 设置参数
		preparedStatement.setObject(1, 1);
		preparedStatement.setObject(2, "tom");
		// 5. 拿到结果集
		ResultSet resultSet = preparedStatement.executeQuery();

上述代码是JDBC原始的代码,不知道大家有没有注意到SQL语句select * from user where id = ? and username=? 它和我们自定义持久层框架的配置文件的SQL语句是不一样的select * from tb_user where id = #{id} and username=#{username} 也就是说我们需要对SQL语句进行解析两步操作:

  1. #{} 替换成?
  2. 记录#{id} 里面的属性值id 因为我们要拿着这个属性值,去参数对象中找到对应的参数值,然后设置给preparedStatement

首先我们要解析SQL后的SQL语句以及记录属性值,保存到BoundSql类中

public class BoundSql {
    //解析过后的sql
    private String sqlText;

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

解析SQL的实现方法用到的是一个工具类,代码如下:

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

TokenHandler主要的作用是将那些内容替换成?,并将内容记录下来

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

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

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

}

ok,那么我们整个执行器的实现类如下:

public class SimpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        // 执行jdbc代码
        // 1. 注册驱动,获取连接 直接从configuration获取DataSource拿到连接
        Connection connection = configuration.getDataSource().getConnection();

        // 2. 获取SQL语句
        // select * from user where id = #{id} and username=#{username}
        // jdbc 识别的占位符只能是?号,为什么使用#{}呢? 可以根据里面的参数名称 来找到传递参数实体类型的属性的值
        String sql = mappedStatement.getSql();
        //转换SQL语句 select * from user where id = ? and username=?
        //还需要对#{}的值进行解析存储,找到实体对象对应属性的值进行添加
        BoundSql boundSql = getBoundSql(sql);

        // 3. 获取预处理对象 preparedStatement,传递解析过后的sql
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

        // 4. 设置参数
        //参数的全类名
        String parameterType = mappedStatement.getParameterType();
        //获取参数的Class
        Class<?> parameterClass = getClassType(parameterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            //反射根据参数名称 获取到实体对象中的属性值
            Field field = parameterClass.getDeclaredField(content);
            //暴力访问
            field.setAccessible(true);
            Object o = field.get(params[0]);
            //设置参数值
            preparedStatement.setObject(i + 1, o);
        }

        // 5. 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        // 6. 封装返回的结果集
        String resultType = mappedStatement.getResultType();
        //获取返回结果的具体的class
        Class<?> resultClass = getClassType(resultType);
        //创建集合
        ArrayList<Object> results = new ArrayList<>();
        while (resultSet.next()) {
            //创建结果类的对象实例
            Object instance = resultClass.newInstance();
            ResultSetMetaData metaData = resultSet.getMetaData();
            //列 循环
            for (int i = 1; i < metaData.getColumnCount(); i++) {
                //字段名
                String columnName = metaData.getColumnName(i);
                //字段值
                Object object = resultSet.getObject(columnName);
//                System.out.println(columnName + ":" + object);
                //使用反射 内省,根据数据库表和实体的对应关系完成封装
                //PropertyDescriptor 提供了内省技术的实现
                //会对resultClass中的columnName的属性进行读写的方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
                //写入方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                //将具体的值写入到对象中
                writeMethod.invoke(instance, object);
            }
            results.add(instance);
        }

        return (List<E>) results;
    }
    
    /**
     * 来完成对#{}的解析工作:
     * 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);
        //#{} 解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
        return boundSql;
    }
}

测试

经历过上述的漫长的编码我们终于写了差不多了,那么下面我们来实现简单的测试: 来看我们设计的自定义持久层框架能否正常跑完逻辑,去新建一个工程,依赖自定义持久层框架 首先编写配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!-- 数据源配置 -->
    <datasource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis_test"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </datasource>

    <!-- mapper配置 -->
    <mapper resource="UserMapper.xml"/>
</configuration>

编写映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.pbatis.dao.IUserDao">
    <select id="findAll" resultType="com.pbatis.pojo.User">
        select * from tb_user
    </select>

    <select id="findById" resultType="com.pbatis.pojo.User" parameterType="com.pbatis.pojo.User">
        select * from tb_user where id = #{id} and username=#{username}
    </select>
 </mapper>

测试代码如下:

public class IPersistenceTest {
    public static void main(String[] args) throws DocumentException, PropertyVetoException {
        InputStream inputStream = Resources.getResourceAsSteam("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(3);
        user.setUsername("jake");
        //传递statementId = namespace + "." + id
        User findUser = sqlSession.selectOne("com.pbatis.dao.IUserDao.findById", user);
        System.out.println(findUser);
        //传递statementId = namespace + "." + id
        List<User> list = sqlSession.selectList("com.pbatis.dao.IUserDao.findAll");
        for (User user1 : list) {
            System.out.println(user1);
        }
    }
}

运行查询结果: image.png

动态代理Mapper

在使用mybatis的开发的时候,都会去创建dao层的接口,那么自定义的持久层框架是否也可以实现呢? 代码如下:IUserDao接口

public interface IUserDao {
    //查询所有用户
    List<User> findAll();

    //根据条件进行用户查询
    User findById(User user);
}

还需要去实现接口:

public class UserDaoImpl implements IUserDao {
    @Override
    public List<User> findAll() {
        try {
            // 存在代码重复的模板:
            InputStream inputStream = Resources.getResourceAsSteam("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();

            // statementId 存在硬编码的问题:
            List<User> list = sqlSession.selectList("user.findAll");
            for (User user1 : list) {
                System.out.println(user1);
            }
            return list;
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public User findById(User user) {
        try {
            // 存在代码重复的模板:
            InputStream inputStream = Resources.getResourceAsSteam("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();

            // statementId 存在硬编码的问题:
            User findUser = sqlSession.selectOne("user.findById", user);
            System.out.println(findUser);
            return findUser;
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        return null;
    }
}

那么在使用的时候就需要去创建IUserDao实例直接调用

        IUserDao iUserDao = new IUserDaoImpl();
        List<User> users = iUserDao.findAll();
        for (User user1 : users) {
            System.out.println(user1);
        }
        User user1 = iUserDao.findById(user);
        System.out.println(user1);

在我们使用DAO的时候存在几个问题,自定义持久层框架问题分析:

  1. Dao层使用自定义持久层框架,存在代码重复,整个操作过程模板重复(加载配置文件、创建sqlSessionFactory、生成SqlSession)
  2. statementid存在硬编码问题,如果发生改变则无法找到指定的id 进行对应的SQL语句执行

在我们使用Mybatis的时候直接声明接口类就可以了,那么Mybatis是如何避免了接口的实现一些重复代码呢? 解决思路:使用代理模式来生成Dao层接口的代理实现类。代理接口的方法实现 实现思路:

  1. 使用JDK动态代理来为DAO接口类生成代理对象
  2. DAO接口方法名必须和mapper映射文件配置的id一致,然后DAO接口的全类名为namespce这样就按照一定的规则去组成statementId获得MappedStatement
  3. 根据MappedStatement获得标签类型,调用SqlSession对应的方法

代理类的生成在SqlSession中去实现,代码逻辑很简单如下所示:

@Override
    public <T> T getMapper(Class<?> daoClass) {
        //使用JDK动态代理来为DAO接口生成代理对象,并返回
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{daoClass}, new InvocationHandler() {
            /**
             *
             * @param proxy 当前代理对象的引用
             * @param method 当前被调用方法的引用
             * @param args 方法传递的参数
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //根据不同的情况来调用selectList和selectOne
                //1. 准备参数1:statementid = namespace + id 但是这里面无法获取到这些所以需要定义一个指定的规则:namespace.id = 接口全限定名.方法名
                String id = method.getName();
                String namespace = method.getDeclaringClass().getName();
                String statementId = namespace + "." + id;
                MappedStatement mappedStatement = configuration.getMapper().get(statementId);
                if (mappedStatement.getMappedType() == MappedType.SELECT) {
                    //2. 准备参数2 Object... 判断返回值的类型如果是集合调用selectList
                    //获取方法返回值类型
                    Type genericReturnType = method.getGenericReturnType();
                    //判断是否进行了泛型类型参数化 如果有泛型那么是list集合
                    if (genericReturnType instanceof ParameterizedType) {
                        List<Object> objects = selectList(statementId, args);
                        return objects;
                    }
                    return selectOne(statementId, args);
                } else if (mappedStatement.getMappedType() == MappedType.INSERT) {
                    int row = insert(statementId, args[0]);
                    return row;
                } else if (mappedStatement.getMappedType() == MappedType.UPDATE) {
                    int row = update(statementId, args[0]);
                    return row;
                } else if (mappedStatement.getMappedType() == MappedType.DELETE) {
                    int row = delete(statementId, args[0]);
                    return row;
                }
                return null;
            }
        });
        return (T) proxyInstance;
    }

这样我们只需要传递接口类,然后拿到代理对象,直接调用方法即可,省去了很多的重复代码,以及statemetId 硬编码的问题

        InputStream inputStream = Resources.getResourceAsSteam("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUserDao iUserDao = sqlSession.getMapper(IUserDao.class);
        //查询所有数据
        List<User> users = iUserDao.findAll();
        for (User user1 : users) {
            System.out.println(user1);
        }
        //根据条件查询
        User user = new User();
        user.setId(3);
        user.setUsername("jake");
        User user1 = iUserDao.findById(user);
        System.out.println(user1);

OK,现在我们也实现了一个简化版的Mybatis,其实在Mybatis和Spring整合的时候,Spring会将Mapper的代理对象存储在IOC容器中,所以我们在使用Mapper的时候只需要添加一个@Autowired注解就可以从IOC容器中取出Mapper代理对象,直接使用。

另外关于剩下的新增、更新、删除等操作其实是一样的原理。 SqlSession中添加相关的方法:

    @Override
    public int insert(String statement, Object params) {
        return update(statement, params);
    }

    @Override
    public int update(String statement, Object params) {
        try {
            //在Executor中完成插入操作
            SimpleExecutor simpleExecutor = new SimpleExecutor();
            //获取再configuration中存储的Mapper映射信息
            MappedStatement mappedStatement = configuration.getMapper().get(statement);
            //执行更新操作
            int row = simpleExecutor.update(configuration, mappedStatement, params);
            return row;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    @Override
    public int delete(String statement, Object params) {
        return update(statement, params);
    }

Executor实现具体的SQL执行:

 @Override
    public int update(Configuration configuration, MappedStatement mappedStatement, Object params) {
        try {
            // 执行jdbc代码
            // 1. 注册驱动,获取连接 直接从configuration获取DataSource拿到连接
            Connection connection = configuration.getDataSource().getConnection();
            // 2. 获取SQL语句
            // select * from user where id = #{id} and username=#{username}
            // jdbc 识别的占位符只能是?号,为什么使用#{}呢? 可以根据里面的参数名称 来找到传递参数实体类型的属性的值
            String sql = mappedStatement.getSql();
            //转换SQL语句 select * from user where id = ? and username=?
            //还需要对#{}的值进行解析存储,找到实体对象对应属性的值进行添加
            BoundSql boundSql = getBoundSql(sql);
            // 3. 获取预处理对象 preparedStatement,传递解析过后的sql
            PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
            //参数的全类名
            String parameterType = mappedStatement.getParameterType();
            //获取参数的Class
            if (parameterType != null) {
                Class<?> parameterClass = getClassType(parameterType);
                List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
                for (int i = 0; i < parameterMappingList.size(); i++) {
                    ParameterMapping parameterMapping = parameterMappingList.get(i);
                    String content = parameterMapping.getContent();
                    //反射根据参数名称 获取到实体对象中的属性值
                    Field field = parameterClass.getDeclaredField(content);
                    //暴力访问
                    field.setAccessible(true);
                    Object o = field.get(params);
                    //设置参数值
                    preparedStatement.setObject(i + 1, o);
                }
            }
            //执行SQL
            int row = preparedStatement.executeUpdate();
            return row;
        } catch (Exception throwables) {
            throwables.printStackTrace();
        }
        return 0;
    }

下一篇文章,根据持久层框架的设计思路,来分析Mybatis的源码就会更轻松易懂。