MyBatis配置原理

MyBatis配置原理

如何整合Spring与Mybatis?
  1. 初始化spring环境。(AppConfig.class)
  2. 创建SqlSessionFactory。(注入数据源)
  3. @MapperScan(com.bafan.spring.mybatis.mapper)。(Mapper接口的扫描路径)
  4. TDao。(Mapper接口)
@Configuration
@ComponentScan("com.bafan.spring.mybatis")
@MapperScan("com.bafan.spring.mybatis.mapper")
public class AppConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }

    //配置数据源
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUrl("jdbc:mysql://10.70.6.34:3306/finance_mall");
        driverManagerDataSource.setUsername("pay");
        driverManagerDataSource.setPassword("pay123");
        return driverManagerDataSource;
    }  
}

//Mapper 
public interface TDao {
    @Select("select * from BankInfo limit 10")
    public List<Map<String, Object>> list();
}

@Service
public class IndexService {

    @Autowired
    private TDao tDao;

    public List<Map<String, Object>> getList() {
        return tDao.list();
    }
  
}

public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println(ac.getBean(IndexService.class).getList());
}
/**
FactoryBean是一个特殊的bean,里面有三个方法
getObject():返回一个你自己定义的bean,在获取它的时候,你可以自己随意处理,例如做个代理之类的
getObjectType():在spring容器初始化的时候,如果你将FactoryBean注入到spring容器中,那么会初始化这个FactoryBean,在当你想要获取到这个FactoryBean的时候,会调用getObjectType()这个方法,这个方法返回一个Class,假如getObjectType()返回的是FactoryService,在获取的时候通过名字"factoryService",判断相等,则会去调用getObject()方法,也即是factoryService这个类是你自己在getObject()中按照你的处理方式返回的类,如果传的是"&factoryService",那么判断就不相等了,返回则是FactoryBean这个类。
isSingleton():是否是单例模式,不重写的话默认就是true。
*/
public interface FactoryBean<T> {
    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

Mybatis到底是怎么能访问到数据库的?为什么通过tDao.list()就能获取到结果呢?tDao本来是一个接口,也没有实现类,为什么能够属性注入到IndexService中呢?

带着这几个问题和上面对FactoryBean的描述来看下Mybatis是怎么做的。

首先看一个类MapperFactoryBean,它继承了FactoryBean

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    
  //需要被代理的类(这里传入的就是Mapper类,在例子中对应着TDao的接口类)
  private Class<T> mapperInterface;
    
  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  
  public void setMapperInterface(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  
  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();

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

    //在初始化过程中,每个Mapper接口在初始化的时候都会调用到这里
    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        //按照Mapper接口的初始化顺序将每个接口顺序放到configuration中
        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();
      }
    }
  }
  
  //实际上要返回的代理类,这里把Mapper传进去,用SqlSession的getMapper方法来对这个类进行代理
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }
  
  @Override
  public Class<T> getObjectType() {
    return this.mapperInterface;
  }

}

通过MapperFactoryBean这个类可以看出,Mybatis会将我们的Mapper接口传到这个类中,并对它进行了代理,然后,在这个类被自动注入的时候,返回这个类的代理类,然后,通过这个代理类来调用操作数据库的方法。

那么有下面两个问题:

  1. 代理类都做了什么?
  2. Spring是怎么初始化这个类的?也可以说是MapperScan这个注解做了什么?
//代理类做了什么?找到这个getMapper的实现类DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
  //上段代码中的Configuration
  private final Configuration configuration;
  
  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }
}

//我们沿着这个方法一直点下去,最终会走到MapperProxyFactory的newInstance方法
public class MapperProxyFactory<T> {

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

}

//我们看下mapperProxy做了什么
public class MapperProxy<T> implements InvocationHandler, Serializable {

  //invoke方法
  @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);
    //实际去执行的方法,点到execute里面去
    return mapperMethod.execute(sqlSession, args);
  }
  
}

//这里就可以很清楚的看到,Mybatis底层在执行sql的时候,会根据Mapper接口中我们自定义的方法来判断怎么执行(insert还是select?返回值的类型?...在这里会自动的判断出你要怎么来执行这条sql语句)
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional() &&
              (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}

//再说Spring是怎么初始化MapperFactoryBean这个类的
//要想把一个类在Spring初始化的时候方法容器中,除了在这个类上添加类似于@Controller之类的注解,还有三种方式
//1.在Spring的AppCofig类中添加,@Configuration @Bean,这种方式相当于要每写一个Mapper接口,就要在配置文件中写一个这样的@Bean,显然Mybatis是不会这么做的。
//2.beanFactory.registerSingleton(直接放到工厂里),调用方式如下,在AnnotationConfigApplicationContext调用refresh()之前,手动将代理类放到Spring工厂中,显然这种方式也不行,理由和第一种方式一样
TDao tDao = (TDao)SqlSession.queryMapper(TDao.class);
ConfigurableListableBeanFactory beanFactory = ac.getBeanFactory();
beanFactory.registerSingleton("tDao", tDao);
//3.实现ImportBeanDefinitionRegistrar,创建一个BeanDefinition放到容器中。Mybatis使用的就是这种方法。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
  ...
}
//MapperScan中的MapperScannerRegistrar方法,实现了ImportBeanDefinitionRegistrar接口
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
  //需要重写这个方法,这个方法会传入AnnotationMetadata(AppConfig上的注解信息,所以能拿到MapperScan中配置的路径,也就能拿到这个路径下的所有Mapper接口,就可以做代理了)和BeanDefinitionRegistry(生成一个BeanDefinitionBuilder,再通过registry将BeanDefinitionBuilder注入到spring中)
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
    }
  }
  //注入到BeanDefinitionBuilder中的一个属性,就是这个MapperFactoryBean
  Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
    }
}

最后再说明一下在AppConfig中需要配置的两个类

//SqlSessionFactory的配置,在factoryBean中注入一个数据源就好,我们看下SqlSessionFactoryBean的getObject()方法
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
  factoryBean.setDataSource(dataSource);
  return factoryBean.getObject();
}

//数据源的配置
@Bean
public DataSource dataSource() {
  DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
  driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
  driverManagerDataSource.setUrl("jdbc:mysql://10.70.6.34:3306/finance_mall");
  driverManagerDataSource.setUsername("pay");
  driverManagerDataSource.setPassword("pay123");
  return driverManagerDataSource;
}

//也就是说,SqlSessionFactory在放到Spring中之前,要调用afterPropertiesSet()方法
@Override
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    //这个里面会初始化很多东西,比如要调用xml文件、解析我们配置的xml文件的地址、连接数据库的配置等
    afterPropertiesSet();
  }

  return this.sqlSessionFactory;
}

Mybatis就是按照上述的方法,让我们能够只通过一个接口就可以直接调用到数据库。

我们自己也可以模拟Mybatis的方式写一个类似于这样的框架。

1.创建一个FactoryBean,与MapperFactoryBean功能类似

public class BafanFactoryBean implements FactoryBean {

    Class bafanMapperInterface;

    public Object getObject() throws Exception {
        Object o = BafanSession.queryMapper(bafanMapperInterface);
        return o;
    }

    public Class<?> getObjectType() {
        return bafanMapperInterface;
    }

    public boolean isSingleton() {
        return true;
    }

    public void setBafanMapperInterface(Class bafanMapperInterface) {
        this.bafanMapperInterface = bafanMapperInterface;
    }
}

2.创建一个SqlSession,用来对Mapper接口进行代理

public class BafanSession {

    public static Object queryMapper(Class clazz) {

        Class[] classes = new Class[]{clazz};
        //jdk代理
        Object proxy = Proxy.newProxyInstance(BafanSession.class.getClassLoader(), classes, new BafanInvocationHandler());
        return proxy;
    }

}

3.创建代理类BafanInvocationHandler,用来模拟Mybatis执行操作数据库

public class BafanInvocationHandler implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //模拟连接DB
        System.out.println("conn db");
        //获取到Select注解
        Select annotation = method.getAnnotation(Select.class);
        if (annotation != null) {
            //打印Select注解里面的value值
            System.out.println(annotation.value()[0]);
        }
        //重写toString方法
        if (method.getName().equals("toString")) {
            return proxy.getClass().getInterfaces()[0].getName();
        }
        return null;
    }

}

4.将BafanFactoryBean放到Spring容器中

//实现ImportBeanDefinitionRegistrar
public class BafanBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //创建BeanDefinitionBuilder,传入BafanFactoryBean
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BafanFactoryBean.class);
        //将tDao生成的BeanDefinition放到Spring中
        AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.getPropertyValues().add("bafanMapperInterface", "com.bafan.spring.mybatis.mapper.TDao");
        registry.registerBeanDefinition("tDao", beanDefinition);
    }
}

5.BafanScan注解

@Retention(RetentionPolicy.RUNTIME)
//注入第4步中的BafanBeanDefinitionRegistrar
@Import(BafanBeanDefinitionRegistrar.class)
public @interface BafanScan {
    String value();
}

6.配置及使用

@Configuration
@ComponentScan("com.bafan.spring.mybatis")
@BafanScan("com.bafan.spring.mybatis.mapper")
public class AppConfig {
}

/**
 * 如何模拟,手写一个类似于mybatis的框架,mybatis是怎么做的
 * 1. 创建一个SqlSession(为你的Mapper接口创建一个代理类)---------BafanSession
 * 2. 代理类里面做了什么(BafanInvocationHandler)(连接DB、获取注解或者说要执行的sql的信息)
 * 3. 如何把我们刚才返回的代理类放到放到spring中去?
 * (1. beanFactory.registerSingleton(直接放到工厂里)
 *  2. @Configuration @Bean
 *  3. 实现ImportBeanDefinitionRegistrar,把注入到BeanDefinition里面)
 * mybatis使用的是第三种方式
 * 4. mybatis如何批量一次性把这些类都注入进去呢?
 * FactoryBean(放在spring容器里面会产生两个bean,一个是他自己,另一个是经过处理的bean)
 * 通过FactoryBean,传入一个原始的类,产生一个他的代理类。
 * 5. 使用注解(BafanSacn)注入BafanBeanDefinitionRegistrar,可以拿到这个这个注解所配置的路径下的所有的类,再通过循环,把属性注入到FactoryBean中,
 * 最后再放到BeanDefinition里面。
 */
public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    System.out.println(ac.getBean("tDao"));
    ac.getBean(TDao.class).list();
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容