maven-bundle-plugin 2.4.0以下版本導出META-INF中的內容到MANIFEST.MF中
今天終於把maven-bundle-plugin不能導出META-INF中的內容到Export-Package中的問題解決了,因為產品用的是OSGI框架,用到的第三方JAR包需要加載META-INF/XX/XX.xml這個內容,但在運行的時候getResource返回null。
經一番調查發現META-XX.XX這個包名沒有導出,手動去修改MANIFEST.MF可以解決問題。但產品的源碼中這個依賴是以maven-bundle-plugin進行打包的,在添加Export-Package:META-INF.xx的時候報錯:說找不到META-INF.XX這個包名(顯然這樣子的包名是不合法的)。
想來想去沒有其它好的解決方案,后來發現maven-bundle-plugin 2.4以上的版本可以導出這樣子的包名。
可問題又來了,產品目前都是2.3.7的插件版本。。。。又搜索了一番,最后發現用<_failok>true</_failok>可以強制導出一些名甚至一些並不存在的包名。如下:
<Export-Package>META-INF.xxx</Export-Package> <_failok>true</_failok>
之前的Opendoc中沒有涉及過此部分的內容,maven又是現在非常流行的java的工具,再加上到目前為止搭建OSGi Maven開發和部署的環境還是比較的麻煩,覺得有必要寫篇這樣的blog,:),在這篇blog中來看下如何搭建一個比較好用的OSGi Maven開發和部署環境,看看我在搭建一個這樣的環境中的痛苦歷程。
首先說下我期望的OSGi Maven開發/部署的環境:
1、META-INF中的manifest.mf文件可以自己控制;
Eclipse對插件工程的開發支持的很好了,在IDE中可以很方便的去修改這個manifest.mf,所以還是自己控制更爽,當然,打包的時候需要打入自己控制的這個manifest.mf。
2、在mvn eclipse:eclipse生成的.classpath中,能夠不把所依賴的bundle的jar包放進去;
因為在OSGi環境中,已經不再通過直接在project的classpath中依賴其他bundle的jar了來調用其他bundle中的 package,而是通過在manifest.mf中增加import-package這樣的方式,所以不能再把依賴的bundle的jar打到 classpath里了,否則會很奇怪,當然,這也源於eclipse有個很好的插件開發環境,讓你可以在不依賴bundle jar的情況下直接寫依賴其他bundle的package的代碼。
3、在mvn clean package的時候能夠把需要依賴的jar打到bundle jar中,並和META-INF/Manifest.mf文件中的Bundle-Classpath是匹配的;
在某些bundle中可能會依賴一些jar,在META-INF中通常會去指定依賴的這些jar,放入bundle-classpath中,因此要求在打包的時候能夠把這些依賴的jar打入相應的路徑下。
說完想法后,首先想到的是在OSGi界中支持maven環境的大名鼎鼎的maven-bundle-plugin(http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html),maven-bundle-plugin基於Peter寫的bnd實現,不說廢話了,按照自己期望的環境來使用maven-bundle-plugin進行搭建:
步驟一
按照上面頁面的指導,在pom.xml中增加maven-bundle-plugin先,接着按照自己的想法,要自己控制manifest.mf,於是在plugin的configuration中增加:
<_include>META-INF/MANIFEST.MF</_include>
滿心歡喜的等待着完美的結果,可惜....不如人意呀,打包出來的jar里面的MANIFEST.MF已經物是人非了,完全不是自己控制的那個,插件給你 自動的加上了一堆的import-package、private-package、export-package,我知道這個插件是基於bnd來寫的, 但沒想到竟然連自己控制的權力都不給我了,完全仍然是通過bnd來計算出import-package、private-package什么的;
步驟二
好,在傷心過后接着仔細看,還好,在plugin的configuration中可以自己指定export-package、private- package這些,於是繼續欣喜的使用,這兩個倒是控制住了,但....import-package自己是不能控制的,這個是不行的,這樣就導致了必 須同時自己維護pom.xml以及project中的META-INF/MANIFEST.MF,讓它們保持一致,否則可能導致打出來的包和你在 project中運行的表現不一致,並且bnd計算出來的import-package並不是我想要的,有點太復雜了,還是自己控制比較好;
步驟三
傷心到極點了,其實到目前為止,已經可以確定maven-bundle-plugin,也是OSGi maven中唯一的插件,不能滿足我的需求,不過還是繼續看看這個插件其他方面的表現,驚喜的發現有一點倒是做的不錯的,它支持一個<Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>,有了這個標簽后,它可以直接把依賴的jar打入bundle jar包中,並且相應的自動在bundle-classpath中加上了,這點倒是不錯的,看起來與我期望的環境的第3點是比較匹配的,可惜了。
還有就是,很當然的,它沒法做到控制mvn eclipse:eclipse時生成的.classpath不包含bundle jar的引用。
按照上面的三個步驟,總結下,有些時候智能是好事,但maven-bundle-plugin就是過於智能了,為什么不給點權力給使用者呢,因此這個插件 要提升到完全可用的情況的話,還需要提供下讓使用者自己控制MANIFEST.MF的權力,相信這點要做到並不困難,而且做到這點后基本也就可以使用了。
繼續尋找,於是靜心分析了下自己的需求,貌似可以自己通過maven現有的幾個插件來達成自己的願望,於是開始了組合拳:
1、MANIFEST.MF文件自己控制
不就是要自己控制這個文件嘛,OK,干脆,就只用maven-jar-plugin,這個插件允許指定所使用的MANIFEST.MF文件,於是,嘗試着在這個plugin的configuration中增加:
<archive>
<manifestFile>META-INF/MANIFEST.MF</manifestFile>
</archive>
恩,很順利,開門紅呀,打出來的jar包中的MANIFEST.MF文件就是自己的那個。
2、mvn eclipse:eclipse生成的.classpath中要去掉bundle jar的依賴
對於我這么一個對maven不是那么熟悉的人來講,這個有點復雜,於是不斷的google,甚至是翻看了maven-eclipse-plugin的源碼...
最終終於功夫不負有心人,找到一個簡單的辦法:
首先將工程方式指定為pde,也就是eclipse插件工程,在maven-eclipse-plugin的configuration配置中增加<pde>true</pde>;
然后在pom.xml中將不希望生成到.classpath中依賴的scope指定為provided;
心驚膽戰的開始運行mvn eclipse:eclipse,OH YEAH!,成功!
ps: 另外也可以通過在maven-eclipse-plugin的configuration中增加exclude配置,來將某些依賴從.classpath中去掉,當然,這方法沒有上面的易用。
3、在mvn clean package的時候能夠把需要依賴的jar打到bundle jar中,並和META-INF/Manifest.mf文件中的Bundle-Classpath是匹配的;
恩,這點,印象中貌似maven是有支持的,於是繼續開始找,終於找到了maven- dependency-plugin(之前還找到了一個maven-shade-plugin,也很帥,不過不滿足需求),通過這個插件可以把需要的依賴 的jar都復制到某個指定的目錄中去,但記得把這個指定的目錄加入到maven-jar-plugin的resources目錄里面去,否則這些jar文 件是不會出現在你的bundle jar里的。
OK,通過上面這套組合拳,終於達成了目的,看來有必要找個時間寫個好用點的maven的OSGi插件,否則真的忒折騰了,上面這個方法仍然有幾個痛苦的地方:
1、如果你的bundle中需要export其中依賴的lib的package的話;
mvn eclipse:eclipse之后你會發現其他bundle即使import了這個package,也會調用不到,這里的原因在於生成 的.classpath中所依賴的那個lib的exported屬性沒有設置為true,google了maven eclipse插件,貌似這是它的一個缺失的功能,因此在目前只能是mvn eclipse:eclipse后,再到build path里把這些lib exported出去。
2、還是得在插件的pom.xml中配置所依賴的其他的bundle;
這是為了讓你在mvn clean package的時候能通過,這點還是挺郁悶的,如果能自己去找到的話就好了(Felix構建bundle repo是有潛質做到這點的),:),這樣導致了在每次在import package后,還得記得去修改了pom,否則的話在eclipse compile什么都正常,到了maven里就掛了。
因此,如果能解決上面兩點的話,那將更加完美。
用maven構建基於osgi的web應用
- 如何用maven創建osgi項目
- 如何啟動osgi框架
- 如何在osgi外部與osgi框架通信
- 如何應用jndi配置資源引用
- 如何發布osgi服務
- 如何創建基於osgi的web應用項目
用maven的目的是使開發相率更高,而且不需要自己修改manifest.mf文件,用maven插件即可自動完成創建manifest並打包。創建這種項目的要求如下:
項目包類型為:<packaging>bundle</packaging>
要使用maven自動創建manifest.mf文件,需要插件maven-bundle-plugin,配置如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>1.4.0</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>${pom.groupId}.${pom.artifactId}</Bundle-SymbolicName>
<Bundle-Name>${pom.name}</Bundle-Name>
<Bundle-Version>${pom.version}</Bundle-Version>
<Bundle-Activator>com.javaworld.sample.tomcat.webActivator</Bundle-Activator>
<Export-Package>com.javaworld.sample.tomcat</Export-Package>
<Import-Package>
org.osgi.framework
</Import-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
使用maven-bundle-plugin請參考:http://wso2.org/library/tutorials/develop-osgi-bundles-using-maven-bundle-plugin ,配置好后在項目目錄下執行mvn install即可
同時還要引用osgi框架支持依賴包:
<dependencies>
<dependency>
<groupId>org.eclipse.osgi</groupId>
<artifactId>framework</artifactId>
<version>3.4.2.R34x_v20080826</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>
注意這里的type和scope,這種用法不僅表名bundel項目可以引用第三方bundle項目,普通項目也可以。下面將會看到。
2.啟動osgi框架,並在外部與osgi框架通信
這里用Equinox OSGi框架,讓EquinoxContainer 實現接口OsgiServices 並實現OsgiContainable 的start和stop方法。對於框架的啟動和停止,我們放在tomcat啟動和停止時觸發。
首先用maven創建普通的項目,在pom中添加依賴
<dependencies>
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>catalina</artifactId>
<version>5.5.12</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.osgi</groupId>
<artifactId>services</artifactId>
<version>3.1.200.v20071203</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.osgi</groupId>
<artifactId>framework</artifactId>
<version>3.4.2.R34x_v20080826</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>
public interface OsgiServices {
public Object getOSGiService(String serviceName);
public Class<?> getBundleClass(String bundleName, String className)
throws ClassNotFoundException;
}
//OsgiContainable 擴展OsgiServices 並提供start和stop接口
public class EquinoxContainer implements OsgiContainable {
private static Logger log = Logger
.getLogger(EquinoxContainer.class.getName());
private static BundleContext context;
private static String equinoxHome;
private static EquinoxContainer instance;
private EquinoxContainer() {
}
public static EquinoxContainer getInstance() {
if (instance == null) {
instance = new EquinoxContainer();
}
return instance;
}
public void start() throws Exception {
configFramework();
EclipseStarter.run(new String[] { "-console" }, null);
// get BundleContext
context = EclipseStarter.getSystemBundleContext();
}
private void configFramework() {
String catalinaHome = System.getProperty("catalina.home");
log.info("The catalina home is " + catalinaHome);
File filePath = new File(catalinaHome, "/osgi/equinox");
if (!filePath.exists()) {
throw new IllegalStateException(
"Can not find Equinox runtime on the specified the directory. "
+ catalinaHome + "/osgi/equinox");
}
equinoxHome = filePath.getAbsolutePath();
log.info("The osgi home is " + equinoxHome);
filePath = new File(filePath, "plugins");
String bundlePath = filePath.getAbsolutePath();
log.info("The bundle home is " + bundlePath);
// set the bundle home
FrameworkProperties.setProperty("osgi.syspath", bundlePath);
filePath = new File(equinoxHome, "configuration");
String confHome = filePath.getAbsolutePath();
// set the configuration home
FrameworkProperties.setProperty("osgi.configuration.area", confHome);
log.info("The configuration home is " + confHome);
//config.ini中至少需要配置osgi.bundles需要啟動的bundles
Properties prop = loadConfigProperties(confHome, "/config.ini");
setSystemProperties(prop);
// set the framework properties
FrameworkProperties.setProperty("osgi.noShutdown", "true");
FrameworkProperties.setProperty("eclipse.ignoreApp", "true");
FrameworkProperties.setProperty("osgi.bundles.defaultStartLevel", "4");
}
。。。
}
為了讓tomcat等啟動時執行EquinoxContainer 的start和stop,需要實現LifecycleListener接口,並在tomcat server.xml文件中配置Listener :
<Listener className="com.javaworld.sample.tomcat.osgi.OsgiLifecycleListener" osgiType="Equinox"/>
public class OsgiLifecycleListener implements LifecycleListener {
private static Logger log = Logger.getLogger(OsgiLifecycleListener.class
.getName());
private static OsgiLifecycleListener listener;
private String osgiType = "Equinox";
private OsgiContainable osgiContent = null;
public OsgiLifecycleListener() {
}
public static OsgiLifecycleListener getInstance() {
return listener;
}
@Override
public void lifecycleEvent(LifecycleEvent event) {
try {
if (Lifecycle.INIT_EVENT.equals(event.getType())) {
log.info("Initializing osgi content. osgi type is " + osgiType);
initContent();
log.info("The osgi content initialized. osgi type is "
+ osgiType);
} else if (Lifecycle.START_EVENT.equals(event.getType())) {
log.info("Starting osgi service.");
osgiContent.start();
log.info("The osgi service started.");
} else if (Lifecycle.STOP_EVENT.equals(event.getType())) {
log.info("Stopping osgi service.");
osgiContent.stop();
log.info("The osgi service stopped.");
}
} catch (Exception e) {
log.info(e.getMessage());
System.exit(-1);
}
}
private void initContent() throws Exception {
listener=this;
log.info("listener"+listener.toString());
osgiContent = OsgiContainerFactory.getInstance().getOsgiContent(osgiType);
}
。。。。
}
隨着tomcat的啟動將執行OsgiLifecycleListener,並通過OsgiLifecycleListener啟動osgi框架,后者將完成bundles的安裝和服務的注冊等。
3。在osgi外部與osgi框架通信
OSGi通過BundleContext來獲取OSGi服務,因此想在OSGi容器外獲取OSGi服務,首要的問題就是要在OSGi容器外獲取到BundleContext, EclipseStarter 中 提供了一個getSystemBundle- Context的方法,通過這個方法可以輕松拿到BundleContext,而通過BundleContext則可以輕易拿到OSGi服務的實例。不過 這個時候要注意的是,如果想執行這個OSGi 服務實例的方法,還是不太好做的,因為容器外的classloader和OSGi服務實例的class所在的classloader並不相同,因此不太好 按照java對象的方式直接去調用,更靠譜的是通過反射去調用。 如果想在容器外獲取到OSGi容器里插件的class,一個可選的做法是通過BundleContext獲取到Bundle,然后通過Bundle 來加載 class,采用這樣的方法加載的class就可以保證是相同的。
public static Object getOSGiService(String serviceName) {
ServiceReference serviceRef = context.getServiceReference(serviceName);
if (serviceRef == null)
return null;
return context.getService(serviceRef);
}
public static Class<?> getBundleClass(String bundleName, String className)
throws Exception {
Bundle[] bundles = context.getBundles();
for (int i = 0; i < bundles.length; i++) {
if (bundleName.equalsIgnoreCase(bundles[i].getSymbolicName())) {
return bundles[i].loadClass(className);
}
}
return null;
}
在實現了OSGi容器外與OSGi交互之后,通常會同時產生一個需求,就是在OSGi容器內的插件要加載OSGi容器外的類,例如OSGi容器內提 供了一個mvc框架,而Action類則在OSGi容器外由其他的容器負責加載,那么這個時候就會產生這個需求了,為了做到這點,有一個比較簡單的解決方 法,就是編寫一個Bundle,在該Bundle中放置一個允許設置外部ClassLoader的OSGi服務,例如:
Java代碼
- public class ClassLoaderService{
- public void setClassLoader(ClassLoader classloader);
- }
基於上面的方法,在外部啟動Equinox 的類中去反射執行ClassLoaderService這個OSGi服務的setClassLoader方法,將外部的classloader設置進來, 然后在OSGi容器的插件中要加載OSGi容器外的類的時候就調用下面這個ClassLoaderService去完成類的加載。
4.應用jndi配置資源引用
上面知道要在osgi外面引用osgi服務或者osgi類,唯一可行的辦法是獲取BundleContext,這點是可以在osgi框架獲取,這樣一來BundleContext就是我們與osgi通信的橋梁了,OsgiServices 也是通過此橋梁與osgi框架通信,為方便對OsgiServices 的獲取,現在將OsgiServices配置到jndi。同樣把Resource添加到server.xml的Host標簽中:
<Resource name="osgi/services" auth="Container"
type="com.javaworld.sample.tomcat.osgi.OsgiServices"
factory="com.javaworld.sample.tomcat.osgi.OsgiServicesObjectFactory" />
在tomcat 的web.xml中添加Resource引用:
<resource-env-ref>
<description>osgi services</description>
<resource-env-ref-name>osgi/services</resource-env-ref-name>
<resource-env-ref-type>
com.javaworld.sample.tomcat.osgi.OsgiServices
</resource-env-ref-type>
</resource-env-ref>
public class OsgiServicesObjectFactory implements ObjectFactory {
private static Logger log = Logger.getLogger(OsgiServicesObjectFactory.class
.getName());
private OsgiServices osgiServices;
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
Map<String, String> attMap = new HashMap<String, String>();
// Customize the bean properties from our attributes
Reference ref = (Reference) obj;
Enumeration<RefAddr> addrs = ref.getAll();
while (addrs.hasMoreElements()) {
RefAddr addr = addrs.nextElement();
String attrName = addr.getType();
String value = (String) addr.getContent();
log.info("the attribute is (" + attrName + " == " + value + ")");
attMap.put(attrName, value);
}
log.info("getObjectInstance called.");
osgiServices=OsgiLifecycleListener.getInstance().getOsgiServices();
return osgiServices;
}
}
5. 發布osgi服務
public interface HelloService {
public String sayHello();
}
public class webActivator implements BundleActivator {
ServiceRegistration serviceRegistration;
public void start(BundleContext bundleContext) {
serviceRegistration = bundleContext.registerService(
HelloService.class.getName(), new HelloServiceImpl(),
new Properties());
}
public void stop(BundleContext bundleContext) {
serviceRegistration.unregister();
}
}
6.創建基於osgi的web應用項目
創建<packaging>war</packaging>web項目,添以下類:
public class TomcatWebCall {
public static String invokeService() {
try {
HelloService service = OsgiServiceFactory.getOsgiService(
"osgi/services", HelloService.class);
return service.sayHello();
} catch (IllegalArgumentException e) {
e.printStackTrace();
e.printStackTrace();
}
return null;
}
}
public class OsgiServiceFactory {
public static <T> T getOsgiService(String jndiName,Class<T> clazz) {
OsgiServices services = (OsgiServices) Utils.lookup(jndiName);
OsgiServiceInvocationHandler handler = new OsgiServiceInvocationHandler(
services, clazz.getName());
return Utils.getProxyObject(clazz,handler);
}
public class OsgiServiceInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
Object service = services.getOSGiService(servicName);
if (service == null) {
throw new IllegalServiceException("Cann't find out osgi service:"
+ servicName);
}
Method[] methods = service.getClass().getDeclaredMethods();
for (Method meth : methods) {
if (meth.getName().equals(method.getName())) {
return meth.invoke(service, args);
}
}
。。。
}
}
}
public class Utils {
private static final String CONTAINER_PREFIX = "java:comp/env/";
public static <T> T getProxyObject(Class<T> clazz, InvocationHandler handler) {
Object o = Proxy.newProxyInstance(clazz.getClassLoader(),
new Class[] { clazz }, handler);
return clazz.cast(o);
}
private static String convertJndiName(String jndiName) {
if (!jndiName.startsWith(CONTAINER_PREFIX)
&& jndiName.indexOf(':') == -1) {
jndiName = CONTAINER_PREFIX + jndiName;
}
return jndiName;
}
public static Object lookup(String jndiName) {
String convertedName = convertJndiName(jndiName);
Object jndiObject = null;
try {
Context context = new InitialContext();
jndiObject = context.lookup(convertedName);
} catch (NamingException e) {
throw new IllegalServiceException(
"The JNDI OSGi services name is error.", e);
} catch (Exception e) {
throw new IllegalServiceException(
"The JNDI OSGi services can not be initialized.", e);
}
return jndiObject;
}
}
簡單的jsp測試:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@page import="com.javaworld.sample.tomcat.TomcatWebCall"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Demo invoke HelloService</title>
</head>
<body>
<%=TomcatWebCall.invokeService() %>
</body>
</html>