zoukankan      html  css  js  c++  java
  • mybaits(七)spring整合mybaits

    与 Spring 整合分析
    http://www.mybatis.org/spring/zh/index.html
    这里我们以传统的 Spring 为例,因为配置更直观,在 Spring 中使用配置类注解是一样的。
     
    在前面的课程里面,我们基于编程式的工程已经弄清楚了 MyBatis 的工作流程、核
    心模块和底层原理。编程式的工程,也就是 MyBatis 的原生 API 里面有三个核心对象:
    SqlSessionFactory、SqlSession、MapperProxy。
    大部分时候我们不会在项目中单独使用 MyBatis 的工程,而是集成到 Spring 里面使
    用,但是却没有看到这三个对象在代码里面的出现。我们直接注入了一个 Mapper 接口,
    调用它的方法。
    所以有几个关键的问题,我们要弄清楚:
    1、 SqlSessionFactory 是什么时候创建的?
    2、 SqlSession 去哪里了?为什么不用它来 getMapper?
    3、 为什么@Autowired 注入一个接口,在使用的时候却变成了代理对象?在 IOC
    的容器里面我们注入的是什么? 注入的时候发生了什么事情?
     
    关键配置
    我们先看一下把 MyBatis 集成到 Spring 中要做的几件事情。
    为了让大家看起来更直观,这里我们依旧用传统的 xml 配置给大家来做讲解,当然
    使用配置类@Configuration 效果也是一样的,对于 Spring 来说只是解析方式的差异。
    除了 MyBatis 的依赖之外,我们还需要在 pom 文件中引入 MyBatis 和 Spring 整合
    的 jar 包(注意版本!mybatis 的版本和 mybatis-spring 的版本有兼容关系)。
     
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.0</version>
    </dependency> 
    然后在 Spring 的 applicationContext.xml 里面配置 SqlSessionFactoryBean,它
    是用来帮助我们创建会话的,其中还要指定全局配置文件和 mapper 映射器文件的路径。
     
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    <property name="dataSource" ref="dataSource"/>
    </bean>
    然后在 applicationContext.xml 配置需要扫描 Mapper 接口的路径。
    在 Mybatis 里面有几种方式,第一种是配置一个 MapperScannerConfigurer。
    <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.gupaoedu.crud.dao"/>
    </bean>
    第二种是配置一个<scan>标签:
    <mybatis-spring:scan base-package="com.gupaoedu.crud.dao"/>
    还有一种就是直接用@MapperScan 注解,比如我们在 Spring Boot 的启动类上加
    上一个注解:
    @SpringBootApplication
    @MapperScan("com.gupaoedu.crud.dao")
    public class MybaitsApp {
    public static void main(String[] args) {
    SpringApplication.run(MybaitsApp.class, args);
     
    这三种方式实现的效果是一样的。
    1、创建会话工厂
    Spring 对 MyBatis 的对象进行了管理,但是并不会替换 MyBatis 的核心对象。也就
    意味着:MyBatis jar 包中的 SqlSessionFactory、SqlSession、MapperProxy 这些都
    会用到。而 mybatis-spring.jar 里面的类只是做了一些包装或者桥梁的工作。
    所以第一步,我们看一下在 Spring 里面,工厂类是怎么创建的。
    我们在 Spring 的配置文件中配置了一个 SqlSessionFactoryBean,我们来看一下这
    个类
    它实现了 InitializingBean 接口,所以要实现 afterPropertiesSet()方法,这个方法
    会在 bean 的属性值设置完的时候被调用。
    另外它实现了 FactoryBean 接口,所以它初始化的时候,实际上是调用 getObject()
    方法,它里面调用的也是 afterPropertiesSet()方法。
    在 afterPropertiesSet()方法里面:
    第一步是一些标签属性的检查,接下来调用了 buildSqlSessionFactory()方法。
    然后定义了一个 Configuration,叫做 targetConfiguration
    426 行,判断 Configuration 对象是否已经存在,也就是是否已经解析过。如果已
    经有对象,就覆盖一下属性。
    433 行,如果 Configuration 不存在,但是配置了 configLocation 属性,就根据
    mybatis-config.xml 的文件路径,构建一个 xmlConfigBuilder 对象。
    436 行,否则,Configuration 对象不存在,configLocation 路径也没有,只能使
    用默认属性去构建去给 configurationProperties 赋值。
    后面就是基于当前 factory 对象里面已有的属性,对 targetConfiguration 对象里面
    属性的赋值。
    Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
    这个方法是 Java8 里面的一个判空的方法。如果不为空的话,就会调用括号里面的
    对象的方法,做赋值的处理。
    在第 498 行,如果 xmlConfigBuilder 不为空,也就是上面的第二种情况,调用了
    xmlConfigBuilder.parse()去解析配置文件,最终会返回解析好的 Configuration 对象。
    在 第 507 行 , 如 果 没 有 明 确 指 定 事 务 工 厂 , 默 认 使 用
    SpringManagedTransactionFactory 。 它创 建的 SpringManagedTransaction 也 有
    getConnection()和 close()方法。
    <property name="transactionFactory" value="" />
    在 520 行,调用 xmlMapperBuilder.parse(),这个步骤我们之前了解过了,它的作
    用是把接口和对应的 MapperProxyFactory 注册到 MapperRegistry 中。
    最后调用sqlSessionFactoryBuilder.build()返回了一个DefaultSqlSessionFactory。
    OK,在这里我们完成了编程式的案例里面的第一步,根据配置文件获得一个工厂类,
    它是单例的,会在后面用来创建 SqlSession。
    用到的 Spring 扩展点总结: 
     
    2、创建 SqlSession
    Q1:可以直接使用 DefaultSqlSession 吗?
    我们现在已经有一个 DefaultSqlSessionFactory,按照编程式的开发过程,我们接
    下来就会创建一个 SqlSession 的实现类,但是在 Spring 里面,我们不是直接使用
    DefaultSqlSession 的,而是对它进行了一个封装,这个 SqlSession 的实现类就是
    SqlSessionTemplate。这个跟 Spring 封装其他的组件是一样的,比如 JdbcTemplate,
    RedisTemplate 等等,也是 Spring 跟 MyBatis 整合的最关键的一个类。
    为什么不用 DefaultSqlSession?它是线程不安全的,注意看类上的注解:
    Note that this class is not Thread-Safe.
    而 SqlSessionTemplate 是线程安全的。
    * Thread safe, Spring managed, {@code SqlSession} that works with Spring
    回顾一下 SqlSession 的生命周期: 
     
    对象                                              生命周期
    SqlSessionFactoryBuiler              方法局部(method)
    SqlSessionFactory(单例)         应用级别(application)
    SqlSession                                   请求和操作(request/method)
    Mapper                                         方法(method)
     
    在编程式的开发中,SqlSession 我们会在每次请求的时候创建一个,但是 Spring
    里面只有一个 SqlSessionTemplate(默认是单例的),多个线程同时调用的时候怎么保
    证线程安全?
    思考:为什么 SqlSessionTemplate 是线程安全的?
    思考:在编程式的开发中,有什么方法保证 SqlSession 的线程安全?
    SqlSessionTemplate 里面有 DefaultSqlSession 的所有的方法:selectOne()、
    selectList()、insert()、update()、delete(),不过它都是通过一个代理对象实现的。这
    个代理对象在构造方法里面通过一个代理类创建:
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor()); 
     
    所有的方法都会先走到内部代理类 SqlSessionInterceptor 的 invoke()方法:
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(
    SqlSessionTemplate.this.sqlSessionFactory,
    SqlSessionTemplate.this.executorType,
    SqlSessionTemplate.this.exceptionTranslator);
    try {
    Object result = method.invoke(sqlSession, args);
    首先会使用工厂类、执行器类型、异常解析器创建一个 sqlSession,然后再调用
    sqlSession 的实现类,实际上就是在这里调用了 DefaultSqlSession 的方法
     
    Q2:怎么拿到一个 SqlSessionTemplate?
    我们知道在 Spring 里面会用 SqlSessionTemplate 替换 DefaultSqlSession,那么
    接下来看一下怎么在 DAO 层拿到一个 SqlSessionTemplate。
    不知道用过 Hibernate 的同学还记不记得,如果不用注入的方式,我们在 DAO 层
    注入一个 HibernateTemplate 的一种方法是什么?
    ——让我们 DAO 层的实现类去继承 HibernateDaoSupport。
    MyBatis 里面也是一样的,它提供了一个 SqlSessionDaoSupport,里面持有一个
    SqlSessionTemplate 对象,并且提供了一个 getSqlSession()方法,让我们获得一个
    SqlSessionTemplate
    public abstract class SqlSessionDaoSupport extends DaoSupport {
    private SqlSessionTemplate sqlSessionTemplate;
    public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
    }
    前面和后面省略…………
    也就是说我们让 DAO 层的实现类继承 SqlSessionDaoSupport,就可以获得
    SqlSessionTemplate,然后在里面封装 SqlSessionTemplate 的方法。
    当 然 , 为 了 减 少 重 复 的 代 码 , 我 们 通 常 不 会 让 我 们 的 实 现 类 直 接 去 继 承
    SqlSessionDaoSupport,而是先创建一个 BaseDao 继承 SqlSessionDaoSupport。在
    BaseDao 里面封装对数据库的操作,包括 selectOne()、selectList()、insert()、delete()
    这些方法,子类就可以直接调用
    public class BaseDao extends SqlSessionDaoSupport {
    //使用 sqlSessionFactory
    @Autowired
    private SqlSessionFactory sqlSessionFactory;
    @Autowired 
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    super.setSqlSessionFactory(sqlSessionFactory);
    }
    public Object selectOne(String statement, Object parameter) {
    return getSqlSession().selectOne(statement, parameter);
    }
    后面省略………… 
     
    然后让我们的实现类继承 BaseDao 并且实现我们的 DAO 层接口,这里就是我们的
    Mapper 接口。实现类需要加上@Repository 的注解。
    在实现类的方法里面,我们可以直接调用父类(BaseDao)封装的 selectOne()方法,
    那么它最终会调用 sqlSessionTemplate 的 selectOne()方法。
    @Repository
    public class EmployeeDaoImpl extends BaseDao implements EmployeeMapper {
    @Override
    public Employee selectByPrimaryKey(Integer empId) {
    Employee emp = (Employee)
    this.selectOne("com.gupaoedu.crud.dao.EmployeeMapper.selectByPrimaryKey",empId);
    return emp;
    }
    后面省略…………
    然后在需要使用的地方,比如 Service 层,注入我们的实现类,调用实现类的方法就
    行了。我们这里直接在单元测试类里面注入:
    @Autowired
    EmployeeDaoImpl employeeDao;
    @Test
    public void EmployeeDaoSupportTest() {
    System.out.println(employeeDao.selectByPrimaryKey(1));
    }
    最终会调用到 DefaultSqlSession 的方法。 
     
    Q3:有没有更好的拿到 SqlSessionTemplate 的方法? 
     
    这么做有一个问题:我们的每一个 DAO 层的接口(Mapper 接口也属于),如果要
    拿到一个 SqlSessionTemplate,去操作数据库,都要创建实现一个实现类,加上
    @Repository 的注解,继承 BaseDao,这个工作量也不小。
    另外一个,我们去直接调用 selectOne()方法,还是出现了 Statement ID 的硬编码,
    MapperProxy 在这里根本没用上。
    我们可以通过什么方式,不创建任何的实现类,就可以把 Mapper 注入到别的地方
    使用,并且可以拿到 SqlSessionTemplate 操作数据库呢?
    这个也确实是我们在 Spring 中的用法。那我们就必要弄清楚,我们只是注入了一个
    接口,在对象实例化的时候,是怎么拿到 SqlSessionTemplate 的?当我们调用方法的时
    候,还是不是用的 MapperProxy?
     
    3、接口的扫描注册
    在 Service 层可以使用@Autowired 自动注入的 Mapper 接口,需要保存在
    BeanFactory(比如 XmlWebApplicationContext)中。也就是说接口肯定是在 Spring
    启动的时候被扫描了,注册过的。
    1、 什么时候扫描的?
    2、 注册的时候,注册的是什么?这个决定了我们拿到的是什么实际对象。
    回 顾 一 下 , 我 们 在 applicationContext.xml 里 面 配 置 了 一 个
    MapperScannerConfigurer。
    MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,
    BeanDefinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的子类,可以
    通过编码的方式修改、新增或者删除某些 Bean 的定义
    我们只需要重写 postProcessBeanDefinitionRegistry()方法,在这里面操作 Bean
    就可以了。
    在这个方法里面:
    scanner.scan() 方 法 是 ClassPathBeanDefinitionScanner 中 的 , 而 它 的 子 类
    ClassPathMapperScanner 覆 盖 了 doScan() 方 法 , 在 doScan() 中 调 用 了
    processBeanDefinitions:
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    if (beanDefinitions.isEmpty()) {
    LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) +
    "' package. Please check your configuration.");
    } else {
    processBeanDefinitions(beanDefinitions);
    }
    return beanDefinitions;
    }
    它先调用父类的 doScan()扫描所有的接口。
     
    processBeanDefinitions 方法里面,在注册 beanDefinitions 的时候,BeanClass
    被改为 MapperFactoryBean(注意灰色的注释)。
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (GenericBeanDefinition) holder.getBeanDefinition();
    String beanClassName = definition.getBeanClassName();
    LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName()
    + "' and '" + beanClassName + "' mapperInterface");
    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); //
    issue #59
    definition.setBeanClass(this.mapperFactoryBean.getClass());
    问题:
    为什么要把 BeanClass 修改成 MapperFactoryBean,这个类有什么作用?
    MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 可 以 拿 到
    SqlSessionTemplate。
     
    4、接口注入使用
    我们使用 Mapper 的时候,只需要在加了 Service 注解的类里面使用@Autowired
    注入 Mapper 接口就好了。
    @Service
    public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;
    public List<Employee> getAll() {
    return employeeMapper.selectByMap(null);
    }
     
    Spring 在启动的时候需要去实例化 EmployeeService。
    EmployeeService 依赖了 EmployeeMapper 接口(是 EmployeeService 的一个属
    性)。
    Spring 会根据 Mapper 的名字从 BeanFactory 中获取它的 BeanDefination,再从
    BeanDefination 中 获 取 BeanClass , EmployeeMapper 对 应 的 BeanClass 是
    MapperFactoryBean(上一步已经分析过)。
    接下来就是创建 MapperFactoryBean,因为实现了 FactoryBean 接口,同样是调
    用 getObject()方法。
    // MapperFactoryBean.java
    public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
    }
    因 为 MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 所 以 这 个
    getSqlSession()就是调用父类的方法,返回 SqlSessionTemplate。
    // SqlSessionDaoSupport.java
    public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
    }
    第二步,SqlSessionTemplate 的 getMapper()方法,里面又有两个方法:
    // SqlSessionTemplate.java
    public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
    }
    第一步:SqlSessionTemplate 的 getConfiguration()方法:
    // SqlSessionTemplate.java
    public Configuration getConfiguration() {
    return this.sqlSessionFactory.getConfiguration();
     
    进入方法,通过 DefaultSqlSessionFactory,返回全部配置 Configuration:
    // DefaultSqlSessionFactory.java
    public Configuration getConfiguration() {
    return configuration;
    }
    第二步:Configuration 的 getMapper()方法:
    // Configuration.java
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
    }
    这 一 步 我 们 很 熟 悉 , 跟 编 程 式 使 用 里 面 的 getMapper 一 样 , 通 过 工 厂 类
    MapperProxyFactory 获得一个 MapperProxy 代理对象。
    也就是说,我们注入到 Service 层的接口,实际上还是一个 MapperProxy 代理对象。
    所以最后调用 Mapper 接口的方法,也是执行 MapperProxy 的 invoke()方法,后面的
    流程就跟编程式的工程里面一模一样了。
     
    思考:@MapperScan 注解是怎么解析的?
     
  • 相关阅读:
    Vue单页面应用
    MVVM模式理解
    Ajax原生四大步骤
    Vue 全家桶介绍
    原生js的dom操作
    vs2015+opencv3.3.1+ maxflow-v3.01 c++实现Yuri Boykov 的Interactive Graph Cuts
    c++迭代递归实现汉诺塔(5种迭代方法满足你)
    opencv3.3.1+vs2015+c++实现直接在图像上画掩码,保存掩码图片
    声明函数指针、回调函数、函数对象------c++程序设计基础、编程抽象与算法策略
    C++/C语言的标准库函数与运算符的区别new/delete malloc/free
  • 原文地址:https://www.cnblogs.com/flgb/p/12688416.html
Copyright © 2011-2022 走看看