在上一篇struts2源码学习之初始化(一)中,详细描述了StrutsPrepareAndExecuteFilter的init()的主要工作,这一篇就详细说说Dispatcher。从上一篇文章中,我们知道了Dispatcher在Filter的init()方法中被创建出来,那么,它的功能是什么呢?Dispatcher类的功能正如它的名字所示,是派发,派发请求。
PrepareOperations类预处理请求,比如找到findActionMapping(),找到之后就要交给Dispatcher,让Dispatcher派发请求,这部分内容在struts2请求处理再详细说。现在先说Dispatcher的创建和初始化。
在Filter的init()中,Dispatcher的如下方法被调用:
public Dispatcher initDispatcher( HostConfig filterConfig ) { Dispatcher dispatcher = createDispatcher(filterConfig); dispatcher.init(); return dispatcher; }
createDispatcher():
private Dispatcher createDispatcher( HostConfig filterConfig ) { Map<String, String> params = new HashMap<String, String>(); for ( Iterator e = filterConfig.getInitParameterNames(); e.hasNext(); ) { String name = (String) e.next(); String value = filterConfig.getInitParameter(name); params.put(name, value); } return new Dispatcher(filterConfig.getServletContext(), params); }
创建一个Dispatcher对象,然后将Filter配置的参数保存入Dispatcher属性,由于在派发请求之前需要将request,session等对象的参数解析出来,所以还要一个servletContext的引用。
关键的操作还是Dispatcher的init(),它做了大量的初始化工作。
public void init() { if (configurationManager == null) { configurationManager = createConfigurationManager(BeanSelectionProvider.DEFAULT_BEAN_NAME); } try { init_FileManager(); init_DefaultProperties(); // [1] init_TraditionalXmlConfigurations(); // [2] init_LegacyStrutsProperties(); // [3] init_CustomConfigurationProviders(); // [5] init_FilterInitParameters() ; // [6] init_AliasStandardObjects() ; // [7] Container container = init_PreloadConfiguration(); container.inject(this); init_CheckWebLogicWorkaround(container); if (!dispatcherListeners.isEmpty()) { for (DispatcherListener l : dispatcherListeners) { l.dispatcherInitialized(this); } } } catch (Exception ex) { if (LOG.isErrorEnabled()) LOG.error("Dispatcher initialization failed", ex); throw new StrutsException(ex); } }
从以上代码可以归纳出,Dispatcher的init()做了如下的工作:
1.创建一个ConfigurationManager对象
这个ConfigurationManager类从它的类名就可以看出类的功能:管理配置信息。它是配置元素的操作接口,我们可以看看它的部分源码:
/** * ConfigurationManager - central for XWork Configuration management, including its ConfigurationProvider. */ public class ConfigurationManager { protected Configuration configuration; private List<ContainerProvider> containerProviders = new CopyOnWriteArrayList<ContainerProvider>(); private List<PackageProvider> packageProviders = new CopyOnWriteArrayList<PackageProvider>(); public ConfigurationManager() public ConfigurationManager(String name) public synchronized Configuration getConfiguration() protected Configuration createConfiguration(String beanName) public synchronized void setConfiguration(Configuration configuration) public List<ContainerProvider> getContainerProviders() public void setContainerProviders(List<ContainerProvider> containerProviders) public void addContainerProvider(ContainerProvider provider) public void clearContainerProviders() private void clearContainerProvider(ContainerProvider containerProvider) public synchronized void destroyConfiguration() public synchronized void conditionalReload() private void updateReloadConfigsFlag() private boolean needReloadPackageProviders() private boolean needReloadContainerProviders(List<ContainerProvider> providers) private void reloadProviders(List<ContainerProvider> providers) public synchronized void reload() }
可能,代码中的Configuration,PackageProvider,ContainerProvider对我们来说还有些陌生,不过没关系,接下来就对这些元素一一讲解。从代码中我们可以看出,所有的方法都是对配置信息的操作。那么配置信息从哪里来,又放在了哪里呢?配置信息的来源就是我们的配置文件了,比如:struts.xml,default.properties,struts.properties,struts-default.xml.等。而这些配置信息被加载到内存中,存储在了Configuration这个类的实例中了。可是这些配置信息又是如何被加载的呢?这就是PackageProvider和ContainerProvider的功劳了。
在ConfigurationManager的代码中,我们看到它维护了一个Configuration的引用,以及PackageProvider和ContainerProvider的链表。Configuration中存储着配置信息,而ConfigurationManager则是对配置信息的操作,这正是将数据的存储和操作分离的设计思想了。
好,接下来我们分析一下Configuration,PackageProvider和ContainerProvider。
先说Configuration,它被设计为一个接口,接口的定义如下:
public interface Configuration extends Serializable { void rebuildRuntimeConfiguration(); PackageConfig getPackageConfig(String name); Set<String> getPackageConfigNames(); Map<String, PackageConfig> getPackageConfigs(); /** * The current runtime configuration. Currently, if changes have been made to the Configuration since the last * time buildRuntimeConfiguration() was called, you'll need to make sure to. * * @return the current runtime configuration */ RuntimeConfiguration getRuntimeConfiguration(); void addPackageConfig(String name, PackageConfig packageConfig); PackageConfig removePackageConfig(String packageName); void destroy(); @Deprecated void reload(List<ConfigurationProvider> providers) throws ConfigurationException; List<PackageProvider> reloadContainer(List<ContainerProvider> containerProviders) throws ConfigurationException; Container getContainer(); Set<String> getLoadedFileNames(); List<UnknownHandlerConfig> getUnknownHandlerStack(); void setUnknownHandlerStack(List<UnknownHandlerConfig> unknownHandlerStack); }
之前说过,配置信息都保存在了Configuration实例中,这配置信息分为两种,一种是由PackageProvider加载的事件映射配置元素,一种是由ContainerProvider加载的容器配置元素。前者也就是我们经常配置的<package>元素了,而后者则是对应<constants>和<bean>两种元素以及default.properties,struts.properties文件中声明的常量,之所以说它是容器元素,是因为它们配置的信息最终反映到Container对象中,也就是容器了。关于容器后面细说,这个很重要。
ok,接着说Configuration,接口只是定义了行为,数据的存储还需要数据结构,这就在实现类了。Configuration只有一个实现类:DefaultConfiguration,其代码较多,我们只看两种配置信息是如何被存储的:
public class DefaultConfiguration implements Configuration { protected Map<String, PackageConfig> packageContexts = new LinkedHashMap<String, PackageConfig>(); protected RuntimeConfiguration runtimeConfiguration; protected Container container; protected String defaultFrameworkBeanName; protected Set<String> loadedFileNames = new TreeSet<String>(); protected List<UnknownHandlerConfig> unknownHandlerStack; ObjectFactory objectFactory; }
从实现类DefaultConfiguration中,我们看到有PackageConfig和Container,前者正是封装了<package>元素的配置信息,后者则是将容器元素从文件中加载后对象化了,这是因为Container对象的创建和初始化需要这些配置信息。属性中还有个RuntimeConfiguration,它封装了运行时的Action配置等信息。
接着说PackageProvider,它也被设计为接口,接口定义如下:
public interface PackageProvider { public void init(Configuration configuration) throws ConfigurationException; public boolean needsReload(); public void loadPackages() throws ConfigurationException; }
其中的loadPackage()方法就是往DefaultConfiguration实例加载配置信息了。
再看ContainerProvider:
/** * Provides beans and constants/properties for the Container */ public interface ContainerProvider { public void destroy(); /** * Initializes with the configuration */ public void init(Configuration configuration) throws ConfigurationException; public boolean needsReload(); /** * Registers beans and properties for the Container */ public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException; }
其中,最重要的自然就是register()方法了,它会将bean,常量注册到Container中。关于bean,在Container中再详细说吧。
以上两个接口被统一为一个ConfigurationProvider接口:
/** * Interface to be implemented by all forms of XWork configuration classes. */ public interface ConfigurationProvider extends ContainerProvider, PackageProvider { }
可以看到这个接口没有新定义什么方法,就是将两个原来的接口统一了。估计是这样做可以让后面的设计看起来好些吧。
关于这个接口的实现类,本篇后面部分会详细说明的。
2.往ConfigurationManager添加一个FileManagerProvider
FileManagerProvider是干嘛的呢?自然是提供FileManager的了,从名字就可以看出来。关键是FileManager又是干嘛的,又如何提供。看看代码就知道了,在Dispatcher的init()中,调用了init_FileManager()方法:
private void init_FileManager() throws ClassNotFoundException { if (initParams.containsKey(StrutsConstants.STRUTS_FILE_MANAGER)) {//Filter参数有没有struts.fileManager,一般情况下是没有 final String fileManagerClassName = initParams.get(StrutsConstants.STRUTS_FILE_MANAGER); final Class<FileManager> fileManagerClass = (Class<FileManager>) Class.forName(fileManagerClassName); if (LOG.isInfoEnabled()) { LOG.info("Custom FileManager specified: #0", fileManagerClassName); } configurationManager.addContainerProvider(new FileManagerProvider(fileManagerClass, fileManagerClass.getSimpleName())); } else { // add any other Struts 2 provided implementations of FileManager configurationManager.addContainerProvider(new FileManagerProvider(JBossFileManager.class, "jboss")); } if (initParams.containsKey(StrutsConstants.STRUTS_FILE_MANAGER_FACTORY)) {//Filter参数有没有struts.fileManagerFactory,一般没有 final String fileManagerFactoryClassName = initParams.get(StrutsConstants.STRUTS_FILE_MANAGER_FACTORY); final Class<FileManagerFactory> fileManagerFactoryClass = (Class<FileManagerFactory>) Class.forName(fileManagerFactoryClassName); if (LOG.isInfoEnabled()) { LOG.info("Custom FileManagerFactory specified: #0", fileManagerFactoryClassName); } configurationManager.addContainerProvider(new FileManagerFactoryProvider(fileManagerFactoryClass)); } }
以上代码会执行:
configurationManager.addContainerProvider(new FileManagerProvider(JBossFileManager.class, "jboss"));
这一条分支语句,也就是一个FileManagerProvider对象被加入到了List中。我们看看FileManagerProvider:
public class FileManagerProvider implements ContainerProvider { private Class<? extends FileManager> fileManagerClass; private String name; public FileManagerProvider(Class<? extends FileManager> fileManagerClass, String name) { this.fileManagerClass = fileManagerClass; this.name = name; } public void destroy() { } public void init(Configuration configuration) throws ConfigurationException { } public boolean needsReload() { return false; } public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException { builder.factory(FileManager.class, name, fileManagerClass, Scope.SINGLETON); } }
代码竟是如此的简单。它直接实现了ContainerProvider接口,真不知道要ConfigurationProvider何用。。不管它,接着看register()的实现,它调用了参数ContainerBuiler的factory()方法,看到builder这样的字眼,应该会有点联想到建造者模式吧。是的,它正是使用了建造者模式,其中的奥妙还是留到Container部分来讲吧。先说factory()方法的作用吧,就是创建一个FileManager。FileManager有什么功能呢?且看源代码:
/** * Basic interface to access file on the File System and to monitor changes */ public interface FileManager { /** * Enables configs reloading when config file changed * * @param reloadingConfigs {@link XWorkConstants#RELOAD_XML_CONFIGURATION} */ void setReloadingConfigs(boolean reloadingConfigs); /** * Checks if given file changed and must be reloaded if {@link #setReloadingConfigs(boolean)} is true * * @param fileName to check * @return true if file changed */ boolean fileNeedsReloading(String fileName); /** * Checks if file represented by provided URL should be reloaded * * @param fileUrl url to a file * @return true if file exists and should be reloaded, if url is null return false */ boolean fileNeedsReloading(URL fileUrl); /** * Loads opens the named file and returns the InputStream * * @param fileUrl - the URL of the file to open * @return an InputStream of the file contents or null * @throws IllegalArgumentException if there is no file with the given file name */ InputStream loadFile(URL fileUrl); /** * Adds file to list of monitored files if {@link #setReloadingConfigs(boolean)} is true * * @param fileUrl {@link URL} to file to be monitored */ void monitorFile(URL fileUrl); /** * Convert URLs to URLs with "file" protocol * @param url URL to convert to a jar url * @return a URL to a file, or null if the URL external form cannot be parsed */ URL normalizeToFileProtocol(URL url); /** * Indicate if given implementation supports current OS File System * * @return true if supports current OS File System */ boolean support(); /** * User's implementation should return false as then it will be taken in first place * * @return true if it's a framework provided implementation */ boolean internal(); Collection<? extends URL> getAllPhysicalUrls(URL url) throws IOException; }
它也是接口啊。那么之前factory()方法会创建出什么类呢?也就是参数传递进去的JBossFileManager类了。看接口开头的注释,可以知道这个类的功能就是访问文件系统中的文件并且监控文件是否被修改了。如果被修改了,可能就要重新载入了。
ok,总结一下,就是init_FileManager的功能就是往configurationManager的List<ContainerProvider>属性添加一个元素。接下来的工作都类似了。所有添加Provider的工作都是为了最终创建出一个可用的Container(到时创建Container时,会一一调用这些provider的register()),以及对象化的事件映射配置元素。因为Provider就是provide的功能嘛。
3.往ConfigurationManager添加一个DefaultPropertiesProvider
和上面的类似,所以直接看源代码吧:
private void init_DefaultProperties() { configurationManager.addContainerProvider(new DefaultPropertiesProvider()); }
好简洁,再跟着看看DefaultPropertiesProvider:
/** * Loads the default properties, separate from the usual struts.properties loading */ public class DefaultPropertiesProvider extends LegacyPropertiesConfigurationProvider { public void destroy() { } public void init(Configuration configuration) throws ConfigurationException { } public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException { Settings defaultSettings = null; try { defaultSettings = new PropertiesSettings("org/apache/struts2/default"); } catch (Exception e) { throw new ConfigurationException("Could not find or error in org/apache/struts2/default.properties", e); } loadSettings(props, defaultSettings); } }
properties文件中的信息被加载到了PropertiesSetting类中,其实没什么神奇的,也就是维护一个Map集合。既然这个类继承了LegacyPropertiesConfigurationProvider类,那我们去拜访一下它咯:
public class LegacyPropertiesConfigurationProvider implements ConfigurationProvider { /** * The Logging instance for this class. */ private static final Logger LOG = LoggerFactory.getLogger(LegacyPropertiesConfigurationProvider.class); public void destroy() { Settings.reset(); } public void init(Configuration configuration) throws ConfigurationException { Settings.reset(); } public void loadPackages() throws ConfigurationException { } public boolean needsReload() { return false; } public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException { final Settings settings = Settings.getInstance(); loadSettings(props, settings); // Set default locale by lazily resolving the locale property as needed into a Locale object builder.factory(Locale.class, new Factory<Locale>() { private Locale locale; public synchronized Locale create(Context context) throws Exception { if (locale == null) { String loc = context.getContainer().getInstance(String.class, StrutsConstants.STRUTS_LOCALE); if (loc != null) { StringTokenizer localeTokens = new StringTokenizer(loc, "_"); String lang = null; String country = null; if (localeTokens.hasMoreTokens()) { lang = localeTokens.nextToken(); } if (localeTokens.hasMoreTokens()) { country = localeTokens.nextToken(); } locale = new Locale(lang, country); } else { if (LOG.isInfoEnabled()) { LOG.info("No locale define, substituting the default VM locale"); } locale = Locale.getDefault(); } } return locale; } }); } /** * @param props * @param settings */ protected void loadSettings(LocatableProperties props, final Settings settings) { // We are calling the impl methods to get around the single instance of Settings that is expected for (Iterator i = settings.listImpl(); i.hasNext(); ) { String name = (String) i.next(); props.setProperty(name, settings.getImpl(name), settings.getLocationImpl(name)); } } }
终于看到实现ConfigurationProvider接口的类了。从名字就大概可以猜出它是加载properties文件的。
4.往ConfigurationManager添加一个StrutsXmlConfigurationProvider
还是添加Provider:
private void init_TraditionalXmlConfigurations() { String configPaths = initParams.get("config"); if (configPaths == null) { configPaths = DEFAULT_CONFIGURATION_PATHS;//被定义为:struts-default.xml,struts-plugin.xml,struts.xml } String[] files = configPaths.split("\\s*[,]\\s*"); for (String file : files) { if (file.endsWith(".xml")) { if ("xwork.xml".equals(file)) { configurationManager.addContainerProvider(createXmlConfigurationProvider(file, false)); } else { configurationManager.addContainerProvider(createStrutsXmlConfigurationProvider(file, false, servletContext)); } } else { throw new IllegalArgumentException("Invalid configuration file name"); } } }
容易看出,会根据每个文件创建一个Provider用于加载xml配置文件。其中,StrutsXmlConfigurationProvider继承自XmlConfigurationProvider,而后者实现了ConfigurationProvider接口。
5.往ConfigurationManager添加一个LegacyPropertiesConfigurationProvider
直接上代码吧:
private void init_LegacyStrutsProperties() { configurationManager.addContainerProvider(new LegacyPropertiesConfigurationProvider()); }
这个前面已经说过了,只不过是加载的文件不同罢了,之前是default.properties,而现在这个则是struts.properties文件。
6.往ConfigurationManager添加用户自定义的xmlprovider
看看代码:
private void init_CustomConfigurationProviders() { String configProvs = initParams.get("configProviders"); if (configProvs != null) { String[] classes = configProvs.split("\\s*[,]\\s*"); for (String cname : classes) { try { Class cls = ClassLoaderUtil.loadClass(cname, this.getClass()); ConfigurationProvider prov = (ConfigurationProvider)cls.newInstance(); configurationManager.addContainerProvider(prov); } catch (InstantiationException e) { throw new ConfigurationException("Unable to instantiate provider: "+cname, e); } catch (IllegalAccessException e) { throw new ConfigurationException("Unable to access provider: "+cname, e); } catch (ClassNotFoundException e) { throw new ConfigurationException("Unable to locate provider class: "+cname, e); } } } }
一般情况下,这个方法自然就是摆设了。主要是作为struus2框架提供给用户的扩展点吧。一般使用struts2估计也用不上。
7.往ConfigurationManager添加一个ConfigurationProvider实现类用于解析Filter参数
看看代码:
private void init_FilterInitParameters() { configurationManager.addContainerProvider(new ConfigurationProvider() { public void destroy() { } public void init(Configuration configuration) throws ConfigurationException { } public void loadPackages() throws ConfigurationException { } public boolean needsReload() { return false; } public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException { props.putAll(initParams); } }); }
用了一个匿名内部类,将filter参数放入容器中,被容器保管。这样,在action中也可以很方便的访问filter参数,恩,还是挺好的。
8.往ConfigurationManager添加一个BeanSelectionProvider
还是看看代码:
private void init_AliasStandardObjects() { configurationManager.addContainerProvider(new BeanSelectionProvider()); }
别看只有一行代码,这里面可是很有内涵的,BeanSelectionProvider也是实现了ConfigurationProvider接口,只看其中的register():
public void register(ContainerBuilder builder, LocatableProperties props) { alias(ObjectFactory.class, StrutsConstants.STRUTS_OBJECTFACTORY, builder, props); alias(FileManagerFactory.class, StrutsConstants.STRUTS_FILE_MANAGER_FACTORY, builder, props, Scope.SINGLETON); alias(XWorkConverter.class, StrutsConstants.STRUTS_XWORKCONVERTER, builder, props); alias(CollectionConverter.class, StrutsConstants.STRUTS_CONVERTER_COLLECTION, builder, props); alias(ArrayConverter.class, StrutsConstants.STRUTS_CONVERTER_ARRAY, builder, props); alias(DateConverter.class, StrutsConstants.STRUTS_CONVERTER_DATE, builder, props); alias(NumberConverter.class, StrutsConstants.STRUTS_CONVERTER_NUMBER, builder, props); alias(StringConverter.class, StrutsConstants.STRUTS_CONVERTER_STRING, builder, props); alias(ConversionPropertiesProcessor.class, StrutsConstants.STRUTS_CONVERTER_PROPERTIES_PROCESSOR, builder, props); alias(ConversionFileProcessor.class, StrutsConstants.STRUTS_CONVERTER_FILE_PROCESSOR, builder, props); alias(ConversionAnnotationProcessor.class, StrutsConstants.STRUTS_CONVERTER_ANNOTATION_PROCESSOR, builder, props); alias(TypeConverterCreator.class, StrutsConstants.STRUTS_CONVERTER_CREATOR, builder, props); alias(TypeConverterHolder.class, StrutsConstants.STRUTS_CONVERTER_HOLDER, builder, props); alias(TextProvider.class, StrutsConstants.STRUTS_XWORKTEXTPROVIDER, builder, props, Scope.DEFAULT); alias(LocaleProvider.class, StrutsConstants.STRUTS_LOCALE_PROVIDER, builder, props); alias(ActionProxyFactory.class, StrutsConstants.STRUTS_ACTIONPROXYFACTORY, builder, props); alias(ObjectTypeDeterminer.class, StrutsConstants.STRUTS_OBJECTTYPEDETERMINER, builder, props); alias(ActionMapper.class, StrutsConstants.STRUTS_MAPPER_CLASS, builder, props); alias(MultiPartRequest.class, StrutsConstants.STRUTS_MULTIPART_PARSER, builder, props, Scope.DEFAULT); alias(FreemarkerManager.class, StrutsConstants.STRUTS_FREEMARKER_MANAGER_CLASSNAME, builder, props); alias(VelocityManager.class, StrutsConstants.STRUTS_VELOCITY_MANAGER_CLASSNAME, builder, props); alias(UrlRenderer.class, StrutsConstants.STRUTS_URL_RENDERER, builder, props); alias(ActionValidatorManager.class, StrutsConstants.STRUTS_ACTIONVALIDATORMANAGER, builder, props); alias(ValueStackFactory.class, StrutsConstants.STRUTS_VALUESTACKFACTORY, builder, props); alias(ReflectionProvider.class, StrutsConstants.STRUTS_REFLECTIONPROVIDER, builder, props); alias(ReflectionContextFactory.class, StrutsConstants.STRUTS_REFLECTIONCONTEXTFACTORY, builder, props); alias(PatternMatcher.class, StrutsConstants.STRUTS_PATTERNMATCHER, builder, props); alias(StaticContentLoader.class, StrutsConstants.STRUTS_STATIC_CONTENT_LOADER, builder, props); alias(UnknownHandlerManager.class, StrutsConstants.STRUTS_UNKNOWN_HANDLER_MANAGER, builder, props); alias(UrlHelper.class, StrutsConstants.STRUTS_URL_HELPER, builder, props); alias(TextParser.class, StrutsConstants.STRUTS_EXPRESSION_PARSER, builder, props); if ("true".equalsIgnoreCase(props.getProperty(StrutsConstants.STRUTS_DEVMODE))) { props.setProperty(StrutsConstants.STRUTS_I18N_RELOAD, "true"); props.setProperty(StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD, "true"); props.setProperty(StrutsConstants.STRUTS_FREEMARKER_TEMPLATES_CACHE, "false"); props.setProperty(StrutsConstants.STRUTS_FREEMARKER_TEMPLATES_CACHE_UPDATE_DELAY, "0"); // Convert struts properties into ones that xwork expects props.setProperty(XWorkConstants.DEV_MODE, "true"); } else { props.setProperty(XWorkConstants.DEV_MODE, "false"); } // Convert Struts properties into XWork properties convertIfExist(props, StrutsConstants.STRUTS_LOG_MISSING_PROPERTIES, XWorkConstants.LOG_MISSING_PROPERTIES); convertIfExist(props, StrutsConstants.STRUTS_ENABLE_OGNL_EXPRESSION_CACHE, XWorkConstants.ENABLE_OGNL_EXPRESSION_CACHE); convertIfExist(props, StrutsConstants.STRUTS_ENABLE_OGNL_EVAL_EXPRESSION, XWorkConstants.ENABLE_OGNL_EVAL_EXPRESSION); convertIfExist(props, StrutsConstants.STRUTS_ALLOW_STATIC_METHOD_ACCESS, XWorkConstants.ALLOW_STATIC_METHOD_ACCESS); convertIfExist(props, StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD, XWorkConstants.RELOAD_XML_CONFIGURATION); LocalizedTextUtil.addDefaultResourceBundle("org/apache/struts2/struts-messages"); loadCustomResourceBundles(props); }
方法的功能后面再说,这里列出,只是为了说明这个Provider还是很重要的。
9.创建并初始化容器
文章篇幅似乎已经很长,这部分又比较重要,所以重新开一篇文章吧。
struts2源码学习之初始化(二)