前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springboot实战之ORM整合(mybatis篇)

springboot实战之ORM整合(mybatis篇)

作者头像
lyb-geek
发布2019-09-25 14:58:39
1.3K0
发布2019-09-25 14:58:39
举报
文章被收录于专栏:Linyb极客之路Linyb极客之路

前言

本文会介绍一下springboot与mybatis、mybatisplus如何进行整合,文章篇幅会有点长

什么是MyBatis

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

MyBatis的功能架构

我们把Mybatis的功能架构分为三层:

API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis的优缺点

优点:

  • 简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。
  • 灵活:mybatis不会对应用程序或者数据库的现有设计强加任何影响。sql写在xml里,便于统一管理和优化。通过sql基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多。
  • 解除sql与程序代码的耦合:通过提供DAL层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
  • 提供映射标签,支持对象与数据库的orm字段关系映射
  • 提供对象关系映射标签,支持对象关系组建维护
  • 提供xml标签,支持编写动态sql。

缺点:

  • 编写SQL语句时工作量很大,尤其是字段多、关联表多时,更是如此。
  • SQL语句依赖于数据库,导致数据库移植性差,不能更换数据库。
  • 框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。
  • 二级缓存机制不佳

springboot与mybatis整合

1、pom.xml引入jar

代码语言:javascript
复制
<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
 <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>

2、编辑application.yml

代码语言:javascript
复制
spring:
  datasource:
    name: druidDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot-learning?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&serverTimezone=UTC
      username: root
      password: config.file=classpath:druid.properties
      filters: stat,log4j,config
      max-active: 100
      initial-size: 1
      max-wait: 60000
      min-idle: 1
      db-type: mysql
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: select 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-open-prepared-statements: 50
      max-pool-prepared-statement-per-connection-size: 20
      connection-properties: config.file=classpath:druid.properties   # 启用加密,配置公钥。
      filter:
        config:
          enabled: true

#mybaits
mybatis:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.github.lybgeek.orm.mybatis.model
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3、集成代码生成器

通过mybatis-generator-maven-plugin和generatorConfig.xml配合自动生成model、dao、mapper.xml模板代码

a、在pom.xml引入mybatis-generator-maven-plugin插件

代码语言:javascript
复制
<build>
    <plugins>
      <!-- mybaitis 自动生成代码配置  mybatis-generator:generate -->
      <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.3.5</version>
        <executions>
          <execution>
            <id>Generate MyBatis Files</id>
            <goals>
              <goal>generate</goal>
            </goals>
            <phase>generate</phase>
            <configuration>
              <verbose>true</verbose>
              <overwrite>true</overwrite>
            </configuration>
          </execution>
        </executions>

        <dependencies>
          <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
          </dependency>
          <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.5</version>
          </dependency>

          <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
          </dependency>

        </dependencies>

      </plugin>

    </plugins>
  </build>

b、在classpath下引入generatorConfig.xml

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!--<properties resource="jdbc.properties" />-->
    <properties url="file:///F:/jdbc.properties" />

    <context id="mysqlTables" targetRuntime="MyBatis3">
         <commentGenerator> 
            <!-- 是否去除自动生成的注释 true:是 : false:否 --> 
            <property name="suppressAllComments" value="true" /> 
        </commentGenerator> 

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
            connectionURL="${jdbc.url}" userId="${jdbc.username}" password="${jdbc.password}" />

        <!-- 指定生成的类型为java类型,避免数据库中number等类型字段 -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!-- 自动生成的实体的存放包路径 -->
        <javaModelGenerator targetPackage="com.github.lybgeek.orm.mybatis.model"
            targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
              <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!--自动生成的*Mapper.xml文件存放路径 -->
        <sqlMapGenerator targetPackage="mapper/bookorderitem"
            targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />

        </sqlMapGenerator>
        <!--自动生成的*Mapper.java存放路径 -->
        <javaClientGenerator type="XMLMAPPER"
            targetPackage="com.github.lybgeek.orm.mybatis.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>


    <table tableName="book_order_item" domainObjectName="BookOrderItem"
        enableCountByExample="true" enableUpdateByExample="true"
        enableDeleteByExample="true" enableSelectByExample="true"
        selectByExampleQueryId="true">
      <generatedKey column="id" sqlStatement="mysql" identity="true" />
    </table>


    </context>

</generatorConfiguration>

4、在启动类上加上@MapperScan注解

代码语言:javascript
复制
@SpringBootApplication
@MapperScan(basePackages = {"com.github.lybgeek.orm.mybatis.dao"})
public class OrmApplication {
    public static void main( String[] args ) {

        SpringApplication.run(OrmApplication.class,args);
    }
}

mybatis其他一些扩展

分页

1、pom引入

代码语言:javascript
复制
<dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>

2、分页代码例子

代码语言:javascript
复制
@Override
    public PageResult<BookOrderDTO> pageBookOrder(PageQuery<BookOrderDTO> pageQuery) {
        Integer pageNo = ObjectUtils.isEmpty(pageQuery.getPageNo()) ? 1 : pageQuery.getPageNo();
        Integer pageSize = ObjectUtils.isEmpty(pageQuery.getPageSize()) ? 5 : pageQuery.getPageSize();
        BookOrderDTO bookOrderDTO = pageQuery.getQueryParams();
        Page<BookOrder> page = PageHelper.startPage(pageNo,pageSize);
        PageHelper.orderBy("bo.create_date DESC");
        List<BookOrderDTO> list = listBookOrders(bookOrderDTO);

        return PageUtil.INSTANCE.getPage(page,list);
    }
通过拦截器扩展,实现字段填充功能

以自动生成数据库创建时间和更新时间为例

1、编写自定义注解

代码语言:javascript
复制
Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
public @interface CreateDate {

    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
public @interface UpdateDate {

    String value() default "";
}

2、编写拦截器

代码语言:javascript
复制
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
        Object.class})})
public class DateTimeInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation)
            throws Throwable {

        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

        // 获取 SQL
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

        // 获取参数
        Object parameter = invocation.getArgs()[1];

        // 获取私有成员变量
        Field[] declaredFields = parameter.getClass().getDeclaredFields();

//        Class parentClz = parameter.getClass().getSuperclass();
//
//        if(parentClz.newInstance() instanceof BaseEntity){
//            declaredFields = parentClz.getDeclaredFields();
//        }


        for (Field field : declaredFields) {
            if (field.getAnnotation(CreateDate.class) != null) {
                if (SqlCommandType.INSERT.equals(sqlCommandType)) {
                    // insert语句插入createDate
                    field.setAccessible(true);
                    field.set(parameter, new Date());
                }
            } else if (field.getAnnotation(UpdateDate.class) != null) {

                if (SqlCommandType.INSERT.equals(sqlCommandType)
                        || SqlCommandType.UPDATE.equals(sqlCommandType)) {
                    // insert 或update语句插入updateDate
                    field.setAccessible(true);
                    field.set(parameter, new Date());
                }
            }
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

3、配置拦截器

代码语言:javascript
复制
@Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.addInterceptor(dateTimeInterceptor());
    }


    @Bean
    public DateTimeInterceptor dateTimeInterceptor() {
        return new DateTimeInterceptor();
    }

4、在需要填充字段上,加上指定注解

代码语言:javascript
复制
@CreateDate
  private Date createDate;


  @UpdateDate
  private Date updateDate;

什么是MyBatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

MyBatis-Plus的特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

springboot与mybatis-plus整合

1、pom引入

代码语言:javascript
复制
<dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>

2、编辑application.yml

代码语言:javascript
复制
spring:
  datasource:
    name: druidDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot-learning?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&serverTimezone=UTC
      username: root
      password: config.file=classpath:druid.properties
      filters: stat,log4j,config
      max-active: 100
      initial-size: 1
      max-wait: 60000
      min-idle: 1
      db-type: mysql
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: select 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-open-prepared-statements: 50
      max-pool-prepared-statement-per-connection-size: 20
      connection-properties: config.file=classpath:druid.properties   # 启用加密,配置公钥。
      filter:
        config:
          enabled: true


mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.github.lybgeek.orm.mybatisplus.model
  global-config:
    #主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
    id-type: 0
    #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
    field-strategy: 1
    #驼峰下划线转换
    db-column-underline: true
    #刷新mapper 调试神器
    refresh-mapper: true
    #数据库大写下划线转换
    #capital-mode: true
    # Sequence序列接口实现类配置
    #key-generator: com.baomidou.mybatisplus.incrementer.OracleKeyGenerator
    #逻辑删除配置
    logic-delete-value: -1
    logic-not-delete-value: 0
    #自定义填充策略接口实现
    #meta-object-handler: com.baomidou.springboot.xxx
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3、集成代码生成器

a、pom.xml引入

代码语言:javascript
复制
<dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-generator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
    </dependency>

b、代码生成器核心代码

代码语言:javascript
复制
// 执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }



    public static void main(String[] args) throws Exception{
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String basePath = CodeGenerator.class.getResource("").getPath();
        String projectPath = basePath.substring(0, basePath.indexOf("/target"));
      //  String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("lyb-geek");
        gc.setOpen(false);
        gc.setBaseColumnList(true);
        gc.setBaseResultMap(true);
        gc.setServiceName("%sService");
       // gc.setSwagger2(true);// 实体属性 Swagger2 注解
        gc.setDateType(DateType.ONLY_DATE);
        mpg.setGlobalConfig(gc);


        String url = YmlUtil.getValue("spring.datasource.druid.url").toString();
        String username = YmlUtil.getValue("spring.datasource.druid.username").toString();
        String pwd = PropertiesUtil.INSTANCE.getProperty("password");
        String publicKey = PropertiesUtil.INSTANCE.getProperty("config.decrypt.key");
        String password = ConfigTools.decrypt(publicKey,pwd);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(url);
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername(username);
        dsc.setPassword(password);
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(scanner("模块名"));
        pc.setParent("com.github.lybgeek.orm");
        pc.setEntity("model");
        pc.setMapper("dao");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
//        focList.add(new FileOutConfig(templatePath) {
//            @Override
//            public String outputFile(TableInfo tableInfo) {
//                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
//                return projectPath + "/src/main/resources/mapperPlus/" + pc.getModuleName()
//                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
//            }
//        });

        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/mybatisplus/" + tableInfo.getEntityName().toLowerCase()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录");
                return false;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setSuperEntityClass("com.github.lybgeek.orm.common.model.BaseEntity");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
       // strategy.setSuperControllerClass("com.github.lybgeek.orm.controller.BaseController");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setSuperEntityColumns("id","create_date","update_date");
        strategy.setControllerMappingHyphenStyle(true);
      //  strategy.setTablePrefix(pc.getModuleName() + "_");
        //移除表的前缀
   //     strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

4、与mybatis一样,在启动类上加上@MapperScan注解

总结

本来想分为两篇分别介绍mybatis和mybatis-plus,但后边觉得mybatis-plus官网的例子已经很详尽了,就没必要多花篇幅介绍,在这边安利一下mybatis-plus,它确实是一个很强大而且易用的orm框架,而且是国人开发的,比较了解国人开发的一些痛点,而且集成了分布式事务,但是暂时支持rabbit实现可靠消息分布式事务3.1.1 以上版本,不过这个方案是否能使用在生产线上,有待验证。其官网链接如下

https://mp.baomidou.com/

参考文档

https://www.w3cschool.cn/mybatis/ https://mp.baomidou.com/

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-orm

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linyb极客之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 什么是MyBatis
  • MyBatis的功能架构
  • MyBatis的优缺点
  • springboot与mybatis整合
  • mybatis其他一些扩展
    • 分页
      • 通过拦截器扩展,实现字段填充功能
      • 什么是MyBatis-Plus
      • MyBatis-Plus的特性
      • springboot与mybatis-plus整合
      • 总结
      • 参考文档
      • demo链接
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档