面向方面编程

编程语言

面向方面的编程 (AOP) 由来已久,但是直到最近才开始获得 Microsoft .NET 开发社区的青睐。任何一项新技术的采纳往往都会产生对该技术及其使用的误解,AOP 也不例外。为了澄清对 AOP 的误解,本文以及下列代码示例将举例说明一个 AOP 的实际应用程序和一些 AOP 能够解决的常见问题。以使用 Web 服务的应用程序为例,我们将扩展该 Web 服务返回的对象功能,方法是通过一个 AOP 框架对返回的对象应用新的方面。这些方面将为此功能独立生成对象模型,从而脱离 WSDL。

简介
在考虑对象及对象与其他对象的关系时,我们通常会想到继承这个术语。例如,定义某一个抽象类— Dog 类。在标识相似的一些类但每个类又有各自的独特行为时,通常使用继承来扩展功能。举例来说,如果标识了 Poodle,则可以说一个 Poodle 是一个 Dog,即 Poodle继承了 Dog。到此为止都似乎不错,但是如果定义另一个以后标识为 Obedient Dog 的独特行为又会怎样呢?当然,不是所有的 Dogs 都很驯服,所以 Dog 类不能包含 obedience 行为。此外,如果要创建从 Dog继承的 Obedient Dog 类,那么 Poodle 放在这个层次结构中的哪个位置合适呢?Poodle 是一个 Dog,但是 Poodle 不一定 obedient;那么 Poodle 是继承于 Dog 还是 Obedient Dog 呢?都不是,我们可以将驯服看作一个方面,将其应用到任何一类驯服的 Dog,我们反对以不恰当的方式强制将该行为放在 Dog 层次结构中。
在软件术语中,面向方面的编程能够独立于任何继承层次结构而应用改变类或对象行为的方面。然后,在运行时或编译时应用这些方面。举一个关于AOP的示例,然后进行描述,说明起来比较容易。首先,定义四个关键的 AOP 术语,这很重要,因为我将反复使用它们:
接合点 (Joinpoint) — 代码中定义明确的可识别的点。
切点 (Pointcut) — 通过配置或编码指定接合点的一种方法。
通知 (Advice) — 表示需要执行交叉切割动作的一种方法
混入 (Mixin) — 通过将一个类的实例混入目标类的实例引入新行为。
为了更好地理解这些术语,可以将接合点看作程序流中定义好的一点。说明接合点的一个很好的示例是:在代码调用一个方法时,发生调用的那一点被认为是一个接合点。切点用于指定或定义希望在程序流中截获的接合点。切点还包含一个通知,该通知在到达接合点时发生。因此,如果在一个调用的特定方法上定义一个切点,那么在调用该方法或接合点时,AOP 框架将截获该切点,同时还将执行切点的通知。通知有几种类型,但是最常见的情况是将其看作要调用的另一个方法。在调用一个带有切点的方法时,要执行的通知将是另一个要调用的方法。要调用的这个通知或方法可以是对象中被截获的方法,也可以是混入的另一个对象中的方法。我们将在后面进一步解释混入。
利弊
一种常见的误解是认为 AOP 是截获,事实并非如此。但是,它确实运用了截获来应用通知以及组合行为。有一些 .NET 代码示例通过 ContextBoundObject 以一种 AOP 翻版风格说明截获。可是用 ContextBoundObject 来说明截获并不合适,因为使用这种方法的先决条件是所有需要进行截获的类都必须从 ContextBoundObject继承。像 ContextBoundObject 这样带有先决条件的 AOP 方法会带来需求产生的负面影响,所以在 AOP 中被视为重方法,应该避免使用。重方法在系统中遗留的大量“足迹”会潜在地影响每个类,阻碍将来更改或修改系统的功能。
我创建了一个名为 Encase 的轻量型框架。用“轻量型”这个术语的意义是整体上对系统没有影响。系统的不同部分仍然受 AOP 影响,但是选择轻量型框架并应用良好的编程实践可以减轻大部分负面问题。Encase 框架的用途是简化切点、混入和方面组合。开发人员能够通过代码在 Encase 中应用方面,从而代替大多数其他轻量型 AOP框架使用的配置文件(例如 XML)。
比较
为了说明更实用的使用 AOP 的方法,我们将创建一个应用程序,从名为 ContactService.Service 的 Web 服务接收 people对象的集合。在 .NET 开发中使用 Web 服务的最常见方法是调用返回 XML 的 Web 服务,该服务通过框架自动反序列化为一个对象。这些对象仅包含数据而不包含任何行为。在 .NET Framework 2.0 中,通过使用 partial 关键字并创建行为,能够对这些自动代码生成的对象添加功能。但是在一些 Web 服务或代理对象之间重用某个特定行为时仍然存在一个问题。如前所述,多数情况下,共享的公共行为将包含在一个抽象类中,其他所有类从该类继承。但是,我们不能使 Web 服务对象继承功能。借此良机,通过这个问题说明 AOP 功能如何强大。
我们的应用程序用于显示联系人信息。最初它的用途是显示信息,但是需要添加某些行为。为了查看代码示例,我们需要创建一个称为 TheAgileDeveloper.ContactService 的虚拟目录。该目录必须指向 TheAgileDeveloper.ContactService 项目在本地计算机上的位置。
注 通过 http://localhost/TheAgileDeveloper.ContactService 可以访问此项目,这一点很重要。
图 1. 应用程序屏幕快照
应用程序有一个视图,它是一个名为 MainForm 的 WinForm,用于显示左侧 ListView 中 Web 服务返回的联系人对象。选定一个联系人时,名字、姓氏和 Web 页将显示在右侧的文本框中。载入 MainForm 时,它调用 ServiceManager 类来获取联系人信息。下列 ServiceManager 类乍看起来似乎没有添加任何值,只不过在窗体和 Web 服务之间添加了另一层。但是,它的价值就在于提供了一个在 Web 服务中添加新功能的位置,而不用重复代码。另一个优点是,它将 Web 服务的“足迹”抽象出来,并从整个应用程序中移除出去。
Public Class ServiceManager
Public Shared Function GetAllContacts() As ContactService.Contact()
Dim service As ContactService.Service = New ContactService.Service
Dim contacts() As ContactService.Contact = service.GetAllContacts
Return contacts
End Function
Public Shared Sub SaveContact(ByVal contact As ContactService.Contact)
Dim service As ContactService.Service = New ContactService.Service
service.SaveContact(contact)
End Sub
End Class
请查看 TheAgileDeveloper.Client 项目中的 Reference.vb 文件。它是在导入 ContactService 的 Web 引用时通过 wsdl.exe 创建的。它从 WSDL 自动生成以下 Contact 类。
'
[Namespace]:=http://。。。。。/TheAgileDeveloper.ContactService/Service1 _ )> _
Public Class Contact
'
Public Id As Integer
'
Public FirstName As String
'
Public LastName As String
'
Public WebSite As String
End Class
注意,Contact对象只处理数据,而且我们不想以任何方式编辑该代码,因为 wsdl.exe 会为我们自动生成,所以下一次生成时更改将丢失。我想引入行为,这样就能够通过调用名为 Save 的方法保存对象,这很容易通过一个混入 来完成。混入 是多继承的翻版,只是它有局限性,例如只能混入接口实现。我们使用的 Encase 框架包含一个 Encaser 类,它负责接收并包装一个对象。包装对象的行为实际上意味着创建新的对象,在本例中就是新的 Contact 对象,它包含配置的混入和切点。
为了创建允许在 Contact对象上调用 Save 方法的混入,需要指定一个接口,我称之为 ISavable。实际混入对象的就是 ISavable 接口。我们需要在另一个称为 ContactSave 的新类中实现该接口。
Public Interface ISaveable
Sub Save()
End Interface
Public Class ContactSave
Implements ISavable
Public Contact As ContactService.Contact
Public Sub Save() Implements ISavable.Save
ServiceManager.SaveContact(Me.Contact)
End Sub
End Class
在我们的应用程序中,混入 Contact对象中 ContactSave 实现的适当位置是 ServiceManager。我们能够混入这个行为,但是不更改任何客户端代码(即,MainForm),因为应用混入后,结合 Contact 和 ContactSave 的新 Contact对象仍然保持为最初的 Contact 类型。以下代码是经过更改的 ServiceManager 的 GetAllContacts 方法,它处理混入行为。
Public Shared Function GetAllContacts() As ContactService.Contact()
Dim service As ContactService.Service = New ContactService.Service
Dim contacts() As ContactService.Contact = service.GetAllContacts
'//Wrap each contact object
For i As Integer = 0 To contacts.Length-1
'//Create a new instance of the
'//encaser responsible for wrapping our object
Dim encaser As encaser = New encaser
'//Add mixin instance of ContactSave
Dim saver As ContactSave = New ContactSave
encaser.AddMixin(saver)
'//Creates a new object with
'//Contact and ContactSave implementations
Dim wrappedObject As Object = encaser.Wrap(contacts(i))
'//Assign our new wrapped contact object
'//to the previous contact object
contacts(i) = DirectCast(wrappedObject, _
ContactService.Contact)
'//Notice the wrapped object is still the same type
'//Assign the new wrapped Contact object to
'//target field of the ContactSave mixed in
saver.Target = contacts(i)
Next
Return contacts
End Function
软件特点
每个框架应用切点、通知或方面的方法都是独特的,但是其目的和概念是相同的。在本文示例中,Encaser 包装一个对象时真正进行的操作是,通过 System.Reflection.Emit 命名空间中的类产生 MSIL 代码,从而随时创建新的 Contact 类型。新 Contact 类型派生于 Contact 类,它仍然共享类型,但是新包装的对象还持有对 ContactSave 对象的引用,后者是我们混入的。ISavable.Save 方法在新的 Contact对象上实现,因此在调用 Save 时,它实际上将调用委托给混入的 ContactSave 对象。这样做的优点是能够将新的 Contact对象转换为在任何混入对象上实现的任何接口。
图 2. 包装对象的 UML 图表。
您或许在想,通过 .NET Framework 2.0 的部分类语言功能,可以在另一个 partial 类中添加 Save 行为。这是可能实现的,但是本文没有采用这种方法,这是为了使代码与 .NET Framework 1.x 的其他版本向后兼容。既然有部分语言功能,那么在正常情况下,前面的示例也就不需要使用混入 了。但是混入 仍然很有价值,因为通过它,开发人员可以混入可重用的对象行为,这些对象可以源自其他不相关的对象层次结构,它实现的功能比 partial 类更多。在使用 partial 关键字时,是在同一个类或类型中添加代码,只不过物理位置不同。下一个混入示例说明的添加行为不只特定于 Contact 类,而是一个名为 FieldUndoer 的可重用类。FieldUndoer 实现了 IUndoable 接口,允许已修改的对象恢复为原来的状态。
Public Interface IUndoable
ReadOnly Property HasChanges() As Boolean
Sub Undo()
Sub AcceptChanges()
End Interface
HasChanges 属性表示,如果发生了更改,Undo 将对象恢复为原来的状态,AcceptChanges 接收对象的当前更改,因此任何时候再调用 Undo 时都会恢复为上一次接收更改的状态。如果该接口是在一个部分类中实现的,那么在每个希望包含该行为的类中,都必须不厌其烦地重复实现这三个方法。作为一个实用主义编程人员,我尝试坚持“一次且仅一次代码”原则,所以我永远不想重复任何代码,复制和粘贴越少越好。通过使用混入,我能够重用实现 IUndoable 的 FieldUndoer对象。在 ServiceManager 中我又混入了这个新功能。所有客户端代码仍然不知道新的混入,而且也不需要更改,除非需要使用 IUndoable 接口。更改 MainForm 中的 Contact对象,然后单击“撤消”,测试这个行为。
Public Shared Function GetAllContacts() As ContactService.Contact()
Dim service As ContactService.Service = New ContactService.Service
Dim contacts() As ContactService.Contact = service.GetAllContacts
'//Wrap each contact object
For i As Integer = 0 To contacts.Length-1
'//Create a new instance of the encaser
'//responsible for wrapping our object
Dim encaser As encaser = New encaser
'//Add mixin instance of ContactSave
Dim saver As ContactSave = New ContactSave
encaser.AddMixin(saver)
'//Add mixin instance of FieldUndoer
Dim undoer As FieldUndoer = New FieldUndoer
encaser.AddMixin(undoer)
'//Creates a new object with Contact
'//and ContactSave implementations
Dim wrappedObject As Object = encaser.Wrap(contacts(i))
'//Assign our new wrapped contact object
'//to the previous contact object
contacts(i) = DirectCast(wrappedObject, _
ContactService.Contact)
'//Notice the wrapped object is still the same type
'//Assign the new wrapped Contact object to target fields
saver.Target = contacts(i)
undoer.Target = contacts(i)
Next
Return contacts
End Function
组合行为
混入还只是冰山一角。真正让 AOP 声名鹊起的功能是组合混入行为。以使用新 Contact对象为例,在调用 ISavable.Save 方法时,客户端代码还需要调用 IUndoable.AcceptChanges 方法,以便在下一次调用 IUndoable.Undo 时恢复到所保存的上一次更改。在这个小的 MainForm 中浏览和添加该对象很容易,但是在任何比用户界面大得多的系统中对该规则编码将是一项繁重的任务。您需要查找所有调用 Save 方法的情况,然后添加另一个对 AcceptChanges 的调用。而且在创建新代码的过程中,开发人员也需要牢记,在每次调用 Save 时都添加这个新功能。这很快就会产生级联效应,很容易会破坏系统稳定姓,引入一些难于跟踪的 bug。而使用面向方面的编程则能够组合这些方法。指定一个切点和通知,在调用 Save 方法时,Contact对象将自动调用后台的 AcceptChanges。
为了在应用程序中实现组合,需要在 ServiceManager 中再添加一行代码。我们在加入 FieldUndoer 混入后添加这行代码。
'//Specify join point save, execute the AcceptChanges method
AddPointcut 方法通过几个不同的签名进行重载,这为指定切点提供了更大的灵活性。我们调用的 AddPointcut 接收了一个字符串类型的接合点名,它表示为 Save 方法,然后又接收了一个名为 AcceptChanges 的方法作为执行的通知。要查看这是否起作用,可以分别在 FieldUndoer.AcceptChanges 方法和 ContactSave.Save 方法前设置一个断点。单击 MainForm 上的 Save 按钮将截获接合点,您首先将中断至通知 — 即 AcceptChanges 方法。通知执行后将执行 Save 方法。
这个简单的示例说明如何添加贯穿整个应用程序的新行为,其功能强大无比。尽管有此功能,但它不仅仅是添加功能的一种很好的新方法。在众多优点中,只有几个涉及代码重用,以及通过简化新需求带来的系统进化来改进系统的可维护性。与此同时,误用 AOP 会对系统的可维护性造成显著的负面效应,因此了解使用 AOP 的时机和方法很重要。
AOP 走了多远?
将 AOP 用于多数大型系统或关键的生产系统还不完全成熟,但是随着语言支持的提高,AOP 的应用将更容易。另外,提高支持也是新的软件开发范例,例如利用面向方面的编程的软件工厂。在 .NET 领域中有几种可用的 AOP 框架,每个框架都有其自己的方法、正面属性和负面属性。
Encase — 本代码示例中的 Encase 框架只是一个工具,帮助您快速了解并运行 AOP,以及理解 AOP 背后的概念。Encase 在运行时期间应用能够单独添加到对象的方面。
Aspect# — 一个针对 CLI 的 AOP 联合兼容框架,提供声明和配置方面的内置语言。
RAIL — RAIL 框架在虚拟机JIT 类时应用方面。
Spring.。。。 — 流行的 Java Spring 框架的一个 .NET 版本。在下一个版本中将实现 AOP。
Eos — 用于 C# 的一个面向方面的扩展。
小结
本文的目的是说明一种比常规日志记录或安全实例更实用的应用 AOP 的新方法。正确应用使用 AOP 会带来很多优点,甚至能够帮助您完成常规编程选项所不能完成的成果任务。我强烈推荐您在 internet 上搜寻大量可用资源,以指导应用 AOP 的方法和场景时机。
全国各地天气预报查询

上海市

  • 市辖区
  • 云南省

  • 临沧市
  • 云南省

  • 丽江市
  • 云南省

  • 保山市
  • 云南省

  • 大理白族自治州
  • 云南省

  • 德宏傣族景颇族自治州
  • 云南省

  • 怒江傈僳族自治州
  • 云南省

  • 文山壮族苗族自治州
  • 云南省

  • 昆明市
  • 云南省

  • 昭通市
  • 云南省

  • 普洱市
  • 云南省

  • 曲靖市
  • 云南省

  • 楚雄彝族自治州
  • 云南省

  • 玉溪市
  • 云南省

  • 红河哈尼族彝族自治州
  • 云南省

  • 西双版纳傣族自治州
  • 云南省

  • 迪庆藏族自治州
  • 内蒙古自治区

  • 乌兰察布市
  • 内蒙古自治区

  • 乌海市
  • 内蒙古自治区

  • 兴安盟
  • 内蒙古自治区

  • 包头市
  • 内蒙古自治区

  • 呼伦贝尔市
  • 内蒙古自治区

  • 呼和浩特市
  • 内蒙古自治区

  • 巴彦淖尔市
  • 内蒙古自治区

  • 赤峰市
  • 内蒙古自治区

  • 通辽市
  • 内蒙古自治区

  • 鄂尔多斯市
  • 内蒙古自治区

  • 锡林郭勒盟
  • 内蒙古自治区

  • 阿拉善盟
  • 北京市

  • 市辖区
  • 吉林省

  • 吉林市
  • 吉林省

  • 四平市
  • 吉林省

  • 延边朝鲜族自治州
  • 吉林省

  • 松原市
  • 吉林省

  • 白城市
  • 吉林省

  • 白山市
  • 吉林省

  • 辽源市
  • 吉林省

  • 通化市
  • 吉林省

  • 长春市
  • 四川省

  • 乐山市
  • 四川省

  • 内江市
  • 四川省

  • 凉山彝族自治州
  • 四川省

  • 南充市
  • 四川省

  • 宜宾市
  • 四川省

  • 巴中市
  • 四川省

  • 广元市
  • 四川省

  • 广安市
  • 四川省

  • 德阳市
  • 四川省

  • 成都市
  • 四川省

  • 攀枝花市
  • 四川省

  • 泸州市
  • 四川省

  • 甘孜藏族自治州
  • 四川省

  • 眉山市
  • 四川省

  • 绵阳市
  • 四川省

  • 自贡市
  • 四川省

  • 资阳市
  • 四川省

  • 达州市
  • 四川省

  • 遂宁市
  • 四川省

  • 阿坝藏族羌族自治州
  • 四川省

  • 雅安市
  • 天津市

  • 市辖区
  • 宁夏回族自治区

  • 中卫市
  • 宁夏回族自治区

  • 吴忠市
  • 宁夏回族自治区

  • 固原市
  • 宁夏回族自治区

  • 石嘴山市
  • 宁夏回族自治区

  • 银川市
  • 安徽省

  • 亳州市
  • 安徽省

  • 六安市
  • 安徽省

  • 合肥市
  • 安徽省

  • 安庆市
  • 安徽省

  • 宣城市
  • 安徽省

  • 宿州市
  • 安徽省

  • 池州市
  • 安徽省

  • 淮北市
  • 安徽省

  • 淮南市
  • 安徽省

  • 滁州市
  • 安徽省

  • 芜湖市
  • 安徽省

  • 蚌埠市
  • 安徽省

  • 铜陵市
  • 安徽省

  • 阜阳市
  • 安徽省

  • 马鞍山市
  • 安徽省

  • 黄山市
  • 山东省

  • 东营市
  • 山东省

  • 临沂市
  • 山东省

  • 威海市
  • 山东省

  • 德州市
  • 山东省

  • 日照市
  • 山东省

  • 枣庄市
  • 山东省

  • 泰安市
  • 山东省

  • 济南市
  • 山东省

  • 济宁市
  • 山东省

  • 淄博市
  • 山东省

  • 滨州市
  • 山东省

  • 潍坊市
  • 山东省

  • 烟台市
  • 山东省

  • 聊城市
  • 山东省

  • 菏泽市
  • 山东省

  • 青岛市
  • 山西省

  • 临汾市
  • 山西省

  • 吕梁市
  • 山西省

  • 大同市
  • 山西省

  • 太原市
  • 山西省

  • 忻州市
  • 山西省

  • 晋中市
  • 山西省

  • 晋城市
  • 山西省

  • 朔州市
  • 山西省

  • 运城市
  • 山西省

  • 长治市
  • 山西省

  • 阳泉市
  • 广东省

  • 东莞市
  • 广东省

  • 中山市
  • 广东省

  • 云浮市
  • 广东省

  • 佛山市
  • 广东省

  • 广州市
  • 广东省

  • 惠州市
  • 广东省

  • 揭阳市
  • 广东省

  • 梅州市
  • 广东省

  • 汕头市
  • 广东省

  • 汕尾市
  • 广东省

  • 江门市
  • 广东省

  • 河源市
  • 广东省

  • 深圳市
  • 广东省

  • 清远市
  • 广东省

  • 湛江市
  • 广东省

  • 潮州市
  • 广东省

  • 珠海市
  • 广东省

  • 肇庆市
  • 广东省

  • 茂名市
  • 广东省

  • 阳江市
  • 广东省

  • 韶关市
  • 广西壮族自治区

  • 北海市
  • 广西壮族自治区

  • 南宁市
  • 广西壮族自治区

  • 崇左市
  • 广西壮族自治区

  • 来宾市
  • 广西壮族自治区

  • 柳州市
  • 广西壮族自治区

  • 桂林市
  • 广西壮族自治区

  • 梧州市
  • 广西壮族自治区

  • 河池市
  • 广西壮族自治区

  • 玉林市
  • 广西壮族自治区

  • 百色市
  • 广西壮族自治区

  • 贵港市
  • 广西壮族自治区

  • 贺州市
  • 广西壮族自治区

  • 钦州市
  • 广西壮族自治区

  • 防城港市
  • 新疆维吾尔自治区

  • 乌鲁木齐市
  • 新疆维吾尔自治区

  • 伊犁哈萨克自治州
  • 新疆维吾尔自治区

  • 克孜勒苏柯尔克孜自治州
  • 新疆维吾尔自治区

  • 克拉玛依市
  • 新疆维吾尔自治区

  • 博尔塔拉蒙古自治州
  • 新疆维吾尔自治区

  • 吐鲁番市
  • 新疆维吾尔自治区

  • 和田地区
  • 新疆维吾尔自治区

  • 哈密市
  • 新疆维吾尔自治区

  • 喀什地区
  • 新疆维吾尔自治区

  • 塔城地区
  • 新疆维吾尔自治区

  • 巴音郭楞蒙古自治州
  • 新疆维吾尔自治区

  • 昌吉回族自治州
  • 新疆维吾尔自治区

  • 自治区直辖县级行政区划
  • 新疆维吾尔自治区

  • 阿克苏地区
  • 新疆维吾尔自治区

  • 阿勒泰地区
  • 江苏省

  • 南京市
  • 江苏省

  • 南通市
  • 江苏省

  • 宿迁市
  • 江苏省

  • 常州市
  • 江苏省

  • 徐州市
  • 江苏省

  • 扬州市
  • 江苏省

  • 无锡市
  • 江苏省

  • 泰州市
  • 江苏省

  • 淮安市
  • 江苏省

  • 盐城市
  • 江苏省

  • 苏州市
  • 江苏省

  • 连云港市
  • 江苏省

  • 镇江市
  • 江西省

  • 上饶市
  • 江西省

  • 九江市
  • 江西省

  • 南昌市
  • 江西省

  • 吉安市
  • 江西省

  • 宜春市
  • 江西省

  • 抚州市
  • 江西省

  • 新余市
  • 江西省

  • 景德镇市
  • 江西省

  • 萍乡市
  • 江西省

  • 赣州市
  • 江西省

  • 鹰潭市
  • 河北省

  • 保定市
  • 河北省

  • 唐山市
  • 河北省

  • 廊坊市
  • 河北省

  • 张家口市
  • 河北省

  • 承德市
  • 河北省

  • 沧州市
  • 河北省

  • 石家庄市
  • 河北省

  • 秦皇岛市
  • 河北省

  • 衡水市
  • 河北省

  • 邢台市
  • 河北省

  • 邯郸市
  • 河南省

  • 三门峡市
  • 河南省

  • 信阳市
  • 河南省

  • 南阳市
  • 河南省

  • 周口市
  • 河南省

  • 商丘市
  • 河南省

  • 安阳市
  • 河南省

  • 平顶山市
  • 河南省

  • 开封市
  • 河南省

  • 新乡市
  • 河南省

  • 洛阳市
  • 河南省

  • 漯河市
  • 河南省

  • 濮阳市
  • 河南省

  • 焦作市
  • 河南省

  • 省直辖县级行政区划
  • 河南省

  • 许昌市
  • 河南省

  • 郑州市
  • 河南省

  • 驻马店市
  • 河南省

  • 鹤壁市
  • 浙江省

  • 丽水市
  • 浙江省

  • 台州市
  • 浙江省

  • 嘉兴市
  • 浙江省

  • 宁波市
  • 浙江省

  • 杭州市
  • 浙江省

  • 温州市
  • 浙江省

  • 湖州市
  • 浙江省

  • 绍兴市
  • 浙江省

  • 舟山市
  • 浙江省

  • 衢州市
  • 浙江省

  • 金华市
  • 海南省

  • 三亚市
  • 海南省

  • 三沙市
  • 海南省

  • 儋州市
  • 海南省

  • 海口市
  • 海南省

  • 省直辖县级行政区划
  • 湖北省

  • 十堰市
  • 湖北省

  • 咸宁市
  • 湖北省

  • 孝感市
  • 湖北省

  • 宜昌市
  • 湖北省

  • 恩施土家族苗族自治州
  • 湖北省

  • 武汉市
  • 湖北省

  • 省直辖县级行政区划
  • 湖北省

  • 荆州市
  • 湖北省

  • 荆门市
  • 湖北省

  • 襄阳市
  • 湖北省

  • 鄂州市
  • 湖北省

  • 随州市
  • 湖北省

  • 黄冈市
  • 湖北省

  • 黄石市
  • 湖南省

  • 娄底市
  • 湖南省

  • 岳阳市
  • 湖南省

  • 常德市
  • 湖南省

  • 张家界市
  • 湖南省

  • 怀化市
  • 湖南省

  • 株洲市
  • 湖南省

  • 永州市
  • 湖南省

  • 湘潭市
  • 湖南省

  • 湘西土家族苗族自治州
  • 湖南省

  • 益阳市
  • 湖南省

  • 衡阳市
  • 湖南省

  • 邵阳市
  • 湖南省

  • 郴州市
  • 湖南省

  • 长沙市
  • 甘肃省

  • 临夏回族自治州
  • 甘肃省

  • 兰州市
  • 甘肃省

  • 嘉峪关市
  • 甘肃省

  • 天水市
  • 甘肃省

  • 定西市
  • 甘肃省

  • 平凉市
  • 甘肃省

  • 庆阳市
  • 甘肃省

  • 张掖市
  • 甘肃省

  • 武威市
  • 甘肃省

  • 甘南藏族自治州
  • 甘肃省

  • 白银市
  • 甘肃省

  • 酒泉市
  • 甘肃省

  • 金昌市
  • 甘肃省

  • 陇南市
  • 福建省

  • 三明市
  • 福建省

  • 南平市
  • 福建省

  • 厦门市
  • 福建省

  • 宁德市
  • 福建省

  • 泉州市
  • 福建省

  • 漳州市
  • 福建省

  • 福州市
  • 福建省

  • 莆田市
  • 福建省

  • 龙岩市
  • 西藏自治区

  • 山南市
  • 西藏自治区

  • 拉萨市
  • 西藏自治区

  • 日喀则市
  • 西藏自治区

  • 昌都市
  • 西藏自治区

  • 林芝市
  • 西藏自治区

  • 那曲市
  • 西藏自治区

  • 阿里地区
  • 贵州省

  • 六盘水市
  • 贵州省

  • 安顺市
  • 贵州省

  • 毕节市
  • 贵州省

  • 贵阳市
  • 贵州省

  • 遵义市
  • 贵州省

  • 铜仁市
  • 贵州省

  • 黔东南苗族侗族自治州
  • 贵州省

  • 黔南布依族苗族自治州
  • 贵州省

  • 黔西南布依族苗族自治州
  • 辽宁省

  • 丹东市
  • 辽宁省

  • 大连市
  • 辽宁省

  • 抚顺市
  • 辽宁省

  • 朝阳市
  • 辽宁省

  • 本溪市
  • 辽宁省

  • 沈阳市
  • 辽宁省

  • 盘锦市
  • 辽宁省

  • 营口市
  • 辽宁省

  • 葫芦岛市
  • 辽宁省

  • 辽阳市
  • 辽宁省

  • 铁岭市
  • 辽宁省

  • 锦州市
  • 辽宁省

  • 阜新市
  • 辽宁省

  • 鞍山市
  • 重庆市

  • 重庆市

  • 市辖区
  • 陕西省

  • 咸阳市
  • 陕西省

  • 商洛市
  • 陕西省

  • 安康市
  • 陕西省

  • 宝鸡市
  • 陕西省

  • 延安市
  • 陕西省

  • 榆林市
  • 陕西省

  • 汉中市
  • 陕西省

  • 渭南市
  • 陕西省

  • 西安市
  • 陕西省

  • 铜川市
  • 青海省

  • 果洛藏族自治州
  • 青海省

  • 海东市
  • 青海省

  • 海北藏族自治州
  • 青海省

  • 海南藏族自治州
  • 青海省

  • 海西蒙古族藏族自治州
  • 青海省

  • 玉树藏族自治州
  • 青海省

  • 西宁市
  • 青海省

  • 黄南藏族自治州
  • 黑龙江省

  • 七台河市
  • 黑龙江省

  • 伊春市
  • 黑龙江省

  • 佳木斯市
  • 黑龙江省

  • 双鸭山市
  • 黑龙江省

  • 哈尔滨市
  • 黑龙江省

  • 大兴安岭地区
  • 黑龙江省

  • 大庆市
  • 黑龙江省

  • 牡丹江市
  • 黑龙江省

  • 绥化市
  • 黑龙江省

  • 鸡西市
  • 黑龙江省

  • 鹤岗市
  • 黑龙江省

  • 黑河市
  • 黑龙江省

  • 齐齐哈尔市