背景
我们现在开发的一个RCP产品,内部需要集成一个后端的引擎,该引擎使用Java开发,由几个核心的jar包及一系列依赖的第三方jar包组成。而Eclipse插件是不支持直接调用普通jar包中的类的,需要先把这些外部jar包封装成插件。Eclipse支持将jar包内置到插件中,然后通过把这些内置的jar包添加到插件的classpath中来供插件内部的类调用,如果要开放给其他插件使用,还需要在插件的配置中导出要开放的包。
因为这个引擎的jar及其依赖比较多,每次发布时如果都需要手动拷贝jar包到插件中也是一件费时费力且容易出错的工作,考虑使用Maven来自动将依赖打包成插件的方式。
Maven方案
如果集成Maven的话,需要支持以下功能:
- 支持打包成Eclipse插件或OSGI Bundle。
- 支持下载pom中定义的依赖,并将这些依赖jar包打包到Eclipse插件或OSGI Bundle内部。
- 支持将工程内部的jar包发布到生成的插件或Bundle中。
- 支持在生成的插件或Bundle的MENIFEST.MF文件中将这些jar添加到Classpath路径中。
- 支持将要导出的包添加到插件或Bundle的MANIFEST.MF文件的Export-Package中。
- 支持自定义插件或Bundle的MANIFEST.MF文件中的其他配置项。
google后发现,maven插件maven-bundle-plugin
完全满足上述要求,它是一个生成OSGI Bundle的插件,完全以Maven的方式,下载pom中定义的依赖,根据配置的MANIFEST信息,生成对应的OSGI Bundle。
项目地址:Apache Felix Maven Bundle Plugin (BND) :: Apache Felix
实施步骤
先假设我们要生成第三方依赖Bundle包的工程为:
- org.ming.bundle-wrapper
+ src
+ META-INF
+ libs
- .project
- build.properties
- pom.xml
先假设我们要包装两个依赖:
一个依赖是Maven仓库中的依赖:
<groupId>org.ming</groupId>
<artifactId>test-engine</artifactId>
<version>1.0.0-SNAPSHOT</version>
另一个是普通jar包,jar包放在工程libs目录下,这个jar需要用到一些本地库,假设路径为:
- libs
- test.jar
- test.dll
- test.so
- test.jnilib
1、构建bundle
定义pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ming</groupId>
<artifactId>org.ming.eclipse.bundlesupport</artifactId>
<packaging>bundle</packaging>
<properties>
<engine-version>1.0.0-SNAPSHOT</engine-version>
</properties>
<dependencies>
<!-- 列出Bundle中要包含的jar包 -->
<!-- 间接依赖不用列,会自动解析间接依赖并下载。 -->
<dependency>
<groupId>org.ming</groupId>
<artifactId>test-engine</artifactId>
<version>${engine-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 添加编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>3.5.0</version>
<extensions>true</extensions>
<configuration>
<instructions>
①<Include-Resource>
libs/test.jar=./libs/test.jar,libs/test.dll=./libs/test.dll,libs/test.so=./libs/test.so,libs/test.jnilib=./libs/test.jnilib
</Include-Resource>
②<Bundle-ClassPath>
.,{maven-dependencies},libs/test.jar
</Bundle-ClassPath>
③<Bundle-NativeCode>
libs/test.dll;osname=win32;osname=Windows10,libs/test.so;osname=linux,libs/test.jnilib;osname=macosx,*
</Bundle-NativeCode>
④<Export-Package>
org.ming.*;org.apache.commons.logging.*;org.apache.logging.log4j.*;org.apache.maven.*;org.codehaus.plexus.*;org.apache.commons.*;com.fasterxml.jackson.*;com.thoughtworks.xstream.*;org.apache.velocity.*;javax.wsdl.*;com.ibm.wsdl.*;org.dom4j.*
</Export-Package>
<Embed-Dependency>*</Embed-Dependency>
⑤<Embed-StripGroup>true</Embed-StripGroup>
⑥<Embed-Transitive>true</Embed-Transitive>
⑦<Import-Package>org.eclipse.core.runtime;org.osgi.framework;org.osgi.framework.wiring
</Import-Package>
⑧<Bundle-Name>Test Bundle</Bundle-Name>
⑨<Bundle-Vendor>MING</Bundle-Vendor>
⑩<Bundle-SymbolicName>org.ming.test.bundle-support</Bundle-SymbolicName>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
①Include-Resource:指示需要把工程内的那些资源打包到bundle包中。格式为“src-path=dest-path”。
②Bundle-ClassPath:要添加到生成的MANIFEST文件中的ClassPath的信息,```${maven-dependencies}是内置变量,表示当前pom中定义的maven依赖。
③Bundle-NativeCode:要添加到生成的MANIFEST文件中的Native-Code的信息,具体解释可以查看关于OSGI Bundle的MANIFEST.MF文件的说明。
④Export-Package:要导出的包的路径,支持通配符,插件会自动搜索当前添加到bundle中的jar中的包路径,如果符合通配符,就会将该包添加到export段中。
⑤Embed-StripGroup:jar包封装到bundle中时是否去掉groupId相关信息,如果值为true,则只会生成artifactId-version.jar
到bundle中;如果值为false,则会按groupId生成对应的文件夹,然后将jar按照artifactId-version.jar
格式生成到对应的groupId文件夹中。
⑥Embed-Transitive:是否下载间接依赖。
⑦Import-Package:要添加到生成的MANIFEST文件中的Import-Package信息。
⑧Bundle-Name:bundle的名称。
⑨Bundle-Vendor:bundle的提供者(生产商)
⑩Bundle-SymbolicName:bundle的标识符(唯一id)
构建完成后,即可以生成符合OSGI规范的bundle jar。该输出可以通过Maven仓库管理,即可以使用mvn install | deploy
来构建完成后安装到本地仓库或远程仓库。
2、集成到RCP产品
如果要让RCP产品在构建时能自动包含该bundle,需要在产品的pom中加入该bundle的依赖,pom.xml的相关配置信息如下:
<modelVersion>4.0.0</modelVersion>
<groupId>org.ming</groupId>
<artifactId>org.ming.product</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencies>
①<dependency>
<groupId>org.ming</groupId>
<artifactId>org.ming.eclipse.bundlesupport</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-maven-plugin</artifactId>
<version>${tycho-version}</version>
<extensions>true</extensions>
<useProjectSettings>false</useProjectSettings> </configuration -->
</plugin>
<plugin>
<groupId>org.eclipse.tycho</groupId>
<artifactId>target-platform-configuration</artifactId>
<version>${tycho-version}</version>
<configuration>
②<pomDependencies>consider</pomDependencies>
<resolver>p2</resolver>
<environments>
<environment>
<os>linux</os>
<ws>gtk</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>win32</os>
<ws>win32</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>macosx</os>
<ws>cocoa</ws>
<arch>x86_64</arch>
</environment>
</environments>
</configuration>
</plugin>
</plugins>
</build>
重要的就两个部分:
①:将bundle依赖加入pom。
②:将tycho的target-platform-configuration插件的pomDependencies
属性设置为consider
,这样tycho才会处理pom中定义的依赖。
如上配置后构建,在生成的RCP产品中,该bundle即会放到plugins,启动时即会以bundle的形式被加载。
3、支持bundle独立更新
前面两步完成后,支持第三方依赖的封装bundle已经可以成功构建并在产品构建时自动加到产品插件列表中,如果某个第三方依赖有更新,则发布步骤为:
- 修改bundle工程的pom,如果依赖有更新版本号,则修改pom中对应依赖的版本号。
- 修改bundle工程pom的版本号,也就是bundle的版本号,便于RCP产品检查更新。
- mvn clean deploy构建该bundle并推送到maven仓库。
- 修改产品的pom中该bundle依赖的版本号。
- mvn clean package构建产品。
- 将构建好的产品安装包和更新包发布到http站点。
可以看到这个发布流程有一个弊端,就是如果仅仅是第三方依赖有更新,RCP产品端没有任何更新,但是也必须完整的构建整个RCP产品,产生的后果一是完整构建产品比较费时间,二是产品构建时除了Eclipse插件外其他的本地插件构建时会生成qulifier小版本号(通常是构建日期),也就是说即使RCP插件没有更新,因为生成的小版本号比之前新,在更新的时候也会同时下载更新所有的本地插件。
所以在这一个章节中,我们要继续改进优化关于第三方依赖bundle,让它可以支持独立更新,这样以后如果仅仅是第三方依赖更新的话,我们只需要让用户更新这个bundle就可以了,不用完整构建产品和更新。
插件的安装和更新可以以插件为单位,也可以以feature为单位,为了以后扩展需要,我们以feature为单位来发布该bundle。
3.1 创建parent工程
创建一个parent的pom工程,将上面的bundle工程加入进来。
3.1 创建feature子工程
在parent工程下创建一个org.ming.eclipse.bundlesupport.feature
的Eclipse feature项目,在feature.xml
中定义好feature的信息(描述、版权、许可等),然后把org.ming.eclipse.bundlesupport
插件添加进来。
这里可以取一个巧,我们可以在feature.xml中把feature的版本号定义为变量,如下:
<feature
id="org.ming.eclipse.bundlesupport.feature"
label="%featureName"
version="${featureVersion}"
provider-name="%featureVendor">
<description>
%description
</description>
<copyright url="%copyrightURL">
%copyright
</copyright>
<license url="%licenseURL">
%license
</license>
version处的${featureVersion}
变量我们可以在pom中来定义和生成,这样在更新版本号时可以不用来手动修改版本号。
pom中的变量定义如下:
<properties>
①<featureVersion>${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}.qualifier</featureVersion>
</properties>
<build>
<resources>
<resource>
<directory>.</directory>
<includes>
<include>feature.xml</include>
<include>feature.properties</include>
</includes>
②<filtering>true</filtering>
</resource>
</resources>
<plugins>
③<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>parse-version</id>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
①:定义feature-version变量,该变量同时使用其他变量来组成其值。末尾的qulifier是指示后续构建该feature时替换为具体的构建时间(Eclipse机制)。
②:定义filter为true,指示maven在复制该资源时替换变量值。
③:引入build-helper-maven-plugin
来生成feature-version定义中使用的版本变量。
3.2 创建updatesite子工程
在parent工程下创建一个org.ming.eclipse.bundlesupport.updatesite
的Eclipse updatesite项目,添加一个category.xml
,在其中定义好一个分组,例如“Bundle Test”,然后将上述bundle的feature添加进来,详细定义如下:
<?xml version="1.0" encoding="UTF-8"?>
<site>
<feature id="org.ming.eclipse.bundlesupport.feature">
<category name="bundle-test"/>
</feature>
<category-def name="bundle-test" label="Bundle Test"/>
</site>
在本工程中,我们要引入p2-maven-plugin
插件来生成p2类型的更新站点,该插件用来将定义好的feature和plugin生成p2站点。
关于该插件的详细信息可以参见:GitHub - reficio/p2-maven-plugin at v1.7.0
定义pom文件:
<properties>
①<bundlesupport.repo.name>Bundle Test Updates</bundlesupport.repo.name>
</properties>
<build>
<plugins>
②<plugin>
<groupId>org.reficio</groupId>
<artifactId>p2-maven-plugin</artifactId>
<version>1.7.0</version>
<executions>
<execution>
<id>generate-p2-site</id>
<phase>package</phase>
<goals>
<goal>site</goal>
</goals>
<configuration>
③<categoryFileURL>${basedir}/category.xml</categoryFileURL>
④<additionalArgs>-metadataRepositoryName "${bundlesupport.repo.name}" -artifactRepositoryName "${bundlesupport.repo.name}"</additionalArgs>
⑤<features>
<artifact>
<id>org.ming:org.ming.eclipse.bundlesupport.feature:${project.version}
</id>
<source>false</source>
<transitive>false</transitive>
</artifact>
</features>
⑥<artifacts>
<!-- specify your depencies here -->
<!-- groupId:artifactId:version -->
<artifact>
<id>org.ming:org.ming.eclipse.bundlesupport:${project.version}</id>
<override>false</override>
<transitive>false</transitive>
</artifact>
</artifacts>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
①:定义生成的bundle更新包中库文件的名称,该名称将会作为更新url的名称显示在Eclipse的更新站点里。
②:引入p2-maven-plugin插件。
③:指定updatesite工程中的category.xml文件,如果不指定的话,会自动生成一个,自动生成的分组信息不太满足要求。
④:要传递给打包器的附加参数,示例中添加的这两个参数是指定生成的artifacts.xml和content.xml中的name的值,如果不指定的话,name值会自动生成为repository的路径加上“-artifacts”或“-metadata”,该名称会同时作为Eclipse更新站点的名称,所以设置该值很有必要。
⑤:指定站点中要包含的feature,feature和⑥中的artifacts中的artifact以“groupId:artifactId:version”的方式指定,支持变量。例如示例中feature的版本号指定为当前pom的版本号,方便统一管理。
⑥:指定站点中要包含的artifacts。
使用上述pom构建后会在target目录中生成一个repository目录,repostory目录即是p2站点库,目录结构如下:
- repository
+ features
+ plugins
- artifacts.jar
- category.xml
- content.jar
3.3 构建
每次如果第三方依赖有更新,可以通过以下步骤来构建和发布更新站点:
- 在命令行或终端下切换到parent目录。
- 使用`mvn versions:set -DnewVersion=x.x.x-SNAPSHOT来统一更新parent及其所有子module的版本。
- 使用
mvn clean package
构建parent及所有子module。 - 配合脚本将构建后的updatesite生成的repository目录发布到http站点。
这样,bundle的更新站点就完成了。
以上步骤可以使用Jenkins来完成一键编译、构建和发布。
3.4 集成RCP产品
如果要RCP产品既要支持内置该bundle,又要支持该bundle独立更新的话,需要做以下改变。
- 修改product文件。
- product的“内容”选项卡中加入该bundle的feature,并将install mode设置为root。设置为root表示该feature将以独立的形式安装到产品中,表现为在“关于”的“安装细节”的“已安装软件”中,root模式的feature是独立列出的,并不包括在产品软件中。
- product的“更新”选项卡中加入该bundle的更新站点url。此处添加的更新站点会在构建产品时自动加入到产品软件的“更新站点”中,Eclipse检查更新的时候只会使用“更新站点”中的url来检查更新。
- 修改product的pom文件,去掉以前的maven形式的依赖,通过p2 url的形式引入。
<properties>
<tycho-version>1.2.0</tycho-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<eclipse>http://ming.org/p2/eclipse416/</eclipse>
<lang-pack>http://ming.org/p2/babel-416/</lang-pack>
<orbit>http://ming.org/p2/orbit/</orbit>
<bundlesupport>http://ming.org/bundlesupport/updates/</bundlesupport>
</properties>
<repositories>
<repository>
<id>eclipse</id>
<layout>p2</layout>
<url>${eclipse}</url>
</repository>
<repository>
<id>lang-pack</id>
<layout>p2</layout>
<url>${lang-pack}</url>
</repository>
<repository>
<id>Orbit</id>
<layout>p2</layout>
<url>${orbit}</url>
</repository>
<repository>
<id>ecd</id>
<layout>p2</layout>
<url>${ecd}</url>
</repository>
<repository>
<id>bundlesupport</id>
<layout>p2</layout>
<url>${bundlesupport}</url>
</repository>
</repositories>
至此,所有工作已完成,如果使用Jenkins来构建bundle的话,那么bundle的独立更新步骤为:
- 在Jenkins上一键构建bundle。
- 产品软件用户点击软件的“帮助”主菜单,选择“检查更新”,就会弹出bundle插件的更新对话框,一路next安装更新后重启即可。甚至可以集成自动更新功能,每次启动软件时检查更新,如果有更新自动弹出更新对话框,用户体验更好。