背景
我们现在开发的一个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仓库中的依赖:
org.ming
test-engine
1.0.0-SNAPSHOT
另一个是普通jar包,jar包放在工程libs目录下,这个jar需要用到一些本地库,假设路径为:
- libs
- test.jar
- test.dll
- test.so
- test.jnilib
1、构建bundle
定义pom.xml:
4.0.0
org.ming
org.ming.eclipse.bundlesupport
bundle
1.0.0-SNAPSHOT
org.ming
test-engine
${engine-version}
org.apache.maven.plugins
maven-compiler-plugin
1.8
UTF-8
org.apache.felix
maven-bundle-plugin
3.5.0
true
①
libs/test.jar=./libs/test.jar,libs/test.dll=./libs/test.dll,libs/test.so=./libs/test.so,libs/test.jnilib=./libs/test.jnilib
②
.,{maven-dependencies},libs/test.jar
③
libs/test.dll;osname=win32;osname=Windows10,libs/test.so;osname=linux,libs/test.jnilib;osname=macosx,*
④
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.*
*
⑤true
⑥true
⑦org.eclipse.core.runtime;org.osgi.framework;org.osgi.framework.wiring
⑧Test Bundle
⑨MING
⑩org.ming.test.bundle-support
①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的相关配置信息如下:
4.0.0
org.ming
org.ming.product
1.0.0-SNAPSHOT
pom
①
org.ming
org.ming.eclipse.bundlesupport
1.0.0-SNAPSHOT
org.eclipse.tycho
tycho-maven-plugin
${tycho-version}
true
false
org.eclipse.tycho
target-platform-configuration
${tycho-version}
②consider
p2
linux
gtk
x86_64
win32
win32
x86_64
macosx
cocoa
x86_64
重要的就两个部分:
①:将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的版本号定义为变量,如下:
%description
%copyright
%license
version处的${featureVersion}
变量我们可以在pom中来定义和生成,这样在更新版本号时可以不用来手动修改版本号。
pom中的变量定义如下:
①${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}.qualifier
.
feature.xml
feature.properties
②true
③
org.codehaus.mojo
build-helper-maven-plugin
1.8
parse-version
parse-version
①:定义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添加进来,详细定义如下:
在本工程中,我们要引入p2-maven-plugin
插件来生成p2类型的更新站点,该插件用来将定义好的feature和plugin生成p2站点。
关于该插件的详细信息可以参见:GitHub - reficio/p2-maven-plugin at v1.7.0
定义pom文件:
①Bundle Test Updates
②
org.reficio
p2-maven-plugin
1.7.0
generate-p2-site
package
site
③${basedir}/category.xml
④-metadataRepositoryName "${bundlesupport.repo.name}" -artifactRepositoryName "${bundlesupport.repo.name}"
⑤
org.ming:org.ming.eclipse.bundlesupport.feature:${project.version}
false
⑥
org.ming:org.ming.eclipse.bundlesupport:${project.version}
false
false
①:定义生成的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的形式引入。
1.2.0
UTF-8
http://ming.org/p2/eclipse416/
http://ming.org/p2/babel-416/
http://ming.org/p2/orbit/
http://ming.org/bundlesupport/updates/
eclipse
p2
${eclipse}
lang-pack
p2
${lang-pack}
Orbit
p2
${orbit}
ecd
p2
${ecd}
bundlesupport
p2
${bundlesupport}
至此,所有工作已完成,如果使用Jenkins来构建bundle的话,那么bundle的独立更新步骤为:
- 在Jenkins上一键构建bundle。
- 产品软件用户点击软件的“帮助”主菜单,选择“检查更新”,就会弹出bundle插件的更新对话框,一路next安装更新后重启即可。甚至可以集成自动更新功能,每次启动软件时检查更新,如果有更新自动弹出更新对话框,用户体验更好。