Spring使用mybatis框架操作数据库

目前,关系型数据库已经成为 Java 应用的标配,由于 JDBC 操作数据库的复杂性,我们通常会选择一款持久层框架,而作为半自动化 ORM 框架的 MyBatis 则成了我们的首选,而 Spring 又成了 Java 事实上的标准,它让我们开发 Java 应用更快、更容易、更安全,因此我们通常会将 MyBatis 与 Spring 整合在一起使用。

MyBatis 脱离 Spring 环境已经可以使用了,那为什么又要将它与 Spring 整合在一起呢?这里我总结出两点:

  • 配置简化,和 Spring 整合在一起后复杂的 xml 配置可以忽略或少量配置。
  • 加入 Spring 事务管理,将 MyBatis 异常转换为 Spring 中的异常。

快速入门

MyBatis 与 Spring 的整合可以分为两块。一块是直接与 Spring Framework 整合,整合后我们就可以在 Spring 的事务管理下使用 MyBatis,此时还需要少量的配置。另一块是与 SpringBoot 整合,整合后几乎可以忽略所有的 MyBatis 配置,极个别配置在 application.properties 添加即可。

本文只介绍如何与Spring整合。

为了将 MyBatis 应用到 Spring,MyBatis 官方提出了一个子项目 mybatis-spring,需要先将其引入到项目中:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.6</version>
</dependency>

SqlSessionFactory 配置及使用

SqlSession 是 Mybatis 执行 SQL 的入口,它是非线程安全的,因此我们可以在 Spring 环境下配置一个线程安全的 SqlSessionFactory 作为 bean。按照非 Spring 环境下的方式创建 SqlSessionFactory 也并无不可,然而为了简化配置及加入 Spring 的事务管理,mybatis-spring 项目中提供了一个 SqlSessionFactoryBean,这是一个 Spring 的 FactoryBean,最终 Spring 容器创建的是一个从 SqlSessionFactoryBean 获取到的 SqlSessionFactory,因此我们配置 SqlSessionFactoryBean 作为 bean 即可

@Configuration
public class MyBatisConfiguration {

    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new PooledDataSource();
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource());

        // 设置 mapper xml 文件路径
        Resource[] mapperResources = new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*Mapper.xml");
        sqlSessionFactoryBean.setMapperLocations(mapperResources);

        // 设置配置
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 字段映射时先将数据库表字段下划线转驼峰
        configuration.setMapUnderscoreToCamelCase(true);
        // 关闭缓存
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
        configuration.setCacheEnabled(false);
        sqlSessionFactoryBean.setConfiguration(configuration);
        
        return sqlSessionFactoryBean;
    }

}

SqlSessionFactoryBean 只有一个必要的 dataSource 属性需要设置,除此之外,我们还配置了 mapper xml 配置文件的路径,以及常用的关闭缓存及下划线转驼峰的字段映射方式,此时我们已经可以使用如下的方式使用 MyBatis:

@Service
public class UserService {

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    public User getById(Integer id) {
        return sqlSessionFactory.openSession().selectOne("com.zzuhkp.blog.mybatis.mapper.UserMapper.getById", id);
    }
}

SqlSessionTemplate 配置及使用

通过上面对 SqlSessionFactoryBean 的配置我们已经可以获取到 SqlSession 来执行 SQL,但是这个 SqlSession 不是线程安全的,因此 MyBatis 又提供了一个线程安全的 SqlSession,为 SqlSessionTempalte,将其配置为 Spring 的 bean 即可方便的使用。

@Configuration
public class MyBatisConfiguration {

    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new PooledDataSource();
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        ... 省略部分代码
        return sqlSessionFactoryBean;
    }

    @Bean
    public SqlSession sqlSession() throws Exception {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory().getObject());
        return sqlSessionTemplate;
    }

}

此时业务代码使用 MyBatis 方式如下:

@Service
public class UserService {

    @Autowired
    private SqlSession sqlSession;

    public User getById(Integer id) {
        return sqlSession.selectOne("com.zzuhkp.blog.mybatis.mapper.UserMapper.getById", id);
    }
}

Mapper 配置及使用

通过上面的配置虽然我们已经有了线程安全的 SqlSessionTemplate,但是通常情况我们更希望使用 Mapper 接口操作数据库,Spring 环境下就需要把 Mapper 接口注入为 bean 了,通过 SqlSession#getMapper 方法获取是一个不错的选择,但是 mybatis 还提供了不通过 SqlSession 获取 Mapper 的 MapperFactoryBean 类,这个类可以创建 Mapper 接口的实现作为 bean。

@Configuration
public class MyBatisConfiguration {

    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new PooledDataSource();
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		...省略部分代码
        return sqlSessionFactoryBean;
    }

    @Bean
    public MapperFactoryBean<UserMapper> userMapper() throws Exception {
        MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>();
        factoryBean.setMapperInterface(UserMapper.class);
        factoryBean.setSqlSessionFactory(sqlSessionFactory().getObject());
        return factoryBean;
    }

}

通过上面的代码,我们就可以按照如下的方式使用 UserMapper 替代 SqlSession 了。

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getById(Integer id) {
        return userMapper.getById(id);
    }
}

Mapper 扫描配置

通过上面的方式我们直接将 Mapper 配置为 bean,如果项目中有几十甚至几百个 Mapper,那配置的工作量将大大增加,为了简化配置,MyBatis 提出了一个 @MpperScan 注解,这个注解会为扫描到的 Mapper 接口创建代理类,然后注入到 Spring 容器中,将这个注解添加到 Spring 配置类上即可。

@MapperScan("com.zzuhkp.blog.mybatis.mapper")
@Configuration
public class MyBatisConfiguration {

    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new PooledDataSource();
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        ... 省略部分代码
        return sqlSessionFactoryBean;
    }
}

使用方式和上面注入 UserMapper 的使用方式相同,从这里也可以看出,MyBatis 在不断简化配置以及降低使用的难度。

MyBatis 与 Spring 整合原理分析

MyBatis 为何提出 SqlSessionFactoryBean?

Spring 环境下,我们常将所使用的对象交由 Spring IOC 容器来管理。MyBatis 执行 SQL 的入口是非线程安全的 SqlSession,因此可以将获取 SqlSession 的线程安全的 SqlSessionFactory 注册为 Spring bean,而 mybatis-spring 项目又提供了一个 SqlSessionFactoryBean 替代原生的 SqlSessionFactory,不免让人产生疑问,为什么呢?

配置简化

在我们的示例中,我们将 SqlSessionFactoryBean 配置为 bean,这个类的功能类似于 SqlSessionFactoryBuilder,都可以创建 SqlSessionFactory ,SqlSessionFactoryBuilder 主要从 xml 文件中读取配置,我们手动配置 SqlSessionFactoryBean bean 的时候却没有指定配置文件地址,并且仅设置了少量的配置项,因此可以认为 SqlSessionFactoryBean 简化了创建 SqlSessionFactory 的配置。

Spring 事务支持

如果只是使用 SqlSessionFactoryBean 替代 SqlSessionFactoryBuilder 以此来取消对 xml 配置文件的使用,那么这个提升可以说并不明显。

springframework 项目中有一个 spring-tx 的模块,统一了事务管理,作为 ORM 框架的 mybatis 整合到 spring 中自然选择了对 Spring 事务管理的支持。那么对 mybatis 改造的入口就是 mybatis 的事务管理器,将这个事务管理器改造为支持 Spring 事务的事务管理器即可。事务管理器作为 mybatis 的配置项自然而然的放到 SqlSessionFactoryBean 创建 SqlSessionFactory 的逻辑中就可以了,当然这也意味着 mybatis 会忽略环境相关的所有配置。查看 SqlSessionFactoryBean 相关源码如下:

public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
        ... 省略部分代码
        targetConfiguration.setEnvironment(new Environment(this.environment,
            this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
            this.dataSource));
        ... 省略部分代码
	}
}

那么 mybatis 又是如何将事务管理器适配成支持 Spring 事务管理的事务管理器呢?跟踪 SpringManagedTransactionFactory 代码。

public class SpringManagedTransactionFactory implements TransactionFactory {

    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new SpringManagedTransaction(dataSource);
    }
	... 省略部分代码
}

SpringManagedTransactionFactory 作为事务工厂创建了一个类型为 SpringManagedTransaction,从名字也可以看出,这是一个 Spring 管理的事务对象,核心代码如下:

public class SpringManagedTransaction implements Transaction {

    @Override
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            openConnection();
        }
        return this.connection;
    }

    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    }

}

MyBatis 调用了DataSourceUtils#getConnection 获取 Connection,这是使用 Spring 事务管理的关键,Spring 会把事务相关的资源存储到 ThreadLocal,而 DataSource 就是其中一个 key,DataSourceUtils#getConnection 可以使用相同的 DataSource 从 ThreadLocal 中获取到 Spring 事务管理存储的 Connection,从而加入 Spring 的事务中。

线程安全的 SqlSessionTemplate 是如何实现的

SqlSessionFactoryBean 的作用主要用来替代 SqlSessionFactoryBuilder 构建支持 Spring 事务的 SqlSessionFactory,SqlSessionTemplate 则是用来替代 SqlSession 保证线程安全,那它是如何实现的呢?先看其类定义:

public class SqlSessionTemplate implements SqlSession, DisposableBean {
}

SqlSessionTemplate 实现了 SqlSession 接口,因此可以说 SqlSessionTemplate 就是 SqlSession,查看其中一个实现方法如下:

public class SqlSessionTemplate implements SqlSession, DisposableBean {

    private final SqlSession sqlSessionProxy;
    @Override
    public <E> List<E> selectList(String statement) {
        return this.sqlSessionProxy.selectList(statement);
    }
}    

可以看出 SqlSessionTemplate 委托给了底层持有的 SqlSession,因此我们可以猜想,这个底层的 SqlSession 应该是线程安全的,那它是怎么实现线程安全的呢?先看看这个 SqlSession 从哪来的。

public class SqlSessionTemplate implements SqlSession, DisposableBean {

    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                              PersistenceExceptionTranslator exceptionTranslator) {
        ...省略部分代码
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
            new Class[]{SqlSession.class}, new SqlSessionInterceptor());
    }

}

可以看到底层的 SqlSession 是 SqlSessionTemplate 在实例化时通过代理创建的,使用了 SqlSessionInterceptor 拦截方法的执行,那我们再跟踪 SqlSessionInterceptor。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        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);
                if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                ... 省略部分代码
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
        }
    }
}

SqlSessionInterceptor 是 SqlSessionTemplate 的内部类,它获取 SqlSession 的实例后再调用其方法,SqlSessionInterceptor 获取的 SqlSession 又有何特殊之处呢?

public final class SqlSessionUtils {
    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
                                           PersistenceExceptionTranslator exceptionTranslator) {
        // 优先从 ThreadLocal 中获取
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

        SqlSession session = sessionHolder(executorType, holder);
        if (session != null) {
            return session;
        }

        // ThreadLocal 中不存在 SqlSession,新创建一个
        session = sessionFactory.openSession(executorType);

        // 尝试将 SqlSession 存放到 ThreadLocal 中
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

        return session;
    }
}

这里发现 MyBatis 将 SqlSessionFactory 作为参数调用了 Spring 的TransactionSynchronizationManager#getResource,这里 MyBatis 将 SqlSessionHolder 作为资源存储到了 ThreadLocal 中,这个 SqlSessionFactory 则是前面我们提到的 SqlSessionFactoryBean 生成的,通过 ThreadLocal 保证了 SqlSessionTemplate 的线程安全。

总结如下:

  • SqlSessionTemplate 实现接口 SqlSession,使用持有的 SqlSessionFactoryBean 生成的 SqlSessionFactory 创建了 SqlSession 的代理,使用这个代理实现各个方法。
  • SqlSession 代理利用 SqlSessionFactory 获取支持 Spring 事务的 SqlSession,并将 SqlSession 作为资源保存到 ThreadLocal 中,保证了线程安全。

Spring 环境下的 Mapper 接口底层是如何实现的

对于单个 Mapper 接口的 Spring bean 注入,MyBatis 提供了一个 MapperFactoryBean 类,这个类是 Spring 的一个 FactoryBean,由这个类创建对应 Mapper 接口实现。关键代码如下:

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
}

Mapper 接口还是通过 SqlSession 获取,其中getSqlSession 方法由父类 SqlSessionDaoSupport 提供,跟踪实现如下:

public abstract class SqlSessionDaoSupport extends DaoSupport {
    private SqlSessionTemplate sqlSessionTemplate;

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
            this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
        }
    }
    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }
}

可以看到 Mapper 最终由根据 SqlSessionFactory 创建的 SqlSessionTemplate 获取,因此可以认为由 MapperFactoryBean 创建的 Mapper 接口实例和我们直接通过 SqlSessionTemplate 并无差别,只是 MyBatis 做了小小的封装,避免了我们手动创建 SqlSessionTemplate 实例而已。

MyBatis的@MapperScan 如何扫描 Mapper 接口的

MyBatis 虽然提供了 MapperFactoryBean 用于创建 Mapper 接口的实例作为 bean,但是如果 Mapper 接口过多,那么配置的工作量将大大增大,为了减少对 Mapper 接口的配置,MyBatis 又提供了一个 @MapperScan 注解,将它添加到配置类后 MyBatis 会扫描 Mapper 接口并自动向 Spring 注入 bean。注解定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    Class<? extends Annotation> annotationClass() default Annotation.class;
    Class<?> markerInterface() default Class.class;
    String sqlSessionTemplateRef() default "";
    String sqlSessionFactoryRef() default "";
    Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
    String lazyInitialization() default "";
    String defaultScope() default AbstractBeanDefinition.SCOPE_DEFAULT;
}

@MapperScan 注解上添加了 @Import 注解,这是实现自动注入 bean 的关键,Spring 中 @Enable* 注解的实现大多如此。

我们继续跟踪 @Import 的参数 MapperScannerRegistrar.class 源码,关键代码如下:

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes
            .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if (mapperScanAttrs != null) {
            registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
                generateBaseBeanName(importingClassMetadata, 0));
        }
    }

    void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
                                 BeanDefinitionRegistry registry, String beanName) {
        // 注册 MapperScannerConfigurer 类型的 bean,使该 bean 继续扫描包注册 mapper 为 bean
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        ...省略属性设置代码
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

    }
}

MapperScannerRegistrar 拿到 @MapperScan 的参数后向 Spring 注册了一个类型为 MapperScannerConfigurer 的 bean,并将 @MapperScan 的参数设置到这个 bean 的属性中,那这个新注册的 bean 有何特殊之处呢?

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
		... 省略部分代码
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
        if (StringUtils.hasText(lazyInitialization)) {
            scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
        }
        if (StringUtils.hasText(defaultScope)) {
            scanner.setDefaultScope(defaultScope);
        }
        scanner.registerFilters();
        scanner.scan(
            StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
}

MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,这是一个特殊的 BeanFactoryPostProcessor,会在 Spring 应用上下文的生命周期中回调其方法,回调方法使用 ClassPathMapperScanner 向 Spring 注入了 Mapper 作为 bean。可以看到,ClassPathMapperScanner 保存了 MapperFactoryBean 所需的信息,据此创建 Mapper 接口的代理对象。

总结如下:

@MapperScan 注入了一个类型为 MapperScannerConfigurer 的 bean,这个 bean 在应用上下文的生命周期回调中扫描包并使用 MapperFactoryBean 创建了 Mapper 接口的代理并向 Spring 注入了 bean。

总结

MyBatis 作为一个独立的项目,从适配 springframework 到 springboot 自动化配置,可以说在不断简化使用方式,我们开发项目时借鉴 mybatis 是个不错的做法,也可以看出,开源框架的设计确实值得我们学习。

 

 

posted @   残城碎梦  阅读(238)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示