学习mybatis源码总结

源码架构分析

在这里插入图片描述
在这里插入图片描述

Mapper映射器的配置

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {//packge优先
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          //表示 resource 值不为 null,且url 值和 mapperClass 值都为null。
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
            //表示 url 值不为 null,且 resource 值和 mapperClass 值都为null。
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
            //表示 mapperClass 值不为 null,且 resource 值和 url 值都为null。
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
            //表示如果三个都为null或者都不为null,或者有两个不为null,都会抛出异常。
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

document type definition 文档类型定义
mapper子标签必须在package标签前面。实际应用中,package标签使用的比较少,这里就不贴源码对package进行分析了(需要注意的是,如果两个子标签同时存在,前面解析完mapper标签后,存在相同的接口名,会抛出异常)

数据源的配置
实质上,Mybatis-config.xml这个主配置文件就代表我们如何连接数据库源,那么哪个负责读取xml就是哪个源码负责连接数据库源

显然这个方法是读取environments标签用的

private void environmentsElement(XNode context) throws Exception {
        //如果<environments>标签不为null
        我们可以不在 mybatis-configuration.xml 文件中配置标签,这是为了和spring整合时,在spring容器中进行配置。
        if (context != null) {
            //如果 environment 值为 null
            //获取中的default属性值,注意第 5 行 首先判断 environment == null 。因为我们可以配置多个环境,也就是连接多个数据库。
            if (environment == null) {
                //获取<environments default="属性值">中的default属性值
                environment = context.getStringAttribute("default");
            }
            //遍历<environments />标签中的子标签<environment />
            for (XNode child : context.getChildren()) {
                //获取<environment id="属性值">中的id属性值
                String id = child.getStringAttribute("id");
                //遍历所有<environment>的时候一次判断相应的id是否是default设置的值
                if (isSpecifiedEnvironment(id)) {
                    //获取配置的事务管理器
                    TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                    //获取配置的数据源信息
                    DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    Environment.Builder environmentBuilder = new Environment.Builder(id)
                            .transactionFactory(txFactory)
                            .dataSource(dataSource);
                    configuration.setEnvironment(environmentBuilder.build());
                }
            }
        }
    }

    private boolean isSpecifiedEnvironment(String id) {
        if (environment == null) {
            throw new BuilderException("No environment specified.");
        } else if (id == null) {
            throw new BuilderException("Environment requires an id attribute.");
        } else if (environment.equals(id)) {
            return true;
        }
        return false;
    }

而在mybatis源码内也是需要通过configxml文件来生产相应的数据源工厂的其实就是我们配置的type属性

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

mybatis中的数据源

mybatis连接池为我们提供了3种方式的配置: ​

POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现 ​
UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。 ​
JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。 ​
注意:如果不是web或者maven的war工程,JNDI是不能使用的。

在mybatis中配置数据源

POOLED数据源的配置方式

<dataSource type="POOLED">
       <property name="driver" value="com.mysql.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
       <property name="username" value="root"/>
       <property name="password" value="root"/>
 </dataSource>

UNPOOLED数据源的配置方式

<dataSource type="UNPOOLED">
           <property name="driver" value="com.mysql.jdbc.Driver"/>
           <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
           <property name="username" value="root"/>
           <property name="password" value="root"/>
</dataSource>

JNDI数据源的配置方式

将数据源的配置文件 context.xml 放到工程的webapp/META-INF/下

context.xml

<Resource 
  name="jdbc/mybatis"                        数据源的名称
  type="javax.sql.DataSource"                        数据源类型
  auth="Container"                                数据源提供者
  maxActive="20"                                    最大活动数
  maxWait="10000"                                    最大等待时间
  maxIdle="5"                                        最大空闲数
  username="root"                                    用户名
  password="1234"                                    密码
  driverClassName="com.mysql.jdbc.Driver"            驱动类
  url="jdbc:mysql://localhost:3306/mybatis"    连接url字符串
/>

然后在mybatis的主配置文件中配置数据源,其中前缀"java:comp/env/ "是固定的,后缀 “jdbc/mybatis” 是在context.xml中取的JNDI数据源名称

<dataSource type="JNDI">
       <property name="data_source" value="java:comp/env/jdbc/mybatis"/>
</dataSource>

Mybatis访问数据库步骤

1.读取配置文件(配置文件要包含数据库连接信息以及mapper文件地址)
2.创建SqlSessionFactoryBuild对象
3.通过SqlSessionFactoryBuild创建SqlSessionFactory工厂

private static SqlSessionFactory getSqlSessionFactory() {
        String resource = "mybatis/mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);//读取配置文件
        } catch (IOException e) {
            e.printStackTrace();
        }
        //创建SqlSessionFactoryBuild对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        return sqlSessionFactory;
}

4.通过SqlSessionFactory创建SqlSession对象
SqlSessionFactory的功能就是获取SqlSession,默认实现类DefaultSqlSession,DefaultSqlSession可以执行curd,也可以通过获取mapper来执行curd DefaultSqlSession是直接执行sql操作数据库,mapper是通过接口方法(实际上还是要映射sql) sqlSession可以指定事务隔离级别:TransactionIsolationLevel:

    NONE(0),
    READ_COMMITTED(2),
    READ_UNCOMMITTED(1),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

DefaultSqlSession的属性和方法

  private Configuration configuration;
    private Executor executor;
    private boolean autoCommit;
    private boolean dirty;
    private List<Cursor<?>> cursorList;
 
    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }
 
    }
 
    public int update(String statement, Object parameter) {
        int var4;
        try {
            this.dirty = true;
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            var4 = this.executor.update(ms, this.wrapCollection(parameter));
        } catch (Exception var8) {
            throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
        } finally {
            ErrorContext.instance().reset();
        }
 
        return var4;
    }
 
   public void commit(boolean force) {
        try {
            this.executor.commit(this.isCommitOrRollbackRequired(force));
            this.dirty = false;
        } catch (Exception var6) {
            throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
        } finally {
            ErrorContext.instance().reset();
        }
 
    }
 
   public <T> T getMapper(Class<T> type) {
        return this.configuration.getMapper(type, this);
}

5.为DAO接口生成代理类

6.动态代理回调SqlSession某方法

  1. SqlSession将要操作的方法转发给Executor(并组装好cacheKey)(paramHandle运用插件的原理)
    Configuration是对mybatis-config.xml解析后的封装,配置文件的节点对应Configuration的属性
    例如:<environment 对应 protected Environment environment;

注意Executor是在SqlSessionFactory中就获取的:Executor executor = this.configuration.newExecutor(tx, execType);
实际上调用的configuration中的方法:

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
 
        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }
 
        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
}

根据参数生成对应的executor的实现 看看抽象类BaseExecutor的方法:

看看其中的SimpleExecutor:还是调用的configuration的方法
8.去查询cacheKey是否命中,如果命中直接返回,如果没有命中

9.Executor交给StatementHandler去执行(里面有个statement.execute(sql)真正执行sql的操作-JDBC访问数据库操作数据)代理模式

  1. Executor通过反射将数据封装POJO返回给sqlsession(resultParamHandle运用插件的原理)

11.把数据返回给调用者
处理sql相关的类 MappedStatement,获取BoundSql

  public BoundSql getBoundSql(Object parameterObject) {
        BoundSql boundSql = this.sqlSource.getBoundSql(parameterObject);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(this.configuration, boundSql.getSql(), this.parameterMap.getParameterMappings(), parameterObject);
        }
 
        Iterator i$ = boundSql.getParameterMappings().iterator();
 
        while(i$.hasNext()) {
            ParameterMapping pm = (ParameterMapping)i$.next();
            String rmId = pm.getResultMapId();
            if (rmId != null) {
                ResultMap rm = this.configuration.getResultMap(rmId);
                if (rm != null) {
                    this.hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }
 
        return boundSql;
}

BoundSql,获取执行的sql
    private String sql;
    private List<ParameterMapping> parameterMappings;
    private Object parameterObject;
    private Map<String, Object> additionalParameters;
    private MetaObject metaParameters;
BaseStatementHandler,获取Statement
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        ErrorContext.instance().sql(this.boundSql.getSql());
        Statement statement = null;
 
        try {
            statement = this.instantiateStatement(connection);
            this.setStatementTimeout(statement, transactionTimeout);
            this.setFetchSize(statement);
            return statement;
        } catch (SQLException var5) {
            this.closeStatement(statement);
            throw var5;
        } catch (Exception var6) {
            this.closeStatement(statement);
            throw new ExecutorException("Error preparing statement.  Cause: " + var6, var6);
        }
    }
SimpleStatementHandler,获取Statement
   protected Statement instantiateStatement(Connection connection) throws SQLException {
        return this.mappedStatement.getResultSetType() != null ? connection.createStatement(this.mappedStatement.getResultSetType().getValue(), 1007) : connection.createStatement();
    }
 
 
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        String sql = this.boundSql.getSql();
        statement.execute(sql);
        return this.resultSetHandler.handleResultSets(statement);
    }

dafulatSqlSession->getMapper方法->mapperRegistry.getMapper->mapperProxyFactory.newInstance->Proxy.newProxyInstance(生成代理对象)

MapperProxy->都会去执行invoke方法->invoke里面都会执行mapperMethod.execute(this.sqlSession, args)

插件原理

Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

在 Spring 中使用 MyBatis

数据源配置 到 Spring 配置文件中。配置完数据源,接下来配置 SqlSessionFactory。再接下来是配置 MapperScannerConfigurer,这个类用于 扫描某个包下的数据访问接口,并将这些接口注册到 Spring 容器中。这样,我们就可以在其 他的 bean 中注入 Dao 接口的实现类,无需再从 SqlSession 中获取接口实现类

Mybatis分页插件安全性问题:

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。

 //代码不安全
    PageHelper.startPage(1,10);
    List<Object> list;
    if(param != null){
        list = Mapper.selectIf(param);
    } else {
        list = Lists.newArrayList();
    }
    //这种情况下由于param 存在nul1 的情况,就会导致Pagelelper生产了-一个分页参数,
    // 但是没有被消费,这个参数就会- -直保留在这个线程上。当这个线程再次被使用时,
    // 就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
    //代码安全
    List<Object> list;
    if(param != null){
        PageHelper.startPage(1,10);
        list = Mapper.selectIf(param);
    } else {
        list = Lists.newArrayList();
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jsxllht

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

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

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

打赏作者

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

抵扣说明:

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

余额充值