关于本文
本文是之前写的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">
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
 <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
 <property name="prefix" value="/web/jsp/spring/"/>
        <property name="prefix" value="/web/jsp/spring/"/>
 <property name="suffix" value=".jsp"/>
        <property name="suffix" value=".jsp"/>
 </bean>
    </bean>
在之前的版本中,这里是没有问题的,可是在spring-osgi-1.2.0中,却会有这个问题,这是因为缺少一个 
 DynamicImport-Package: *
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);
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)
private PackageSource findDynamicSource(String pkgName)  {
{

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

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

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

 synchronized (importedSources)
                synchronized (importedSources)  {
{
 importedSources.add(source);
                    importedSources.add(source);
 }
                }
 return source;
                return source;
 }
            }
 }
        }
 return null;
        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
private void init() throws JasperException  {
{
 if (initialized) return;
        if (initialized) return;

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

 } catch (Exception ex)
        } catch (Exception ex)  {
{
 throw new JasperException(Localizer.getMessage(
            throw new JasperException(Localizer.getMessage(
 "jsp.error.internal.tldinit", ex.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()
//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
    synchronized void registerServlet(String alias, Servlet servlet, Dictionary initparams, HttpContext context, Bundle bundle) throws ServletException, NamespaceException  {
{
 checkAlias(alias);
        checkAlias(alias);
 if (servlet == null)
        if (servlet == null)
 throw new IllegalArgumentException("Servlet cannot be null"); //$NON-NLS-1$
            throw new IllegalArgumentException("Servlet cannot be null"); //$NON-NLS-1$

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

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

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

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

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

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

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

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

 while (e.hasMoreElements())
                while (e.hasMoreElements())  {
{
 URL entryURL = (URL) e.nextElement();
                    URL entryURL = (URL) e.nextElement();
 result.add(entryURL.getFile().substring(bundleResourcePath.length()));
                    result.add(entryURL.getFile().substring(bundleResourcePath.length()));
 }
                }
 }
            }
 return result;
            return result;
 }
        }
代码中的bundleResourcePath,就是注册这个Servlet填写的alias——/web/jsp
现在,已经了解如何查找tld了,只要将/WEB-INF目录放入/web/jsp中就可以了或者注册servlet的时候这样写:
 httpService.registerResources("/", "/web", null);
httpService.registerResources("/", "/web", null);
 httpService.registerServlet("/*.jsp", new JspServlet(context .getBundle(), "/web"), null, 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 dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setContextConfigLocation("META-INF/dispatcher/petstore-servlet.xml");
DispatcherServlet读取配置文件中的bean,存放于DispatcherServlet的ApplicationContext的BeanFactory中,某个bean需要使用到OSGi的bean 引用时,例如:
 <osgi:reference id="petStoreOsgi"
<osgi:reference id="petStoreOsgi"
 interface="org.extwind.osgi.demo.jpetstoreosgi.domain.logic.PetStoreFacade" />
        interface="org.extwind.osgi.demo.jpetstoreosgi.domain.logic.PetStoreFacade" /> <bean name="/shop/viewCategory.do"
<bean name="/shop/viewCategory.do"
 class="org.extwind.osgi.demo.jpetstoreosgi.springmvc.controller.ViewCategoryController">
        class="org.extwind.osgi.demo.jpetstoreosgi.springmvc.controller.ViewCategoryController">
 <property name="petStore" ref="petStoreOsgi" />
        <property name="petStore" ref="petStoreOsgi" />
 </bean>
    </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.
     * Initialize and publish the WebApplicationContext for this servlet.
 * <p>Delegates to {@link #createWebApplicationContext} for actual creation
     * <p>Delegates to {@link #createWebApplicationContext} for actual creation
 * of the context. Can be overridden in subclasses.
     * of the context. Can be overridden in subclasses.
 * @return the WebApplicationContext instance
     * @return the WebApplicationContext instance
 * @throws BeansException if the context couldn't be initialized
     * @throws BeansException if the context couldn't be initialized
 * @see #setContextClass
     * @see #setContextClass
 * @see #setContextConfigLocation
     * @see #setContextConfigLocation
 */
     */

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

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

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

 public static WebApplicationContext getWebApplicationContext(ServletContext sc)
    public static WebApplicationContext getWebApplicationContext(ServletContext sc)  {
{
 return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
        return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
 }
    }
      可以看到,使用的是ServletContext的属性来存放ApplicationContext,因此在ApplicationContextAware.setApplicationContext(ApplicationContext bundleApplicationContext)中,可以通过下面的代码来设置:
    

 HttpServlet tmpHttpServlet = new HttpServlet()
HttpServlet tmpHttpServlet = new HttpServlet()  {
{

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

 OsgiBundleXmlWebApplicationContext webApplicationContext = new OsgiBundleXmlWebApplicationContext()
                OsgiBundleXmlWebApplicationContext webApplicationContext = new OsgiBundleXmlWebApplicationContext()  {
{

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

 config.getServletContext().setAttribute(
                config.getServletContext().setAttribute(
 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
 webApplicationContext);
                        webApplicationContext);
 }
            }
 };
        };
 httpService.registerServlet("/init-context-loader-petsore-web", tmpHttpServlet, null,
        httpService.registerServlet("/init-context-loader-petsore-web", tmpHttpServlet, null,
 httpContext);
                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 阅读(4516) 
评论(6)  编辑  收藏  所属分类: 
OSGI