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

@Service
public class IndexService {

    @Autowired
    private TDao tDao;

    public List> 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 {
    @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 extends SqlSessionDaoSupport implements FactoryBean {
    
  //需要被代理的类(这里传入的就是Mapper类,在例子中对应着TDao的接口类)
  private Class mapperInterface;
    
  public MapperFactoryBean(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  
  public void setMapperInterface(Class 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 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 getMapper(Class type) {
    return configuration.getMapper(type, this);
  }
}

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

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

}

//我们看下mapperProxy做了什么
public class MapperProxy 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 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();
}

你可能感兴趣的:(MyBatis配置原理)