場景
單機應用已經越來越不能符合目前越來越復雜的產品需求了。即使是小型應用,至少也需要部署2臺以上的服務器做集群。且應用必須24小時對外服務,可用性得達到n個9前翎。這就對發(fā)布有了更高的要求。
也就催生了灰度發(fā)布這樣的發(fā)布過程畅涂。而即使是這樣港华,還是需要經歷大致如下的發(fā)布過程:
- 下載代碼
- 打包
- 停止服務器
- 部署
- 啟動服務器
而業(yè)界一直詬病JVM的啟動速度,再加上如果項目比較大毅戈,編譯過程比較長,發(fā)布機器比較多愤惰,那么做一次完整的發(fā)布可能需要幾個小時苇经。萬一中途出了問題,要回退宦言,又要幾個小時扇单。
是否可以解決這樣的問題呢?而OSGi恰是一個不錯的選擇!
OSGi
OSGi是一個優(yōu)雅奠旺、完整和動態(tài)的組件模型蜘澜,提供了完整的模塊化運行環(huán)境。
應用程序(稱為bundle)無需重新引導可以被遠程安裝响疚、啟動鄙信、升級和卸載。
其主要應用在嵌入式開發(fā)中忿晕,而在JavaSE和JavaEE方面則少有建樹装诡。其最著名的使用就是eclipse了。究其原因主要有:
- 增加開發(fā)難度:需要開發(fā)人員更關心模塊的劃分践盼,處理模塊與模塊之間的依賴關系(模塊間的導入導出),這是一個好的方面鸦采,但是
- 沒有完善的工具:模塊間的依賴關系需要開發(fā)人員手動處理(有相應的工具,但不能百分百處理依賴關系)咕幻。
- 額外的運行環(huán)境:應用(bundle)需要運行在實現了OSGi規(guī)范的容器內渔伯,導致了模塊間的依賴關系需要在運行時才能驗證是否有問題。也就是說無法在編譯期驗證模塊間的關系肄程。同時也增加了測試及調試的難度锣吼。
可以看出选浑,OSGi的主要缺點是開發(fā)較繁瑣。而針對前面所提到的問題吐限,OSGi解決了如下幾個問題:
- 項目模塊化鲜侥,對于項目的更新與發(fā)布不再需要發(fā)布整個項目,只需要發(fā)布需要更新的模塊即可诸典。提高了編譯打包的速度描函。
- OSGi可在運行時的對bundle進行安裝、啟動狐粱、升級和卸載舀寓。提高了部署的速度。(這里就需要吐槽一下eclipse了肌蜻,它是基于osgi的互墓,但是每次安裝插件都要重啟是要搞哪樣?蒋搜!)
- 支持多版本發(fā)布豆挽。在OSGi容器內可發(fā)布相同應用的不同版本
OSGi是如何做到這些的呢?其實OSGi實現了一套自身的ClassLoader帮哈,具體可見此文
OSGi容器
目前OSGi容器主要有Knopflerfish, Apache Felix, Equinox, Spring DM。其具體比較請見此文
以及其上的一些應用咖刃,方便在OSGi上進行開發(fā),比如Karaf,ServiceMix等憾筏。
OSGi的使用方式
OSGi容器有兩種使用方式:
-
作為容器使用:
OSGi容器作為外層氧腰,所有的應用均部署在OSGi容器內容贝。那么所有的應用都需要bundle化斤富,但是上面說了,bundle化不是一個方便的過程满力。 且OSGi在非嵌入式領域并不是很流行,雖然之前業(yè)界一直在推廣刻帚,但最終效果并不理想,Spring最后也放棄了對OGSi的支持顷歌。 所以當你的應用較大時,bundle化會是一個比較大的絆腳石眯漩。
-
嵌入式使用
基于上面的原因赦抖,我們可以將OSGi容器作為嵌入式容器使用队萤,即基本的模塊在OSGi外部運行浮禾,也就不需要bundle化了, 變動比較頻繁的模塊部署到OSGi容器內,使用OSGi便利的部署機制旁钧。 比如:項目中依賴的Spring,Mybatis等jar包可以在OSGi容器外部署,而業(yè)務模塊則部署到OSGi容器內
Felix安裝
這里使用felix作為OSGi容器來演示嵌入式OSGi的使用寄猩!felix可到Apache網站下載!
felix目錄結構如下:
-bin:felix.jar路徑,其實felix只需要這個jar包就可以運行了
-bundle:部署的bundle目錄,如果你有需要部署的bundle剪况,將其拷貝到此目錄下译断,啟動felix時會自動部署
-cache:bundle緩存目錄
-conf:配置文件目錄
-doc:相關文檔
在根目錄運行如下命令即可啟動
java -jar bin/felix.jar
- bundle目錄下默認有四個bundle,提供了類似命令行功能翎蹈。啟動時自動部署了荤堪〕窝簦可以輸入lb,來查看已安裝的bundle
g! lb
START LEVEL 1
ID|State |Level|Name
0|Active | 0|System Bundle (5.4.0)|5.4.0
1|Active | 1|Apache Felix Bundle Repository (2.0.6)|2.0.6
2|Active | 1|Apache Felix Gogo Command (0.16.0)|0.16.0
3|Active | 1|Apache Felix Gogo Runtime (0.16.2)|0.16.2
4|Active | 1|Apache Felix Gogo Shell (0.10.0)|0.10.0
g!
這是普通的使用felix的方式肮塞。不做過多介紹枕赵。主要介紹嵌入式Felix的應用。
嵌入啟動Felix
創(chuàng)建Maven項目
- 首先創(chuàng)建一個普通的Maven項目
- 在pom.xml中添加felix依賴
<dependencies>
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.main</artifactId>
<version>5.4.0</version>
</dependency>
</dependencies>
編寫啟動類
- 啟動felix的核心代碼如下
FrameworkFactory factory = getFrameworkFactory();
m_fwk = factory.newFramework(configProps);
m_fwk.init();
AutoProcessor.process(configProps, m_fwk.getBundleContext());
m_fwk.start();
m_fwk.waitForStop(0);
System.exit(0);
- getFrameworkFactory()方法通過jdk6的ServiceLoader來加載FrameworkFactory實現副瀑,并實例化返回糠睡,具體代碼如下
- 通過configProps來構建Framework狈孔,configProps是Map<String, String>類型均抽,里面為felix及osgi相關配置,具體配置后面介紹
- 初始化Framework
- 配置屬性和發(fā)布bundle
- 啟動Framework
//getFrameworkFactory()方法實現代碼
private static FrameworkFactory getFrameworkFactory() throws Exception {
URL url = Main.class.getClassLoader().getResource(
"META-INF/services/org.osgi.framework.launch.FrameworkFactory");
if (url != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
try {
for (String s = br.readLine(); s != null; s = br.readLine()) {
s = s.trim();
// Try to load first non-empty, non-commented line.
if ((s.length() > 0) && (s.charAt(0) != '#')) {
return (FrameworkFactory) Class.forName(s).newInstance();
}
}
} finally {
if (br != null)
br.close();
}
}
throw new Exception("Could not find framework factory.");
}
Felix屬性
框架屬性
- org.osgi.framework.executionenvironment - osgi執(zhí)行JVM環(huán)境,不必特殊設置
- org.osgi.framework.storage - bundle緩存的完整路徑惋鹅,可使用下面的felix.cache.rootdir作為前綴拼接
- felix.cache.rootdir - bundle緩存的root地址
- org.osgi.framework.storage.clean - 是否需要刷新bundle緩存,"none"或"onFirstInit"武鲁,默認為"none"
- felix.cache.filelimit - 限制bundle緩存數量洞坑,默認是0,即無限制
- felix.cache.locking - 是否開啟鎖,限制并發(fā)訪問锅尘,默認開啟
- felix.cache.bufsize - 設置緩存的緩沖區(qū)大小藤违,默認4096
- org.osgi.framework.system.packages - 以逗號隔開的包名议街,來確定哪些包通過系統bundle來加載特漩,就是lb命令列出的第一個bundle
- org.osgi.framework.system.packages.extra - 和org.osgi.framework.system.packages功能相同涂身,放額外的包
- org.osgi.framework.bootdelegation - 以逗號隔開的包(支持模糊匹配,上面兩個屬性不支持)悍抑,委托給父ClassLoader加載(由org.osgi.framework.bundle.parent定義)搜骡,OSGi容器內的bundle不需要Import即可使用此包。OSGi不建議使用此屬性摸吠,破壞了模塊化
- org.osgi.framework.bundle.parent - 指明哪個ClassLoader將用來加載bootdelegation屬性所指定的包寸痢。boot表示啟動的根ClassLoader,app表示應用ClassLoader,ext表示ExtClassLoader,framework表示容器的ClassLoader,默認是boot
- felix.bootdelegation.implicit - 配置容器是否要判斷哪些包是否是delegate的,默認開啟
- felix.systembundle.activators - 用來配置系統Bundle的啟動器對象献烦,這個配置只能通過類來配置巩那,不能通過配置文件即横,因為設置的值是個對象實例
- felix.log.logger - 設置一個org.apache.felix.framework.Logger實例东囚,同樣只能通過類來配置
- felix.log.level - 日志級別(1 = error, 2 = warning, 3 = information, and 4 = debug). 默認為1
- org.osgi.framework.startlevel.beginning - 框架啟動級別舔庶,默認為1
- felix.startlevel.bundle - bundle啟動級別,默認為1
- felix.service.urlhandlers - 是否開啟URL Handler瞧甩,默認開啟肚逸。開啟后會調用URL.setURLStreamHandlerFactory()和URLConnection.setContentHandlerFactory()
啟動屬性
- felix.auto.deploy.dir - 配置自動部署bundle的目錄朦促,默認為當前目錄下的bundle目錄
- felix.auto.deploy.action - 使用一個以逗號隔開的字符串配置在auto-deploy目錄中的bundle所要執(zhí)行的動作,包括install, update, start和uninstall。如果沒有配置箩退,或者配置出錯戴涝,則auto-deploy目錄中的bundle將不會做任何動作
- felix.auto.install.<n> - 空格隔開的bundle URL,<n>是啟動級別郑什,當啟動級別低于felix.startlevel.bundle設置的值,則自動安裝
- felix.auto.start.<n> - 空格隔開的bundle URL兜粘,<n>是啟動級別剃法,當啟動級別低于felix.startlevel.bundle設置的值贷洲,則自動安裝并啟動
- felix.shutdown.hook - 配置是否需要一個關閉鉤子优构,來進行關閉時的清理工作。默認為true
與Felix交互
構建bundle
- 創(chuàng)建Maven項目彪腔,在pom.xml配置如下插件,打包方式為bundle
<packaging>bundle</packaging>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.5.3</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-Name>demo</Bundle-Name>
<Bundle-SymbolicName>demo</Bundle-SymbolicName>
<Implementation-Title>demo</Implementation-Title>
<Implementation-Version>1.0.0</Implementation-Version>
<Export-Package></Export-Package>
<Import-Package></Import-Package>
<Bundle-Activator>org.embedosgi.activator.Activator</Bundle-Activator>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
- Export-Package和Import-Package為空,說明此bundle不對外導出任何包盲厌,也不導入任何包
- Bundle-Activator定義了一個啟動器org.embedosgi.activator.Activator,它包含了在OSGi容器啟動此bundle時需要做的處理,代碼如下
package org.embedosgi.activator;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import java.util.Dictionary;
import java.util.Hashtable;
/**
* Created by wangyifan on 2015/11/9.
*/
public class Activator implements BundleActivator {
public void start(BundleContext bundleContext) throws Exception {
Dictionary<String,String> dict = new Hashtable<String, String>();
String version = bundleContext.getBundle().getVersion().toString();
dict.put("version",version);
Object bean = Class.forName("org.embedosgi.demo.impl.HelloImpl").newInstance();
bundleContext.registerService("org.embedosgi.demo.Hello", bean, dict);
System.out.println("Reg Hello Service End!" + version);
}
public void stop(BundleContext bundleContext) throws Exception {
}
}
其主要作用就是將HelloImpl對象對外發(fā)布為Hello服務,并設置版本號為自身bundle的版本號阀湿,即在pom.xml中設置的Version
Hello和HelloImpl很簡單
package org.embedosgi.demo;
/**
* Created by wangyifan on 2015/11/9.
*/
public interface Hello {
String say(String name);
}
package org.embedosgi.demo.impl;
import org.embedosgi.demo.Hello;
/**
* Created by wangyifan on 2015/11/9.
*/
public class HelloImpl implements Hello {
public String say(String name) {
return "Hello " + name;
}
}
通過maven的package命令打包,即可打包成一個bundle
本地部署與debug
打包成bundle后灾挨,一般情況下你需要把bundle發(fā)布到OSGi容器內去部署劳澄,這里就是發(fā)布到felix中莫矗。而目前我們使用了內嵌式的felix,可直接在本地部署食磕。方便調試彬伦。
- 首先,將上面的Felix啟動應用打包搂橙,發(fā)布到本地maven倉庫中
- 在bundle項目中添加依賴
<!--這是我在上面創(chuàng)建的Felix啟動項目的依賴配置区转,請根據自己的項目做修改-->
<dependencies>
<dependency>
<groupId>com.ivan.osgi</groupId>
<artifactId>osgi</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
將felix中的conf目錄拷貝到bundle應用的根目錄
新建一個Application啟動
設置Main Class為Felix啟動應用中的Main類,我這里是org.embedosgi.main.Main
-
在VM options中添加-Dfelix.auto.start.2=file:/E:/code/embedosgi/demo/target/demo-1.0.0.jar
上面的配置可參考前面的屬性說明!這里路徑指到bundle應用的打包路徑蜻韭。
添加一個運行前的mvn package動作
最后運行
這樣的話,每次運行時都會打包這個bundle俯画,然后自動將其部署到了Felix容器中了泡仗。且支持debug
外部類獲取OSGi服務
這里測試如何在org.embedosgi.main.Main中調用bundle中發(fā)布的Hello服務沮焕。
其實很簡單辣辫,OSGi通過BundleContext來管理bundle,在上面發(fā)布服務的時候你也看到了姐浮,也是通過BundleContext來發(fā)布服務的卖鲤。
而Framework提供了獲取BundleContext的方法getBundleContext(),只要獲取到BundleContext就可以獲取服務了区匣。相關代碼如下:
BundleContext context = m_fwk.getBundleContext();
String filter = "(&(objectClass=org.embedosgi.demo.Hello)(version=1.0.0))";
Filter f = context.createFilter(filter);
ServiceTracker serviceTracker = new ServiceTracker(context, f, null);
serviceTracker.open();
Object o = serviceTracker.getService();
Class clz = o.getClass();
System.out.println(this.getClass().getClassLoader() + " | " + clz.getClassLoader());
Method method = clz.getDeclaredMethod("say", String.class);
System.out.println(method.invoke(o, "Ivan"));
- 獲取BundleContext
- 構建LDAP Filter語法字符串.LDAP語法請見此處
- 根據Filter語法字符串創(chuàng)建Filter
- 通過BundleContext和Filter構建ServiceTracker欺旧,此類可以通過Filter在Context中查找到符合條件的Service
- 打開ServiceTracker彻坛,必要操作
- 獲取service
- 后面就是通過反射調用了
內部bundle調用外部類
我們在Felix中啟動項目中新增一個類HostHello
package org.embedosgi.host;
/**
* Created by wangyifan on 2015/11/9.
*/
public class HostHello {
public String name(){
return "HostName";
}
}
只是簡單的返回一個字符串
在Bundle項目中,修改HelloImpl類來獲取這個類
package org.embedosgi.demo.impl;
import org.embedosgi.demo.Hello;
import org.embedosgi.host.HostHello;
/**
* Created by wangyifan on 2015/11/9.
*/
public class HelloImpl implements Hello {
public String say(String name) {
return "Hello " + name + new HostHello().name();
}
}
如何能使HelloImpl調用到HostHello的name方法呢?
其實有兩個方法可以實現!
- 配置org.osgi.framework.system.packages或org.osgi.framework.system.packages.extra
- 配置org.osgi.framework.bootdelegation和org.osgi.framework.bundle.parent
我們先看第一個方法:
只需要在conf/config.properties中配置
org.osgi.framework.system.packages=org.embedosgi.host
或者
org.osgi.framework.system.packages.extra=org.embedosgi.host
然后修改Bundle項目的pom.xml文件
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.5.3</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-Name>demo</Bundle-Name>
<Bundle-SymbolicName>demo</Bundle-SymbolicName>
<Implementation-Title>demo</Implementation-Title>
<Implementation-Version>1.0.0</Implementation-Version>
<Export-Package></Export-Package>
<Import-Package>*</Import-Package>
<Bundle-Activator>org.embedosgi.activator.Activator</Bundle-Activator>
</instructions>
</configuration>
</plugin>
*號表示自動生成需要的導入包,你也可以將Import-Package標簽刪除疙渣,默認就是自動導入
也就是說,通過系統Bundle導出了org.embedosgi.host這個包啦租,然后在Bundle項目中導入了這個包。這樣就可以在Bundle中調用了
第二種方法
配置
org.osgi.framework.bootdelegation=sun.*,com.sun.*,org.osgi.framework,org.osgi.framework.*,org.embedosgi.host
org.osgi.framework.bundle.parent=app
這里的意思是所有以sun,com.sun,org.osgi.framework和org.embedosgi開頭的包都通過AppClassLoader加載,加載后對所有bundle可見嘉蕾。
Bundle項目不需要做任何導入導出荆针!
ClassLoader結構圖
第二種方式的ClassLoader結構圖如下:
HelloImpl在遇到HostHello類時棱貌,發(fā)現配置了org.osgi.framework.bootdelegation今魔,那么直接委托給AppClassLoader來加載错森。
可以稍微修改下代碼,打印出ClassLaoder即可得到!
package org.embedosgi.demo.impl;
import org.embedosgi.demo.Hello;
import org.embedosgi.host.HostHello;
/**
* Created by wangyifan on 2015/11/9.
*/
public class HelloImpl implements Hello {
public String say(String name) {
System.out.println("HostHello ClassLoader = " + HostHello.class.getClassLoader());
return "Hello " + name + new HostHello().name();
}
}
打印結果
HostHello ClassLoader = sun.misc.Launcher$AppClassLoader@610f7612
多版本
OSGi支持多版本發(fā)布袁波,即可以在一個OSGi容器內發(fā)布多個不同版本的應用瓦阐。比如這里我們有一個demo-1.0.0.jar的應用蜗侈。
我們可以對HelloImpl稍做修改,發(fā)布一個demo-1.0.1.jar的版本睡蟋。兩個版本可以并存。調用時只需要通過LDAP過濾即可戳杀。
總結
本文介紹了
- 如何嵌入式的啟動一個OSGi容器
- 如何與OSGi bundle進行交互
- 多版本
通過如上內容该面,我們可以將應用中基礎的部分固化,而業(yè)務代碼動態(tài)化豺瘤,來加快代碼的迭代速度吆倦。
同時也可實現如服務框架听诸,結合MVVM模式坐求,可實現易擴展的Web應用。想象空間還是很大的晌梨。
最后給出項目代碼