`
dowhathowtodo
  • 浏览: 805679 次
文章分类
社区版块
存档分类
最新评论

Windows Azure Storage 客户端Java版概览

 
阅读更多

我们发布了支持Windows Azure Blob、Queue和Table的存储客户端Java版。我们的目标是继续提高在编写使用Windows Azure Storage的云计算应用程序时的开发体验。这次发布的是微软提供支持的社区技术预览版(CTP)。因此,我们结合了来自客户和当前.NET类库论坛的反馈,来帮助我们创建更加无缝的既强大而又易用的应用程序接口(API)。本篇文章提供了这个库的概览,并且包含了一些在开发Java云计算应用程序时有助于理解的实现细节。另外,我们提供另外两篇文章,涉及BlobTable服务的一些特性和编程模型。

存储客户端Java版以Windows Azure SDK Java版的jar包分发(位置查看下面)。为了得到最好的开发体验,可以直接导入客户端的子包(com.microsoft.windowsazure.services.[blob|queue|table].client)。本文讨论客户端这一层。

相关的包根据服务可以分为:

公共包

com.microsoft.windowsazure.services.core.storage – 这个包包含所有存储的基本元素,比如存储账号(CloudStorageAccount),存储凭据(StorageCrendentials),重试策略,等等。

服务包

com.microsoft.windowsazure.services.blob.client – 这个包包含所有用于Windows Azure Blob服务的所有功能,比如Blob客户端(CloudBlobClient),Blob(CloudBlob)等等。

com.microsoft.windowsazure.services.blob.client – 这个包包含所有用于Windows Azure Queue服务的所有功能,比如Queue客户端(CloudQueueClient),Queue(CloudQueue)等等。

com.microsoft.windowsazure.services.table.client –这个包包含所有用于Windows Azure Table服务的所有功能,比如Table客户端(CloudTableClient),Queue(TableServiceEntity)等等。

服务

虽然本文描述上面这些包的基本概念,但还是值得简要总结一下每个客户端库的能力。Blob和Table分别有一些有意思的特性值得进一步讨论。为此,我们写了另一些文章,下面有链接。客户端API接口设计成容易使用且容易理解,同时为了适应更加复杂的场景,在必要的地方我们提供了可选的扩展点。

Blob

Blob API接口支持所有的基本操作(上传、下载、快照、设置/读取元数据和列目录),以及基本的Container操作(创建、删除和列出Blob)。但是我们更进一步,提供一些额外的便利功能,比如恢复下载、稀疏页Blob支持、简化的MD5场景以及简化访问条件。

为了更好地解释Blob API的这些特性,我们发布了另一篇详细讨论的文章。你也可以在另一篇文章《在Java中如何使用Blob存储服务》中看到更多的示例。

示例 – 上传文件至Block Blob

// 导入必要的包
import com.microsoft.windowsazure.services.blob.client.CloudBlobClient;
import com.microsoft.windowsazure.services.blob.client.CloudBlobContainer;
import com.microsoft.windowsazure.services.blob.client.CloudBlockBlob;
import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount;
 
// 初始化账号
CloudStorageAccount account = CloudStorageAccount.parse([ACCOUNT_STRING]);
 
// 创建blob客户端
CloudBlobClient blobClient = account.createCloudBlobClient();
 
// 获取新创建Container的引用
CloudBlobContainer container = blobClient.getContainerReference("mycontainer");
 
// 用本地文件创建或者覆盖名为myimage.jpg的blob
CloudBlockBlob blob = container.getBlockBlobReference("myimage.jpg");
File source = new File("c:\\myimages\\myimage.jpg");
blob.upload(new FileInputStream(source), source.length());

(注意:如果可以的话,最好总是提供上传数据的长度。如果长度未知的话,也可以指定为-1)

Table

Table API提供了一套精简的客户端接口,易于使用同时开放足够的扩展点来支持更高级的NoSQL场景。这包括了对POJO、基本HashMap的“属性包”(property bag)实体以及投影(projection)的内置支持。另外,我们提供了额外的可选扩展点,允许客户端自定义实体的序列化与反序列化,用以支持如用多个属性创建联合键这样更高级的场景。

由于上面谈到的一些特别场景,Table服务有一些不同于Blob和Queue服务的需求和功能。为了更好的解释这些功能并且提供更加全面的Table API概览,我们发表了另一篇深度的文章,涵盖了Table的总体设计、相关的最佳实践以及普通场景的代码示例。你也可以在《如何在Java中使用Table存储服务》中看到更多的示例。

示例 – 上传一个实体到Table中

// 导入必要的包
import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount;
import com.microsoft.windowsazure.services.table.client.CloudTableClient;
import com.microsoft.windowsazure.services.table.client.TableOperation;
 
// 从连接语句中获得存储账户
CloudStorageAccount storageAccount = CloudStorageAccount.parse([ACCOUNT_STRING]);
 
// 创建table客户端
CloudTableClient tableClient = storageAccount.createCloudTableClient();
         
// 创建一个新客户实体
CustomerEntity customer1 = new CustomerEntity("Harp", "Walter");
customer1.setEmail("Walter@contoso.com");
customer1.setPhoneNumber("425-555-0101");
 
// 创建将新客户加入到People表的操作
TableOperation insertCustomer1 = TableOperation.insert(customer1);
 
// 提交操作到Table服务
tableClient.execute("people", insertCustomer1);

Queue

Queue API对所有功能都包含了以REST方式开放的快捷方法:创建、修改和修改queue,增加、查看(peek)、获取、删除和更新消息,以及获取消息总数。下面是一个创建queue并增加一条消息的示例,另外您也可以参看《如何在Java中使用Queue存储服务》。

示例 – 创建Queue并加入一条消息

// 导入必要的包
import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount;
import com.microsoft.windowsazure.services.queue.client.CloudQueue;
import com.microsoft.windowsazure.services.queue.client.CloudQueueClient;
import com.microsoft.windowsazure.services.queue.client.CloudQueueMessage;
// 从连接语句中获取存储账号
CloudStorageAccount storageAccount = CloudStorageAccount.parse([ACCOUNT_STRING]);
 
// 创建queue客户端
CloudQueueClient queueClient = storageAccount.createCloudQueueClient();
 
// 获取queue的引用
CloudQueue queue = queueClient.getQueueReference("myqueue");
 
// 如果同名的queue不存在的话就创建一个
queue.createIfNotExist();
 
// 创建一条消息并加入到queue中
CloudQueueMessage message = new CloudQueueMessage("Hello, World");
queue.addMessage(message);

设计

在设计存储客户端Java版时,我们在开发全程都遵循一系列设计准则。为了体现我们对从事Azure开发的Java社区的承诺,我们就决定设计一套全新的Java开发人员熟悉的库。尽管基本的对象模型与.NET存储客户端有一些相似,但是在功能上、一致性上以及易用性上都有许多改进,满足高级用户与首次使用这些服务的用户的需求。

准则

  • 方便且高性能 - 默认的实现易于使用,但是我们总是可以支持对性能苛刻的场景。例如,Blob上传API为了身份验证要求有数据的长度。如果长度未知的话,用户也可以传入-1,然后库会在运行时去计算。但是,对于性能要求苛刻的程序,最好还是传入正确的字节数。
  • 用户拥有请求 - 我们提供了一些机制允许用户确定REST调用的确切次数、相关的请求标识、HTTP状态代码等等。(详细请查看下面在《对象模型》一节中讨论的《操作上下文》)我们也给每个可能发出REST请求的方法加上了注释@DoesServiceRequest。这些可以保证开发者能够,甚至比如在重试这种场景中,容易理解和控制应用程序产生的请求。在重试场景中,在操作成功前Java存储客户端可能生成多个请求。
  • 观感 –
    • 一致的命名。逻辑反义词用于互补的操作,比如上传和下载、创建和删除、获取与释放。
    • 遵循Java约定的get/set前缀,保留用于本地客户端属性。
    • 对于同一方法,最少的重载。一个方法只含有最少必需的参数,另一个重载方法包含所有的可选参数,这些可选参数可能为null。一个例外是列目录方法,它有两个重载方法来适应普遍的含有前缀的列目录场景。
  • 最少的API接口 – 为了使API接口尽量少,我们减少了多余的助手方法。比如,Blob包含一个使用Input/OutputStream的上传和下载方法。用户如果想要以文本或者字节的方式来处理数据,可以简单地传入相关的stream。
  • 看得见的高级特性 – 为了保持核心API简单和易于理解,高级特性可以通过使用请求选项或者可选参数实现。
  • 一致的异常处理 – 在请求发送给服务器前,任何异常都会被立刻抛出。在请求的执行阶段发生的任何异常会被包装进StorageException。
  • 一致 – 在开放的API接口和功能方面,对象都是一致的。比如Blob、Container和Queue都有exists()方法。

对象模型

Java存储客户端使用本地客户端对象来与服务器上的对象进行交互。我们提供了更多的特性来帮助确定是否该执行一项操作、如何执行以及提供当它执行时会发生什么的信息。(参看下面的《配置与执行》一节)

对象

存储账号

逻辑的起点是CloudStorageAccount。它包含了存储账号的访问点和凭据信息。然后这个账号对每个合适的服务创建逻辑服务客户端:CloudBlobClient、CloudQueueClient和CloudTableClient。CloudStorageAccount也提供了一个静态工厂方法来配置你的程序使用本地存储模拟器。该模拟器和Windows Azure SDK一起发行的。

CloudStorageAccount可以通过解析下面这种形式的账号字符串来创建:

"DefaultEndpointsProtocol=http[s];AccountName=<账户名>;AccountKey=<账户密钥>"

如果想指定该服务非默认的DNS访问点,你可以在连接字符串中包含一个或多个下面的语句:

“BlobEndpoint=<访问点> ”, “QueueEndpoint=<访问点> ”, “TableEndpoint=<访问点>

示例 – 通过账号字符串创建CloudStorageAccount

// 初始化账号
CloudStorageAccount account = CloudStorageAccount.parse([ACCOUNT_STRING]);

服务客户端

任何服务级的操作都存在于服务端中。默认的配置选项,比如超时、重试策略和其他服务相关的设置也存储在这里。这里的其他服务相关的设置指那些与客户端有关的对象所引用的设置。

例如:

  • 为blob服务打开存储分析功能:CloudBlobClient.uploadServiceProperties(properties)
  • 列出所有queue:CloudQueueClient.listQueues()
  • 对客户端相关的对象设置其默认的超时为30秒钟:Cloud[Blob|Queue|Table]Client.setTimeoutInMs(30 * 1000)

云对象

用户对指定的服务创建客户端后就可以直接开始使用那个服务的云对象。云对象有:CloudBlockBlob,CloudPageBlob,CloudBlobContainer和CloudQueue。它们每个都包含了与它们所代表的资源的交互方法。

下面是一些简单的示例,演示如何创建Blob Container、Queue和Table。参看《服务》一节关于如何与云对象交互的示例。

Blobs

// 获得先前创建的Container的引用
CloudBlobContainer container = blobClient.getContainerReference("mycontainer");
 
// 如果同名的Container不存在的话就创建一个
container.createIfNotExist()

Queues

//获得Queue的引用
CloudQueue queue = queueClient.getQueueReference("myqueue");
 
// 如何该Queue不存在的话就创建一个
queue.createIfNotExist();

Tables

注意:您可能注意到与Blob和Queue不同,Table服务不使用云对象来代表它。这是因为Table服务的特性决定的。这个特性在这篇文章中详细讨论。相应地,Table的操作通过CloudTableClient对象来完成。

// 如果该Table不存在就创建一个
tableClient.createTableIfNotExists("people");

配置与执行

在每个方法的含有最多参数的重载方法中,你会注意到根据服务的不同,有多出两到三个可选参数。这些参数都接受null值。这些参数允许用户只使用需要的一部分特性。例如,只想使用请求选项(RequestOptions)参数,只需要传入null值给访问条件(AccessCondition)参数和操作上下文(OperationContext)参数。为这些可选参数传入的这些对象提供用户一个简单的方法来决定是否要执行一项操作、如何执行操作以及获得当操作完成时它是如何执行的额外信息。

访问条件

访问条件(AccessCondition)对象的首要目的是决定一项操作是否该执行。Blob服务支持该对象。特别地,访问条件封装了Blob租契,以及If-Match、If-None-Match、If-Modified_Since和If-Unmodified-Since这几个HTTP头信息。一个访问条件可以跨操作复用,只要指定的条件仍然是有效的。例如,用户可能只想删除一个blob,如果它从上星期起就没有被修改过。通过使用访问条件对象,库会发送HTTP头信息If-Unmodified-Since到服务器上。如果条件不成立的话,服务器可能不会执行这项操作。另外,Blob租契可以通过访问条件对象来指定,这样只有持有合适租契的用户才能成功执行操作。

AccessCondition提供了方便的静态工厂方法来生成适用于大多数情况(IfMatch、IfNoneMatch、IfModifiedSince、IfNotModifiedSince和租契)的访问条件实例。但是你仍然可以通过调用实例上合适的setter来组合这些情况。

下面这个示例演示了如何使用访问条件来只上传元数据,当blob是某个特定的版本时。

blob.uploadMetadata(AccessCondition.generateIfMatchCondition(currentETag), null /* 请求选项 */, null/* 操作上下文 */);

下面是一些例子:

//如果给定的资源不是特定的版本的话执行操作:
AccessCondition.generateIfNoneMatchCondition(eTag)
 
//如果给定的资源在指定的日期后被修改后就执行操作:
AccessCondition. generateIfModifiedSinceConditionlastModifiedDate)
 
//如果给定的资源在指定的日期后未被修改就执行操作:
AccessCondition. generateIfNotModifiedSinceCondition(date)
 
//使用指定的租契执行操作(只适用于Blob):
AccessCondition. generateLeaseCondition(leaseID)
 
//如果资源在指定的日期后未被修改则使用指定的租契来执行操作:
AccessCondition condition = AccessCondition. generateLeaseCondition (leaseID);
condition. setIfUnmodifiedSinceDate(date);

RequestOptions(请求选项)

每个客户端都定义了服务相关的请求选项(RequestOptions),也就是BlobRequestOptions、QueueRequestOptions和TableRequestOptions。��们可以用来改变指定请求的执行行为。所有服务的请求选择都提供了为给定的操作指定不同的超时时限和重试策略。一些服务可能还提供了更多的选项。比如,BlobRequestOptions包含指定在上传Blob时使用的并发。请求选项是无状态的,可以跨操作复用。因此,程序通常会为不同类型的工作设计请求选项。例如,一个程序可能定义了用于并发上传大Blob的BlobRequestOptions,以及当上传元数据时使用较小超时时限的BlobRequestOptions.

下面这个示例演示了如何使用BlobRequestOptions来以8个并发操作、每项操作30秒超时时限来上传一个blob:

BlobRequestOptions options = new BlobRequestOptions();
 
// 设置并发请求数为 8
options.setConcurrentRequestCount(8);
 
// 设置超时时限为30秒
options.setTimeoutIntervalInMs(30 * 1000); 
 
blob.upload(new ByteArrayInputStream(buff),
     blobLength,
     null /* 访问条件 */,
     options,
     null /* 操作上下文 */);

操作上下文

操作上下文(OperationContext)用于提供关于一个指定的操作如何执行的相关信息。根据定义可以看出,这个对象是有状态的,所以不可以跨操作复用。另外,操作上下文定义了一个事件处理器,程序可以向这个事件处理器订阅服务器响应的提醒。有了这项功能,用户可以开始上传一个100GB的Blob,然后每成功上传4MB就更新进度条。

也话操作上下文最强大的功能是提供了用户查看操作执行得如何的能力。对于每个REST请求,操作上下文存储了RequestResult对象。它包含了诸如HTTP状态码、服务请求标识(Service Request ID)、开始/结束日期、etag以及可能发生的任何异常的引用这些相关的信息。这对于确定重试策略是否被调用以及一项操作是否多于一次尝试才成功特别有用。另外,当向微软反馈问题时,服务请求标识和开始/结束时间就很有用。

以下示例演示了如何使用操作上下文来显示最近一次操作的HTTP操作码。

OperationContext opContext = new OperationContext();
queue.createIfNotExist(null /* 请求选项 */, opContext);
System.out.println(opContext.getLastResult().getStatusCode());

重试策略

重试策略(Retry Policy)可以设计来估计对各种HTTP状态码是否进行重试。尽管默认的策略在遇到400这一类状态码时不会重试,用户可以通过创建自己的重试策略来改变这个行为。另外,重试策略对于每项操作是有状态的,这些在对给定的场景微调重试策略提供了更大的灵活性。

Java存储客户端自带了3个标准的可以用户自定义的重试策略。所有操作的默认重试策略是多达3次重试的指数级递增的时间差,如下所示:

new RetryExponentialRetry(  
    3000 /* minBackoff 以毫秒为单位 */,
    30000 /* delatBackoff以毫秒为单位*/,
    90000 /* maxBackoff以毫秒为单位 */,
    3 /* 最大重试次数 */);

按照上面的默认策略,重试大约会在以下时间发生:3,000毫秒,35,691毫秒和90,000毫秒。

如果想要增加重试次数,可以使用以下例子:

new RetryExponentialRetry(  
    3000 /* minBackoff以毫秒为单位 */,
    30000 /* delatBackoff以毫秒为单位 */,
    90000 /* maxBackoff以毫秒为单位 */,
    6 /* 最大重试次数 */);

根据上面的策略,重试大约会在以下时间发生:3,000毫秒,28,442毫秒,80,000毫秒,90,000毫秒,90,000毫秒和90,000毫秒。

注意:上面提供的时间只是粗略估算,因为指数策略带有+/-20%的随机范围,下面详述。

不重试(NoRetry) - 操作不会重试

线性重试(LinearRetry) - 这个重试策略执行指定次数的重试,重试之间的时间间隔是指定的固定时间。

指数重试(ExponentialRetry,默认策略) – 这个策略执行指定次数的重试,使用带有随机性的指数后退时间方式来决定重试之间的时间间隔。这项策略带有+/-20%的随机允许范围来平衡流量。

用户可以直接在服务客户端上为所有操作配置重试策略,或者对特定的方法调用在方法请求中指定一个。以下示例演示了如何配置一个客户端使用线性重试,重试之间有3秒的后退时间,对于给定的操作有最多3次额外的重试。

serviceClient.setRetryPolicyFactory(new RetryLinearRetry(3000,3));

或者

TableRequestOptions options = new TableRequestOptions();
options.setRetryPolicyFactory(new RetryLinearRetry(3000, 3));

自定义策略

重试策略有两个方面:策略自身和它相关的工厂。要实现一个自定义的接口,用户必须从抽象基类RetryPolicy中继承,并且实现相应的方法。另外,必须提供相关的工厂类。这个工厂类实现了RetryPolicyFactory接口,为每个逻辑操作生成不同的实例。为了简单起见,上面提到的策略也都实现了RetryPolicyFactory接口,但是仍然可以使用独立的两个类。

关于.NET存储客户端的注脚

在开发Java库时,我们看到了这套API可以更好地工作的许多重大改进。我们承诺会把这些改进带回.NET平台,同时还记得很多用户在当前的API上开发和部署了应用程序。所以敬请期待。

总结

我们投入了许多工作用于向工作在Windows Azure Storage的Java社区提供真正一流的开发体验。我们非常感谢所有来自客户、论坛的反馈,请继续给我们反馈。请随意在此留下你们的意见。

Joe Giardino

开发者

Windows Azure Storage

资源

获得Windows Azure SDK Java版

了解更多关于 Windows Azure Storage客户端Java版

了解更多关于 Windows
Azure Storage

本文翻译自:http://blogs.msdn.com/b/windowsazurestorage/archive/2012/03/05/windows-azure-storage-client-for-java-overview.aspx

分享到:
评论

相关推荐

    Azure Storage Explorer修改版

    这款修改版的Azure Storage Explorer尤其针对中国市场进行了优化,允许用户轻松连接到位于中国的Azure服务节点,确保了在中国境内的数据访问效率和稳定性。 在描述中提到的关键点是"Use Https"选项。在默认情况下,...

    Erasure Coding in Windows Azure Storage.pdf

    ### Erasure Coding in Windows Azure Storage #### 概述 Erasure Coding 在 Windows Azure Storage 中的应用是一种数据冗余技术,主要用于提高大规模分布式存储系统的可靠性和效率。该技术通过将原始数据分成多个...

    Windows Azure Storage

    Windows Azure Storage (WAS) is a cloud storage system that provides customers the ability to store seemingly limitless amounts of data for any duration of time. WAS customers have access to their data...

    Windows Azure Storage paper

    Windows Azure Storage (WAS) is a cloud storage system that provides customers the ability to store seemingly limitless amounts of data for any duration of time. WAS customers have access to their ...

    Windows Azure Storage示例源码

    【Windows Azure Storage 示例源码详解】 Windows Azure Storage是微软云平台提供的一项服务,它为开发者提供了可靠的云存储解决方案。此示例源码旨在帮助开发者更好地理解和应用Azure Storage服务,包括Blob、...

    Microsoft.WindowsAzure.Storage-7.2.1.0

    《Microsoft.WindowsAzure.Storage SDK 7.2.1.0:深入了解云存储的基石》 Microsoft.WindowsAzure.Storage 是微软为开发者提供的一个关键库,用于与 Azure 存储服务进行交互,包括 Blob 存储、表存储、队列存储以及...

    Windows Azure Storage(讲稿)

    ### Windows Azure 存储服务详解 #### 一、概述 Windows Azure 存储服务是微软提供的云端存储解决方案,旨在为企业和个人用户提供可靠、高效且可扩展的存储能力。它支持多种类型的存储需求,如文件存储(Blobs)、...

    Windows Azure的MSMQ--Queue Storage 例子

    在本文中,我们将深入探讨Windows Azure中的MSMQ(Microsoft Message Queuing)与Queue Storage的集成,以及如何通过具体的示例来理解这一概念。Queue Storage是Windows Azure存储服务的一部分,它提供了一个可伸缩...

    Windows Azure

    ### Windows Azure 应用程序服务平台详解 #### 一、Windows Azure 概述 Windows Azure 是由微软提供的公有云应用程序平台,在中国大陆区域的服务由世纪互联运营。该平台旨在为企业和个人开发者提供灵活、强大的...

    windows azure在xp下的安装程序

    Windows Azure,现称为Azure,是微软提供的一个全球分布式云计算平台,用于构建、部署和管理应用程序和服务。虽然XP操作系统(Windows XP)已经非常老旧,并且官方支持已在2014年结束,但有些开发者可能仍需在这样的...

    Windows Azure入门教学系列

    Windows Azure 入门教学系列 本教程系列旨在帮助初学者快速入门 Windows Azure 平台,学习如何创建、部署和管理云端应用程序。通过本系列教程,读者将了解 Windows Azure 的基本概念、开发工具和部署流程。 知识点...

    Azure SDK for Java 文档

    ### Azure SDK for Java 文档概览 #### 一、引言 随着云计算技术的迅猛发展,微软Azure作为全球领先的云服务平台之一,提供了丰富的服务和工具,以满足不同开发者的需求。对于Java开发者而言,Azure SDK for Java...

    走进云计算Windows Azure实战手记光盘

    全书共12章,内容包括云计算概论、云计算技术概观、初探Windows Azure、Windows Azure应用程序开发基础、Windows Azure应用程序开发:Table存储服务、Windows Azure应用程序开发:BLOB存储服务、Windows Azure应用...

    Microsoft Windows Azure Platform 白皮书

    Windows Azure 平台由三个主要组件构成:Windows Azure(计算服务)、SQL Azure(数据库服务)和Windows Azure Storage(存储服务)。这三者协同工作,为企业提供灵活、可扩展且高可用的云环境。 1. Windows Azure...

    70-583 Windows Azure 题库

    - **问题背景**:在尝试初始化一个Windows Azure Queue连接时,不断收到错误消息。 - **解决方法**:确保队列名称中不包含非法字符。 - **技术要点**: - **队列名称规则**:Windows Azure Queue服务对队列名称有...

    Windows Azure 概述

    Windows Azure 概述

    azure-storage-3.0.0.jar

    Microsoft Azure Storage所用jar包

    Windows Azure使用入门 第二课:建立自己的网站.pdf

    ### Windows Azure 使用入门:建立自己的网站 #### 一、Windows Azure 中的网站概念 随着互联网技术的发展,网站已经成为企业和组织对外展示形象、提供服务的重要窗口。传统的网站搭建往往需要自行购买服务器、...

    Windows Azure快速入门——为什么选择Windows Azure

    在接入网络方面,中国版 Windows Azure 数据中心直接连接到中国移动、中国电信、中国联通等主流运营商的骨干网络,为用户提供高速的网络访问体验。 #### 云计算的简化 虽然“云计算”这个概念听起来既时髦又复杂,...

Global site tag (gtag.js) - Google Analytics