前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring 学习笔记 - 核心容器

Spring 学习笔记 - 核心容器

作者头像
知识分子没文化
发布2023-07-01 16:04:01
1430
发布2023-07-01 16:04:01
举报
文章被收录于专栏:Roookie博客Roookie博客

学习之前

1、Spring 要学习哪些技术:
  • IOC
  • AOP
  • Spring 的事务处理
  • 整合 Mybatis 框架
2、怎么学
  • 学习 Spring 框架设计思想
  • 学习基础操作,思考操作与思想建的联系
  • 学习案例,熟练应用操作的同时,体会思想
3、初识 Spring
Spring 家族:

官网:https://spring.io

基础框架:Spring Framework ——是其他项目的根基

简化、加速开发:Spring Boot

分布式开发:Spring Cloud

Spring发展史:
01
01

学习路线:

02
02
4、Spring的核心Maven依赖
代码语言:javascript
复制
  <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.9</version>
</dependency>


<!-- JDBC -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.9</version>
</dependency>

参考:spring maven依赖 - 烟火里的尘埃 - 博客园 (cnblogs.com)


一、核心概念

代码书写现状:耦合度偏高

解决方案:使用对象时,在程序中不要主动使用 new 产生对象,而是转换为由外部提供对象。

1.1、IoC(Inversion of Control)控制反转

使用对象时,由程序主动 new 产生对象转换为由外部提供对象,此过程中对象的创建控制权也由程序转移到外部,这种思想称为控制反转。

  • 实质上是对象的创建控制权发生了变化
  • Spring 提供了一个容器,称为 IOC 容器,用l来充当 IOC 思想中的“外部”
  • IOC 容器负责对象的创建、初始化等一系列工作,被创建或管理的对象在 IoC 容器中统称为 Bean

目标:

  • 使用 IoC 容器管理 bean —— IOC
  • 在 IoC 容器中将有依赖关系的 bean 进行关系绑定 —— DI

最终效果:使用对象是不仅可以从 IOC 容器中获取,并且获取到的 bean 中已经绑定了所有的依赖关系。

IOC入门思路分析:

  1. 管理什么?(Service 与 Dao)
  2. 如何将被管理的对象告知 IoC 容器?(配置 applicationContext.xml 文件)
  3. 被管理的对象交给 IoC 容器,使用时如何获取到 IoC 容器?(接口)
  4. IoC 容器得到之后,如何从容器中获取 bean?(接口方法 getBean() )
  5. 使用 Spring 导入哪些坐标?(pom.xml)
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--    1.导入spring坐标spring-context,对应版本是5.2.10.RELEASE-->

    <!--    2.配置bean -->
    <!--    bean 标签表示配置 bean
            id 属性表示给 bean 起名字,在同一个上下文中不能重复
            class 属性表示给 bean 定义类型 -->
    <bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl"/>

</beans>
1.2、DI(Dependency Inject)依赖注入

在容器中建立 bean 与 bean 之间的依赖关系的整个过程称为依赖注入

DI 入门案例思路分析:

  1. 基于 IoC 管理 bean
  2. Service 中使用 new 形式创建的 Dao对象是否保留(否)
  3. Service 中需要的 Dao 对象如何进入到 Service 中(提供方法)
  4. Service 与 Dao 间的关系如何描述(配置)

二、IOC

2.1、bean 的配置
2.1.1、bean 的基础配置(id与class)
代码语言:javascript
复制
<!-- bean 标签表示配置 bean
     id 属性表示给 bean 起名字,在同一个上下文中不能重复
     class 属性表示给 bean 定义类型,后面接的是全路径类名 -->
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl"/>


<bean id="bookService" class="com.wlplove.service.impl.BookServiceImpl">
    <!--   配置service与dao的关系     -->
    <!--  property 标签表示配置当前 bean 的属性,需要其设置相应的set方法
          name 属性表示配置哪一个具体的属性,是当前属性的名称
          ref 属性表示参照哪一个 bean,是当前容器中存在的 bean -->
    <property name="bookDao" ref="bookDao"/>
</bean>

类别

描述

名称

bean

类型

属性

所属

beans 标签

功能

定义 Spring 核心容器管理的对象

格式

属性列表

id:bean 的 id,使用容器可以通过 id 值获取对应的 bean,在一个 IoC 容器中 id 值唯一class:bean 的类型,即配置的 bean 的全路径类名

范例

2.1.2、bean的别名配置(name)
代码语言:javascript
复制
<!-- name 属性表示别名 -->
    <bean id="bookService" name="service service2 bookEbi" class="com.wlplove.service.impl.BookServiceImpl"/>

类别

描述

名称

name

类型

属性

所属

bean 标签

功能

定义 bean 的别名,可定义多个,使用逗号、分号、空格分开

范例

2.1.3、bean的作用范围(scope)

控制创建的 bean 实体的数量(是否单例)

代码语言:javascript
复制
<!-- name 属性表示别名 -->
    <bean id="bookService" name="service service2 bookEbi" class="com.wlplove.service.impl.BookServiceImpl"/>

类别

描述

名称

scope

类型

属性

所属

bean 标签

功能

定义 bean 的作用范围,可选:singleton(单例,默认)、prototype(非单例)

范例

  • 为什么 bean 默认为单例?
  • 适合交给容器管理的 bean
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 不适合交给容器管理的 bean
    • 封装实体的域对象
2.2、实例化bean

bean 的实例化过程 主要解决两部分内容,分别是

  • bean是如何创建的
  • 实例化 bean 的三种方式,构造方法静态工厂实例工厂

在讲解这三种创建方式之前,我们需要先确认一件事:

bean 本质上就是对象,对象在 new 的时候会使用构造方法完成,那创建bean也是使用构造方法完成的。

2.2.1、构造方法(常用)

提供可访问的构造方法:

代码语言:javascript
复制
public interface BookDao {
    public void save();
}

public class BookDaoImpl implements BookDao {
    private BookDaoImpl() {
        System.out.println("book dao constructor is running ....");
    }

    public void save() {
        System.out.println("book dao save ...");
    }
}

配置:

代码语言:javascript
复制
<bean id="orderDao" class="com.wlplove.factory.OrderDaoFactory" 
    factory-method="getOrderDao"/>
  • 无参构造方法如果不存在,将抛出异常 BeanCreationException
2.2.2、静态工厂(了解)
代码语言:javascript
复制
//静态工厂创建对象
public class OrderDaoFactory {
    // 工厂方法
    public static OrderDao getOrderDao(){
        return new OrderDaoImpl();
    }
}

配置:

代码语言:javascript
复制
<bean id="userDao" class="com.wlplove.factory.UserDaoFactoryBean" />
2.2.3、实例工厂(了解)

FactoryBean(实用,较重要)

代码语言:javascript
复制
public class UserDaoFactoryBean implements FactoryBean {
    //代替原始实例工厂中创建对象的方法
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }

    public Class getObjectType() {
        return UserDao.class;
    }
}

配置:

代码语言:javascript
复制
2.3、bean的生命周期
  • bean 生命周期:bean 从创建到销毁的整个过程
代码语言:javascript
复制

小结:

(1)关于Spring中对bean生命周期控制提供了两种方式:

  • 在配置文件中的bean标签中添加init-methoddestroy-method属性
  • 类实现InitializingBeanDisposableBean接口,这种方式了解下即可。

(2)整个生命周期:

  • 初始化容器
    • 创建对象(内存分配)
    • 执行构造方法
    • 执行属性注入(set 注入)
    • 执行 bean 初始化方法
  • 使用 bean
    • 执行业务操作
  • 关闭/销毁容器
    • 执行 bean 销毁方法

(3)关闭容器的两种方式:

  • ConfigurableApplicationContext是ApplicationContext的子类
    • close()方法
    • registerShutdownHook()方法

三、DI

  1. 向一个类中传递数据的方式有几种:
    • 普通方法(set 方法)
    • 构造方法
  2. 依赖注入描述了在容器中简历 bean 与 bean 之间依赖关系的过程,如果 bean 运行需要的是数字或者字符串呢?
    • 简单类型(基本数据类型与 String)
    • 引用类型
  3. 依赖注入方式
    • setter 注入
      • 简单类型
      • 引用类型
    • 构造器注入
      • 简单类型
      • 引用类型
3.1、setter 注入

分为 简单类型引用类型 的注入

类中需要提供可访问的 set 方法

代码语言:javascript
复制
public void setConnectionNum(int connectionNum) {
    this.connectionNum = connectionNum;
}
public void setDatebaseName(String datebaseName) {
    this.datebaseName = datebaseName;
}

public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
}
public void setBookDao(BookDaoImpl bookDao) {
    this.bookDao = bookDao;
}

xml文件配置:注入标签需要用 “”

代码语言:javascript
复制
<!-- 简单类型 (通过 value 属性注入普通类型值) -->
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl">
  <property name="datebaseName" value="mysql"></property>
    <property name="connectionNum" value="100"></property>
</bean>

<!-- 引用类型 (通过 ref 属性注入引用类型值) -->
<bean id="bookService" class="com.wlplove.service.impl.BookServiceImpl">
  <property name="bookDao" ref="bookDao"></property>
    <property name="userDao" ref="userDao"></property>
</bean>
3.2、构造方法注入

同样地,分为 简单类型引用类型 的注入

类似于 setter 注入需要提供 set 方法,构造方法注入需要在类中提供构造方法:

代码语言:javascript
复制
public BookDaoImpl(int connectionNum, String datebaseName) {
    this.connectionNum = connectionNum;
    this.datebaseName = datebaseName;
}

public BookServiceImpl(BookDao bookDao, UserDaoImpl userDao) {
    this.bookDao = bookDao;
    this.userDao = userDao;
}

xml 文件中的配置:注入标签需要用 “”

代码语言:javascript
复制
<!-- 普通类型 (通过 value 属性注入引用类型值,name属性的值是构造方法形参的值)-->
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl">
    <constructor-arg name="connectionNum" value="666"></constructor-arg>
    <constructor-arg name="datebaseName" value="mysql"></constructor-arg>
</bean>

<bean id="userDao" class="com.wlplove.dao.impl.UserDaoImpl"></bean>

<!-- 引用类型 (通过 ref 属性注入引用类型值,name 属性的值是构造方法形参的值)-->
<bean id="bookService" class="com.wlplove.service.impl.BookServiceImpl">
    <constructor-arg name="bookDao" ref="bookDao"/>
    <constructor-arg name="userDao" ref="userDao"/>
</bean>
3.3、构造器注入的其他xml配置写法:
3.3.1、type 方式

配置中使用 constructor-arg 标签、type 属性,设置按构造方法的形参类型注入:

代码语言:javascript
复制
<!--type方式,解决 name 属性绑定了形参名称的问题-->
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl">
    <constructor-arg type="int" value="666"></constructor-arg>
    <constructor-arg type="java.lang.String" name="datebaseName" value="mysql"></constructor-arg>
</bean>
3.3.2、index 索引方式

配置中使用 constructor-arg 标签 index 属性设置按形参类型注入

代码语言:javascript
复制
<!-- index索引方式,解决参数类型重复问题,使用索引进行参数匹配对应 -->
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl">
    <constructor-arg index="0" value="6"></constructor-arg>
    <constructor-arg index="1" name="datebaseName" value="mysql"></constructor-arg>
</bean>
3.4、依赖注入方式的选择
  • 尽量选择 setter 注入:自己开发的模块推荐使用 setter 注入
  • 强制依赖使用构造器进行注入,但是使用 setter 注入时不进行注入的话会导致 null 现象出现
  • 可选依赖使用 setter 注入进行,灵活性强
  • Spring 框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  • 如果有必要可以两者也可同时使用,使用构造器注入完成强制依赖的注入,使用 setter 注入完成可选依赖的注入
  • 实际开发过程中还要根据实际情况分析,如果受控对象没有提供 setter 方法就必须使用构造器注入

四、自动装配

IoC 容器根据 bean 所依赖的资源在容器中自动查找并注入到 bean 中的过程称为自动装配。

自动装配有这么几种方式:

  • 按类型(常用)
  • 按名称
  • 按构造方法

自动装配时需要注意:

  • 需要注入属性的类中对应属性的 setter 方法不能省略
  • 被注入的对象必须要被 Spring 的 IoC 容器管理

自动装配只需要修改applicationContext.xml配置文件即可:

  1. 将 标签删除
  2. 在 标签中添加 autowire 属性
4.1、按类型 byType(推荐使用)

如果容器中只存在一个属性类型的 bean,则让属性自动装配。 如果存在多个,则会引发异常,这表明不能为该 bean 使用 byType 自动装配。 如果没有匹配的 bean,则不会发生任何事情(未设置属性)。

会自动在容器上下文中查找,和对象 set() 方法后面的值对应的 bean 的 id

需要保证所有 bean 的类型唯一,并且这个 bean 需要和自动注入的属性的 set() 方法的值一致。

按照类型自动装配时,如果在 Spring 的 IoC 容器中如果找到多个对象,会报 NoUniqueBeanDefinitionException 异常

代码语言:javascript
复制
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl"/>

<!-- autowire="byType" 属性按类型自动装配,甚至都可以不指定 id 属性 -->
<bean id="bookService" class="com.wlplove.service.impl.BookServiceImpl" autowire="byType"/>
4.2、按名称 byName(不建议使用)
  • 按照名称注入中的名称指的是什么?
03
03
  • bookDao 是 private 修饰的,外部类无法直接访问,只能通过属性的 set 方法进行访问
  • 对外部类来说,setBookDao 是方法名,去掉 set 后首字母小写是其属性名
    • 为什么是去掉 set 首字母小写?
    • 这个规则是 set 方法生成的默认规则,set方法的生成是把属性名首字母大写前面加set形成的方法名
  • 所以按照名称注入,其实是和对应的 set 方法有关,但是如果按照标准起名称,属性名和 set 对应的名是一致的
  • 如果按照名称去找对应的 bean 对象,找不到则注入 Null
  • 当某一个类型在 IoC 容器中有多个对象,按照名称注入只找其指定名称对应的 bean 对象,不会报错
代码语言:javascript
复制
<bean id="bookDao" class="com.wlplove.dao.impl.BookDaoImpl"/>

<!-- autowire="byName" 按名称自动装配 -->
<bean id="bookService" class="com.wlplove.service.impl.BookServiceImpl" autowire="byName"/>

最后对于依赖注入,需要注意一些其他的配置特征:

  1. 自动装配只能用于引用类型依赖注入,不能对简单类型进行操作
  2. 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
  3. 使用按名称装配时(byName)必须保障容器中具有指定名称的 bean,因变量名与配置耦合,不推荐使用
  4. 自动装配优先级低于 setter 注入与构造器注入,同时出现时自动装配配置失效

五、集合的注入

接下来的例子以 setter 注入为例进行说明,构造器注入也是类似的,将 标签修改为 即可。

5.1、注入数组类型数据

注入数组对象:

代码语言:javascript
复制
  <!-- name 属性设置将数据注入哪一个数组 -->
<property name="test_array">
    <!-- 此处 array 与 list 可以混用 -->
    <array>
        <value>123</value>
        <value>456</value>
        <value>789</value>
        <!--  引用类型可以这样写:-->
        <!--  <ref bean="beanId"></ref>-->
    </array>
</property>
5.2、注入 Set 类型数据

注入 Set 对象:

代码语言:javascript
复制
<property name="test_set">
    <set>
        <value>set_qwe1</value>
        <value>set_asd2</value>
        <value>set_zxc3</value>
        <!-- 重了会自动过滤 -->
        <value>set_zxc3</value>
    </set>
</property>
5.3、注入 List 类型数据

注入 List 对象:

代码语言:javascript
复制
  <property name="test_list">
    <!-- 此处 array 与 list 可以混用 -->
    <list>
        <value>list_qwe1</value>
        <value>list_asd2</value>
        <value>list_zxc3</value>
    </list>
</property>
5.4、注入 Map 类型数据

注入 Map 对象:

代码语言:javascript
复制
<property name="test_map">
    <map>
        <entry key="contry" value="China"></entry>
        <entry key="province" value="Gansu"></entry>
        <entry key="city" value="Lanzhou"></entry>
    </map>
</property>
5.5、注入 property 类型数据
代码语言:javascript
复制
    <property name="test_properties">
    <props>
        <prop key="contry">China</prop>
        <prop key="province">Gansu</prop>
        s<prop key="city">Lanzhou</prop>
    </props>
</property>

说明:

  • property 标签表示 setter 方式注入,构造方式注入 constructor-arg 标签内部也可以写、、、、标签
  • List 的底层也是通过数组实现的,所以 和 标签是可以混用
  • 集合中要添加引用类型,只需要把 标签改成 标签,但这种方式用的比较少

六、加载properties文件

第一步:xml文件中添加 context 命名空间:

原文件:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

通过复制修改过的新文件:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
+       http://www.springframework.org/schema/context
+       http://www.springframework.org/schema/context/spring-context.xsd
        ">
</beans>

第二步:在 xml 文件中添加,使用 context 空间加载指定 properties 文件

代码语言:javascript
复制
<context:property-placeholder location="jdbc.properties"/>

<!-- 可以加载多个文件 -->
<context:property-placeholder location="jdbc.properties,jdbc2.properties"/>
<!-- 或者用通配符加载所有文件 -->
<context:property-placeholder location="*.properties"/>

<!-- 用 system-properties-mode="NEVER" 禁用系统属性 -->
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>

<!-- 从类路径或 jar 包中搜索并加载所有 properties 文件 -->
<context:property-placeholder location="classpath*:*.properties"/>

最规范的写法可以这样:

代码语言:javascript
复制
<context:property-placeholder location="jdbc.properties"/>

<!-- 可以加载多个文件 -->
<context:property-placeholder location="jdbc.properties,jdbc2.properties"/>
<!-- 或者用通配符加载所有文件 -->
<context:property-placeholder location="*.properties"/>

<!-- 用 system-properties-mode="NEVER" 禁用系统属性 -->
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>

<!-- 从类路径或 jar 包中搜索并加载所有 properties 文件 -->
<context:property-placeholder location="classpath*:*.properties"/>

第三步:使用 ${} 读取加载的属性值

properties 的文件内容为:

代码语言:javascript
复制
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db
jdbc.username=root
jdbc.password=root

则这样写:

代码语言:javascript
复制
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>

七、容器的核心操作

7.1、创建容器

方式一:通过类路径来加载配置文件的方式创建容器

代码语言:javascript
复制
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

方式二:通过文件路径来加载配置文件的方式创建容器

代码语言:javascript
复制
ApplicationContext ctx = new FileSystemXmlApplicationContext("E:\\applicationContext.xml");

以上两种方式也都支持加载多个配置文件:

代码语言:javascript
复制
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml","bean2.xml");

ApplicationContext ctx = new FileSystemXmlApplicationContext("E:\\bean1.xml","E:\\bean2.xml");
7.2、获取 bean

方式一:使用 bean 名称获取

代码语言:javascript
复制
// 需要设置类型转换
BookDao bookDao = (BookDao) ctx.getBean("bookDao");

方式二:使用 bean 名称获取并指定类型

代码语言:javascript
复制
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);

方式三:使用 bean 类型获取

代码语言:javascript
复制
// 注意:参数是 类.class
BookDao bookDao = ctx.getBean(BookDao.class);
7.3、容器类层次结构
7.4、BeanFactory

BeanFactory是延迟加载,只有在获取 bean 对象的时候才会去创建

ApplicationContext 是立即加载,容器加载的时候就会创建 bean 对象

ApplicationContext 要想成为延迟加载,只需要按照如下方式进行配置,设置 “ lazy-init="true" ”

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"  lazy-init="true"/>
</beans>
7.5、核心容器内容总结
7.5.1、容器相关
  • BeanFactory 是 IoC 容器的顶层接口,初始化 BeanFactory 对象时,加载的 bean 延迟加载
  • ApplicationContext 接口是Spring容器的核心接口,初始化时 bean 立即加载
  • ApplicationContext 接口提供基础的 bean 操作相关方法,通过其他接口扩展其功能
  • ApplicationContext 接口常用初始化类
    • ClassPathXmlApplicationContext(常用)
    • FileSystemXmlApplicationContext
7.5.2、bean 相关
04
04
7.5.3、依赖注入相关
05
05
本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 学习之前
    • 一、核心概念
      • 二、IOC
        • 三、DI
          • 四、自动装配
            • 五、集合的注入
              • 六、加载properties文件
                • 七、容器的核心操作
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                http://www.vxiaotou.com