关于本文
本文是之前写的Developing Equinox/Spring-osgi/Spring Framework Web Application系列的升级版,Tomcat-OSGi的基础Demo之一,主要演示传统web application到OSGi application的转换,由于是升级版,所以本文的侧重点不再是基础配置的演示。
一、准备工作
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: *
为何要使用动态引入?
因为无法在spring-beans中import定义的bean,因此如果不使用动态引入,那么spring-beans就无法load定义的bean,而下面统一使用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,它通过查找类调用堆中距离本次loadClass调用最近的DefaultClassLoader(bundle的classloader)去加载一个类。
DefaultClassLoader中封装了ClassLoaderDelegate(BundleLoader)查找类的过程
spring-beans加载bean class的过程:
读取配置文件 -> 发现一个bean配置 -> 通过ClassUtils加载 -> 使用Thread.currentThread().getContextClassLoader()加载bean class
ContextFinder加载bean class的过程:
从类调用堆中找到距离最近DefaultClassLoader,使用ClassUtils的DefaultClassLoader来加载bean class -> 使用ClassUtils所在bundle的BundleLoader去查找一个类
BundleLoader加载bean class的过程在OSGi规范中有比较详细的介绍,这里主要看一下动态引入
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;
}
现在,spring-bean是如何加载一个bean的过程就变得非常明了了,ClassUtils在spring-core中,当使用spring-core的BundleLoader去加载一个bean class时,如果没有动态引入,则会出现找不到class的情况。
很明显,spring-osgi-1.2.0中的spring-core并没有配置动态引入,在这个版本中或许是通过操作classloader来实现bean的加载,这个没有研究。
同理,对于数据库驱动找不到的问题,也可以这样来解决。
2. 找不到tld
index.jsp中包含了2个标签库,在上一个版本中,将其放入/web/WEB-INF目录中就可以正常显示,可是在新版本中却不行。
当一个对jsp的请求到达时,先将jsp生称java文件,之后进行编译。而生称java文件时,需要处理tld资源。
tld资源路径的处理是由TldLocationsCache来完成的,当它第一次初始化时,会在 "/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部分的实现上行不通)
在方法processTldsInFileSystem("/WEB-INF/")中使用的是当前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
现在,已经了解如何查找tld了,只要将/WEB-INF目录放入/web/jsp中就可以了或者注册servlet的时候这样写:
httpService.registerResources("/", "/web", null);
httpService.registerServlet("/*.jsp", new JspServlet(context .getBundle(), "/web"), null, null);
为何上一个版本可以呢?时间距离太远,也不太好找源码,所以没有研究这部份,我猜测应该是在scanJars()方法中,通过classloader的URLs遍历来获取的,
equinox在处理相同HttpContext的ServletContext时,只是将attributes共享,而并没有共享资源访问,在这个例子中,应该是将相同HttpContext中的资源遍历,在Tomcat-OSGi中,使用的是naming.DirContext去处理资源的查找。
3. SpringMVC中的Controller 的问题
在上一个版本中,对于无法找到Controller的问题,是通过BundleContextAware来解决的,因为它是在Spring-OSGi中完成,因此其本质就是修改ClassLoader来解决的。
而更好的解决办法其实是export controller所在的package,使用动态引入功能,因为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并没有关联,因此无法找到。
这个两个ApplicationContext的创建顺序是这样的:
1. 程序中注册DispatcherServlet后,它被初始化时创建ApplicationContext,加载contextConfigLocation中定义的bean
2. Spring-OSGi当监听到bundle started的事件时,为该bundle创建ApplicationContext,加载bundle中/META-INF/spring/*.xml中定义的bean
因此,这个问题的解决办法就是,让DispatcherServlet的ApplicationContext在bundle的ApplicationContext之后创建,并设置成parent-child关系
实际上这个问题应该是由mvc框架去考虑的,在Spring-OSGi的文档中有解决方法,它是通过OsgiBundleXmlWebApplicationContext来实现的,也就是说它无法在本例中使用,因为当DispatcherServlet被初始化时,使用的Equinox的ServletConfig。
>
如何让bundle的ApplicationContext成为DispatcherServlet的ApplicationContext的parent?
在DispatcherServlet的ApplicationContext在创建时的部分代码如下:
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)中,可以通过下面的代码来设置:
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之后创建?
按照上一个版本中的方法,使用spring中的ApplicationContextAware接口,在这个接口的setParentApplicationContext方法之后,进行资源注册。
这里需要注意的是在bundle停止时注销注册的资源
以上几点基本就是在新版中遇到的问题。
Demo下载:
http://extwind.googlecode.com/svn/JPetStoreOSGi_Workspace.rar
如何使用这个Demo:
建议新建一个workspace,java编译器需要6.0版本
将所有的bundles导入后,需要再将 org.extwind.osgi.demo.jpetstoreosgi.launcher 导入
本例中不包含DB数据,因此还需要准备Spring 2.5.6中的jpetstore
运行 spring-framework-2.5.6\samples\jpetstore\db\hsqldb\server.bat
在Eclipse中运行 org.extwind.osgi.demo.jpetstoreosgi.launcher.Launcher
访问首页地址:http://localhost/shop/index.do
-----------------------------------------------------------------------------------------------------------
稍后将介绍如何在Tomcat-OSGi中使用JPetStoreOSGi
posted on 2009-04-19 03:31
Phrancol Yang 阅读(4475)
评论(6) 编辑 收藏 所属分类:
OSGI