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 等html

擴展類加載器(Extension ClassLoader)

負責加載 Java 的擴展類庫,默認加載 JAVA_HOME/jre/lib/ext/目下的全部 jarjava

應用類加載器(Application ClassLoader)

負責加載應用程序 classpath 目錄下的全部 jar 和 class 文件。web

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

判別兩個類是否相同,除了是相同的 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.Classapi

OSGI 類加載器

OSGI 類加載器並不遵循 Java 的雙親委派模型,OSGi 爲每一個 bundle 提供一個類加載器,該加載器可以加載 bundle 內部的類和資源,bundle 之間的交互是從一個 bundle 類加載器委託到另外一個 bundle 類加載器,全部 bundle 都有一個父類加載器。springboot

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)app

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 應用嵌入到任何其餘的系統中是一個啓發和幫助,但願有興趣的讀者能夠作進一步的瞭解和實現。

相關文章
相關標籤/搜索