MyBatis 与 Spring 整合原理分析

简介: 前言我们常常将 Spring 与 MyBatis 结合在一起使用,由于篇幅问题,上篇《MyBatis 快速整合 Spring》仅介绍了将 MyBatis 整合到 Spring 的方式,这篇在上篇的基础上总结出几个问题,并尝试通过分析其底层源码进行回答。

前言


我们常常将 Spring 与 MyBatis 结合在一起使用,由于篇幅问题,上篇《MyBatis 快速整合 Spring》仅介绍了将 MyBatis 整合到 Spring 的方式,这篇在上篇的基础上总结出几个问题,并尝试通过分析其底层源码进行回答。


MyBatis 为何提出 SqlSessionFactoryBean?


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


配置简化


在上篇文章中,我们将 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 的事务中。关于 Spring 事务管理,你还可以参考《如何正确打开 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* 注解的实现大多如此,如果你感兴趣,还可以参阅《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。


SpringBoot 环境下 MyBatis 如何做自动化配置的?


mybatis-spring 项目添加了 spring 事务的支持,但是需要手动进行一些配置才可以在 spring 中使用,为了进一步简化 mybatis 在 springboot 项目中的使用,mybatis 又开发了一个 mybatis-spring-boot-starter 项目,这个项目充分利用了 springboot 自动化配置的特性,引入这个项目之后,我们可以在代码里直接注入 Mapper 接口,大大方便了 mybatis 的用户,下面我们来看它是如何实现的。


下载 mybatis-spring-boot 项目源码后,可以发现,mybatis-spring-boot-starter 是一个空项目。


image.png


在 pom 文件中,mybatis 引入了一个 mybatis-spring-boot-autoconfigure 模块,这是实现自动化配置的原因所在。


image.png


mybatis-spring-boot-autoconfigure 模块中 META-INF 目录下的 spring.factories 文件中指定了 MybatisAutoConfiguration 作为自动化的配置。


@org.springframework.context.annotation.Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
                                    ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                    ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                    ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        this.properties = properties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.typeHandlers = typeHandlersProvider.getIfAvailable();
        this.languageDrivers = languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
    }
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      ...省略实现代码
    }
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
      ...省略实现代码
    }
}


MybatisAutoConfiguration 注入了 Spring 容器中 MyBatis 相关的组件,然后为我们自动配置了 SqlSessionFactory、SqlSessionTemplate 作为 bean。


除此之外,MybatisAutoConfiguration 中还包含两个静态类用于 Mapper 的扫描注册。


    @org.springframework.context.annotation.Configuration
    @Import(AutoConfiguredMapperScannerRegistrar.class)
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
    ...省略不重要代码
    }
    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
        private BeanFactory beanFactory;
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      ...省略部分代码
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
            ... 省略属性配置代码
            registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
        }
        @Override
        public void setBeanFactory(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }
    }    


通过上面的代码可以看到 MybatisAutoConfiguration 内部注入了 MapperScannerConfigurer 作为 bean,从而进一步注册 Mapper 接口作为 bean。


可以看出 mybatis-spring-boot-starter 对 mybatis 的配置和我们自己的配置基本是一致的,只是 mybatis 帮我们做了自动化的配置。


总结

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


目录
相关文章
|
19天前
|
SQL Java 数据库连接
对Spring、SpringMVC、MyBatis框架的介绍与解释
Spring 框架提供了全面的基础设施支持,Spring MVC 专注于 Web 层的开发,而 MyBatis 则是一个高效的持久层框架。这三个框架结合使用,可以显著提升 Java 企业级应用的开发效率和质量。通过理解它们的核心特性和使用方法,开发者可以更好地构建和维护复杂的应用程序。
108 29
|
30天前
|
SQL Java 数据库连接
【潜意识Java】深入理解MyBatis的Mapper层,以及让数据访问更高效的详细分析
深入理解MyBatis的Mapper层,以及让数据访问更高效的详细分析
60 1
|
30天前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
54 2
|
1月前
|
SQL JavaScript Java
Spring Boot 3 整合 Mybatis-Plus 实现数据权限控制
本文介绍了如何在Spring Boot 3中整合MyBatis-Plus实现数据权限控制,通过使用MyBatis-Plus提供的`DataPermissionInterceptor`插件,在不破坏原有代码结构的基础上实现了细粒度的数据访问控制。文中详细描述了自定义注解`DataScope`的使用方法、`DataPermissionHandler`的具体实现逻辑,以及根据用户的不同角色和部门动态添加SQL片段来限制查询结果。此外,还展示了基于Spring Boot 3和Vue 3构建的前后端分离快速开发框架的实际应用案例,包括项目的核心功能模块如用户管理、角色管理等,并提供Gitee上的开源仓库
246 11
|
2月前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
153 14
|
2月前
|
XML Java 数据格式
Spring Core核心类库的功能与应用实践分析
【12月更文挑战第1天】大家好,今天我们来聊聊Spring Core这个强大的核心类库。Spring Core作为Spring框架的基础,提供了控制反转(IOC)和依赖注入(DI)等核心功能,以及企业级功能,如JNDI和定时任务等。通过本文,我们将从概述、功能点、背景、业务点、底层原理等多个方面深入剖析Spring Core,并通过多个Java示例展示其应用实践,同时指出对应实践的优缺点。
74 14
|
2月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
146 4
|
2月前
|
SQL Java 数据库连接
spring和Mybatis的各种查询
Spring 和 MyBatis 的结合使得数据访问层的开发变得更加简洁和高效。通过以上各种查询操作的详细讲解,我们可以看到 MyBatis 在处理简单查询、条件查询、分页查询、联合查询和动态 SQL 查询方面的强大功能。熟练掌握这些操作,可以极大提升开发效率和代码质量。
165 3
|
3月前
|
Java 开发者 Spring
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
|
3月前
|
Java 数据库连接 数据库
spring和Mybatis的逆向工程
通过本文的介绍,我们了解了如何使用Spring和MyBatis进行逆向工程,包括环境配置、MyBatis Generator配置、Spring和MyBatis整合以及业务逻辑的编写。逆向工程极大地提高了开发效率,减少了重复劳动,保证了代码的一致性和可维护性。希望这篇文章能帮助你在项目中高效地使用Spring和MyBatis。
112 1