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 是个不错的做法,也可以看出,开源框架的设计确实值得我们学习。


目录
相关文章
|
17天前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
15天前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
10天前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
15天前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
18天前
|
Java 程序员 数据库连接
女朋友不懂Spring事务原理,今天给她讲清楚了!
该文章讲述了如何解释Spring事务管理的基本原理,特别是针对女朋友在面试中遇到的问题。文章首先通过一个简单的例子引入了传统事务处理的方式,然后详细讨论了Spring事务管理的实现机制。
女朋友不懂Spring事务原理,今天给她讲清楚了!
|
1天前
|
Java 数据库连接 测试技术
SpringBoot 3.3.2 + ShardingSphere 5.5 + Mybatis-plus:轻松搞定数据加解密,支持字段级!
【8月更文挑战第30天】在数据驱动的时代,数据的安全性显得尤为重要。特别是在涉及用户隐私或敏感信息的应用中,如何确保数据在存储和传输过程中的安全性成为了开发者必须面对的问题。今天,我们将围绕SpringBoot 3.3.2、ShardingSphere 5.5以及Mybatis-plus的组合,探讨如何轻松实现数据的字段级加解密,为数据安全保驾护航。
12 1
|
10天前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
10天前
|
SQL Java 关系型数据库
SpringBoot 系列之 MyBatis输出SQL日志
这篇文章介绍了如何在SpringBoot项目中通过MyBatis配置输出SQL日志,具体方法是在`application.yml`或`application.properties`中设置MyBatis的日志实现为`org.apache.ibatis.logging.stdout.StdOutImpl`来直接在控制台打印SQL日志。
SpringBoot 系列之 MyBatis输出SQL日志
|
17天前
|
前端开发 Java 数据库连接
一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)
这篇文章总结了Java面试中的十个问题,包括Spring事务传播机制、Spring事务失效条件、Bean自动装配方式、Spring、Spring MVC和Spring Boot的区别、Spring MVC的工作流程和主要组件、Spring Boot的自动配置原理和Starter概念、嵌入式服务器的使用原因,以及MyBatis的优缺点。
|
16天前
|
Java 数据库连接 mybatis
基于SpringBoot+MyBatis的餐饮点餐系统
本文介绍了一个基于SpringBoot和MyBatis开发的餐饮点餐系统,包括系统的主控制器`IndexController`的代码实现,该控制器负责处理首页、点餐、登录、注册、订单管理等功能,适用于毕业设计项目。
26 0
基于SpringBoot+MyBatis的餐饮点餐系统
下一篇
云函数