關(guān)于本文
本文是之前寫的Developing Equinox/Spring-osgi/Spring Framework Web Application系列的升級版,Tomcat-OSGi的基礎(chǔ)Demo之一,主要演示傳統(tǒng)web application到OSGi application的轉(zhuǎn)換,由于是升級版,所以本文的側(cè)重點不再是基礎(chǔ)配置的演示。
一、準(zhǔn)備工作
1,JDK 1.6
2,Eclipse 3.4-jee
3,Spring-framework-2.5.6
4,spring-osgi-1.2.0
5, org.eclipse.equinox源碼,可從 :pserver:anonymous@dev.eclipse.org:/cvsroot/rt 中獲得
二、顯示首頁中的幾個問題
1. ClassNotFoundException: org.springframework.web.servlet.view.InternalResourceViewResolver
META-INF/dispatcher/petstore-servlet.xml中定義的bean:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/web/jsp/spring/"/>
<property name="suffix" value=".jsp"/>
</bean>
在之前的版本中,這里是沒有問題的,可是在spring-osgi-1.2.0中,卻會有這個問題,這是因為缺少一個
DynamicImport-Package: *
為何要使用動態(tài)引入?
因為無法在spring-beans中import定義的bean,因此如果不使用動態(tài)引入,那么spring-beans就無法load定義的bean,而下面統(tǒng)一使用spring-core中的ClassUtils.forName來查找bean class,是一個非常好的做法。
spring-beans中是這樣load一個bean的
Class resolvedClass = ClassUtils.forName("org.springframework.web.servlet.view.InternalResourceViewResolver", loader);
Thread.currentThread().getContextClassLoader()中的ClassLoader是org.eclipse.core.runtime.internal.adaptor.ContextFinder,它是osgi framework的classloader,它通過查找類調(diào)用堆中距離本次loadClass調(diào)用最近的DefaultClassLoader(bundle的classloader)去加載一個類。
DefaultClassLoader中封裝了ClassLoaderDelegate(BundleLoader)查找類的過程
spring-beans加載bean class的過程:
讀取配置文件 -> 發(fā)現(xiàn)一個bean配置 -> 通過ClassUtils加載 -> 使用Thread.currentThread().getContextClassLoader()加載bean class
ContextFinder加載bean class的過程:
從類調(diào)用堆中找到距離最近DefaultClassLoader,使用ClassUtils的DefaultClassLoader來加載bean class -> 使用ClassUtils所在bundle的BundleLoader去查找一個類
BundleLoader加載bean class的過程在OSGi規(guī)范中有比較詳細(xì)的介紹,這里主要看一下動態(tài)引入

private PackageSource findDynamicSource(String pkgName)
{

if (isDynamicallyImported(pkgName))
{
ExportPackageDescription exportPackage = bundle.getFramework().getAdaptor().getState().linkDynamicImport(proxy.getBundleDescription(), pkgName);

if (exportPackage != null)
{
PackageSource source = createExportPackageSource(exportPackage, null);

synchronized (this)
{
if (importedSources == null)
importedSources = new KeyedHashSet(false);
}

synchronized (importedSources)
{
importedSources.add(source);
}
return source;
}
}
return null;
}
現(xiàn)在,spring-bean是如何加載一個bean的過程就變得非常明了了,ClassUtils在spring-core中,當(dāng)使用spring-core的BundleLoader去加載一個bean class時,如果沒有動態(tài)引入,則會出現(xiàn)找不到class的情況。
很明顯,spring-osgi-1.2.0中的spring-core并沒有配置動態(tài)引入,在這個版本中或許是通過操作classloader來實現(xiàn)bean的加載,這個沒有研究。
同理,對于數(shù)據(jù)庫驅(qū)動找不到的問題,也可以這樣來解決。
2. 找不到tld
index.jsp中包含了2個標(biāo)簽庫,在上一個版本中,將其放入/web/WEB-INF目錄中就可以正常顯示,可是在新版本中卻不行。
當(dāng)一個對jsp的請求到達(dá)時,先將jsp生稱java文件,之后進(jìn)行編譯。而生稱java文件時,需要處理tld資源。
tld資源路徑的處理是由TldLocationsCache來完成的,當(dāng)它第一次初始化時,會在 "/WEB-INF/",classpath中的jar包,web.xml中查找tld文件并緩存起來。

private void init() throws JasperException
{
if (initialized) return;

try
{
processWebDotXml();
scanJars();
processTldsInFileSystem("/WEB-INF/");
initialized = true;

} catch (Exception ex)
{
throw new JasperException(Localizer.getMessage(
"jsp.error.internal.tldinit", ex.getMessage()));
}
}
這里主要看一下為什么這個版本中直接將tld文件放入/web/WEB-INF目錄中會提示找不到tld
processWebDotXml()方法是處理web.xml的
scanJars()是處理classpath資源
processTldsInFileSystem("/WEB-INF/"); 是查找web-inf目錄中的tld (這個方法在equinox部分的實現(xiàn)上行不通)
在方法processTldsInFileSystem("/WEB-INF/")中使用的是當(dāng)前Servlet的ServletContext.getResourcePaths()方法來獲取web-inf目錄中的tld資源,注冊equinox-JspServlet的過程中,做了2層封裝,ServletRegistration和org.eclipse.equinox.jsp.jasper.JspServlet,分別生成了2個ServletConfig和ServletContext,大概過程如下:
ProxyServlet:
//Effective registration of the servlet as defined HttpService#registerServlet()

synchronized void registerServlet(String alias, Servlet servlet, Dictionary initparams, HttpContext context, Bundle bundle) throws ServletException, NamespaceException
{
checkAlias(alias);
if (servlet == null)
throw new IllegalArgumentException("Servlet cannot be null"); //$NON-NLS-1$

ServletRegistration registration = new ServletRegistration(servlet, proxyContext, context, bundle, servlets);
registration.checkServletRegistration();

ServletContext wrappedServletContext = new ServletContextAdaptor(proxyContext, getServletContext(), context, AccessController.getContext());
ServletConfig servletConfig = new ServletConfigImpl(servlet, initparams, wrappedServletContext);

registration.init(servletConfig);
registrations.put(alias, registration);
}
equinox-JspServlet:

public void init(ServletConfig config) throws ServletException
{
ClassLoader original = Thread.currentThread().getContextClassLoader();

try
{
Thread.currentThread().setContextClassLoader(jspLoader);
jspServlet.init(new ServletConfigAdaptor(config));

} finally
{
Thread.currentThread().setContextClassLoader(original);
}
}
那么processTldsInFileSystem("/WEB-INF/")方法中的ServletContext就是從equinox-JspServlet$ServletContextAdaptor開始的

public Set getResourcePaths(String name)
{
Set result = delegate.getResourcePaths(name);
Enumeration e = bundle.findEntries(bundleResourcePath + name, null, false);

if (e != null)
{
if (result == null)
result = new HashSet();

while (e.hasMoreElements())
{
URL entryURL = (URL) e.nextElement();
result.add(entryURL.getFile().substring(bundleResourcePath.length()));
}
}
return result;
}
代碼中的bundleResourcePath,就是注冊這個Servlet填寫的alias——/web/jsp
現(xiàn)在,已經(jīng)了解如何查找tld了,只要將/WEB-INF目錄放入/web/jsp中就可以了或者注冊servlet的時候這樣寫:
httpService.registerResources("/", "/web", null);
httpService.registerServlet("/*.jsp", new JspServlet(context .getBundle(), "/web"), null, null);
為何上一個版本可以呢?時間距離太遠(yuǎn),也不太好找源碼,所以沒有研究這部份,我猜測應(yīng)該是在scanJars()方法中,通過classloader的URLs遍歷來獲取的,
equinox在處理相同HttpContext的ServletContext時,只是將attributes共享,而并沒有共享資源訪問,在這個例子中,應(yīng)該是將相同HttpContext中的資源遍歷,在Tomcat-OSGi中,使用的是naming.DirContext去處理資源的查找。
3. SpringMVC中的Controller 的問題
在上一個版本中,對于無法找到Controller的問題,是通過BundleContextAware來解決的,因為它是在Spring-OSGi中完成,因此其本質(zhì)就是修改ClassLoader來解決的。
而更好的解決辦法其實是export controller所在的package,使用動態(tài)引入功能,因為Spring-bean都是通過Spring-core中的ClassUtils.forName來查找的。
4. DispatcherServlet中的URI-Bean與osgi-bean引用的問題
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setContextConfigLocation("META-INF/dispatcher/petstore-servlet.xml");
DispatcherServlet讀取配置文件中的bean,存放于DispatcherServlet的ApplicationContext的BeanFactory中,某個bean需要使用到OSGi的bean 引用時,例如:
<osgi:reference id="petStoreOsgi"
interface="org.extwind.osgi.demo.jpetstoreosgi.domain.logic.PetStoreFacade" />
<bean name="/shop/viewCategory.do"
class="org.extwind.osgi.demo.jpetstoreosgi.springmvc.controller.ViewCategoryController">
<property name="petStore" ref="petStoreOsgi" />
</bean>
可以看到/shop/viewCategory.do是一個bean,它被保存在DispatcherServlet的ApplicationContext中,
osgi bean引用petStoreOsgi是存放在bundle的ApplicationContext中,這2個ApplicationContext并沒有關(guān)聯(lián),因此無法找到。
這個兩個ApplicationContext的創(chuàng)建順序是這樣的:
1. 程序中注冊DispatcherServlet后,它被初始化時創(chuàng)建ApplicationContext,加載contextConfigLocation中定義的bean
2. Spring-OSGi當(dāng)監(jiān)聽到bundle started的事件時,為該bundle創(chuàng)建ApplicationContext,加載bundle中/META-INF/spring/*.xml中定義的bean
因此,這個問題的解決辦法就是,讓DispatcherServlet的ApplicationContext在bundle的ApplicationContext之后創(chuàng)建,并設(shè)置成parent-child關(guān)系
實際上這個問題應(yīng)該是由mvc框架去考慮的,在Spring-OSGi的文檔中有解決方法,它是通過OsgiBundleXmlWebApplicationContext來實現(xiàn)的,也就是說它無法在本例中使用,因為當(dāng)DispatcherServlet被初始化時,使用的Equinox的ServletConfig。
>
如何讓bundle的ApplicationContext成為DispatcherServlet的ApplicationContext的parent?
在DispatcherServlet的ApplicationContext在創(chuàng)建時的部分代碼如下:
FrameworkServlet.java

/** *//**
* Initialize and publish the WebApplicationContext for this servlet.
* <p>Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @throws BeansException if the context couldn't be initialized
* @see #setContextClass
* @see #setContextConfigLocation
*/

protected WebApplicationContext initWebApplicationContext() throws BeansException
{
WebApplicationContext wac = findWebApplicationContext();

if (wac == null)
{
// No fixed context defined for this servlet - create a local one.
WebApplicationContext parent =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
wac = createWebApplicationContext(parent);
}
WebApplicationContextUtils.java

/** *//**
* Find the root WebApplicationContext for this web application, which is
* typically loaded via {@link org.springframework.web.context.ContextLoaderListener} or
* {@link org.springframework.web.context.ContextLoaderServlet}.
* <p>Will rethrow an exception that happened on root context startup,
* to differentiate between a failed context startup and no context at all.
* @param sc ServletContext to find the web application context for
* @return the root WebApplicationContext for this web app, or <code>null</code> if none
* @see org.springframework.web.context.WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
*/

public static WebApplicationContext getWebApplicationContext(ServletContext sc)
{
return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
可以看到,使用的是ServletContext的屬性來存放ApplicationContext,因此在ApplicationContextAware.setApplicationContext(ApplicationContext bundleApplicationContext)中,可以通過下面的代碼來設(shè)置:

HttpServlet tmpHttpServlet = new HttpServlet()
{

public void init(ServletConfig config)
{
// config.getServletContext().setAttribute(OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE,
// bundleContext);

OsgiBundleXmlWebApplicationContext webApplicationContext = new OsgiBundleXmlWebApplicationContext()
{

protected String[] getDefaultConfigLocations()
{
return new String[0];
}
};
webApplicationContext.setParent(applicationContext);
webApplicationContext.setServletContext(config.getServletContext());
webApplicationContext.refresh();

config.getServletContext().setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
webApplicationContext);
}
};
httpService.registerServlet("/init-context-loader-petsore-web", tmpHttpServlet, null,
httpContext);
讓tmpHttpServlet和DispatcherServlet具有相同的HttpContext,那么DispatcherServlet就可以得到parent-ApplicationContext了。
>如何讓DispatcherServlet的ApplicationContext在bundle的ApplicationContext之后創(chuàng)建?
按照上一個版本中的方法,使用spring中的ApplicationContextAware接口,在這個接口的setParentApplicationContext方法之后,進(jìn)行資源注冊。
這里需要注意的是在bundle停止時注銷注冊的資源
以上幾點基本就是在新版中遇到的問題。
Demo下載:
http://extwind.googlecode.com/svn/JPetStoreOSGi_Workspace.rar
如何使用這個Demo:
建議新建一個workspace,java編譯器需要6.0版本
將所有的bundles導(dǎo)入后,需要再將 org.extwind.osgi.demo.jpetstoreosgi.launcher 導(dǎo)入
本例中不包含DB數(shù)據(jù),因此還需要準(zhǔn)備Spring 2.5.6中的jpetstore
運(yùn)行 spring-framework-2.5.6\samples\jpetstore\db\hsqldb\server.bat
在Eclipse中運(yùn)行 org.extwind.osgi.demo.jpetstoreosgi.launcher.Launcher
訪問首頁地址:http://localhost/shop/index.do
-----------------------------------------------------------------------------------------------------------
稍后將介紹如何在Tomcat-OSGi中使用JPetStoreOSGi
posted on 2009-04-19 03:31
Phrancol Yang 閱讀(4485)
評論(6) 編輯 收藏 所屬分類:
OSGI