2014-3-13:因有很多网友有类似的问题,我把我的代码放到sourceforge里了,网址如下:
https://sourceforge.net/projects/s4vp/
-----------------------------------------------------------
首先说一下此插件的功能:使用此插件可以在有一个主Struts Bundle的前提下,编写其他附属struts bundle,他们的Struts配置信息可以共享,而内部JSP等文件的定位是独立的。
可能目前人们对OSGi的关注度不高,Virgo的可能更少,要不然Spring也不会捐出去,但我相信OSGi也将是一个趋势,比如anroid的低层容器就是通过OSGi实现的。
在选择OSGi容器的时候,我本来是想用felix的,因为Struts2官方的OSGi的插件是使用felix的,但存在一些缺点:
- 只能用Struts的2.1.8.1的版本,和felix的1.X.X版本(Felix都到4了好不好)
- 不支持默认的JSP result,只支持freemark和velocity
- 在tomcat的容器上再创建OSGi容器,在性能上肯定有所损失
- 无法在主web context和其bundle里共享spring的配置信息(我是在配置Spring Security时发现的)
- 最严重的是Struts项目对OSGi的投入力度太少,热情不高,我在apache官网提了一个BUG,居然要到下一个大版本才给发布。。。
后来,我就想直接使用Spring的OSGi容器,不过已经捐给Eclipse了,也就是Virgo。当然virgo也有缺点,就是没有支持的struts插件,这也是我不得不花很时间写此插件的原因。
我的设计思路主要参照Struts的OSGi插件和Virgo的snap插件。其中使用struts-osgi插件主要实现Struts的配置OSGi化。而virgo-snap主要实现JSP等文件的定位问题。
配置的OSGi化:通过修改原Struts的配置来实现的。
实现要点:(大家可以对照Struts-osgi插件的实现)
- 增加struts-plugin.xml配置文件,将其在META-INF中export出去,主struts bundle会自动加载此配置文件
- 配置文件中ObjectFactory,PackageProvider,ClassloaderInterface需要重新实现
- PackageProvider中主要是加入对附属Struts bundle的过滤,找到之后就加载bundle内的struts.xml配置文件,使之与主bundle的配置合并。然后就是对之后容器内的bundle进行监听,新增bundle就添加,停用的bundle就删除
- ObjectFactory中主要是加入对无法找到的类,重新定位到当前附属bundle内查找实现
- 获取当前附属bundle的方法是,在加载package的配置时,记录package name与bundle的对应关系,然后在之后查询是,通过ActionContext间接获取当前的Action所属的package name,再对应其bundle就OK了
- ClassloaderInterface与上面的原理差不多就不多说了
- 还有就是Freemark,Velocity和其他静态资源的访问定位,差不多都是通过修改内容文件的入口点至当前bundle就行,具体可以看原Struts-osgi插件的实现
bundel内文件的定位:通过更改ServletContext来编译jsp来实现的。
实现要点:(大家可以对照Virgo-Snaps插件的实现)
- 因tomcat最终定位JSP文件位置的地方是在ServletContext内,而ServletContext的实现只能通过容器来获取。最终实现是通过监听和搜索主Struts bundle发布的ServletContext服务得到的。
- JSP的编译用的是原tomcat内的JspServlet类,可以直接实例化,然后wrapper下容器ServletContext,改变其获取资源的方式,用其对servlet进行init,然后这个servlet就可以编译jsp了
- 其中JspServlet的初始化时所使用的classloader一定要改成附属bundle的Classloader,而这个classloader还必须是WebBundleClassLoader,所以必须将附属bundle通过virgo的 Transformer 进行转化,而classloader可以通过Spring进来的WebBundleClassLoaderFactory进行创建
要注意的是web bundle默认的classpath的路径为"WEB-INF/classes",如果你的JSP文件是想放在根目录就必须添加"."到classpath中
相关代码:@Override public void transform(GraphNode<InstallArtifact> installGraph, InstallEnvironment installEnvironment) throws DeploymentException { installGraph.visit(new ExceptionThrowingDirectedAcyclicGraphVisitor<InstallArtifact, DeploymentException>() { public boolean visit(GraphNode<InstallArtifact> node) throws DeploymentException { InstallArtifact installArtifact = node.getValue(); if (OsgiUtil.isNeedStrutsVirgoSupport(installArtifact)) { BundleManifest bundleManifest = OsgiUtil.getBundleManifest((BundleInstallArtifact) installArtifact); doTransform(bundleManifest, getSourceUrl(installArtifact)); } return true; } }); } void doTransform(BundleManifest bundleManifest, URL sourceUrl) throws DeploymentException { logger.info("Transforming bundle at '{}'", sourceUrl.toExternalForm()); try { bundleManifest.setModuleType(STRUTS_MODULE_TYPE); bundleManifest.setHeader("SpringSource-DefaultWABHeaders", "true"); bundleManifest.setHeader(Constants.BUNDLE_CLASSPATH, "."); InstallationOptions installationOptions = installOptionFactory.createDefaultInstallOptions(); this.manifestTransformer.transform(bundleManifest, sourceUrl, installationOptions, false); } catch (IOException ioe) { logger.error(String.format("Error transforming manifest for struts '%s' version '%s'", bundleManifest.getBundleSymbolicName().getSymbolicName(), bundleManifest.getBundleVersion()), ioe); throw new DeploymentException("Error transforming manifest for struts '" + bundleManifest.getBundleSymbolicName().getSymbolicName() + "' version '" + bundleManifest.getBundleVersion() + "'", ioe); } }
Transformer的实现类需要通过spring配置文件进行声明:<osgi:reference id="webBundleManifestTransformer" interface="org.eclipse.gemini.web.core.WebBundleManifestTransformer"/> <osgi:reference id="webBundleClassLoaderFactory" interface="org.eclipse.gemini.web.tomcat.spi.WebBundleClassLoaderFactory"/> <osgi:reference id="eventLogger" interface="org.eclipse.virgo.medic.eventlog.EventLogger"/> <osgi:service ref="lifecycleListener" interface="org.eclipse.virgo.kernel.install.artifact.InstallArtifactLifecycleListener"/> <osgi:service ref="transformer" interface="org.eclipse.virgo.kernel.install.pipeline.stage.transform.Transformer" ranking="1500"/>
<bean id="lifecycleListener" class="org.apache.struts2.osgi.virgo.internal.deployer.StrutsLifecycleListener"> <constructor-arg ref="webBundleClassLoaderFactory"/> <constructor-arg ref="eventLogger"/> </bean> <bean id="transformer" class="org.apache.struts2.osgi.virgo.internal.deployer.StrutsVirgoTransformer"> <constructor-arg ref="webBundleManifestTransformer"/> </bean> <bean id="strutsFactoryMonitor" class="org.apache.struts2.osgi.virgo.internal.StrutsFactoryMonitor" init-method="start" destroy-method="stop"> <constructor-arg ref="bundleContext"/> <constructor-arg ref="eventLogger"/> </bean>
- 下面要解决的问题是主Struts bundle和附属Struts bundle之间如何进行信息传递的问题。这个问题的产生原因是,我们监听事件的bundle是我们的插件, 而不是主bundle。
如果你要问,为什么不通过主Struts bundle来实现此功能。那么我的回答是:当然可以,不过你要把class文件放到你的主struts bundle内。也就是说class文件在哪个bundle内,哪么这个类的实现就在哪个bundle的领空内,这个你无法改变,因为这个是virgo内部的classloader机制的基础。
解决问题的方法是通过我们的插件来传递信息,传递的方向是:附属bundle -> 插件 ->主bundle。之所以是这个方向,是因为主bundle才是外界访问的接口。
传递信息前需要做的一件事情是:收集信息。
· 首先通过Virgo的InstallArtifactLifecycleListenerSupport,在附属bundle在安装时,动态添加一个StrutsFactory服务,里面包含重要的classloader等信息
· 然后插件在factorymonitor里监听这个服务,收到信息后,通过查找对应的主bundle来创建struts实例,并动态添加为服务
· 最后主bundle通过监听这个服务,获取struts类,而struts类里包含编译jsp文件的所有功能
所以我们可以通过添加一个简单的filter,将jsp文件通过struts来进行解析编译,就OK了
代码如下:final class StrutsLifecycleListener extends InstallArtifactLifecycleListenerSupport { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Map<InstallArtifact, ServiceRegistrationTracker> registrationTrackers = new ConcurrentHashMap<InstallArtifact, ServiceRegistrationTracker>(); private final WebBundleClassLoaderFactory classLoaderFactory; private final EventLogger eventLogger; public StrutsLifecycleListener(WebBundleClassLoaderFactory classLoaderFactory, EventLogger eventLogger) { this.classLoaderFactory = classLoaderFactory; this.eventLogger = eventLogger; } /** * {@inheritDoc} */ @Override public void onStarted(InstallArtifact installArtifact) throws DeploymentException { if (OsgiUtil.isNeedStrutsVirgoSupport(installArtifact)) { Bundle bundle = ((BundleInstallArtifact) installArtifact).getBundle(); BundleManifest bundleManifest = OsgiUtil.getBundleManifest((BundleInstallArtifact) installArtifact); ServiceRegistration<StrutsFactory> registration = createAndRegisterStrutsFactoryService(bundle, bundleManifest); ServiceRegistrationTracker registrationTracker = new ServiceRegistrationTracker(); registrationTracker.track(registration); this.registrationTrackers.put(installArtifact, registrationTracker); } } ServiceRegistration<StrutsFactory> createAndRegisterStrutsFactoryService(Bundle bundle, BundleManifest bundleManifest) { logger.info("Creating a StrutsFactory for bundle '{}'", bundle); StrutsFactory strutsFactory = new WebAppStrutsFactory(bundle, this.classLoaderFactory, this.eventLogger); StrutsHostDefinition hostDefinition = OsgiUtil.getStrutsHostHeader(bundleManifest); Dictionary<String, String> serviceProperties= new Hashtable<String, String>(); serviceProperties.put(Scope.PROPERTY_SERVICE_SCOPE, Scope.SCOPE_ID_GLOBAL); // expose service outside any containing scope serviceProperties.put(StrutsFactory.FACTORY_NAME_PROPERTY, hostDefinition.getSymbolicName()); serviceProperties.put(StrutsFactory.FACTORY_RANGE_PROPERTY, hostDefinition.getVersionRange().toParseString()); ServiceRegistration<StrutsFactory> registration = bundle.getBundleContext().registerService(StrutsFactory.class, strutsFactory, serviceProperties); return registration; } /** * {@inheritDoc} */ @Override public void onStopping(InstallArtifact installArtifact) { logger.info("Destroying StrutsFactory for bundle '{}'", installArtifact.getName()); ServiceRegistrationTracker serviceRegistrationTracker = this.registrationTrackers.remove(installArtifact); if (serviceRegistrationTracker != null) { serviceRegistrationTracker.unregisterAll(); } } }
当然也需要spring配置文件的声明,之前的代码已经贴出来了。
因为附属bundle的ServletContext需要transform的转换,所以我们不能同时获取,因此,我们需要先发布下StrutsFacotry服务,言外之意是我已经收集好前面的信息了;然后通过监听此服务,当收到信息时再监听ServletContext服务,来收集全部信息。
代码如下:
关于spring配置的问题同上就不再多说了。package org.apache.struts2.osgi.virgo.internal; import java.util.Collection; import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.apache.struts2.osgi.virgo.internal.deployer.StrutsFactory; import org.eclipse.virgo.medic.eventlog.EventLogger; import org.eclipse.virgo.util.osgi.ServiceRegistrationTracker; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceEvent; import org.osgi.framework.ServiceListener; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class StrutsFactoryMonitor implements ServiceTrackerCustomizer<StrutsFactory, Object> { public final static String KEY_HOST_ID = "struts.host.id"; public final static String KEY_CONTEXT_PATH = "struts.context.path"; public final static String KEY_NAME = "struts.name"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final BundleContext bundleContext; private final ServiceTracker<StrutsFactory, Object> strutsFactoryTracker; private final EventLogger eventLogger; public StrutsFactoryMonitor(BundleContext bundleContext, EventLogger eventLogger) { this.bundleContext = bundleContext; this.strutsFactoryTracker = new ServiceTracker<StrutsFactory, Object>(bundleContext, StrutsFactory.class, this); this.eventLogger = eventLogger; } public void start() { this.strutsFactoryTracker.open(); } public void stop() { this.strutsFactoryTracker.close(); } public Object addingService(ServiceReference<StrutsFactory> reference) { StrutsFactory strutsFactory = this.bundleContext.getService(reference); if (strutsFactory != null) { BundleContext strutsBundleContext = reference.getBundle().getBundleContext(); StrutsBinder strutsBinder = new StrutsBinder(strutsBundleContext, strutsFactory, StrutsHostDefinition.fromServiceReference(reference), this.eventLogger); strutsBinder.start(); return strutsBinder; } logger.warn("Unable to create StrutsBinder due to missing StrutsFactory"); return null; } public void modifiedService(ServiceReference<StrutsFactory> reference, Object service) { } public void removedService(ServiceReference<StrutsFactory> reference, Object service) { logger.info("Destroying StrutsBinder for bundle '{}'", reference.getBundle()); ((StrutsBinder) service).destroy(); } private static enum StrutsLifecycleState { AWAITING_INIT, INIT_SUCCEEDED, INIT_FAILED } private static final class StrutsBinder implements ServiceListener { private static final String SNAP_ORDER = "struts.order"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final BundleContext context; private final HostSelector hostSelector; private final Object hostStateMonitor = new Object(); private final Object strutsStateMonitor = new Object(); private boolean queriedInitialHosts = false; private ServiceReference<ServletContext> hostReference; private final ServiceRegistrationTracker registrationTracker = new ServiceRegistrationTracker(); private final EventLogger eventLogger; private Struts struts; private final StrutsFactory factory; public StrutsBinder(final BundleContext context, final StrutsFactory strutsFactory, final StrutsHostDefinition hostDefinition, final EventLogger eventLogger) { this.context = context; this.hostSelector = new HostSelector(hostDefinition, (String) context.getBundle().getHeaders().get("Module-Scope")); this.eventLogger = eventLogger; this.factory = strutsFactory; } private void start() { registerHostListener(); } private void registerHostListener() { try { this.context.addServiceListener(this, "(objectClass=javax.servlet.ServletContext)"); logger.info("Listening for hosts to be registered."); searchForExistingHost(); } catch (InvalidSyntaxException e) { logger.error("Filter syntax invalid"); } } private void hostPublished(ServiceReference<ServletContext> hostReference) { assert (!Thread.holdsLock(this.hostStateMonitor)); ServletContext servletContext = this.context.getService(hostReference); if (servletContext != null) { synchronized (this.hostStateMonitor) { Collection<ServiceReference<ServletContext>> references = new HashSet<ServiceReference<ServletContext>>(); references.add(hostReference); ServiceReference<ServletContext> matchedHost = this.hostSelector.selectHost(references); if (matchedHost == null) { logger.info("Host {} did not match {} ", hostReference.getBundle().getSymbolicName(), this.hostSelector.getHostDefinition().toString()); return; } } Bundle hostBundle = hostReference.getBundle(); StrutsLifecycleState newState = StrutsLifecycleState.INIT_FAILED; Struts struts = this.factory.createStruts(new Host(hostBundle, servletContext)); try { logger.info("Initializing struts '{}'", struts.getContextPath()); struts.init(); newState = StrutsLifecycleState.INIT_SUCCEEDED; logger.info("Publishing struts '{}'", struts.getContextPath()); publishStrutsService(struts, hostBundle); } catch (ServletException e) { this.eventLogger.log(StrutsLogEvents.STRUTS_INIT_FAILURE, servletContext.getContextPath() + " --> " + struts.getContextPath(), e.getMessage()); } finally { synchronized (this.strutsStateMonitor) { if (newState == StrutsLifecycleState.INIT_SUCCEEDED) { this.struts = struts; } } } } } private void publishStrutsService(Struts struts, Bundle hostBundle) { Hashtable<Object, Object> props = struts.getStrutsProperties(); Dictionary<String, Object> serviceProperties = new Hashtable<String, Object>(); for (Object key : props.keySet()) { serviceProperties.put(key.toString(), props.get(key)); } String strutsOrder = (String) serviceProperties.get(SNAP_ORDER); if (strutsOrder != null) { serviceProperties.put(Constants.SERVICE_RANKING, Integer.parseInt(strutsOrder)); } serviceProperties.put(KEY_HOST_ID, Long.toString(hostBundle.getBundleId())); serviceProperties.put(KEY_CONTEXT_PATH, struts.getContextPath()); serviceProperties.put(KEY_NAME, (String) this.context.getBundle().getHeaders().get("Bundle-Name")); ServiceRegistration<Struts> registration = this.context.registerService(Struts.class, struts, serviceProperties); this.registrationTracker.track(registration); logger.info("Published struts service for '{}'", struts.getContextPath()); } private void destroy() { try { destroyStruts(); } finally { unregisterHostListener(); } } private void unregisterHostListener() { logger.info("No longer listening for hosts to be registered."); this.context.removeServiceListener(this); } public void serviceChanged(ServiceEvent event) { synchronized (this.hostStateMonitor) { while (!queriedInitialHosts) { try { this.hostStateMonitor.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } int type = event.getType(); @SuppressWarnings("unchecked") ServiceReference<ServletContext> serviceReference = (ServiceReference<ServletContext>) event.getServiceReference(); if (type == ServiceEvent.REGISTERED && this.hostReference == null) { hostPublished(serviceReference); } else if (type == ServiceEvent.UNREGISTERING) { if (serviceReference.equals(this.hostReference)) { hostRetracted(serviceReference); } } } private void hostRetracted(ServiceReference<ServletContext> serviceReference) { try { destroyStruts(); } finally { synchronized (this.hostStateMonitor) { this.hostReference = null; } } } private void destroyStruts() { Struts s = null; synchronized (this.strutsStateMonitor) { s = this.struts; this.struts = null; } this.registrationTracker.unregisterAll(); if (s != null) { logger.info("Retracted struts service for '{}'", s.getContextPath()); s.destroy(); } } private void searchForExistingHost() { ServiceReference<ServletContext> existingHost = null; Collection<ServiceReference<ServletContext>> candidates = findHostCandidiates(); if (candidates != null && !candidates.isEmpty()) { logger.info("{} host candidates found", candidates.size()); } else { logger.info("No host candidates found"); } synchronized (this.hostStateMonitor) { try { existingHost = this.hostSelector.selectHost(candidates); this.queriedInitialHosts = true; } finally { this.hostStateMonitor.notifyAll(); } } if (existingHost != null) { hostPublished(existingHost); } } private Collection<ServiceReference<ServletContext>> findHostCandidiates() { try { return this.context.getServiceReferences(ServletContext.class, null); } catch (InvalidSyntaxException ise) { throw new IllegalStateException("Unexpected invalid filter syntax with null filter", ise); } } } }
Struts类的关键代码/** * {@inheritDoc} * * @throws ServletException */ public final void init() throws ServletException { logger.info("Initializing struts '{}'", this.strutsBundle.getSymbolicName()); StrutsServletContext servletContext = new StrutsServletContext(this.host.getServletContext(), this.strutsBundle); servletContext.setAttribute(WebContainer.ATTRIBUTE_BUNDLE_CONTEXT, this.strutsBundle.getBundleContext()); this.strutsClassLoader = this.classLoaderFactory.createWebBundleClassLoader(this.strutsBundle); try { ((Lifecycle) strutsClassLoader).start(); } catch (LifecycleException e) { logger.error("Failed to start struts's class loader", e); throw new ServletException("Failed to start web bundle's class loader", e); } this.initServlet(servletContext); this.eventLogger.log(StrutsLogEvents.STRUTS_BOUND, this.strutsBundle.getSymbolicName()); } private final void initServlet(final StrutsServletContext servletContext) throws ServletException { try { ManagerUtils.doWithThreadContextClassLoader(this.strutsClassLoader, new ClassLoaderCallback<Void>() { public Void doWithClassLoader() throws ServletException { try { WebAppStruts.this.servlet = (Servlet)SERVLET_CLASS.newInstance(); } catch (Exception e) { throw new ServletException("Create Servlet Fail", e); } ImmutableServletConfig servletConfig = new ImmutableServletConfig(servletContext); WebAppStruts.this.servlet.init(servletConfig); return null; } }); } catch (IOException e) { logger.error("Unexpected IOException from servlet init", e); throw new ServletException("Unexpected IOException from servlet init", e); } } /** * {@inheritDoc} */ public final void destroy() { ClassLoader strutsClassLoader = this.strutsClassLoader; if (strutsClassLoader != null) { try { ((Lifecycle) strutsClassLoader).stop(); } catch (LifecycleException e) { logger.error("Failed to stop struts's class loader", e); throw new StrutsException("Failed to stop web bundle class loader", e); } } else { // TODO Log warning that class loader was null during destroy } this.eventLogger.log(StrutsLogEvents.STRUTS_UNBOUND, this.strutsBundle.getSymbolicName()); } /** * {@inheritDoc} */ public final void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (servlet != null) { servlet.service(request, response); } else { // TODO Log warning that dispatcher is not present throw new ServletException("handleRequest invoked when virtual container was null"); } }
先说这么多,没有将全部代码放出来,主要是因为刚刚实现我想要的功能,细节地方和测试工作还没有完善,放出来也是不能直接使用的东西,就不误导大家了。而写这篇文章,主要是在写代码的过程中一直在摸索,写完了没有整体的理解,这次也算是给自己一次沉淀的过程,也希望能给使用virgo的同学一点可以参考的资料,毕竟国内关于virgo的资料太少了。
最后有什么问题还请大家拍砖