文章目錄
文章目錄
一、概要
二、前置知識點-FreeMarker
三、前置知識點-AbstractHttpMessageConverter
3.1 描述
3.2 應用
四、前置知識點-AbstractDecorator
4.1描述
4.2 應用
五、工作空間查詢解讀
5.1 模板解讀
5.2 請求轉換器解讀
一、概要
關于geoserver的rest服務,其實官網有一個簡單的描述,此處不多搬運詳情可以查看它官網描述(點我),但是需要重點了解的是最新的GeoServer是使用SpringMVC來實現的REST服務,拋棄了Restlet。GeoServer擴展之REST_geoserver過時了-CSDN博客?從GeoServer2.12版(2017)開始采用的SpringMVC,?它的Wiki中也做了個簡單描述,但是開發文檔沒有更新,重要的事情說兩遍開發文檔沒有更新。所以官網描述看看就可以了,不用跟著它的指引做。?本文著重從源碼角度梳理整個rest服務的流程
二、前置知識點-FreeMarker
在上一篇文章中看到geoserver的模板框架是FreeMarker
主體框架 | spring(不是spring boot) |
UI框架 | Wicket(類似jsp) |
通信框架(前后臺交互) | Servlet |
地理處理框架 | GeoTools |
模板框架 | FreeMarker |
這個東西主要就是用于格式化REST接口的
用法的話參照下面的代碼(AI生成的,可能細節上有問題,看看即可)
1.環境配置
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setDirectoryForTemplateLoading(new File("templates")); // 設置模板目錄
cfg.setDefaultEncoding("UTF-8"); // 設置默認編碼
2.加載模板
Template template = cfg.getTemplate("example.ftl");
3.數據模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("title", "FreeMarker 示例");
dataModel.put("message", "這是一個 FreeMarker 模板!");
4.處理模板
StringWriter out = new StringWriter();
template.process(dataModel, out);
String result = out.toString();
System.out.println(result);
三、前置知識點-AbstractHttpMessageConverter
AbstractHttpMessageConverter
?一般與rest 接口聯合使用,用于根據前端需求返回不同格式的結果,就比如工作空間的三種請求方式
3.1 描述
下面是AI(智普清言)生成的,可能細節上有問題,看看即可
AbstractHttpMessageConverter
?是 Spring 框架中用于處理 HTTP 請求和響應的轉換的一個抽象類。它為具體的 HTTP 消息轉換器提供了一種模板方法模式,用于將請求體或響應體轉換為 Java 對象,或者將 Java 對象轉換為響應體。
如果你需要自定義一個消息轉換器,你可以擴展這個類,并實現其中的抽象方法。下面是擴展?AbstractHttpMessageConverter
?的基本步驟:
-
確定支持的媒體類型:在構造函數中設置你的轉換器將支持哪些媒體類型(例如?
application/json
,?text/xml
?等)。 -
實現?
supports
?方法:這個方法需要判斷傳入的 Java 類型是否為你的轉換器所支持的類型。 -
實現?
read
?方法:這個方法負責將請求體轉換為 Java 對象。 -
實現?
write
?方法:這個方法負責將 Java 對象轉換為響應體。
以下是一個簡單的示例,展示了如何創建一個自定義的?AbstractHttpMessageConverter
:
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;import java.io.IOException;
import java.nio.charset.Charset;public class CustomMessageConverter extends AbstractHttpMessageConverter<MyObject> {public CustomMessageConverter() {// 設置支持的媒體類型super(new MediaType("application", "custom", Charset.forName("UTF-8")));}@Overrideprotected boolean supports(Class<?> clazz) {// 判斷傳入的類型是否為 MyObject 或其子類return MyObject.class.isAssignableFrom(clazz);}@Overrideprotected MyObject readInternal(Class<? extends MyObject> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException {// 實現從請求體到 MyObject 的轉換邏輯// ...return new MyObject();}@Overrideprotected void writeInternal(MyObject myObject, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException {// 實現從 MyObject 到響應體的轉換邏輯// ...}
}
在上述代碼中,MyObject
?是你希望轉換的目標對象類型。你需要實現?readInternal
?和?writeInternal
?方法來完成具體的轉換邏輯。
最后,不要忘記將你的自定義轉換器注冊到 Spring 的?HttpMessageConverter
?列表中,這通常是通過配置一個?WebMvcConfigurer
?來實現的:
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(new CustomMessageConverter());}
}
這樣,當 Spring MVC 處理請求和響應時,就會使用你的自定義轉換器來處理?MyObject
?類型的數據。
博客園里面有一篇文章寫的也不錯可以參考(點我)
3.2 應用
在geoserver中,設置轉換器的配置代碼在RestConfiguration中
src/rest/src/main/java/org/geoserver/rest/RestConfiguration.java
在applicationContext.xml中可以看到掃描的是整個包下面的類
<?xml version="1.0" encoding="UTF-8"?>
<beans><!-- <mvc:annotation-driven/> --><context:component-scan base-package="org.geoserver.rest"/>
</beans>
當掃描到RestConfiguration時就會自動注冊消息轉換器
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/** Configure various aspects of Spring MVC, in particular message converters */
@Configuration
public class RestConfiguration extends WebMvcConfigurationSupport {/** 配置消息轉換器 */@Overrideprotected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {Catalog catalog = (Catalog) applicationContext.getBean("catalog");List<BaseMessageConverter> gsConverters =GeoServerExtensions.extensions(BaseMessageConverter.class);gsConverters.add(new FreemarkerHTMLMessageConverter("UTF-8"));gsConverters.add(new XStreamXMLMessageConverter());gsConverters.add(new XStreamJSONMessageConverter());gsConverters.add(new XStreamCatalogListConverter.XMLXStreamListConverter());gsConverters.add(new XStreamCatalogListConverter.JSONXStreamListConverter());gsConverters.add(new InputStreamConverter());EntityResolver entityResolver = catalog.getResourcePool().getEntityResolver();for (StyleHandler sh : Styles.handlers()) {for (Version ver : sh.getVersions()) {gsConverters.add(new StyleReaderConverter(sh.mimeType(ver), ver, sh, entityResolver));gsConverters.add(new StyleWriterConverter(sh.mimeType(ver), ver, sh));}}if (applicationContext.containsBean("gwcConverter")) {converters.add((HttpMessageConverter<?>) applicationContext.getBean("gwcConverter"));}gsConverters.sort(Comparator.comparingInt(BaseMessageConverter::getPriority));for (BaseMessageConverter converter : gsConverters) {converters.add(converter);}converters.removeIf(Jaxb2RootElementHttpMessageConverter.class::isInstance);converters.add(0, new Jaxb2RootElementHttpMessageConverter());super.addDefaultHttpMessageConverters(converters);}
}
上面的一對轉換器都是針對geoserver一些特定對象的封裝,像workspace、layer、datastore等,最下面那個比較特殊,也是比較常見的一個,它用于將java對象轉換成json或者xml返回給前端
converters.add(0, new Jaxb2RootElementHttpMessageConverter());
比如果當請求工作空間時一般有下面的幾種請求
http://localhost:8080/geoserver/rest/workspaces (瀏覽器預覽居多)
或
http://localhost:8080/geoserver/rest/workspaces.json (作為前端調用的接口居多)
或
http://localhost:8080/geoserver/rest/workspaces.xml (作為前端調用的接口居多)
Jaxb2RootElementHttpMessageConverter 轉換器,會根據前端請求的Accept請求頭自動適配出前端需要的格式
其優先級是 格式拼接到請求地址上(http://localhost:8080/gisserver/rest/workspaces.json)大于 請求地址什么都不加 但是header有Accept參數
四、前置知識點-AbstractDecorator
4.1描述
?org.geotools.util.decorate.AbstractDecorator
?是 GeoTools 庫中的一個類,它提供了一個基礎實現,用于創建裝飾者模式(Decorator Pattern)的裝飾器。裝飾者模式允許你動態地給一個對象添加額外的職責,而不需要修改其原有的代碼。通俗來說就是子類定義一個delegate變量,在子類方法中直接代用父類的方法,并且這個變量一般是通過依賴注入的,不用單獨的給賦值。
在 GeoTools 中,AbstractDecorator
?類是一個抽象類,它實現了?Decorator
?接口,并提供了一個構造函數,接受一個要裝飾的對象作為參數。這個被裝飾的對象通常是一個接口的實現,而?AbstractDecorator
?類則負責將所有的調用委派給這個對象。
舉例:
import org.geotools.util.decorate.AbstractDecorator;public class MyDecorator extends AbstractDecorator<MyInterface> {public MyDecorator(MyInterface delegate) {super(delegate);}@Overridepublic void doSomething() {// 在調用原有方法之前,可以添加一些額外的邏輯System.out.println("Before doing something");// 調用被裝飾對象的方法delegate.doSomething();// 在調用原有方法之后,也可以添加一些額外的邏輯System.out.println("After doing something");}
}public interface MyInterface {void doSomething();
}public class MyImplementation implements MyInterface {@Overridepublic void doSomething() {System.out.println("Doing something");}
}public class Main {public static void main(String[] args) {MyInterface myImplementation = new MyImplementation();MyDecorator myDecorator = new MyDecorator(myImplementation);myDecorator.doSomething();}
}
4.2 應用
說AbstractDecorator
?的目的主要是為了理解WorkspaceController類
從源碼中可以看到
public class WorkspaceController extends AbstractCatalogController {private static final Logger LOGGER = Logging.getLogger(WorkspaceController.class);@Autowiredpublic WorkspaceController(@Qualifier("catalog") Catalog catalog) {super(catalog);}
}
擴展的說一下@Autowired是個依賴注入,
構造函數中有一個catalog,但是WorkspaceController是個servlet接口,沒有實例化的地方,構造函數怎么能夠傳過來呢,查看applicationContext.xml可以看到
<alias name="localWorkspaceCatalog" alias="catalog"/>
<bean id="localWorkspaceCatalog" class="org.geoserver.catalog.impl.LocalWorkspaceCatalog"><constructor-arg ref="advertisedCatalog" />
</bean><bean id="advertisedCatalog" class="org.geoserver.catalog.impl.AdvertisedCatalog"><constructor-arg ref="secureCatalog" /><property name="layerGroupVisibilityPolicy"><bean id="org.geoserver.catalog.LayerGroupVisibilityPolicy.HIDE_NEVER" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/></property>
</bean>
catalog就這通過applicationContext.xml的配置實現初始情況下就可以構造函數注入進去
再繼續看LocalWorkspaceCatalog?,跟蹤源碼可以看到下面的代碼
public class LocalWorkspaceCatalog extends AbstractCatalogDecorator implements Catalog {}public class AbstractCatalogDecorator extends AbstractDecorator<Catalog> implements Catalog {public AbstractCatalogDecorator(Catalog catalog) {super(catalog);}
}// 反編譯的AbstractDecorator
public class AbstractDecorator<D> implements Wrapper, Serializable {protected D delegate;public AbstractDecorator(D delegate) {if (delegate == null) {throw new NullPointerException("Cannot delegate to a null object");} else {this.delegate = delegate;}}
}
通過一步步的查看父對象可以看到最終繼承自?org.geotools.util.decorate.AbstractDecorator
?,也就是說可以直接用delegate去操作父類的一些操作
五、工作空間查詢解讀
一般來說工作空間的查詢地址是
http://localhost:8080/geoserver/rest/workspaces (瀏覽器預覽居多)
或
http://localhost:8080/geoserver/rest/workspaces.json (作為前端調用的接口居多)
或
http://localhost:8080/geoserver/rest/workspaces.xml (作為前端調用的接口居多)
當瀏覽器訪問http://localhost:8080/geoserver/rest/workspaces的servlet代碼位置在如下位置(??引申的說一下,geoserver的rest代碼大多在 gs-restconfig 包下面)
src/restconfig/src/main/java/org/geoserver/rest/catalog/WorkspaceController.java
@GetMappingpublic RestWrapper workspacesGet() {List<WorkspaceInfo> wkspaces = catalog.getWorkspaces();return wrapList(wkspaces, WorkspaceInfo.class);}
從@GetMapping 能看出來它是個普通的spring servlet接口,RestWrapper是對返回結果的一個包裝器,catalog是針對geoserver文件目錄映射出來的一個方法類
查詢結果是這樣的
如果不用包裝器的話返回結果是這樣的
@GetMapping("/details")public List<WorkspaceInfo> getAllWorkspacesDetails() {List<WorkspaceInfo> workspaces = catalog.getWorkspaces();return workspaces;}
可以看出來如果不用包裝器的話會把查出的數據原封不動的返回出來,而且兼容xml和json,實際不管使用不使用包裝器時上面?三、前置知識點-AbstractHttpMessageConverter?講到Jaxb2RootElementHttpMessageConverter 轉換器都會生效,也就是說一直支持xml個json請求,而當使用包裝器時就用到了另一個模板框架二、前置知識點-FreeMarker
5.1 模板解讀
往下看wrapList源碼
protected <T> RestWrapper<T> wrapList(Collection<T> list, Class<T> clazz) {return new RestListWrapper<>(list, clazz, this, getTemplate(list, clazz));}
這里面終于找到了一個跟模板相關的東西getTemplate(list, clazz)
在WorkspaceController的基類RestBaseController中找到下面獲取模板的代碼?
protected Template getTemplate(Object o, Class<?> clazz) {Template template = null;Configuration configuration = createConfiguration(clazz);。。。。。。(此處省略n行代碼)return tryLoadTemplate(configuration, templateName);}
里面的代碼看著沒啥營養我替你們看過了,跟著代碼就能找到模板的位置,也就是這個地方
src/restconfig/src/main/java/org/geoserver/rest/catalog/ftl-templates/workspaces.ftl
<#include "head.ftl">
Workspaces
<ul>
<#list values as w><li><a href="${page.pageURI(w.properties.name + '.html')}">${w.properties.name}</a><#if w.properties.isDefault> [default] 哈哈 </#if></li>
</#list>
</ul>
<#include "tail.ftl">
最后那兩個“哈哈”是我自己加的,瀏覽器訪問可以看到下面效果
如果你那兒是亂碼的可以在頭部模板里面加個<meta charset="UTF-8" />
src/restconfig/src/main/java/org/geoserver/rest/catalog/ftl-templates/head.ftl
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head><title>GeoServer Configuration</title><meta charset="UTF-8" /><meta name="ROBOTS" content="NOINDEX, NOFOLLOW"/>
</head>
<body><#setting number_format="#0.0#">
但是訪問的時候我還有一個疑問,直接訪問是用的模板,但當我訪問json的接口時兒返回結果貌似沒有走這個模板
這是因為什么呢 ,再返回去查getTemplate方法,原因是默認是根據模板名查詢模板的,也就是說根據workspace就能查到模板,換成workspace.json 就不行,如果想要用json類型的模板,就得再定義個workspace.json.flt文件,
if (template == null) template = tryLoadTemplate(configuration, templateName + ".ftl");
總的來說,如果不加干預的話直接請求
http://localhost:8080/geoserver/rest/workspaces
就會使用FreeMarker模板,然后經過轉換器(此處是Jaxb2RootElementHttpMessageConverter 、FreemarkerHTMLMessageConverter、XStreamXMLMessageConverter、XStreamJSONMessageConverter。。。)傳給前端,如果是訪問
http://localhost:8080/geoserver/rest/workspaces.json
的話則會跳過模板直接經過轉換器(此處是Jaxb2RootElementHttpMessageConverter )然后傳給前端
到這里FreeMarker的框架算是基本上梳理完了 ,感覺就像是個放大版的StringBuilder。
5.2 請求轉換器解讀
看了前面描述的三、前置知識點-AbstractHttpMessageConverter?可以知道在查詢完之后會執行一次查詢結果的轉換操作
再次看查詢工作空間的代碼
@GetMappingpublic RestWrapper workspacesGet() {List<WorkspaceInfo> wkspaces = catalog.getWorkspaces();return wrapList(wkspaces, WorkspaceInfo.class);}
🔎 下鉆查看wrapList的代碼如下
protected <T> RestWrapper<T> wrapList(Collection<T> list, Class<T> clazz) {return new RestListWrapper<>(list, clazz, this, getTemplate(list, clazz));}
🔎 繼續下鉆查看RestListWrapper以及它的基類RestWrapperAdapter
public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) {controller.configurePersister(persister, converter);}
從這里能看出來包裝器有個關于轉換器的配置的方法,而且類型是XStreamMessageConverter converter,繼續跟蹤代碼,查找下它是在哪里被調用的
這里看到有幾個繼承類,但是只有里面的類型和RestListWrapper是一樣的
public abstract class XStreamCatalogListConverterextends XStreamMessageConverter<RestListWrapper<?>>
根據spring mvc的自動根據參數類型適配的原則,它用的轉換器就是XStreamCatalogListConverter,而且從注釋中也能看出來
/*** A wrapper for all Collection type responses using the {@link XStreamCatalogListConverter} (XML* and JSON output). Also supports Collection type responses using the {@link* FreemarkerHTMLMessageConverter}, but is not required for such responses.** <p>In the previous rest API this wasn't needed because in each individual rest request the* Collections were aliased to*/
在XStreamCatalogListConverter.java中能夠看到具體的轉換方法
protected void configureXStream(XStream xstream, Class<?> clazz, RestListWrapper<?> wrapper) {XStreamPersister xp = xpf.createXMLPersister();wrapper.configurePersister(xp, this);final String name = getItemName(xp, clazz);xstream.alias(name, clazz);xstream.registerConverter(new CollectionConverter(xstream.getMapper()) {@Overridepublic boolean canConvert(@SuppressWarnings("rawtypes") Class type) {return Collection.class.isAssignableFrom(type);}@Overrideprotected void writeCompleteItem(Object item,MarshallingContext context,HierarchicalStreamWriter writer) {writer.startNode(name);context.convertAnother(item);writer.endNode();}});xstream.registerConverter(new Converter() {@Overridepublic boolean canConvert(Class type) {return clazz.isAssignableFrom(type);}@Overridepublic void marshal(Object source,HierarchicalStreamWriter writer,MarshallingContext context) {String ref;// Special case for layer list, to handle the non-workspace-specific// endpoint for layersif (clazz.equals(LayerInfo.class)&& OwsUtils.getter(clazz, "prefixedName", String.class) != null&& RequestInfo.get() != null&& !RequestInfo.get().getPagePath().contains("/workspaces/")) {ref = (String) OwsUtils.get(source, "prefixedName");} else if (OwsUtils.getter(clazz, "name", String.class) != null) {ref = (String) OwsUtils.get(source, "name");} else if (OwsUtils.getter(clazz, "id", String.class) != null) {ref = (String) OwsUtils.get(source, "id");} else if (OwsUtils.getter(clazz, "id", Long.class) != null) {// For some reason Importer objects have Long ids so this catches that// caseref = OwsUtils.get(source, "id").toString();} else {throw new RuntimeException("Could not determine identifier for: " + clazz.getName());}writer.startNode(wrapper.getItemAttributeName());writer.setValue(ref);writer.endNode();encodeLink(encode(ref), writer);}@Overridepublic Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {return null;}});}
我修改了上述代碼中的
writer.startNode(wrapper.getItemAttributeName() +"test");
然后再次請求接口就能看到修改后的數據
寫在最后,文章難免有寫的不對或者不完善的地方,歡迎提出糾正意見