刚完成Struts的Virgo插件,分享一下设计思路和Virgo OSGi内部的独特机制

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服务,来收集全部信息。
    代码如下:
    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);
    			}
    		}
    	}
    }
     关于spring配置的问题同上就不再多说了。

    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的资料太少了。

最后有什么问题还请大家拍砖

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值