标题

正如本章介绍中所讨论的,该org.springframework.beans.factory 包提供了用于管理和操作bean的基本功能,包括以编程方式。该org.springframework.context软件包添加了 ApplicationContext 扩展BeanFactory界面的界面,以及扩展其他界面以提供更多应用程序框架导向风格的附加功能。许多人使用ApplicationContext完全声明的方式,甚至没有以编程方式创建它,而是依赖支持类ContextLoader来自动实例化 ApplicationContextJava EE Web应用程序正常启动过程的一部分。
为了增强BeanFactory面向框架的风格的功能,上下文包还提供以下功能:

  • 通过MessageSource界面访问i18n风格的消息。
  • 通过ResourceLoader界面访问资源,如URL和文件。
  • 事件发布到即实现ApplicationListener接口的bean ,通过使用ApplicationEventPublisher接口。

加载多个(分层)上下文,通过HierarchicalBeanFactory接口允许每个上下文关注某个特定层,例如应用程序的Web层 。


一、使用MessageSource进行国际化

该ApplicationContext接口扩展了一个称为的接口MessageSource,因此提供了国际化(i18n)功能。Spring还提供了HierarchicalMessageSource可以分层解析消息的接口。这些接口一起为Spring特效消息解析提供了基础。这些接口上定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):用于从中检索消息的基本方法MessageSource。如果未找到指定语言环境的消息,则使用默认消息。使用MessageFormat标准库提供的功能,传入的任何参数都将成为替换值。
  • String getMessage(String code, Object[] args, Locale loc):与前面的方法基本相同,但有一点不同:不能指定默认消息; 如果无法找到消息,NoSuchMessageException则会抛出a。
  • String getMessage(MessageSourceResolvable resolvable, Locale locale):在前面的方法中使用的所有属性也都包含在名为的类中 MessageSourceResolvable,您可以使用该方法。

当一个ApplicationContext被加载时,它会自动搜索MessageSource 上下文中定义的一个bean。这个bean必须有名字messageSource。如果找到这样的一个bean,所有对前面方法的调用都被委托给消息源。如果找不到消息源,则ApplicationContext尝试查找包含具有相同名称的bean的父项。如果是这样,它使用该bean作为MessageSource。如果 ApplicationContext无法找到任何消息源,DelegatingMessageSource则会实例化一个空 以便能够接受对上面定义的方法的调用。

Spring提供了两个MessageSource实现,ResourceBundleMessageSource并且 StaticMessageSource。两者都是HierarchicalMessageSource为了做嵌套消息传递而实现的。这StaticMessageSource是很少使用,但提供了编程方式来添加消息到源。在ResourceBundleMessageSource被示出在下面的例子:

<beans><bean id="messageSource"class="org.springframework.context.support.ResourceBundleMessageSource"><property name="basenames"><list><value>format</value><value>exceptions</value><value>windows</value></list></property></bean>
</beans>

在这个例子中,假设你在你的类路径中定义了三个资源包,分别叫做format,exceptions和windows。任何解析消息的请求都将以通过ResourceBundles解析消息的JDK标准方式进行处理。出于示例的目的,假设上述两个资源包文件的内容是……

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

MessageSource下一个示例显示了执行功能的程序。请记住,所有ApplicationContext实现都是MessageSource 实现,因此可以转换为MessageSource接口。

public static void main(String[] args) {MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");String message = resources.getMessage("message", null, "Default", null);System.out.println(message);
}

从上述程序产生的输出将是…

Alligators rock!

总之,这个MessageSource被定义在一个叫做的文件中beans.xml,它存在于你的类路径的根目录下。该messageSourcebean定义是指通过它的一些资源包的basenames属性。这是在列表中传递的三个文件basenames属性存在于你的classpath根目录的文件,被称为format.properties,exceptions.properties和 windows.properties分别。
下一个示例显示传递给消息查找的参数; 这些参数将转换为字符串并插入查找消息中的占位符。

<beans><!-- this MessageSource is being used in a web application --><bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"><property name="basename" value="exceptions"/></bean><!-- lets inject the above MessageSource into this POJO --><bean id="example" class="com.foo.Example"><property name="messages" ref="messageSource"/></bean></beans>
public class Example {private MessageSource messages;public void setMessages(MessageSource messages) {this.messages = messages;}public void execute() {String message = this.messages.getMessage("argument.required",new Object [] {"userDao"}, "Required", null);System.out.println(message);}
}

调用该execute()方法的结果输出将是…

The userDao argument is required.

关于国际化(i18n),Spring的各种MessageSource 实现遵循与标准JDK相同的区域设置分辨率和回退规则 ResourceBundle。总之,和继续该示例messageSource先前定义的,如果你想解析British(消息en-GB)语言环境中,您将创建文件名为format_en_GB.properties,exceptions_en_GB.properties和 windows_en_GB.properties分别。

通常,区域设置解析由应用程序的周围环境管理。在这个例子中,(英国)消息将被解析的地区是手动指定的。

#在exceptions_en_GB.properties中
argument.required = Ebagum lad,我认为需要{0}参数。
public static void main(final String[] args) {MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");String message = resources.getMessage("argument.required",new Object [] {"userDao"}, "Required", Locale.UK);System.out.println(message);
}

从上述程序运行得到的输出将是…

Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用该MessageSourceAware界面来获取MessageSource已定义的任何参考 。任何在ApplicationContext实现MessageSourceAware接口的bean中定义 MessageSource的bean都会在创建和配置bean时注入应用程序上下文。

作为一种选择ResourceBundleMessageSource,Spring提供了一个 ReloadableResourceBundleMessageSource类。该变体支持相同的包文件格式,但比标准的基于JDK的ResourceBundleMessageSource实现更灵活 。特别是,它允许从任何Spring资源位置(而不仅仅是从类路径)读取文件,并支持热重载bundle属性文件(同时有效地缓存它们)。

二、标准和自定义事件

ApplicationContext通过ApplicationEvent 类和ApplicationListener接口提供事件处理。如果实现ApplicationListener接口的beanA 部署到上下文中,则每次 ApplicationEvent发布到该ApplicationContextbean时,都会通知该beanA。实质上,这是标准Observer设计模式。
Spring提供了以下标准事件:

Event Explanation
ContextRefreshedEvent 在ApplicationContext上下文中初始化或者刷新。例如,使用ConfigurableApplicationContext中的refresh()方法。这里的“初始化”意味着所有的bean都被加载,检测并激活后处理器bean,单例被预先实例化,并且该ApplicationContext对象已准备好使用。只要上下文尚未关闭,刷新可以多次触发,前提是所选内容ApplicationContext实际上支持“热”刷新。例如,XmlWebApplicationContext支持热点刷新,但GenericApplicationContext不支持 。
ContextStartedEvent 在ApplicationContext启动时发布,使用ConfigurableApplicationContext上下文中的start()方法。这里的“开始”意味着所有的Lifecycle bean都会收到明确的启动信号。通常,此信号用于在显式停止后重新启动Bean,但它也可用于启动尚未配置为自动启动的组件,例如尚未启动初始化的组件。
ContextStoppedEvent 在ApplicationContext停止时发布,使用ConfigurableApplicationContext上下文中的stop()方法。这里“停止”意味着所有的Lifecycle bean都会收到明确的停止信号。停止的上下文可以通过start()呼叫重新启动 。
ContextClosedEvent 在ApplicationContext关闭时发布,使用界面close()上的方法 ConfigurableApplicationContext。这里的“关闭”意味着所有的单例bean被销毁。封闭的环境达到其生命的尽头; 它不能被刷新或重新启动。
RequestHandledEvent 一个特定于web的事件,告知所有bean HTTP请求已被服务。此事件在请求完成后发布。此事件仅适用于使用Spring的Web应用程序DispatcherServlet。

您还可以创建和发布自己的自定义事件。这个例子演示了一个扩展Spring ApplicationEvent基类的简单类:

public class BlackListEvent extends ApplicationEvent {private final String address;private final String test;public BlackListEvent(Object source, String address, String test) {super(source);this.address = address;this.test = test;}// accessor and other methods...
}
  • 发布
    要发布自定义ApplicationEvent,请调用ApplicationEventPublisher中的publishEvent()方法 。通常这是通过创建一个实现ApplicationEventPublisherAware并注册为Spring bean 的类来完成的 。以下示例演示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {private List<String> blackList;private ApplicationEventPublisher publisher;public void setBlackList(List<String> blackList) {this.blackList = blackList;}public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {this.publisher = publisher;}public void sendEmail(String address, String text) {if (blackList.contains(address)) {BlackListEvent event = new BlackListEvent(this, address, text);publisher.publishEvent(event);return;}// send email...}
}

在配置时,Spring容器将检测到该EmailService实现 ApplicationEventPublisherAware并将自动调用 setApplicationEventPublisher()。实际上,传入的参数将是Spring容器本身; 你只是通过它的ApplicationEventPublisher接口与应用程序上下文进行 交互。

  • 接收
    要接收该定制ApplicationEvent,请创建一个实现 ApplicationListener并将其注册为Spring bean的类。以下示例演示了这样一个类:
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {private String notificationAddress;public void setNotificationAddress(String notificationAddress) {this.notificationAddress = notificationAddress;}public void onApplicationEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...}
}

请注意,ApplicationListener它通常用您的自定义事件的类型进行参数化BlackListEvent。这意味着该onApplicationEvent()方法可以保持类型安全,避免任何向下转换的需要。您可以根据需要注册许多事件侦听器,但请注意,默认情况下事件侦听器会同步接收事件。这意味着publishEvent()方法会阻塞,直到所有听众完成处理事件。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文内部运行。如果需要另一个事件发布策略,请参考Spring ApplicationEventMulticaster界面的javadoc 。

以下示例显示了用于注册和配置上述每个类的bean定义:

<bean id="emailService" class="example.EmailService"><property name="blackList"><list><value>known.spammer@example.org</value><value>known.hacker@example.org</value><value>john.doe@example.org</value></list></property>
</bean><bean id="blackListNotifier" class="example.BlackListNotifier"><property name="notificationAddress" value="blacklist@example.org"/>
</bean>

综合起来,当调用bean 的sendEmail()方法时emailService,如果有任何应该被列入黑名单的电子邮件,BlackListEvent则会发布类型的自定义事件 。这个blackListNotifierbean被注册为一个 ApplicationListener并且因此接收到BlackListEvent,在此时它可以通知适当的各方。

Spring的事件机制被设计为在同一个应用程序上下文中的Spring bean之间进行简单的通信。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration项目为构建轻量级,面向模式的事件驱动架构提供完全支持, 该架构基于着名的Spring编程模型。

三、基于注释的事件监听器

从Spring 4.2开始,可以通过@EventListener注释在托管bean的任何公共方法上注册事件侦听器。该BlackListNotifier可改写如下:

public class BlackListNotifier {private String notificationAddress;public void setNotificationAddress(String notificationAddress) {this.notificationAddress = notificationAddress;}@EventListenerpublic void processBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...}
}

正如您在上面看到的,方法签名再次声明它监听的事件类型,但是这次使用灵活的名称并且不实现特定的监听器接口。只要实际事件类型在其实现层次结构中解析泛型参数,事件类型也可以通过泛型进行缩小。

如果你的方法应该监听几个事件,或者如果你想要根本没有参数定义它,事件类型也可以在注释本身上指定:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {...
}

也可以通过condition注释的属性添加额外的运行时过滤,该过滤器定义了一个SpEL表达式,该表达式应匹配以实际调用特定事件的方法。

例如,如果test事件的属性等于foo:我们的通知器可以被重写为仅被调用:

@EventListener(condition = "#blEvent.test == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {// notify appropriate parties via notificationAddress...
}

每个SpEL表达式再次评估一个专用的上下文。下表列出了可用于上下文的项目,以便可以将它们用于条件事件处理:
Table 8. Event SpEL available metadata

Name Location Description Example
Event root object The actual ApplicationEvent root.event
root.event root.event 用于调用目标的参数(如数组) 用于调用目标的参数(如数组)
Argument name Argument name 任何方法参数的名称。如果由于某种原因名称是不可用(例如,没有调试信息),参数名称也是在现有的#a<#arg> 地方#arg代表的说法指数(从0开始)。 blEvent或者#a0(也可以使用#p0或#p<#arg>标记作为别名)

注意#root.event,即使您的方法签名实际引用了已发布的任意对象,也可以访问基础事件。

如果您需要发布一个事件作为处理另一个事件的结果,只需更改方法签名以返回应该发布的事件,如下所示:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress and// then publish a ListUpdateEvent...
}

异步监听器不支持这个东西
这种新方法将为上述方法的ListUpdateEvent每个BlackListEvent处理发布一个新的方法。如果您需要发布多个事件,则只需返回一些Collection事件。

四、异步监听器

如果您希望特定的侦听器异步处理事件,simply reuse the regular @Async support:

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {// BlackListEvent is processed in a separate thread
}

使用异步事件时请注意以下限制:
如果事件监听器抛出Exception它不会传播给调用者,请检查AsyncUncaughtExceptionHandler更多细节。
这种事件监听器不能发送回复。如果您需要发送另一个事件作为处理结果,请注入ApplicationEventPublisher以手动发送事件。

五、(Ordering listeners)监听器的顺序

如果您需要在另一个之前调用侦听器,只需将该@Order 注释添加到方法声明中:

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...
}

六、通用事件

您也可以使用泛型来进一步定义事件的结构。考虑 创建实际实体的类型 EntityCreatedEvent<T>在哪里T。您可以创建以下侦听器定义以仅接收EntityCreatedEvent以下内容 Person:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {...
}

由于类型擦除,只有当被触发的事件解析了事件侦听器过滤的泛型参数时(这是类似的class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ }),这才会起作用 。

在某些情况下,如果所有事件都遵循相同的结构(这应该是上述事件的情况),则这可能变得非常乏味。在这种情况下,您可以实现ResolvableTypeProvider以引导框架超出运行时环境所提供的范围:

public class EntityCreatedEvent<T>extends ApplicationEvent implements ResolvableTypeProvider {public EntityCreatedEvent(T entity) {super(entity);}@Overridepublic ResolvableType getResolvableType() {return ResolvableType.forClassWithGenerics(getClass(),ResolvableType.forInstance(getSource()));}
}

This works not only for ApplicationEvent but any arbitrary object that you’d send as an event.

七、方便地访问低水平资源

为了最佳使用和理解应用程序上下文,用户通常应该熟悉Spring的Resource抽象,如“ 资源 ”一章所述 。

应用程序上下文是一个ResourceLoader可以用来加载Resources 的应用程序上下文。A Resource本质上是JDK类的功能更丰富的版本java.net.URL,实际上,在适当Resource的java.net.URL地方包装一个实例。A Resource可以以透明的方式从几乎任何位置获取底层资源,包括类路径,文件系统位置,任何可用标准URL描述的地方以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源来自特定且适合于实际应用程序上下文类型。

您可以配置一个部署到应用程序上下文中的bean来实现特殊的回调接口,ResourceLoaderAware在初始化时自动调用回应用程序上下文本身作为 ResourceLoader。您还可以公开Resource用于访问静态资源的类型属性; 它们将像其他任何属性一样被注入到它中。您可以将这些Resource属性指定为简单的String路径,并依赖PropertyEditor由上下文自动注册的特殊JavaBean ,以便Resource在部署Bean时将这些文本字符串转换为实际对象。

提供给ApplicationContext构造函数的位置路径实际上是资源字符串,并且以简单形式适当地处理特定的上下文实现。ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL中加载定义,而不管实际的上下文类型如何。

八、方便的Web应用程序的ApplicationContext实例化

您可以ApplicationContext通过使用例如a来声明性地创建实例 ContextLoader。当然,您也可以ApplicationContext使用其中一种ApplicationContext实现方式编程创建实例。
您可以ApplicationContext使用ContextLoaderListener如下注册一个:

<context-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听者检查contextConfigLocation参数。如果该参数不存在,则侦听器将/WEB-INF/applicationContext.xml用作默认值。当参数确实存在时,侦听器使用预定义的分隔符(逗号,分号和空白)来分隔字符串,并将这些值用作应用程序上下文将被搜索的位置。也支持Ant风格的路径模式。例子是/WEB-INF/Context.xml名称以“Context.xml”结尾的所有文件,驻留在“WEB-INF”目录中,并且/WEB-INF/*/*Context.xml对于“WEB-INF”的任何子目录中的所有这些文件。

九、将Spring ApplicationContext部署为Java EE RAR文件

可以将Spring ApplicationContext部署为RAR文件,将上下文及其所有必需的bean类和库JAR封装到Java EE RAR部署单元中。这相当于引导了一个独立的ApplicationContext,它只是在Java EE环境中托管,能够访问Java EE服务器设施。RAR部署是部署无头WAR文件的场景中更自然的选择,实际上,WAR文件没有任何HTTP入口点,仅用于在Java EE环境中引导Spring ApplicationContext。

RAR部署非常适合不需要HTTP入口点但仅包含消息端点和预定作业的应用程序上下文。在这种情况下,Bean可以使用应用服务器资源,例如JTA事务管理器和JNDI绑定的JDBC DataSources和JMS ConnectionFactory实例,也可以通过Spring的标准事务管理和JNDI和JMX支持工具向平台的JMX服务器注册。应用程序组件还可以通过Spring的TaskExecutor抽象与应用程序服务器的JCA WorkManager进行交互。

要将Spring ApplicationContext简单部署为Java EE RAR文件:将所有应用程序类打包到RAR文件中,该文件是具有不同文件扩展名的标准JAR文件。将所有必需的库JAR添加到RAR归档的根目录中。添加一个“META-INF / ra.xml”部署描述符(如SpringContextResourceAdapters javadoc中所示)和相应的Spring XML bean定义文件(通常为“META-INF / applicationContext.xml”),并放弃生成的RAR文件到您的应用程序服务器的部署目录。

这种RAR部署单元通常是独立的; 它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块。与基于RAR的ApplicationContext的交互通常通过它与其他模块共享的JMS目标发生。例如,基于RAR的ApplicationContext也可以调度一些作业,对文件系统中的新文件(或诸如此类)作出反应。如果需要允许从外部进行同步访问,则可以导出RMI端点,这当然可以由同一台机器上的其他应用程序模块使用。

好啦,ApplicationContext的附加功能,就是这么多东西,平时多看看,在用的时候,就能得心应手。

SpringFramework核心技术一(IOC:ApplicationContext的附加功能)相关推荐

  1. SpringFramework核心技术一(IOC:自定义一个bean的本质)

    自定义一个bean的本质 本篇讨论如何自定义一个Bean和这个Bean的本质. 一.生命周期回调简介 要与bean生命周期的容器管理进行交互,可以实现Spring InitializingBean和D ...

  2. 2.15 Spring Framework 5.x 之ApplicationContext附加功能

    1.15 附加功能ApplicationContext 正如章节介绍中所讨论的,该org.springframework.beans.factory 包提供了管理和操作bean的基本功能,包括以编程方 ...

  3. 从Spring起,Java EE 6必须具备哪些附加功能?

    我是一名高级Java开发人员,必须研究应用程序架构师选择的技术. 我最多只能表达对特定技术的看法,不能做出/影响技术选择的决定. 因此,在我的正式项目中,我别无选择从Spring迁移到JavaEE6或 ...

  4. 从Spring开始,Java EE 6必须具备哪些附加功能?

    我是一名高级Java开发人员,必须研究应用程序架构师选择的技术. 我最多只能表达对特定技术的看法,不能做出/影响技术选择的决定. 因此,在我的正式项目中,我别无选择从Spring迁移到JavaEE6或 ...

  5. django 1.8 官方文档翻译:9-2 本地特色附加功能

    "本地特色"附加功能 由于历史因素,Django自带了django.contrib.localflavor – 各种各样的代码片段,有助于在特定的国家地区或文化中使用.为了便于维护 ...

  6. 玩转 MATLAB 附加功能/硬件支持包安装

    解决不能下载附加功能.硬件支持包的使用方法. MATLAB 作为一个开放的科学计算.系统仿真与设计.以及软件产品开发的工程平台,其生态圈有非常丰富的内容. 尽管时至今日 MathWorks 已在最新的 ...

  7. 蓝牙信标有哪些附加功能?蓝牙信标的工业用途知多少?

    蓝牙信标是一款基于低功耗蓝牙BLE广播协议的硬件设备,兼容Ibeacon协议或者eddystone,可通过信标的接收信号强度指标(RSSI)与信标在广播数据包中广播的数据这两个主要技术手段来构建一个有 ...

  8. 组件分享之后端组件——基于Golang实现的database/sql附加功能组件dbr

    组件分享之后端组件--基于Golang实现的database/sql附加功能组件dbr 背景 近期正在探索前端.后端.系统端各类常用组件与工具,对其一些常见的组件进行再次整理一下,形成标准化组件专题, ...

  9. 探索【Stable-Diffusion WEBUI】的附加功能:图片缩放抠图

    文章目录 (零)前言 (一)附加功能(图片处理) (1.1)处理对象(Source) (1.2)缩放(Scale) (1.2.1)缩放设置 (1.2.2)缩放模型(Upscaler) (1.2.3)G ...

最新文章

  1. elasticsearch组件的安装启动,测试等(windows)
  2. Linux显示某文件中有关某字符串的信息
  3. HTML技巧100例(三)
  4. http method
  5. Chrome 开发者工具 workspace 的概念
  6. 《iVX 高仿美团APP制作移动端完整项目》01 标题需求分析思路及制作流程
  7. KPI到底是个啥东东,如何设置KPI,我想你需要懂这些
  8. java 泛型 多态_Java 多态
  9. 包含html语言的超链接标记的网页_HTML是什么?
  10. 安卓模拟器切换横屏之后怎么返回竖屏
  11. 递归法:快速掌握递归核心方法
  12. 闲谈 Kubernetes 的主要特性和经验分享
  13. vofuria的开发(4)更换目标图片(target)
  14. 推荐20款每个人都会用到的办公软件
  15. 测试tf卡读写速度软件,【图】测试工具,看看你的导航使用的TF卡速度有多快?...
  16. java web 打包工具_java web 项目打包(war 包)并部署
  17. 怎样用ipad录制游戏视频?ipad如何录制视频?
  18. creo中公制单位的设定问题(永久设定)
  19. 优化策略5 Label Smoothing Regularization_LSR原理分析
  20. 万物互联背景下的边缘计算安全需求与挑战

热门文章

  1. 时序数据取样方法_数据科学的抽样方法
  2. 数据结构与算法学习笔记-树和二叉树
  3. 贪吃蛇大作战【C++游戏】
  4. CocosCreator之KUOKUO总结微信排行榜子域工程深坑
  5. Git中smart Checkout与force checkout
  6. 跟着小梅哥初学FPGA ,vivdao开发平台,二选一多路选择器。
  7. android电话录音(整理自网络)
  8. 分布式计算的基本原理
  9. 后台控制游戏开关_今天购买这些开关游戏
  10. 干货 | Dart 并发机制详解