Springboot Application 集成 OSGI 框架開發


內容來源:https://www.ibm.com/developerworks/cn/java/j-springboot-application-integrated-osgi-framework-development/index.html

Springboot Application 集成 OSGI 框架開發

Comments
 
0

Java 類加載器

啟動類加載器 (Bootstrap ClassLoader)

是 Java 類加載層次中最頂層的類加載器,負責加載 JDK 中的核心類庫,如:rt.jar、resources.jar、charsets.jar 等

擴展類加載器(Extension ClassLoader)

負責加載 Java 的擴展類庫,默認加載 JAVA_HOME/jre/lib/ext/目下的所有 jar

應用類加載器(Application ClassLoader)

負責加載應用程序 classpath 目錄下的所有 jar 和 class 文件。

ClassLoader 使用的是雙親委托模型來搜索類的,每個 ClassLoader 實例都有一個父類加載器的引用(不是繼承的關系,是一個包含的關系),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它 ClassLoader 實例的的父類加載器。當一個 ClassLoader 實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器 Bootstrap ClassLoader 試圖加載,如果沒加載到,則把任務轉交給 Extension ClassLoader 試圖加載,如果也沒加載到,則轉交給 App ClassLoader進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等 URL 中加載該類。如果它們都沒有加載到這個類時,則拋出 ClassNotFoundException 異常。否則將這個找到的類生成一個類的定義,並將它加載到內存當中,最后返回這個類在內存中的 Class 實例對象。

判別兩個類是否相同,除了是相同的 class 字節碼,還必須由同一類加載器加載。比如類 Example,javac 編譯之后生成字節碼文件 Example.class,ClassLoaderA 和 ClassLoaderB 這兩個類加載器並讀取了 Example.class 文件,並分別定義出了 java.lang.Class 實例來表示這個類,對於 JVM 來說,它們是兩個不同的實例對象,但它們確實是同一份字節碼文件,如果試圖將這個 Class 實例生成具體的對象進行轉換時,就會拋運行時異常 java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.Class

OSGI 類加載器

OSGI 類加載器並不遵循 Java 的雙親委派模型,OSGi 為每個 bundle 提供一個類加載器,該加載器能夠加載 bundle 內部的類和資源,bundle 之間的交互是從一個 bundle 類加載器委托到另一個 bundle 類加載器,所有 bundle 都有一個父類加載器。

Fragment bundle 是一種特殊的 bundle,不是獨立的 bundle,必須依附於其他 bundle 來使用。通過 Fragment-Host 來指定宿主 bundle,同時也可以通過這種方式使用宿主的類加載器。

圖 1.OSGI 類加載器

OSGI 框架根據 Bundle 的 MANIFEST.MF 文件中描述的數據信息進行解析處理 Bundle 間的依賴關系。Fragment Bundle 的宿主 bundle 的檢查在 bundle 解析之前已經完成,所以 Fragement Bundle 可以獲取到宿主 bundle 的加載器信息。

Equinox OSGI ServletBridge 實現原理及源碼解析

BridgeServlet 與 OSGI 容器

Equinox 提供了 servletbridge.jar 將 OSGI framework 和 servlet container 橋接起來,並且提供了一系列的 bundle 可以將 Equinox OSGI 應用嵌入到現有的 web 服務器中(eg. Tomcat)。servletbridge.jar 包含如下兩個文件 (package: org.eclipse.equinox.servletbridge)

BridgeServlet – 負責請求處理

FrameworkLauncher – 負責 OSGI bundle 啟動管理

Web 工程被加載到 web 容器中,比如 Tomcat,容器讀取 web 工程 WEB-INF 目錄下的 web.xml 文件,通過 servlet mapping 指定相應的類處理請求,如下所示:

清單 1.BridgeServlet 配置

1
2
< servlet-name >equinoxbridgeservlet</ servlet-name >
< servlet-class >org.eclipse.equinox.servletbridge.BridgeServlet</ servlet-class >

Web 容器自動加載 org.eclipse.equinox.servletbridge.BridgeServlet 這個 Servlet,所有發向 Web 容器的請求都被這個 Servlet 處理。

在 BridgeServlet 中的 init()方法中完成了對 OSGI 容器的啟動。如下所示:

清單 2.BridgeServlet 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void init() throws ServletException {
super.init();
setInstance(this);
String enableFrameworkControlsParameter = getServletConfig().getInitParameter("enableFrameworkControls");
this.enableFrameworkControls = ((enableFrameworkControlsParameter != null)
&& (enableFrameworkControlsParameter.equals("true")));
String frameworkLauncherClassParameter = getServletConfig().getInitParameter("frameworkLauncherClass");
if (frameworkLauncherClassParameter != null) {
try {
Class frameworkLauncherClass = getClass().getClassLoader().loadClass(frameworkLauncherClassParameter);
this.framework = ((FrameworkLauncher) frameworkLauncherClass.newInstance());
} catch (Exception e) {
throw new ServletException(e);
}
} else {
this.framework = new FrameworkLauncher();
}
this.framework.init(getServletConfig());
this.framework.deploy();
this.framework.start();
}

初始化所用到的參數都是從 web.xml 中獲取,如果指定了 frameworkLauncherClass 參數即啟動器的實現類,則用該啟動類作為框架啟動器,如果沒有指定,默認采用 org.eclipse.equinox.servletbridge. FrameworkLauncher 作為框架啟動器。

OSGI 啟動包括 init(ServletConfig),deploy(),start()三個方法,其中 init()完成了 config 和 context 等的一系列初始化工作,deploy 完成了相應的 osgi bundle 等的拷貝,以及相應的目錄建立和橋接擴展器 bundle 的創建,start 完成了類加載器的切換和通過反射調用 EclipseStart 的 startup 方法對於 osgi 的啟動。具體看源碼分析。

清單 3.osgi 框架初始化

1
2
3
4
5
6
7
void init(ServletConfig servletConfig) {
this.config = servletConfig;
this.context = servletConfig.getServletContext();
init();
}
String commandLine = this.config.getInitParameter("commandline");
String extendedExports = this.config.getInitParameter("extendedFrameworkExports");

Init 完成初始化所有資源配置的工作。

清單 4.osgi 框架部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized void deploy() {
if (this.platformDirectory != null) {
this.context.log("Framework is already deployed");
return;
}
File servletTemp = (File) this.context.getAttribute("javax.servlet.context.tempdir");
this.platformDirectory = new File(servletTemp, "eclipse");
if (!this.platformDirectory.exists()) {
this.platformDirectory.mkdirs();
}
copyResource("/WEB-INF/eclipse/configuration/", new File(this.platformDirectory, "configuration"));
copyResource("/WEB-INF/eclipse/features/", new File(this.platformDirectory, "features"));
File plugins = new File(this.platformDirectory, "plugins");
copyResource("/WEB-INF/eclipse/plugins/", plugins);
deployExtensionBundle(plugins);
copyResource("/WEB-INF/eclipse/.eclipseproduct", new File(this.platformDirectory, ".eclipseproduct"));
}

deploy

首先將 configuration,features,plugins 等資源拷貝到臨時目錄下,然后部署一個 extension bundle,這一步非常重要,這個 bundle 會作為一個 fragment 附加到系統 bundle 之上,同時導出 org.eclipse.equinox.servletbridge 和其他一些 package。這一步的操作其實是進行了一個 classcloader 的切換操作,使得 package 名為 org.eclipse.equinox.servletbridge 的 bundle 可以獲取到 system bundle 的 classloader,下面會進行程序演示。

清單 5.osgi 框架啟動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public synchronized void start(){
if (this.platformDirectory == null) {
throw new IllegalStateException("Could not start the Framework - (not deployed)");
}
if (this.frameworkClassLoader != null) {
this.context.log("Framework is already started");
return;
}
Map initalPropertyMap = buildInitialPropertyMap();
String[] args = buildCommandLineArguments();
ClassLoader original = Thread.currentThread().getContextClassLoader();
try {
System.setProperty("osgi.framework.useSystemProperties", "false");
URL[] osgiURLArray = { new URL((String)initalPropertyMap.get("osgi.framework")) };
this.frameworkClassLoader = new ChildFirstURLClassLoader(osgiURLArray, getClass().getClassLoader());
Class clazz = this.frameworkClassLoader.loadClass("org.eclipse.core.runtime.adaptor.EclipseStarter");
Method setInitialProperties = clazz.getMethod("setInitialProperties", new Class[] {Map.class });
setInitialProperties.invoke(null, new Object[] { initalPropertyMap });
Method runMethod = clazz.getMethod("startup", new Class[] { [Ljava.lang.String.class, Runnable.class });
runMethod.invoke(null, new Object[] { args });
this.frameworkContextClassLoader = Thread.currentThread().getContextClassLoader();
} catch (InvocationTargetException ite) {
Throwable t = ite.getTargetException();
if (t == null)
t = ite;
this.context.log("Error while starting Framework", t);
throw new RuntimeException(t.getMessage());
} catch (Exception e) {
this.context.log("Error while starting Framework", e);
throw new RuntimeException(e.getMessage());
} finally {
Thread.currentThread().setContextClassLoader(original);
}
}

其中 buildInitialPropertyMap 完成了 osgi 容器的配置參數初始化,osgi.install.area,osgi.configuration.area,osgi.framework 等,osgi 啟動的時候會自動讀取這些參數的路徑。args 參數是容器配置的 commandline 參數,是 osgi 命令行啟動所需的參數。

ClassLoader original = Thread.currentThread().getContextClassLoader();

獲取當前線程的 contextClassLoader(AppClassLoader)接下來等完成反射調用之后,還需要把 contextClassLoader 切換回去。

通過 ChildFirstURLClassLoader 加載 EclipseStarter,反射調用 setInitialProperties 和 startup 方法完成 osgi 啟動。

此時 frameworkContextClassLoader 應為 org.eclipse.core.runtime.internal.adaptor.ContextFinder

完成 osgi 啟動之后,finally 應該將當前線程的 contextClassLoader 切換回去。

BridgeServlet 請求處理

BridgeServlet 作為 web 容器和 OSGI 的橋接方法即是 BridgeServlet 接收所有的 HTTP 請求同時將所有經過他的請求轉發給 DelegateServlet,DelegateServlet 作為 OSGI 里面的 bundle,bundle 之前是可以互相溝通的,此時就完成了橋接工作,具體源碼分析如下所示:

所有 HTTP 請求都發給 ServletBridge 的 service 方法處理。

清單 6.BridgeServlet 請求處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException{
if (req.getAttribute("javax.servlet.include.request_uri") == null) {
String pathInfo = req.getPathInfo();
if ((pathInfo == null) && (isExtensionMapping(req.getServletPath()))) {
req = new ExtensionMappingRequest(req);
}
if ((!this.enableFrameworkControls) ||
(pathInfo == null) || (!pathInfo.startsWith("/sp_")) ||
(!serviceFrameworkControls(req, resp))) {}
}
else
{
String pathInfo = (String)req.getAttribute("javax.servlet.include.path_info");
if ((pathInfo == null) || (pathInfo.length() == )) {
String servletPath = (String)req.getAttribute("javax.servlet.include.servlet_path");
if (isExtensionMapping(servletPath)) {
req = new IncludedExtensionMappingRequest(req);
}
}
}
ClassLoader original = Thread.currentThread().getContextClassLoader();
HttpServlet servletReference = acquireDelegateReference();
if (servletReference == null) {
resp.sendError(404, "BridgeServlet: " + req.getRequestURI());
return;
}
try {   Thread.currentThread().setContextClassLoader(this.framework.getFrameworkContextClassLoader());
servletReference.service(req, resp);
} finally {
releaseDelegateReference();
Thread.currentThread().setContextClassLoader(original);
}
}

首先獲取通過 acquireDelegateReference 獲取 delegate,這個 delegate 是如何初始化的呢?

清單 7.BridgeServlet 注冊接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static synchronized void registerServletDelegate(HttpServlet servletDelegate) {
if (instance == null) {
return;
}
if (servletDelegate == null) {
throw new NullPointerException("cannot register a null servlet delegate");
}
synchronized (instance) {
if (instance.delegate != null) {
throw new IllegalStateException("A Servlet Proxy is already registered");
}
try {
servletDelegate.init(instance.getServletConfig());
} catch (ServletException e) {
instance.getServletContext().log("Error initializing servlet delegate", e);
return;
}
instance.delegate = servletDelegate;
}
}

我們注意到有這樣一個方法對 delegate 進行了初始化,這個方法是在另外一個 osgi bundle 中被調用,在 org.eclipse.equinox.http.servletbridge 的 Activator 的 start 方法中對 delegate 進行了初始化

清單 8.注冊委托對象

1
2
3
4
5
public void start(BundleContext context)throws Exception
{
this.httpServiceServlet = new HttpServiceServlet();
BridgeServlet.registerServletDelegate(this.httpServiceServlet);
}

但我們注意到在 org.eclipse.equinox.http.servletbridge 的 bundle 中並沒有對與 BridgeServlet 的 jar 包引用,那他是如何調用到 BridgeServlet 的 registerServletDelegate 的方法的呢?但是在其 MANIFEST 描述文件中有如下:

Import-Package: org.eclipse.equinox.servletbridge;version="1.0.0"

並且 Equinox 還提供了一個 bundle 名為 org.eclipse.equinox.servletbridge,那么這個 bundle 跟我們加入到 classpath 的 servletbridge.jar 是什么關系呢?實際上這個 bundle 只是將 servletbridge.jar 給包裝起來,並將其導出

Export-Package: org.eclipse.equinox.servletbridge;version="1.1.0"

由此我們了解了 ServletBridge 的工作原理,因此我們可以實現 OSGI 直接嵌入到 Springboot Application 中。

Spring boot 啟動 OSGI bundle

根據上面的分析,接下來我們完成在 Springboot 中 OSGI 的啟動。

首先創建一個 maven 工程

圖 2.Springboot 工程創建

pom.xml 添加對 spring-boot-starter-web 的依賴

圖 3. Springboot 工程目錄

SpringbootApplication 作為啟動類,MyConfig 和 MyContext 完成 OSGI 啟動的配置工作,分別實現 ServletConfig 和 ServletContext 接口,MyFrameworkLauncher 繼承 org.eclipse.equinox.servletbridge.FrameworkLauncher 來完成 OSGI 的啟動工作。

我們將需要啟動的 OSGI bundle 放到當前工程的 temp 目錄下,目錄結構如下所示:

圖 4. OSGI 插件目錄

SpringbootApplication 實現如下所示:

清單 9.SpringbootApplication 實現

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication(scanBasePackages = { "com.test.springboot" })
public class SpringbootApplication {
@Autowired
MyFrameworkLauncher framework;
public static void main(String[] args) {
MyBridge bridge = new MyBridge();
bridge.init();
System.out.println("init classloader"+ SpringbootApplication.class.getClassLoader());
SpringApplication.run(SpringbootApplication.class, args);
}
}

MyContext 需要實現 getResource 和 getAttribute 方法 ,分別來指定 osgi bundle 的的讀取路徑和存儲路徑,為簡單操作,這里我都指定當前工程下的 temp 路徑下,分別實現 MyContext 類中 getAttribute 和 getResources 方法。

MyFrameworkLauncher 需要初始化父類的 config 和 context 變量,並且調用父類的 deploy 和 start 方法來啟動 osgi bundle。

清單 10.OSGI 框架實現

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service("framework")
public class MyFrameworkLauncher extends FrameworkLauncher{
@PostConstruct
public void initialize() {
this.config = new MyConfig();
this.context = config.getServletContext();
File servletTemp = (File) this.context.getAttribute("javax.servlet.context.tempdir");
File platformDirectory = new File(servletTemp, "eclipse");
File plugins = new File(platformDirectory, "plugins");
deployBridgeExtensionBundle(plugins);
this.deploy();
this.start();
}

運行 SpringbootApplication 如下所示:

圖 5. Springboot 運行結果

圖 6. OSGI 命令行

我們看到 OSGI bundle 已經在 Springboot Application 中 run 起來了。

Spring boot 與 OSGI bundle 交互

想要完成 springboot 與 OSGI bundle 的交互,我們按照 servletbridge 的原理來實現類似的機制,我們也為我們的橋接插件部署一個擴展的插件使得橋接插件可以獲取到系統的類加載器。

首先我們建立一個 Bridge 項目 SpringbootBridge ,分別提供 registerDelegat 和 acquireDelegateReference 方法。

清單 11. registerDelegate 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static synchronized void registerDelegate(Object delegate) {
if (instance == null) {
System.out.println("intance is null");
return;
}
if (delegate == null) {
System.out.println("delegate is null");
throw new NullPointerException("Cannot register a null delegate");
}
synchronized (instance) {
if (instance.delegate != null) {
System.out.println("A delegate is already registered");
throw new IllegalStateException("A delegate is already registered");
}
instance.delegate = delegate;
}
}

acquireDelegateReference 方法

1
2
3
4
5
public synchronized Object acquireDelegateReference() {
if (this.delegate != null)
this.delegateReferenceCount += 1;
return this.delegate;
}

將這個 Java 項目導出 jar 包 com.test.bridge.jar

圖 7. 導出 JAR 包

接下來在 Eclipse for RCP and RAP Developers 中創建一個 bundle 將我們的 com.test.bridge.jar 封裝起來,並且導出。

圖 8. Bundle 創建

清單 12. MANIFEST.MF 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Bridge
Bundle-SymbolicName: com.test.bridge.wrapper
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.test.bridge.Activator
Bundle-Vendor: TEST
Require-Bundle: org.eclipse.core.runtime
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-ActivationPolicy: lazy
Export-Package: com.test.bridge
Bundle-ClassPath: com.test.bridge.jar,
.

接下來需要創建一個 bundle 來初始化委托的對象 delegate(com.test.registry) 首先需要導入 com.test.bridge 添加到 classpath,然后調用橋接類的注冊方法來注冊 delegate 對象。

清單 13. 注冊委托對象

1
2
3
4
public void start(BundleContext bundleContext) throws Exception {
Activator.context = bundleContext;
MyBridge.registerDelegate(new Integer(123456));
}

清單 14. MANIFEST.MF 文件

1
2
3
4
5
6
7
8
9
10
11
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Registry
Bundle-SymbolicName: com.test.registry
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.test.registry.Activator
Bundle-Vendor: TEST
Require-Bundle: org.eclipse.core.runtime
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-ActivationPolicy: lazy
Import-Package: com.test.bridge

將這兩個 bundle 導出跟其他 osgi bundle 放在一起(本文目錄為${工程目錄}/temp/eclipse/plugins)

圖 9. 導出 plugins

此時我們再回歸到 SpringbootOSGI 項目,初始化 MyBridge 對象然后調用 acquireDelegateReference,讀者可能疑問這里,在 SpringbootOSGI 項目和注冊插件中我們同時添加了對於 com.test.bridge.jar 的依賴,在插件注冊時獲取的是哪一個呢?首先添加 jar 包的依賴解決了兩邊的編譯問題,在運行時,我們在 SpringbootOSGI 項目一啟動就對 Mybridge 類進行了初始化,此時注冊類已經獲取了系統的類加載器也就是 SpringbootOSGI 中的 App 類加載器,所以注冊類插件可以獲取到 Mybridge 對象,同時注冊類插件注冊了委托對象,那么自然可以通過 acquireDelegateReference 獲取到注冊的委托對象,下面我們看程序演示:

獲取 delegateReference。

清單 15. SpringbootApplication 實現

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication(scanBasePackages = { "com.test.springboot" })
public class SpringbootApplication {
@Autowired
MyFrameworkLauncher framework;
public static void main(String[] args) {
MyBridge bridge = new MyBridge();
bridge.init();
System.out.println("init classloader"+ SpringbootApplication.class.getClassLoader());
SpringApplication.run(SpringbootApplication.class, args);
}
}

清單 16. Springboot REST API

然后提供 REST 獲取 delegate

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/test")
public class TestRestController {
@RequestMapping(value = "/getObj", method = RequestMethod.GET)
@ResponseBody
public Object test(){
Object obj = MyBridge.getInstance().acquireDelegateReference();
System.out.println(obj);
return obj;
}
}

run SpringbootApplication 啟動 OSGI bundle

圖 10. 啟動 bundle

可以看到 bundle 已經以 lazy 方式啟動了,但是啟動 registry 的時候發現 instance is null

我們已經在 SpringbootApplication 中對 MyBridge 進行了實例化,但是在 OSGI bundle (com.test.registry)去注冊 delegate 的時候,MyBridge 的實例卻是 null,這是因為 SpringbootApplication 和 com.test.registry 的類加載器是不相同的,SpringbootApplication 是 AppClassLoader, com.test.registry 是默認 bundle 加載器,那么解決方案就是按照上述說的,我們需要創建一個 fragment bundle,將其掛在到系統 bundle 上面,那么就能將加載器切換為 AppClassLoader.具體 fragment bundle 的創建如下所示。並再次 run SpringbootApplication 啟動 OSGI bundle。

清單 17. 部署 fragement 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void deployBridgeExtensionBundle(File plugins){
File extensionBundle = new File(plugins, "com.test.bridge.extensionbundle.jar");
File extensionBundleDir = new File(plugins, "com.test.bridge.extensionbundle");
if ((extensionBundle.exists()) || ((extensionBundleDir.exists()) && (extensionBundleDir.isDirectory()))) {
return;
}
Manifest mf = new Manifest();
Attributes attribs = mf.getMainAttributes();
attribs.putValue("Manifest-Version", "1.0");
attribs.putValue("Bundle-ManifestVersion", "2");
attribs.putValue("Bundle-Name", "bridge Extension Bundle");
attribs.putValue("Bundle-SymbolicName", "com.test.bridge.extensionbundle");
attribs.putValue("Bundle-Version", "1.0.0");
attribs.putValue("Fragment-Host", "system.bundle; extension:=framework");
String packageExports = "com.test.bridge";
attribs.putValue("Export-Package", packageExports);
try {
JarOutputStream jos = null;
try {
jos = new JarOutputStream(new FileOutputStream(extensionBundle), mf);
jos.finish();
} finally {
if (jos != null) {
jos.close();
}
}
} catch (IOException e) {
System.out.println("Error generating extension bundle" + e);
}
}
圖 11. Bundle 狀態

發送 GET 請求

http://localhost:8080/test/getObj

圖 12. 發送 GET 請求

如果需要 osgi bunld 默認直接啟動,只需要在 temp/eclipse/configuration/config.ini 中添加相應的啟動配置。

osgi.bundles=xxx@start

在我們上述例子中,我們只是傳遞了一個 Integer 對象,如果此時我們需要傳遞我們自定義對象,並且需要調用相應的方法,我們需要怎么做呢?首先我們應該定義一個接口工程,在 Springboot 項目和注冊 bundle 中分別添加對於接口的依賴,同時在注冊 bundle 中對該接口進行實現,並在注冊 bundle 的啟動方法中將實現了的對象傳遞給橋接對象,這樣在 Springboot 項目中我們就可以獲取到該對象,並且進行方法的調用。

首先我們定義一個 Java 項目 spring-boot-interface

定義接口 APIInterface,添加接口方法 testFun

清單 18. 自定義接口

1
2
3
4
package com.test;
public interface APIInterface {
String testFun(int a1,String a2);
}

將這個 Java 項目導出 jar 包 com.test.interface.jar

接下來在 Eclipse for RCP and RAP Developers 中創建一個 bundle 將我們的 com.test.interface.jar 封裝起來,並且將該接口 package 進行導出。

清單 19. MANIFEST.MF 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Wrapper
Bundle-SymbolicName: com.test.inter.wrapper
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.test.inter.wrapper.Activator
Bundle-Vendor: TEST
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.osgi.framework;version="1.3.0"
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: com.test.inter.jar,
.
Export-Package: com.test.inter

接下來需要在 com.test.registry 中添加對 APIInterface 的實現,並且作為 delegate 對象注冊以供使用。

清單 20. 自定義接口實現

1
2
3
4
5
6
7
public class MyInterfaceImpl implements APIInterface {
@Override
public String testFun(int arg0, String arg1) {
String message = "show the para passsing: arg1 :" + arg1 +", arg0: "+ arg0;
return message;
}
}

接下來我們將實現的這個對象作為委托對象注冊到橋接類中。

清單 21. 注冊實現類

1
2
3
4
5
public void start(BundleContext bundleContext) throws Exception {
Activator.context = bundleContext;
MyInterfaceImpl inImpl = new MyInterfaceImpl();
MyBridge.registerDelegate(inImpl);
}

下面我們將 com.test.registry,com.test.bridge.wrapper,com.test.api.inter.wrapper 這三個插件導出 bundle 的 jar 包,與 OSGI 其他 bundle 放到同一目錄下,在本文中為${SpringbootOSGI}/temp/eclipse/plugins

接下來我們在 SpringbootOSGI 中添加相應包的依賴和方法的調用,如下所示:

清單 22. Springboot REST API

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/test")
public class TestRestController {
@RequestMapping(value = "/getObj", method = RequestMethod.GET)
@ResponseBody
public Object test(){
APIInterface obj = (APIInterface) MyBridge.getInstance().acquireDelegateReference();
String result = obj.testFun(2018, "happy new year!");
return result;
}
}

如果此時我們運行 springbootOSGI 項目,同時把相應的 bundle 啟動會發現報錯

java.lang.ClassCastException: com.test.api.inter.impl.MyInterfaceImpl cannot be cast to com.test.api.inter.APIInterface

這是因為不同的 classloader 導致的,因為我們缺少了將 interface 的 bundle 掛載到系統的 bundle 上面。

清單 23. 部署 fragement bundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void deployInterfaceExtensionBundle(File plugins){
File extensionBundle = new File(plugins, "com.test.api.inter.extensionbundle.jar");
File extensionBundleDir = new File(plugins, "com.test.api.inter.extensionbundle");
if ((extensionBundle.exists()) || ((extensionBundleDir.exists()) && (extensionBundleDir.isDirectory()))) {
return;
}
Manifest mf = new Manifest();
Attributes attribs = mf.getMainAttributes();
attribs.putValue("Manifest-Version", "1.0");
attribs.putValue("Bundle-ManifestVersion", "2");
attribs.putValue("Bundle-Name", "interface Extension Bundle");
attribs.putValue("Bundle-SymbolicName", "com.test.api.inter");
attribs.putValue("Bundle-Version", "1.0.0");
attribs.putValue("Fragment-Host", "system.bundle; extension:=framework");
String packageExports = "com.test.api.inter";
attribs.putValue("Export-Package", packageExports);
try {
JarOutputStream jos = null;
try {
jos = new JarOutputStream(new FileOutputStream(extensionBundle), mf);
jos.finish();
} finally {
if (jos != null) {
jos.close();
}
}
} catch (IOException e) {
System.out.println("Error generating extension bundle" + e);
}
}

測試 API 啟動情況

發送 GET 請求

http://localhost:8080/test/getObj

圖 13. 發送 GET 請求

常見錯誤

java.lang.ClassCastException/ Loader constraint violation :loader

不同的 classloader 即使是相同的 class 文件也不是同一實例。解決方案即為使用同一 classloader,在這個項目中,報出此錯是因為 bundle 和 springboot application 不是同一個 classloader 來加載的,需要為 bundle 建立一個 Fragement bundle 並且掛在到 system bundle 下。

NullPointerException

報出此錯一般是因為橋接的 instance 沒有被初始化,或者調用注冊的 bundle 沒有啟動注冊。

Load Class Circle Error

在使用 Springboot 打包 jar 包之后,如果我們使用的 OSGI 的 plugins 是 3.4 版本的話,啟動失敗,ChildFirstClassLoader 與 URLClassloader 互相調用時出現了死循環,這個問題在 osgi 更高版本已經得到了解決。

總結

本文從 Java 類加載器說起,探討了 OSGI 的類加載器原理並對 Equinox 中的 Servletbridge 原理實現進行了詳細的研究,同時擴展到使用這一原理如何在 Spring boot 應用中嵌入 OSGI 開發和 Spring boot 應用如何與 OSGI 插件之間進行相互調用。使用一個例子來對這一系列的使用做了進一步的講解。並對它的實現方法做了進一步的探討,這些探討對於將 OSGI 應用嵌入到任何其他的系統中是一個啟發和幫助,希望有興趣的讀者可以做進一步的了解和實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM