MyBatis源码分析(1)

1.手写持久层框架-IMybatis

1.1 JDBC操作数据库_问题分析

JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据

MyBatis源码分析(1)_第1张图片

代码示例:

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

1.2 JDBC问题分析&解决思路

剖开代码,逐个分析:

(1)加载驱动,获取链接:

MyBatis源码分析(1)_第2张图片

硬编码:原生的JDBC在执行SQL操作时,常常需要编写SQL语句以及与数据库连接相关的配置信息,这些配置信息通常以硬编码的方式直接写在代码中。这种做法被认为是硬编码,因为配置信息与代码紧密耦合在一起,无法在运行时进行动态修改。

  • 存在问题1:数据库配置信息存在硬编码问题

    优化思路:使用配置文件!

  • 存在问题2:频繁创建、释放数据库连接问题。

    优化思路:使用数据连接池

(2)定义sql、设置参数、执行查询:

MyBatis源码分析(1)_第3张图片

  • 存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。

    优化思路:使用配置文件!

(3)遍历查询结果集:

MyBatis源码分析(1)_第4张图片

  • 存在问题4:手动封装返回结果集,较为繁琐

    优化思路:使用Java反射、内省!

针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,

统一汇总:

存在问题 优化思路
数据库配置信息存在硬编码问题 使用配置文件
频繁创建、释放数据库连接问题 使用数据连接池
SQL语句、设置参数、获取结果集参数均存在硬编码问题 使用配置文件
手动封装返回结果集,较为繁琐 使用Java反射、内省

1.3 自定义持久层框架_思路分析

JDBC是个人作战,凡事都是亲力亲为,低效而高险,自己加载驱动,自己建连接,自己手动创建和释放数据库资源,处理事务和异常等,是一种相对原始且底层的数据库访问方式。虽然JDBC提供了直接与数据库交互的API,但它需要开发者编写大量冗余的代码来处理数据库连接、结果集的封装等,容易出现重复性代码和错误,使得开发变得繁琐且容易出错。

持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …

优化思路: 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。

使用JDBC和使用持久层框架区别:

MyBatis源码分析(1)_第5张图片

通过上图可以发现,拥有一套持久层框架是如此的舒适,我们仅仅需要干两件事:

  • 配置数据源(地址/数据名/用户名/密码)
  • 编写SQL与参数准备(SQL语句/参数类型/返回值类型)

框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:

  • 使用端(实际项目)
  • 持久层框架本身

以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:MyBatis源码分析(1)_第6张图片

核心接口/类重点说明:

分工协作 角色定位 类名定义
负责读取配置文件 资源辅助类 Resources
负责存储数据库连接信息 数据库资源类 Configuration
负责存储SQL映射定义、存储结果集映射定义 SQL与结果集资源类 MappedStatement
负责解析配置文件,创建会话工厂SqlSessionFactory 会话工厂构建者 SqlSessionFactoryBuilder
负责创建会话SqlSession 会话工厂 SqlSessionFactory
指派执行器Executor 会话 SqlSession
负责执行SQL (配合指定资源Mapped Statement) 执行器 Executor

正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。

如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。

项目使用端:

(1)调用框架API,除了引入自定义持久层框架的jar包

(2)提供两部分配置信息:1.sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径

​ 2.mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息

自定义框架本身:

1、加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。

创建Resource类,提供加载流方法:InputStream resourceAsStream(String path)

2、 创建两个javaBean(容器对象):存放配置文件解析出来的内容

Configuration核心配置类: 解析存储sqlMapConfig.xml里面的内容
MappedStatement 配置映射类:存储mapper.xml解析出来的内容

3、解析配置文件(使用dom4j) ,并创建SqlSession会话对象

创建类:SqlSessionFactoryBuilder方法:build(Inputstream in)
>使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
>创建SqlSessionFactory对象,生产sqlSession会话对象(工厂模式)

4、创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory

创建openSession()方法,生产sqlSession

5、创建SqlSession接口以及实现类DefaultSqlSession

定义对数据库的CRUD操作:
> selectList();
> selectOne();
> update();
> delete();

6、创建Executor接口以及实现类SimpleExecutor

创建query(Configuration configuration, MappedStatement mappedStatement, Object param)
实际执行的就是JDBCD代码。

基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:

MyBatis源码分析(1)_第7张图片

最终手写的持久层框架结构参考:

MyBatis源码分析(1)_第8张图片

1.4 自定义持久层框架_编码

    <properties>
        
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8maven.compiler.encoding>
        <java.version>8java.version>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

使用端

在使用端项目中创建配置配置文件

创建 sqlMapConfig.xml
<configuration>
    
    <dataSource>
        <property name="driverClassName" value="com.mysql.jdbc.Driver">property>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC">property>
        <property name="username" value="root">property>
        <property name="password" value="root">property>
    dataSource>

    
    <mappers>
        <mapper resource="mapper/UserMapper.xml">mapper>
        <mapper resource="mapper/ProductMapper.xml">mapper>
    mappers>

configuration>
编写mapper.xml

(此时namespace还不是全类名的形式,如:“com.xc.mapper.UserMapper”)

<mapper namespace="User">
    
   
    <select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">
        select * from user where id= #{id} and username= #{username}
    select>

  
    
    <select id="selectList" resultType="com.xc.pojo.User">
        select * from user
    select>
mapper>
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 + '\'' + '}';
  }
}

自定义框架本身

pom文件引入依赖

新建一个Maven工程在pom文件引入需要用到的依赖

    <properties>
        
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8maven.compiler.encoding>
        <java.version>8java.version>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.6version>
        dependency>

        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
            
            <scope>testscope>
        dependency>

        
        <dependency>
            <groupId>dom4jgroupId>
            <artifactId>dom4jartifactId>
            <version>1.6.1version>
        dependency>

        
        <dependency>
            <groupId>jaxengroupId>
            <artifactId>jaxenartifactId>
            <version>1.1.6version>
        dependency>


        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.21version>
        dependency>

        
        <dependency>
            <groupId>log4jgroupId>
            <artifactId>log4jartifactId>
            <version>1.2.17version>
        dependency>

    dependencies>

Resources

MyBatis源码分析(1)_第9张图片

Configuration

/**
 * @author xingchen
 * @description 核心配置类: 解析存储sqlMapConfig.xml里面的内容
 */
public class Configuration {

    // 数据源对象
    private DataSource dataSource;

    // key: statementId  value :封装好的MappedStatement对象
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();

    public DataSource getDataSource() {
        return dataSource;
    }

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

    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }

    public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
        this.mappedStatementMap = mappedStatementMap;
    }
}

MappedStatement

/**
 * @author xingchen
 * @description 配置映射类:存储mapper.xml解析出来的内容
 * mapper.xml中 
 * select * from user where id= #{id} and username= #{userName}
 * 
 */
public class MappedStatement {

    // 1.唯一标识 (statementId 由namespace.id组成)
    private String statementId;
    // 2.返回结果类型
    private String resultType;
    // 3.参数类型
    private String parameterType;
    // 4.要执行的sql语句
    private String sql;

    // 5.mapper代理:sqlCommandType 标签类型,如select
    private String sqlCommandType;

    public String getStatementId() {
        return statementId;
    }

    public void setStatementId(String statementId) {
        this.statementId = statementId;
    }

    public String getResultType() {
        return resultType;
    }

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

    public String getParameterType() {
        return parameterType;
    }

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

    public String getSql() {
        return sql;
    }

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

    public String getSqlCommandType() {
        return sqlCommandType;
    }

    public void setSqlCommandType(String sqlCommandType) {
        this.sqlCommandType = sqlCommandType;
    }
}

SqlSessionFactoryBuilder

MyBatis源码分析(1)_第10张图片

XMLMapperBuilder

/**
 * @author xingchen
 * @description
 */
public class XMLMapperBuilder {

    private Configuration configuration;

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

    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);

        Element rootElement = document.getRootElement();

        List<Element> selectList = rootElement.selectNodes("//select");

        /**
         *     
         */
        String namespace = rootElement.attributeValue("namespace");
        for (Element element : selectList) { //id的值
            //解析标签,封装mappedStatement
            String id = element.attributeValue("id");

            //返回结果class
            String resultType = element.attributeValue("resultType");
            //输入参数class
            String parameterType = element.attributeValue("parameterType");
            //sql语句
            String sql = element.getTextTrim();
            //statementId
            String statementId = namespace + "." + id;
            //封装 mappedStatement
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setStatementId(statementId);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sql);
            mappedStatement.setSqlCommandType("select");

            //存到Configuration里面的map集合
            configuration.getMappedStatementMap().put(statementId, mappedStatement);
        }
    }
}

sqlSessionFactory 接口及DefaultSqlSessionFactory 实现类

/**
 * @author xingchen
 * @description
 */
public interface SqlSessionFactory {
    
    /**
     * 生产sqlSession :封装着与数据库交互的方法
     */
    SqlSession openSession();
}

/**
 * @author xingchen
 * @description
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

    /**
     * (1)创建执行器对象 (2)创建SqlSession对象
     *
     * @return
     */
    @Override
    public SqlSession openSession() {
        // 执行器创建出来
        Executor executor = new SimpleExecutor();
        return new DefaultSqlSession(configuration, executor);
    }
}

sqlSession 接口及 DefaultSqlSession 实现类(初步实现)

/**
 * @author xingchen
 * @description
 */
public interface SqlSession {

    /**
     * 查询所有的方法 select * from user where username like '%aaa%' and sex = ''
     * 参数1:唯一标识
     * 参数2:入参
     */
    public <E> List<E> selectList(String statementId,Object parameter) throws Exception;


    /**
     * 查询单个的方法
     */
    public <T> T selectOne(String statementId,Object parameter) throws Exception;
}
/**
 * @author xingchen
 * @description
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private Executor executor;

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

    @Override                    // user.selectList      1 tom user
    public <E> List<E> selectList(String statementId, Object params) throws Exception {

        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        // 将查询操作委派给底层的执行器
        List<E> list = executor.query(configuration,mappedStatement,params);

        return list;
    }

    @Override
    public <T> T selectOne(String statementId, Object params) throws Exception {
        List<Object> list = this.selectList(statementId, params);
        if(list.size() == 1){
            return (T) list.get(0);
        }else if(list.size() > 1){
            throw new RuntimeException("返回结果过多");
        }else {
            return null;
        }
    }
} 

Executor

/**
 * @author xingchen
 * @description
 */
public interface Executor {

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;
}

SimpleExecutor

/**
 * @author xingchen
 * @description
 */
public class SimpleExecutor implements Executor {

    /**
     * 真正执行底层的JDBC操作
     *
     * @param configuration
     * @param mappedStatement
     * @param param
     * @param 
     * @return
     */
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {

        //1.获取数据库链接
        Connection connection = configuration.getDataSource().getConnection();

        //2.获取预编译对象 prepareStatement
        //select * from user where id= #{id} and username= #{userName}
        //select * from user where id= ? and username= ?
        //获取出来的sql还需要处理 转换占位符: (1) #{}转换成? (2) #{}里面的值要保存下来
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        String finalSql = boundSql.getFinalSql();
        PreparedStatement preparedStatement = connection.prepareStatement(finalSql);

        //3.设置参数
        //问题1:param类型是不确定? map|user|String|product
        String parameterType = mappedStatement.getParameterType();
        if (parameterType != null) {
            //user
            Class<?> parameterTypeClass = Class.forName(parameterType);
            //问题2:该把user对象里面哪个属性赋值给哪个占位符呢
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                // #{} id or username
                String fileName = parameterMapping.getContent();
                //通过字段名获取属性对象
                Field declaredField = parameterTypeClass.getDeclaredField(fileName);
                // 暴力访问
                declaredField.setAccessible(true);
                //获取到参数的值
                Object fileValue = declaredField.get(param);

                preparedStatement.setObject(i + 1, fileValue);
            }
        }

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

        //5.封装返回结果集
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = Class.forName(resultType);
        ResultSetMetaData metaData = resultSet.getMetaData();

        List<E> list = new ArrayList<>();

        while (resultSet.next()) {
            Object o = resultTypeClass.getConstructor().newInstance();
            //比如说此处查询有id username字段
            // 整个for循环的遍历针对表中的属性依次处理 id对象封装到user对象的id属性值,username封装到user对象的username属性值
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                String columnName = metaData.getColumnName(i);
                Object columnValue = resultSet.getObject(columnName);
                //根据映射关系进行封装
                Field declaredField = resultTypeClass.getDeclaredField(columnName);
                declaredField.setAccessible(true);
                declaredField.set(o, columnValue);

            }
            list.add((E) o);
        }
        return list;
    }

    /**
     * (1) #{}转换成? (2) #{}里面的值要保存下来
     *
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理器: 配合标记解析器完成占位符的解析
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //标记解析器
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //替换成了?的sql
        String finalSql = genericTokenParser.parse(sql);
        //#{}里面的值集合封装
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();

        return new BoundSql(finalSql, parameterMappings);

    }
}

BoundSql

/**
 * @author xingchen
 * @description
 */
public class BoundSql {

    //解析过后的sql语句
    private String finalSql;
    //解析出来的参数
    private List<ParameterMapping> parameterMappings;

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

    public String getFinalSql() {
        return finalSql;
    }

    public void setFinalSql(String finalSql) {
        this.finalSql = finalSql;
    }

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

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

BoundSql

/**
 * @author xingchen
 * @description
 */
public class BoundSql {

    //解析过后的sql语句
    private String finalSql;
    //解析出来的参数
    private List<ParameterMapping> parameterMappings;

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

    public String getFinalSql() {
        return finalSql;
    }

    public void setFinalSql(String finalSql) {
        this.finalSql = finalSql;
    }

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

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

需要用到的工具类

GenericTokenParser

用于解析 ${}#{} 这样的占位符。在 MyBatis 的 SQL 语句中,我们可以使用 ${} 来直接替换变量的值,而 #{} 则会被解析为预编译的占位符。这个类通过传入的 openTokencloseToken 参数来标识占位符的开始标记和结束标记,然后通过传入的 TokenHandler 参数来处理解析后的占位符的值。GenericTokenParserparse 方法会遍历 SQL 语句中的占位符,将占位符解析为实际的参数,并通过 TokenHandler 处理后返回最终的 SQL 语句。

/**
 * Copyright 2009-2017 the original author or authors.
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.xc.utils; /** * @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

用于存储 SQL 语句中的参数映射信息。在 MyBatis 解析占位符时,会将占位符的内容作为 ParameterMapping 的实例进行存储,以便后续处理。

package com.xc.utils;

public class ParameterMapping {
    // id || username
    private String content;

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

    public String getContent() {
        return content;
    }

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

ParameterMappingTokenHandler

实现了 TokenHandler 接口,主要用于处理解析后的占位符。在 handleToken 方法中,它会将解析后的占位符内容构建成 ParameterMapping 实例,并添加到 parameterMappings 列表中。这样,parameterMappings 列表中就存储了 SQL 语句中的所有参数映射信息。

package com.xc.utils;

import java.util.ArrayList;
import java.util.List;

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

这个接口定义了一个 handleToken 方法,用于处理解析后的占位符。具体的处理逻辑由实现 TokenHandler 接口的类来实现。

/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.xc.utils;

/**
 * @author Clinton Begin
 */
public interface TokenHandler {
  String handleToken(String content);
}

综合起来,这些工具类在 MyBatis 中的配合使用,可以将 SQL 语句中的占位符解析为真正的参数,并存储参数映射信息,方便后续的数据库操作。通过这种方式,MyBatis 可以实现动态 SQL 的功能,使得 SQL 语句更加灵活和易于维护。

1.5 自定义持久层框架_优化

通过上述的自定义框架,已经解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,手动封装返回结果集等问题,但是现在继续来分析刚刚完成的自定义框架代码,还有一些问题

问题如下:

  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)

  • dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码,如下所示

        @Test
        public void test1() throws Exception {
            //根据配置文件,加载成字节输入流
            InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");
    
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            SqlSession sqlSession = sqlSessionFactory.openSession();
    
            User user1 = new User();
            user1.setUsername("tom");
            user1.setId(1);
            List<User> list1 = sqlSession.selectList("user.selectList");
            List<User> list2 = sqlSession.selectList("user.selectOne", user1);
            System.out.println(list1);
            System.out.println(list2);
    
        }
    

解决:使用代理模式来创建接口的代理对象

    @Test
    public void test2() throws Exception {
        //根据配置文件,加载成字节输入流
        InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        User user1 = new User();
        user1.setUsername("tom");
        user1.setId(1);

        // proxy 代理对象 jdk动态代理生成的!
        //代理对象调用接口中的任意方法都会被拦截执行底层的invoke()
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> allUser = mapper.findAllUser();

        User userObj = mapper.findUserByCondition(user1);

        System.out.println(userObj);
        System.out.println(allUser);

    }

用户端UserMapper.java更改

public interface UserMapper {

    /**
     * 查询所有
     */

    List<User> findAllUser();


    /**
     * 查询单个
     */
    User findUserByCondition(User user);
}

UserMapper.xml

<mapper namespace="com.xc.mapper.UserMapper">


    

    
    <select id="findAllUser" resultType="com.xc.pojo.User">
        select * from user
    select>

    

    
    <select id="findUserByCondition" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">
        select * from user where id= #{id} and username= #{username}
    select>

mapper>

在sqlSession中添加getMappper()方法

public interface SqlSession {

    /**
     * 查询所有的方法 select * from user where username like '%aaa%' and sex = ''
     * 参数1:唯一标识
     * 参数2:入参
     */
     List selectList(String statementId) throws Exception;

     List selectList(String statementId, Object param) throws Exception;

    /**
     * 根据条件查询
     */
     T selectOne(String statementId, Object param) throws Exception;

    /**
     * mapper代理方式
     */
     T getMapper(Class mapperClass);
}

DefaultSqlSession实现类

/**
 * @author xingchen
 * @description
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private Executor executor;


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


    @Override
    public <E> List<E> selectList(String statementId) throws Exception {
        return selectList(statementId, null);
    }

    @Override
    public <E> List<E> selectList(String statementId, Object param) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<E> list = executor.query(configuration, mappedStatement, param);

        return list;
    }

    @Override
    public <T> T selectOne(String statementId, Object param) throws Exception {
        List<Object> list = this.selectList(statementId, param);
        if (list.size() == 1) {
            return (T) list.get(0);
        } else if (list.size() > 1) {
            throw new RuntimeException("返回结果过多");
        }

        return null;
    }

    /**
     * mapperClass接口,使用动态代理生成代理对象 jdk动态代理 cglib动态代理
     *
     * @param mapperClass
     * @param 
     * @return
     */
    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass},
                new InvocationHandler() {

                    /**
                     * proxy:代理对象 method:当前被调用的方法对象 例:findAllUser args : 查询参数
                     * @param proxy
                     * @param method
                     * @param args
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 查询数据库 增删改查 执行JDBC
                        //调用像selectOne或者selectList

                        //问题1:能不能获取到statementId
                        // 当前 statementId = 类名.方法名
                        //获取当前对象的类名 全路径,和UserMapper.xml中的namespace对应了
                        String className = method.getDeclaringClass().getName();
                        //获取正在调用的方法名
                        String methodName = method.getName();
                        String statementId = className + "." + methodName;
                        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
                        String sqlCommandType = mappedStatement.getSqlCommandType();

                        //问题2:怎么知道调用增删改查的哪个方法?
                        //解决:根据sqlCommandType判断
                        switch (sqlCommandType) {
                            case "select":
                                //问题3:现在是调用selectOne还是SelectList
                                //解决:根据返回值类型
                                Class<?> returnType = method.getReturnType();
                                boolean assignableFrom = Collection.class.isAssignableFrom(returnType);
                                if (assignableFrom) {
                                    //查询所有
                                    if (args != null) {
                                        //判断是否有查询参数
                                        return selectList(statementId, args[0]);
                                    } else {
                                        return selectList(statementId);
                                    }
                                }
                                return selectOne(statementId, args[0]);
                            case "update":
                                //TODO 更新操作
                                break;
                            case "delete":
                                //TODO 更新操作
                                break;
                            case "insert":
                                //TODO 更新操作
                                break;
                        }

                        return null;
                    }
                }

        );
        return (T) proxyInstance;
    }
}

1.6 编写测试代码

MyBatis源码分析(1)_第11张图片

1.6.1执行mapper.findAllUser()方法

MyBatis源码分析(1)_第12张图片

MyBatis源码分析(1)_第13张图片

MyBatis源码分析(1)_第14张图片

MyBatis源码分析(1)_第15张图片

MyBatis源码分析(1)_第16张图片

1.6.2执行mapper.findUserByCondition()方法

MyBatis源码分析(1)_第17张图片

MyBatis源码分析(1)_第18张图片

MyBatis源码分析(1)_第19张图片

MyBatis源码分析(1)_第20张图片

1.6.3执行结果

MyBatis源码分析(1)_第21张图片

2. MyBatis架构原理&主要组件

2.1 MyBatis的架构设计

MyBatis源码分析(1)_第22张图片

mybatis架构四层作用是什么呢?

  • Api接口层:提供API 增加、删除、修改、查询等接口,通过API接口对数据库进行操作。
  • 数据处理层:主要负责SQL的 查询、解析、执行以及结果映射的处理,主要作用解析sql根据调用请求完成一次数据库操作.
  • 框架支撑层:负责通用基础服务支撑,包含事务管理、连接池管理、缓存管理等共用组件的封装,为上层提供基础服务支撑.
  • 引导层:引导层是配置和启动MyBatis 配置信息的方式

2.2 MyBatis主要组件及其相互关系

组件关系如下图所示:

MyBatis源码分析(1)_第23张图片

组件介绍:

  • SqlSession:是Mybatis对外暴露的核心API,提供了对数据库的DRUD操作接口。

  • Executor:执行器,由SqlSession调用,负责数据库操作以及Mybatis两级缓存的维护

  • StatementHandler:封装了JDBC Statement操作,负责对Statement的操作,例

  • PrepareStatement参数的设置以及结果集的处理。

  • ResultSetHandler:是StatementHandler内部一个组件,主要负责对ResultSet结果集的处理,封装成目标对象返回

  • TypeHandler:用于Java类型与JDBC类型之间的数据转换,ParameterHandler和ResultSetHandler会分别使用到它的类型转换功能

  • MappedStatement:是对Mapper配置文件或Mapper接口方法上通过注解申明SQL的封装

  • Configuration:Mybatis所有配置都统一由Configuration进行管理,内部由具体对象分别管理各自的小功能模块

你可能感兴趣的:(mybatis,架构)