擴展服務開發
擴展服務開發
概述
擴展服務適用場景:需要基於MapGIS傳統內核Api而開發的中小型Web應用程序
不適用場景:不適合大型應用構建(大型應用使用獨立服務或微服務構建),因為其能力受制於插件機制,不支持SpringBoot等大型框架。
擴展服務以插件形式承載到IGServer服務器中,以插件自己獨立的classLoader加載到IGServer的JVM進程中,避免了Java類衝突,保證各個插件與IGServer框架之間的類不會相互汙染,同時也保障了各個子模塊的安全。
獨立的classLoader非常重要,因為java生態圈的三方依賴非常多,依賴的版本衝突問題非常常見。各個插件、框架,彼此之間存在公共依賴的情況時常發生,通過獨立的classLoader解決了插件、框架的兼容問題。
依賴
插件中依賴如果已經存在於igserver-webapp包中,將其設置為<scope>provided</scope>,因為插件可以共享框架中已存在的依賴,如果將重複的依賴加入到插件,只會增加插件的大小;
注意如果插件中某個依賴與框架依賴同時存在,在版本不一致,這時需要在插件添加其依賴,保證插件正常運行,如果插件和框架同時存在某個依賴,插件依賴優先,類似tomcat中的webapp,這樣保證不會出現類的衝突。
插件框架自身的依賴必須使用igs框架,插件中必須設置<scope>provided</scope>,否則插件無法加載。
插件框架自身的依賴包括
org.pf4j:pf4jorg.pf4j:pf4j-springorg.springframework:spring-webmvc(spring其他依賴如果框架存在,也應該使用框架的)io.swagger.core.v3:swagger-annotationsjakarta.servlet:jakarta.servlet-apiorg.slf4j:slf4j-api。
平臺內核中間層依賴必須使用框架的,插件中必須設置<scope>provided</scope>,否則插件無法加載。
依賴聲明
插件必須有如下依賴
<dependencies>
<!--插件主框架-->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
</dependency>
<!--igserver擴展接口-->
<dependency>
<groupId>com.zondy.mapgis.igs</groupId>
<artifactId>mapgis-igs-api</artifactId>
<!--必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>10.X.X.X</version>
<scope>provided</scope>
</dependency>
<!--springmvc標準-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
<!--open api3標準-->
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
</dependency>
<!--java servelet標準-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
</dependency>
<!--推薦日誌框架-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<!-- 必須與igs框架依賴版本保持一致,如果可連內網私有倉庫,並通過依賴管理導入mapgis-igs,則不需要標記版本 -->
<version>X.X.X</version>
<scope>provided</scope>
</dependency>
<!--平臺中間層依賴,可選,如果需要則添加-->
<dependency>
<groupId>com.zondy.mapgis</groupId>
<artifactId>mapgis_geoanalysis</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zondy.mapgis</groupId>
<artifactId>mapgis_geomap</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zondy.mapgis</groupId>
<artifactId>mapgis_geoobjects</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zondy.mapgis</groupId>
<artifactId>mapgis_geodatabase</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>無私有maven倉庫環境
igserver框架的所有三方依賴可將igserver-webapp-*.jar使用壓縮工具打開,如下圖。

其中壓縮包下BOOT-INF/layers.idx文件中節點dependencies下記錄了所有三方依賴的jar包,application節點記錄了內部依賴。
其中lib文件夾為所有依賴的jar,需要解壓獲取依賴jar,並手動安裝依賴
安裝mapgis-igs-api-10.x.x.x.jar,注意修改groupId、artifactId、version與插件依賴的一致
@REM 注意修改${dir}、version參數值和jar包文件名
mvn install:install-file -DgroupId=com.zondy.mapgis.igs -DartifactId=mapgis-igs-api -Dversion=10.x.x.x -Dpackaging=jar -Dfile=${dir}/mapgis-igs-api-10.x.x.x.jar如果需要開發MapGIS內核相關功能,需要安裝平臺中間層jar包mapgis_geomap-1.0.0.jar、mapgis_geoobjects-1.0.0.jar、mapgis_geodatabase-1.0.0.jar、mapgis_geoanalysis-1.0.0.jar,注意修改groupId、artifactId、version與插件依賴的一致
@REM 注意修改${dir}
mvn install:install-file -DgroupId=com.zondy.mapgis -DartifactId=mapgis_geomap -Dversion=1.0.0 -Dpackaging=jar -Dfile=${dir}/mapgis_geomap-1.0.0.jar
mvn install:install-file -DgroupId=com.zondy.mapgis -DartifactId=mapgis_geoobjects -Dversion=1.0.0 -Dpackaging=jar -Dfile=${dir}/mapgis_geoobjects-1.0.0.jar
mvn install:install-file -DgroupId=com.zondy.mapgis -DartifactId=mapgis_geodatabase -Dversion=1.0.0 -Dpackaging=jar -Dfile=${dir}/mapgis_geodatabase-1.0.0.jar
mvn install:install-file -DgroupId=com.zondy.mapgis -DartifactId=mapgis_geoanalysis -Dversion=1.0.0 -Dpackaging=jar -Dfile=${dir}/mapgis_geoanalysis-1.0.0.jar使用私有maven倉庫
內網maven倉庫地址為http://10.10.130.70:8081/nexus,研發中心內網可用,pom中添加倉庫
<repositories>
<repository>
<id>igs</id>
<url>http://10.10.130.70:8081/nexus/repository/maven-releases/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>擴展插件項目可通過com.zondy.mapgis:mapgis-igs:${igserver.version}`管理igs的依賴,在項目中添加如下
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.zondy.mapgis</groupId>
<artifactId>mapgis-igs</artifactId>
<version>${igserver.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>這時,插件中的igs依賴,則無需指定版本號。
打包
打包為jar (one-jar)
推薦插件打包使用maven-assembly-plugin,打包為one-jar,如下
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${project.artifactId}-plugin</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>打包為zip
插件同時支持打包為zip格式,顯著的差別是將插件的依賴jar原封不動的放到lib文件夾下,zip格式插件包在運行時會自動解壓到同級目錄。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
<Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>unzip jar file</id>
<phase>package</phase>
<configuration>
<target>
<unzip src="target/${project.artifactId}-${project.version}.${project.packaging}"
dest="target/plugin-classes"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>
src/main/assembly/assembly.xml
</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>在src/main/assembly中添加assembly.xml,內容如下
<assembly>
<id>plugin</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact>false</useProjectArtifact>
<scope>runtime</scope>
<outputDirectory>lib</outputDirectory>
<includes>
<include>*:jar:*</include>
</includes>
</dependencySet>
</dependencySets>
<!--
<fileSets>
<fileSet>
<directory>target/classes</directory>
<outputDirectory>classes</outputDirectory>
</fileSet>
</fileSets>
-->
<fileSets>
<fileSet>
<directory>target/plugin-classes</directory>
<outputDirectory>classes</outputDirectory>
</fileSet>
</fileSets>
</assembly>打包生成zip包,內部包括classes文件夾和lib文件夾
插件屬性
其中必須指定插件的屬性,注意插件id必須保證唯一性,class為Plugin實現類,如果是擴展的SpringMvc REST服務,服務承載到IGServer中時將添加/igs/rest/services/{plugin-id}/ExtensionServer,示例如下
<properties>
<!-- Override below properties in each plugin's pom.xml -->
<!-- 插件實現中必須包括如下配置項-->
<plugin.id>igs-demo-plugin</plugin.id>
<plugin.class>com.zondy.mapgis.igs.plugins.demo.DemoPlugin</plugin.class>
<plugin.version>0.0.1</plugin.version>
<plugin.provider>MapGIS</plugin.provider>
<plugin.dependencies/>
</properties>在插件項目目錄下執行
mvn clean package將在target目錄下生成插件文件mvc-service-extension-demo-plugin.jar
源碼調試,在插件模塊目錄下創建plugin.properties,示例如下
plugin.id=igs-demo-plugin
plugin.class=com.example.plugins.server.DemoPlugin
plugin.version=0.0.1
plugin.provider=MapGIS
plugin.dependencies=如果多個插件之間有依賴關係,這裡通過plugin.dependencies屬性聲明插件的依賴項,具體依賴聲明寫法參考pf4j插件
開發
創建plugin類,即上一節中的plugin.class,示例如下
@Slf4j
public class DemoPlugin extends SpringPlugin implements FrameworkContextCapable {
public DemoPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Override
public void start() {
log.info("DemoPlugin.start,mode:{}", wrapper.getRuntimeMode());
}
@Override
public void stop() {
log.info("DemoPlugin.stop()");
}
@Override
protected ApplicationContext createApplicationContext() {
//這裡創建插件自身的IOC容器
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.setClassLoader(getWrapper().getPluginClassLoader());
applicationContext.register(SpringConfiguration.class);
applicationContext.refresh();
return applicationContext;
}
@Override
public void setFrameworkApplicationContext(ApplicationContext applicationContext) {
//這裡獲取框架的IOC容器
log.info("framework applicationContext:" + applicationContext.getApplicationName());
}
}上述代碼,聲明瞭一個SpringPlugin,並創建了一個插件自身的IOC容器,注意如果插件內部不需要spring IOC,則實現Plugin接口即可。
這裡可選擇實現FrameworkContextCapable接口來獲取框架的IOC容器。
創建SpringMvc Controller擴展服務實現類,添加spring的REST服務註解@RestController或@Controller,同時必須添加註解@Extension和實現Rest擴展服務接口ControllerExtension,可選擇實現FrameworkContextCapable接口來獲取框架的IOC容器,示例如下
@Tag(name = "demo-plugin擴展服務")
@RestController
@Extension
@RequestMapping("test")
public class TestController implements ControllerExtension, FrameworkContextCapable {
private ApplicationContext applicationContext;
@Autowired
private MessageProvider messageProvider;
@Override
public void setFrameworkApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Operation(summary = "獲取結果信息")
@RequestMapping(value = "/res", method = {RequestMethod.GET})
public ResObj getRes() {
String msg = messageProvider.getMessage();
SrsManager srsManager = applicationContext.getBean(SrsManager.class);
ResObj resObj = new ResObj();
resObj.setMessage(msg);
resObj.setData(srsManager.getMapgisSrsName(4326));
return resObj;
}
}其中註解@Tag,@Operation為OpenApi3註解,用於生成swagger文檔
上述示例REST服務的訪問基地址為http://localhost:8089/igs/rest/services/igs-demo-plugin/ExtensionServer
其中
igs-demo-plugin為插件id,對應上文plugin.properties或pom.xml文件中plugin.id的屬性值
IGServer自動為擴展服務添加的REST服務前綴,接口ControllerExtension默認實現了服務自述信息方法
public interface ControllerExtension extends ExtensionPoint {
/**
* 當前REST擴展服務的自述信息、
* 用於在IGServer服務管理中調用服務基地址時,返回的服務信息
* 開發者可根據實際需要,實現該方法,將擴展服務的詳細自述信息通過該接口提供給使用者
*
* @return 返回text的信息
*/
@GetMapping("")
default ResponseEntity<?> getServiceInfo() {
return ResponseEntity.ok("這是一個基於SpringMVC的REST擴展服務:" + this.getClass().getName());
}
}當在IGServer中添加擴展服務,並重啟IGServer服務(注意:插件添加、刪除後必須重啟服務) ,請求服務基地址http://localhost:8089/igs/rest/services/igs-demo-plugin/ExtensionServer,將返回如下信息
這是一個基於SpringMVC的REST擴展服務:com.example.plugins.server.TestController開發者也可實現接口,實現自定義的服務自述信息接口,示例如下
@Override
@Operation(summary = "服務信息")
public ResponseEntity<?> getServiceInfo() {
return ResponseEntity.ok("這是插件igs-demo-plugin提供的REST擴展服務");
}swagger支持,進入http://localhost:8089/webjars/swagger-ui/index.html,選擇ExtensionApi,可查看所有擴展服務的REST接口文檔
插件的classloader
參考 pf4j classloader 類加載的次序,默認是框架優先, PluginClassLoader 使用如下次序依次嘗試加載類:
- 如果類名是 java.開頭,則使用java system類加載器
- 如果類名是org.pf4j.開頭,則使用框架的類加載器
- 嘗試使用當前的插件類加載器
- 嘗試使用當前插件dependencies的其他插件類加載器加載
- 嘗試使用框架的類加載器
插件與框架的關係
如圖所示,

框架、各個插件各自擁有獨立的spring IOC容器和classloader。
插件中的所有extension均會註冊到框架的IOC容器中,但插件中的普通bean並不會註冊到框架IOC容器中。
插件與插件、插件與框架之間的交互,可通過FrameworkContextCapable接口(plugin、extension實現均可)獲取框架的IOC容器,在通過框架IOC容器獲取任何extension(不論哪個插件中定義的),或框架的普通bean。
注意插件與插件之間有公共的api時,為了打破插件與插件之間classloader隔離的問題,公共的api只能放到框架或共有的dependency插件中,這裡建議創建公共的dependency插件,然後在需要通信的插件中均依賴該插件。
上圖中,插件1、插件2均依賴公共插件1;插件1的擴展A1、擴展A2、擴展AN,公共插件1的擴展B1、擴展B2、擴展BN,插件2的擴展C1、擴展C2、擴展CN均會註冊到框架的IOC容器中,插件1和插件2可通過框架的IOC正常調用公共插件1的擴展B1、擴展B2、擴展BN;但是插件1是無法調用插件2的擴展C1,因為類隔離的問題,插件1無法解析擴展C1。
多插件相互依賴的場景,可參考IGServer擴展服務示例程序multiple-common-api-plugin,multiple-demo1-plugin,multiple-demo2-plugin
開發模式下調試
開發模式下,框架直接找maven編譯好的class文件,而不是jar包或zip包。
這種模式下,插件元數據信息由plugin.properties 定義。
帶IGServer源碼的調試
單體模式下,mapgis-server模塊啟動項添加JVM參數-Dpf4j.mode=development開啟調試模式,添加JVM參數-Dpf4j.pluginsDir=mapgis-igs-plugins設置插件目錄
無IGServer源碼的調試
如果無IGServer源碼,事業部或外部開發者需要在插件源碼項目中開發調試插件,則需要關閉keepalived模式(igserver_for_java/bin/env.config中修改),並在igserver_for_java/bin/start-igs.bat中添加相關啟動參數。
SET "JAVA_OPTS=-Dfile.encoding=UTF-8 -DCONSOLE_CHARSET=GBK -DCONSOLE_WITH_JANSI=true -Dspring.output.ansi.enabled=ALWAYS -Dmapgis.home=.. -Digs.home=."
@REM 追加下面一行,注意修改%plugins_dir%的值
SET "JAVA_OPTS=%JAVA_OPTS% -Dpf4j.mode=development -Dpf4j.pluginsDir=%plugins_dir% -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"注意參數-Dpf4j.pluginsDir=指向開發者的插件目錄結構,參考pf4j官方plugins demo
開發模式下的插件依賴
調試模式下,插件的classloader默認只會找編譯後target/classes文件夾,但插件還依賴了除框架(provided)之外的其他依賴,則需要使用
插件dependency用來將插件的依賴拷貝到生成目錄中target/lib文件夾下
<!-- 如果插件存在本地(非框架提供)依賴項,比如項目依賴plugin-common-lib,調試狀態下,只能通過package將依賴項拷貝到target/lib才能加入到插件類加載器中 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>運行模式下調試
運行模式下,將打包好的插件放到igserver_for_java/plugins文件夾下
需要關閉keepalived模式(igserver_for_java/bin/env.config中修改),並在igserver_for_java/bin/start-igs.bat中添加參數
SET "JAVA_OPTS=-Dfile.encoding=UTF-8 -DCONSOLE_CHARSET=GBK -DCONSOLE_WITH_JANSI=true -Dspring.output.ansi.enabled=ALWAYS -Dmapgis.home=.. -Digs.home=."
@REM 追加下面一行
SET "JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"插件示例程序
代碼見 mapgis-igserver 項目
mapgis-igs
├─── mapgis-igs-plugin # 插件目錄,設置-Dpf4j.pluginsDir參數指向的目錄
│ ├── demo-plugin # 單體示例插件
│ ├── demo-whth-deps-plugin # 單體帶本地依賴的示例插件,依賴為demo-plugin-lib
│ ├── multiple-common-api-plugin # 多插件示例的公共插件
│ ├── multiple-demo1-plugin # 多插件示例的插件1
│ └── multiple-demo2-plugin # 多插件示例的插件2
└── mapgis-igs-plugins-deps # 插件依賴項
└── demo-plugin-lib # demo-whth-deps-plugin插件的依賴問題
Package Sealing(包密封)
https://docs.oracle.com/javase/tutorial/deployment/jar/sealman.html
在生成Jar文件時我們可以指定是否將整個Jar或者其中某幾個Package進行密封,如果是將Jar文件整個進行密封,那就意味著其內所有的Package都被密封了。Package一旦密封,那麼Java 虛擬機一旦成功裝載密封Package中的某個類後,其後所有裝載的帶有相同Package名的類必須來自同一個Jar文件,否則將觸發Sealing Violation安全異常。
部分三方庫,比如org.hsqldb:hsqldb,就配置了sealed:true。因此就不能出現框架跟插件均包括該庫的情況,該庫目前已經存在框架中,則插件必須手動排除。
SpringPlugin
插件類型為SpringPlugin,這時插件中的Spring IOC是由框架提供支撐的,這時插件的Classloader是不能重複出現pf4j-spring相關的(spring-context、spring-core)依賴的,否則插件中的IOC容器會報錯。