上一章我们通过实现一个服务对如何扩展GeoServer有了一定的了解,但是,对于为何要这样做并没有说明,本章我们重点来说说GeoServer的结构,下图来自GeoServer官网(希望没有侵权),它很好的揭示了GeoServer处理请求的全过程。
我们说GeoServer使用Spring框架来构建,这里就可以看到Spring的使用,虚线框中的Restlet就是用Spring引入系统的,每个服务包的“applicationContext.xml”文件里都包含了描述Route映射的信息,例如WMS就有如下片段:
<bean id="wmsURLMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="alwaysUseFullPath" value="true"/> <property name="mappings"> <props> <prop key="/wms">dispatcher</prop> <!-- prop key="/wms/putstyles">putStylesWrapper</prop--> <prop key="/wms/*">dispatcher</prop> </props> </property> </bean>
它将形如“/wms/”的请求映射到“dispatcher”对象,而这个对象就是上一章调试的重点“org.geoserver.ows.Dispatcher”。所以,如果我们希望我们的服务也和WMS有相似的处理方式,我们就需要在自己的配置文件里加上类似的一段。
本章我准备先两个方面入手讲解GeoServer是如何处理OWS请求的。首先介绍GeoServer的运行时环境,包括对象是如何创建并且引用的,以及SpringFramework的配置体系。然后以Dispatcher类的处理算法入手,重点介绍扩展点,熟悉了扩展点,我们就可以对GeoServer的OWS处理进行扩展,开发符合我们要求的应用。
搞清楚处理机制后,我会谈谈GeoServer的资源API。这个部分主要罗列了GeoServer的一些接口,以及它们代表的概念。
一 OWS请求处理
我们知道applicationContext.xml文件是SpringFramework的配置文件,许多对象都在这个文件中定义,但是为什么是这个文件呢,它们之间有没有关系呢。
让我们先来看看“web-app”的“web.xml”文件,中间有这样一段
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/applicationContext.xml classpath*:/applicationSecurityContext.xml</param-value> </context-param>
可以看到,这里定义了classpath下面的applicationContext.xml文件和applicationSecurityContext.xml作为配置文件,系统运行起来后SpringFramework会扫描classpath下面所有的applicationContext.xml文件和applicationSecurityContext.xml文件,创建里面定义的对象。所以只要我们提供的包里面包含这两个文件中的任意一个,就可以把自定义的对象装载到系统运行时中,同时还可以引用已经创建的对象。
main包中的applicationContext.xml文件定义了许多基础对象,很重要的有:catalog,geoServer和dispatcher,这几个对象将是我们后面讲解的重点。
上面这幅图描述了GeoServer处理OWS请求的步骤,右边的红色方框是每个步骤的扩展点,每个扩展点都对应一个抽象类或者接口。可以通过实现这些类和接口来扩展GeoServer的功能。下面我们来介绍Dispatcher类处理OWS请求的步骤,每一步都有具体的函数相对应。
第一步 解析HTTP请求参数(见org.geoserver.ows.Dispatcher.init(Request))
程序并不会直接采用HTTP的参数,它们是字符串键值对(KVP)或者是XML字符串,程序会先把它们转换成相应的对象。类型org.geoserver.ows.Request是代表请求参数的类型,它包含转换后的KVP Map就已经将原来的字符串变成了对象。
转换参数的工作由Parser来完成。举例说明:参数“BBOX=-180,-90,180,90”的值是字符串“-180,-90,180,90”,经过org.geoserver.wfs.kvp.BBoxKvpParser的处理,就变成了org.geotools.geometry.jts.ReferencedEnvelope的对象,显然它更容易使用。
设想,如果我们有一个参数需要传递数据表,我们可以这样构造参数格式:TABLE=cell11,cell12,cell13|cell21,cell22,cell23|cell31,cell32,cell33。当程序遇到这个参数的时候我们就可以使用我们开发的Parser对象来将它转换成一个表对象,供后续代码使用。我们只需要将这个Parser对象放到applicationContext.xml文件立即可,就像下面这样
<bean id="tableKvpParser" class="examples.TableKvpParser"/>
所有Parser都必须从org.geoserver.ows.KvpParser继承。
第二步 匹配服务对象(见org.geoserver.ows.Dispatcher.service(Request))
程序根据参数SERVICE和VERSION的值来选择合适的服务,例如:http://www.dummy.com/geoserver/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetCapabilities,这个URL是在请求1.1.1版本的WMS服务。所以注册服务时需要指明服务ID和版本号,下面是wms服务的注册代码
<bean id="wmsService2" class="org.geoserver.wms.DefaultWebMapService"> <constructor-arg ref="wms"/> </bean> <alias name="wmsService2" alias="webMapService"/> <bean id="wmsServiceDescriptor" class="org.geoserver.platform.Service"> <constructor-arg index="0" value="wms"/> <constructor-arg index="1" ref="wmsService2"/> <constructor-arg index="2" value="1.1.1"/> <constructor-arg index="3"> <list> <value>Capabilities</value> <value>GetCapabilities</value> <value>DescribeLayer</value> <value>GetFeatureInfo</value> <value>GetLegendGraphic</value> <value>GetMap</value> <value>Map</value> <value>reflect</value> <value>kml</value> <value>GetStyles</value> </list> </constructor-arg> </bean>
来看Service类的构造函数
public Service(String id, Object service, Version version, List<String> operations)
不难看出上面的配置信息其实定义的是构造函数的参数,其中id,version,operations的含义都不难猜出,然我们来看看service这个参数。它是一个Object,也就说对service这个对象并没有强类型要求。但是并非任何对象都可以作为service参数传进来,关于这个问题,我们会在后面加以说明,在这个例子里,service是类org.geoserver.wms.DefaultWebMapService的对象。
上面的配置信息将一个ID是“wms”版本是“1.1.1”的OWS服务注册到系统运行时中,当一个OWS请求到来时,系统就会遍历所有注册的服务,寻找符合要求的服务。
第三步 执行操作(见org.geoserver.ows.Dispatcher.dispatch(Request, Service))
这一步主要的操作就是创建执行对象org.geoserver.platform.Operation,这个对象采用了Java的反射原理来实现函数调用,所以需要创建函数参数数组。前面提到过OWS参数的格式有KVP和XML两种,因此对参数的处理也分为两种,具体到类就是org.geoserver.ows.KvpRequestReader和org.geoserver.ows.XmlRequestReader。这两个类将参数字符串转换成相应的对象,在这点上与前面的Parser类似,不同的是这里的转换对应的是一个服务调用,而不是具体一个参数。例如org.geoserver.wms.kvp.GetMapKvpRequestReader就负责将GetMap的参数转换成org.vfny.geoserver.wms.requests.GetMapRequest对象。
这一步的扩展点就是以上两个Reader,来看WMS中GetMap Reader的定义
<bean id="getMapKvpReader" class="org.geoserver.wms.kvp.GetMapKvpRequestReader"> <constructor-arg ref="wms"/> </bean>
<bean id="getMapXmlReader" class="org.geoserver.wms.xml.WMSXmlRequestReaderAdapter"> <constructor-arg index="0" value="http://www.opengis.net/ows"/> <constructor-arg index="1" value="GetMap"/> <constructor-arg index="2" ref="wms"/> <constructor-arg index="3" value="org.vfny.geoserver.wms.requests.GetMapXmlReader"/> </bean>
与前面的扩展一样,我们只需要把我们设计的相关Reader注册到系统中,程序就会自己找到它。下面说说匹配Reader的算法。
第二步提到了一个service对象,除了知道它是一个Object之外,我们并没有过多说明。实际上,Operation类会持有这个对象,并且从里面查找与注册的操作同名的公共成员函数,这个函数将通过反射来调用。显然,我们提供给这个函数的参数必须符合它声明的参数类型。所以,匹配KVP Reader的算法就是匹配参数类型的过程。DefaultWebMapService的getMap函数的签名如下:
public GetMapResponse getMap(GetMapRequest request)
程序会遍历所有注册的GetMapKvpRequestReader,将注册函数的参数类型与org.geoserver.ows.KvpRequestReader.getRequestBean()的返回值比较,如果两者可以交换(Assignable)则匹配成功。
XML Reader的匹配与KVP Reader完全不同,这一点很奇怪,它是根据注册的操作名称,服务ID和服务版本来匹配的。
至于执行操作,实在没什么需要特殊说明的,就是调用方法而已,唯一值得注意的是它的返回值,因为我们要把它写到返回流(Response Stream)中。而这是下一步的事情了。
第四步 返回结果(见org.geoserver.ows.Dispatcher.response(Object, Request, Operation))
现在需要把结果返回给客户端了,这个步骤叫做Response。这一步的扩展点是一个叫org.geoserver.ows.Response的类,程序会遍历所有注册的Response类(与前面的那些匹配完全一样),比较返回值的类型与org.geoserver.ows.Response.getBinding()的值,如果两者可以交换(Assignable)则匹配成功。匹配成功后就调用org.geoserver.ows.Response.write(Object, OutputStream, Operation)函数回写结果。下面是WMS GetMap的Response配置信息
<bean id="getMapResponse" class="org.geoserver.ows.adapters.ResponseAdapter"> <constructor-arg value="org.vfny.geoserver.wms.responses.GetMapResponse"/> <constructor-arg ref="geoServer"/> </bean>
这里用到了一个叫ResponseAdapter的类主要是为了适配接口。
到此OWS的处理就介绍完了,下面来看看GeoServer的资源API。
二 资源对象模型
这是我起的名称,实际上是一套接口。在包“main”的命名空间org.geoserver.catalog下面有许多接口描述了GeoServer中许多基本概念,搞清楚这些是学习GeoServer的关键之一。
特别需要说明一下,GeoServer所有的资源都派生自接口org.geoserver.catalog.Info,它唯一的方法是org.geoserver.catalog.Info.getId()。这说明,GeoServer里面所有的资源都有一个全局唯一的ID。我们会在后面的文章中详细介绍,包括它的产生和保存。
1 Catalog
这里有一段摘录自main配置文件的脚本
<bean id="rawCatalog" class="org.geoserver.catalog.impl.CatalogImpl"> <property name="resourceLoader" ref="resourceLoader"/> </bean> <bean id="secureCatalog" class="org.geoserver.security.SecureCatalogImpl"> <constructor-arg ref="rawCatalog" /> </bean> <!-- Switch this when you want to enable the secure catalog by default --> <alias name="secureCatalog" alias="catalog"/>
里面定义了一个叫“catalog”的变量,下面是一个引用它的例子
<bean id="geoServer" class="org.geoserver.config.impl.GeoServerImpl"> <property name="catalog" ref="catalog"/> </bean>
这就是我们要说的Catalog。查看Catalog接口的代码,会看到它定义了许多函数(代码太长就不贴出了),这些函数基本涵盖了“增删改查”所有的方法,而每一套“增删改查”都对应了我们将要介绍一个概念,例如这一段:
void add(LayerInfo layer); void remove(LayerInfo layer); void save(LayerInfo layer); LayerInfo getLayer(String id);
后面还有很多getLayer方法就不赘述了。
我们可以这样定义:Catalog是一个抽象概念,它提供了一套访问GeoServer资源的方法,通过这些方法程序可以对GeoServer的资源进行“增删改查”的操作,而无需知道资源的具体保存形式。当然,目前唯一的实现就是CatalogImpl,但是我们完全可以用我们自己的Catalog来替换它,只需要修改一下上面的配置信息就可以了。
需要说明的是,很多时候我们都是通过GeoServer这个对象来获得Catalog的,下面我们就来说说GeoServer。
2 GeoServer
它的完整名称是org.geoserver.config.GeoServer,根据注释的解释,它是用来访问GeoServer服务器的配置信息的接口,而它的名称也反映出这个特点。我们可以通过它来获得与具体服务无关的数据,例如服务的字符集,服务的联系人,发布了哪些OWS服务等。当然还有服务的资源接口Catalog。在自定义的配置文件里,可以用“geoServer”来引用它。
3 Layer
Layer是空间数据源与表现样式的组合,WMS GetMap中我们指定的参数LAYERS指的就是它。org.geoserver.catalog.LayerInfo是它的代码形式。通过这个接口,我们可以访问与Layer相关联的资源,主要有空间数据源(Resource),样式(Style),图例(Legend)等。Layer可以相互嵌套形成LayerGroup,LayerGroup在行为上与Layer完全一样,这是一个组合模式的应用。
4 Resource
这里的Resource并非泛指的资源,而是与空间相关联的资源,所以org.geoserver.catalog.ResourceInfo中有访问空间参考(SRS或CRS)的方法。另外,通过这个接口我们可以访问资源的Store(又是一个概念,我们姑且就使用原文),也就是资源的存储器。GeoServer中有两个概念是从Resource派生来的,Coverage和FeatureType,并且它们有各自的Store。
5 Store
Store表示Resource的存储。org.geoserver.catalog.StoreInfo是它的代码形式,最重要的函数是org.geoserver.catalog.StoreInfo.getConnectionParameters(),返回连接参数,参数的具体含义由具体的存储介质来决定。Coverage和FeatureType都有各自的Store,org.geoserver.catalog.CoverageStoreInfo和org.geoserver.catalog.DataStoreInfo。
6 Coverage
Coverage一直是让我迷惑的概念,我把wiki的解释原文抄录在这里:In geographic information systems, a coverage is a mapping of one aspect of data in space.大意是:在GIS领域,一个Coverage就是一附地图,它反应了空间数据的一个方面。(很笼统是吧,如果你有准确的解释,希望你能发给我,我将不胜感激)。org.geoserver.catalog.CoverageInfo是这个概念的代码形式。
7 FeatureType
要解释FeatureType就必须先解释Feature。Feature,即要素,是一个具有空间意义的实体,并且拥有附加的属性。例如:某个城市,它的位置是东经116.46度 北纬 39.92 度,它的名字是“北京”,它的常住人口是1972万。我们就可以用一个要素来表示它,这个要素是一个点,点的坐标是[116.46 39.92],它的属性表示为
属性名称 |
属性值 |
Name | 北京 |
Popu | 1972 |
如果我们有许多城市数据,都具有相似的特征,我们就可以定义一个叫“CITIES”的FeatureType,它的几何类型是Point(点),它的属性Scehma是
属性名称 |
值类型 |
Name | 字符串 |
Popu | 数值 |
可以把FeatureType与Feature的关系想象为类与类实例的关系。也可以把它们的关系想象为数据表与数据记录的关系。后者其实更实用,因为许多时候Feature数据就是以表的形式组织和访问的。
org.geoserver.catalog.FeatureTypeInfo是它的代码形式。它最重要的方法就是org.geoserver.catalog.FeatureTypeInfo.getFeatureSource(ProgressListener, Hints),这个方法会返回数据源,我们可以用这个数据源来查询Feature。
8 Style
Style,可以翻译成渲染样式,提供了一套方法用来描述如何渲染Feature。WMS GetMap中我们指定的参数STYLES指的就是它。OGC的标准SLD提供了一种语言用来描述Style。GeoServer采用了这个语言,其他很多GIS平台也支持这个语言。推荐我的一篇文章《OGC之路(2) 之 Style之谜》说得比较详细,还附有代码。
org.geoserver.catalog.StyleInfo是它的代码形式。它最重要的方法就是org.geoserver.catalog.StyleInfo.getStyle(),返回样式对象org.geotools.styling.Style。我们可以通过它来获得SLD里面定义的元素。
至此,我把GeoServer的结构做了一个简单的介绍,从OWS处理请求的方式到GeoServer资源访问的API。其实GeoServer的结构远不止这么简单,它还提供了一套完整的管理服务以及相关GUI,这个部分不是我们介绍的内容,所以在此略过,但是作为用户接触最频繁的部分,它的结构还是很值得研究的。此外,GeoServer保存资源的方式也很重要,GeoServer采用文件目录结构来分类保存各种资源,并且设置了访问权限,我们将在下一章对此做详细说明。