Mybatis的源码与流程解析

使用Mybatis需要配置好数据源与SqlSessionFactory,如果要使用事务则需额外配置事务管理器,我自己的小框架使用的是基于Java形式的配置,也可以使用XML文件形式配置。我把我自己的配置文件放上来,想尝试这种方式的可以借鉴下。


@Configuration
@MapperScan(value = "com.bob.mvc.mapper", markerInterface = BaseMapper.class)
public class DataAccessContextConfig {

    private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "lanboal";

    /**
     * MySQL的JDBC URL编写方式:jdbc:mysql://主机名称:连接端口/数据库的名称?参数=值
     * 避免中文乱码要指定useUnicode和characterEncoding
     * 执行数据库操作之前要在数据库管理系统上创建一个数据库,名字自己定,
     * 下面语句之前就要先创建project数据库
     */
    private static final String URL = "jdbc:mysql://localhost:3306/project?useUnicode=true&characterEncoding=UTF8&useSSL=false";

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(DRIVER_CLASS_NAME);
        //针对mysql获取字段注释
        dataSource.addConnectionProperty("useInformationSchema", "true");
        //dataSource.addConnectionProperty("remarksReporting","true");  针对oracle获取字段注释
        dataSource.setUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        dataSource.setMaxTotal(50);
        dataSource.setMinIdle(5);
        dataSource.setMaxIdle(10);
        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager txManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 配置MapperConfig
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();

        //当数据库集群时,配置多个数据源,通过设置不同的DatebaseId来区分数据源,同时sql语句中通过DatabaseId来指定匹配哪个数据源
        //configuration.setDatabaseId("Mysql-1");

        // 这个配置使全局的映射器启用或禁用缓存
        configuration.setCacheEnabled(true);

        // 允许 JDBC 支持生成的键,需要适合的驱动(如MySQL,SQL Server,Sybase ASE)。
        // 如果设置为 true 则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如 Derby)。
        // 但是在 Oracle 中一般不需要它,而且容易带来其它问题,比如对创建同义词DBLINK表插入时发生以下错误:
        // "ORA-22816: unsupported feature with RETURNING clause" 在 Oracle
        // 中应明确使用 selectKey 方法
        //configuration.setUseGeneratedKeys(false);

        // 配置默认的执行器:
        // SIMPLE :> SimpleExecutor  执行器没有什么特别之处;
        // REUSE :> ReuseExecutor 执行器重用预处理语句,在一个Service方法中多次执行SQL字符串一致的操作时,会复用Statement及Connection,
        // 也就是说不需要再预编译Statement,不需要重新通过DataSource生成Connection及释放Connection,能大大提高操纵数据库效率;
        // BATCH :> BatchExecutor 执行器重用语句和批量更新
        configuration.setDefaultExecutorType(ExecutorType.REUSE);
        // 全局启用或禁用延迟加载,禁用时所有关联对象都会即时加载
        configuration.setLazyLoadingEnabled(false);
        // 设置SQL语句执行超时时间缺省值,具体SQL语句仍可以单独设置
        configuration.setDefaultStatementTimeout(5000);

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfiguration(configuration);
        // 匹配多个 MapperConfig.xml, 使用mappingLocation属性
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/bob/mvc/mapper/*Mapper.xml"));
        return sqlSessionFactoryBean.getObject();
    }
}

配置Mybatis最主要的是配置SqlSessionFactory实例,当前配置基于Java形式,通过sqlSessionFactoryBean.getObject()方法生成SqlSessionFactory 实例,在此方法中会解析指定的Mybatis Mapper文件,将XML文件内的元素节点按类别解析生不同的java类对象,存入SqlSessionFactory的Configuration中。

public class XMLMapperBuilder extends BaseBuilder {
     ......
     private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap节点,生成ResultMap对象,存入Configuraion中
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析增删改查节点,生成MappedStatement对象,存入Configuraion中
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

}

如何解析,流程很繁琐,我就不一一列举了,感兴趣的可以就着XMLMapperBuilder 的方法自行查看。

解析Mybatis的Mapper文件,将生成的结果存入Conguration中,Conguration就相当于Myabtis的配置容器,Conguration又被SqlSessionFactory持有,当通过SqlSessionFactory生成SqlSession实例时,也会持有Conguration对象。

解析Mapper配置文件和扫描Mapper接口没有必然的先后顺序,这里只是先讲解解析,后讲解扫描。

通过@MapperScan注解,指定扫描的Mapper接口包。@MapperScan会引入MapperScannerRegistrar这个Bean,它会根据注解的属性相应的执行自己的逻辑。MapperScannerRegistrar扫描指定包下的所有接口,根据指定的规则对这些接口做过滤,针对这个接口注册BeanDefinition,如果注解上有自定义信息,这个将这些信息注入接口的BeanDefinition中,在通过这些BeanDefinition生成Bean实例时会从容器中寻找相应的Bean注入到接口的Bean中。这些接口的Bean实例是MapperFactoryBean对象(一个FactoryBean),真正注入Mapper对象时,实际注入的是通过MapperFactoryBean对象的getObject()方法生成的对象。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  private Class<T> mapperInterface;

  private boolean addToConfig = true;

  public MapperFactoryBean() {
    //intentionally empty 
  }

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  /**第2步
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

  /**第3步
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

}

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSession sqlSession;

  private boolean externalSqlSession;

  //第1步
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      //这步其实很关键,但是需要配合着事务来说,在下一篇博客中会重点讲解。
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

}

MapperFactoryBean对象在实例化时 :

  1. 第一步会调用其父类SqlSessionDaoSupport 的setSqlSessionFactory()方法, 当@MapperScan指定SqlSessionFactory或者SqlSessionTemplate的BeanName时,从Spring容器中获取相应名称的Bean;当未指定时,Mapper接口的BeanDefinition的autowireMode=AUTOWIRE_BY_TYPE,Spring通过BeanDefinition实例化Bean时,会从解析BeanDefinition内未赋值的非简单属性,从容器中获取SqlSessionFactory,SqlSessionTemplate类型的Bean注入其中,当你定义了相应类型的Bean就能赋值入Mapper接口生成的Bean中。属性的赋值使用setter方法。当赋值SqlSessionFactory时将sqlSession属性初始化为一个SqlSessionTemplate对象。
  2. 第二步会调用Configuration的addMapper()方法,这个方法会将当前接口的Class和一个MapperProxyFactory对象关联映射起来,间接存入Configuration中。
  3. 通过MapperFactoryBean的getObject()方法获取Mapper接口实际注入的对象,这个方法最终是调用MapperProxyFactory的newInstance(SqlSession)方法,
    先通过sqlSession及Mapper接口生成MapperProxy对象,MapperProxy是一个InvocationHandler实现类,最后通过JDK Proxy的newProxyInstance方法,以MapperProxy,Mapper接口,及ClassLoader为参数,为Mapper接口生成动态代理对象,这个对象就是实际被注入到Service层的Mapper对象,所以实际的Mybatis执行逻辑都在MapperProxy对象的invoke()方法中。
public class MapperProxyFactory<T> {

  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  ......
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

MapperProxy在执行时会先通过当前方法生成MapperMethod,在MapperMethod里会先通过此方法找到对应的Mapper.xml里的相应节点解析成的MappedStatement,接口名称对应namespace,方法名称对应节点名称,获取这个节点的sql类型,然后提取出当前方法的参数,然后通过sqlSession和方法参数执行相应的操作。

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
  ......
}

执行增删改查的哪一种由sql节点的类型确定,返回单个还是多个由方法的返回值类型确定,这两个因素确定后通过sqlSession的相应方法执行,获取结果。

这里的sqlSession实际上是一个DefaultSqlSession对象,SqlSessionTemplate持有一个sqlSession类型的属性,名称是sqlSessionProxy,自身调用方法均会传递给此属性执行,这个属性是通过JDK动态代理生成的,代理了DefaultSqlSession对象,DefaultSqlSession对象时每个线程独立的,当没有事务时立即通过sessionFactory实时生成一个,当有事务时可以复用此DefaultSqlSession对象。

DefaultSqlSession在执行时会先通过从Configuration中找到当前方法对应的MappedStatement对象,然后MappedStatement根据当前方法的参数,动态的生成BoundSql对象,基本是通过当前参数集合,与DynamicSqlSource的MixSqlNode相匹配(循环递归式),如果匹配上,就加上SqlNode里的sql字符串,匹配完之后就生成一个sql字符串,然后将里面的参数”#{}”替换为”?”,同时生成有序的List< ParameterMapping > ,这个顺序一定要和?的顺序相同。之后将替换过的sql字符串和参数集合放进一个BoundSql对象里返回。

当得到了BoundSql对象时,就可以去执行了,此时需要执行器对象,默认配置是SimpleExecutor实例。

一般的MappedStatement的StatementType是PREPARED,也就是说会有预编译的语句类型,每一个sql在执行前会预编译(若sql存在字符拼接或者不存在参数则不预编译),这样就能防止sql注入。

SimpleExecutor先用PreparedStatementHandler去预编译sql语句,生成Statement,如果当前方法配置了事务,且执行器配置为ReuserExexutor,则这个Statement可以实现复用,若没有则不能复用。
PreparedStatementHandler之后将参数设置到编译后的sql中,形成当前参数下的Statement。

最后PreparedStatementHandler执行此Statement,解析结果,将得到的结果返回。

关键点总结:

  1. 通过配置文件或者Configuration的配置实例化SqlSessionFactory。
  2. 在实例化SqlSessionFactory时解析Mapper.xml文件,按节点类型不同生成不同的Java对象存入Configuration中。
  3. 扫描指定包下的Mapper接口,注册BeanDefinition,最后这些Bean被注入到Service层时是通过JDK Proxy动态生成的接口代理类对象。MapperProxy是InvocationHandler,Mapper的执行逻辑都在其Invoke方法中。
  4. MapperProxy根据当前方法生成MapperMethod。
  5. MapperMethod在实例化时会确定当前方法的执行类型,时增删改查的哪种,然后提取参数,最后由SqlSession去执行此操作。
  6. SqlSession执行时,先找到当前方法对应的MapperStatement,然后由Executor去执行。
  7. Executor在执行时,先根据当前参数从MapperStatement生成BoundSql,然后由StatementHandler根据BoundSql生成Statement,MappedStatement的StatementType是PREPARED,也就是在Statement生成后会预编译sql语句,这样能防止sql注入。
  8. StatementHandler执行相应的Statement,解析结果,返回。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值