struts中action名称反复导致的神秘事件
2018-04-10 10:37
351 查看
近期由于项目需求变更。须要本人对当中的某个业务功能进行改动。本人依照前台页面找action,依据action找代码的逻辑进行了改动(公司项目是ssh框架,struts配置全部是通过注解的方式进行。配置简单方便)。当然測试人员也成功的进行了測试,发现没有不论什么问题,成功发版。奇葩事情来了。在发版环境中,改动的代码总是没用!
没办法,问题还是要解决。在确认了发版环境的确是最新代码之后,回自己座位找原因。
这次我用action名称全局搜索项目project,尼玛发现两个重名action,当然我仅仅改动了当中一个文件。还有一个文件的action中的代码逻辑没有改动,找到原因之后,发现两个action之前的逻辑一模一样,不同之处仅仅在于我刚改动的部分,以防万一。先把这个没有改动的文件里新增我的逻辑代码。再次提交公布,ok。
那么问题来了。两个重名的action。会导致什么问题呢,为何在我电脑上改动之后就生效。
打版发tag版本号后就没实用呢?百思不得其解。上网搜了一下action文件名反复的问题,结果网上全是struts怎样避免struts反复,无非就是说struts通过文件名及命名空间包名称来避免action反复。标记唯一。可是action文件名反复会导致什么问题却没有搜到相关技术文章。
然后问了项目的相关技术人员。也不知道根本原因。没办法。自己动手,丰衣足食。仅仅能自己推測是struts可能在自己搜索action名称相应的action的时候具有随机性质吧。
那么详细什么原因呢。回家后下载struts源代码看看看呗。
以下把看到的代码解说一下为何在我的电脑上能够,公布后不能够。
1.首先我们都知道,struts项目中,我们都会在web.xml中配置struts封装的过滤器,为何呢。由于struts为我们封装了非常多现成的东西。这样我们才干通过action来请求相关,而不必用传统的servlet方式请求。
既然我们配置的StrutsPrepareAndExecuteFilter是一个过滤器。那么我们在项目启动的时候必定会运行过滤器的init方法。
看上面源代码会明确,第10行会把web.xml配置的一些基本配置给创建进来。12行通过这些配置创建dispatcher,当然。非常多核心的功能都是环绕着dispaatcher进行的,创建这个dispatcher会发生什么呢,我们看initDispatcher的源代码
2.以下是关于第12行InitOperations中创建dispatcher的源代码
看上面第10行创建了dispatcher之后。11行立刻进行了初始化,那么都干了些什么呢,继续往下深入。
3.以下是关于dispatcher中init方法的实现
从上面的12-18行我们看到这些代码都是一些主要的初始化操作,无非就是讲struts默认封装的一些properties文件,默认格式的struts.xml,struts-plugin.xml之类的文件的解析类放入配置管理器中。通过这几部之后将以下几个重要的DefaultPropertiesProvider(默认有个jboss),DefaultPropertiesProvider,StrutsXmlConfigurationProvider(默认三个。各自是解析struts默认扫描文件struts-default.xml,struts-plugin.xml及struts.xml,假设在web.xml中配置filter时传入config配置时,会默认扫描config的配置,而不会扫面上面这三个配置了)全部加入到配置管理器中的containerProviders属性中。真正核心的代码部分在20行。这里面包括了解析的全过程。以下看代码。
4.
上面源代码中第2行。首先获取container,当然也是用的ThreadLocal模式。不懂得能够百度,这里不说了,直接说重点22行在获取容器container的过程中。首先创建配置管理器ConfigurationManager,通过配置管理器获取容器Configuration。那么26行到底干了什么呢,继续往下看。
5.
27行没有configuration先创建一个,创建的是42行的默认配置DefaultConfiguration,然后在29行调用了reloadContainer方法,通过名称应该能明确。意思为又一次载入容器。这种方法应该是统一封装的。个人推測开发改动代码后又一次载入不用重新启动服务应该也是调用的这种方法,这种方法的參数为containerProviders,也就是在3中的那些解析处理类,那么该方法都有什么东东呢,继续。。
6.
上面这块代码的核心部分为40-46。以及50-56部分,40-46行部分代码中心逻辑是遍历之前放入configurationManager中的containerProviders,当然struts的三个默认配置文件或者自己在filter中配置的文件会在这里解析,而50行-56行的部分便是struts针对相关插件进行的处理。比方我们公司项目用注解的方式进行配置,那么就须要在项目中加入struts-convention.jar的jar包,因此这里先分析注解方式下的action解析。核心重点在54行
7.那么54行的provider的详细实现类是什么呢?我们能够在struts-convention的jar包中找到实现了PackageProvider的ClasspathPackageProvider类,该类包括了ActionConfigBuilder的一个引用。而ActionConfigBuilder的实现类为PackageBasedActionConfigBuilder,源代码例如以下
那么我们看上面的代码buildActionConfigs方法中核心代码33行中的findActions,该方法的意思是找出jar包及class文件里全部匹配指定格式的文件列表,然后过滤有action。actions,struts,struts2的全部类文件,54行就是匹配符合条件的action。也就是我们普通情况下写在action包中的那些全部的class文件列表,从这里能够看出findClasses文件返回的是一个列表,然后将列表放入一个hashset中,我们知道,hashset的一个特点就是不保证我们存放的顺序,看到这里我们也就恍然大悟了,全部在符合action,actions,struts,struts2路径下的定义的文件。都会在hashset中存储。那么存放的地址是不确定的,这样在后面34行的解析全部的action时候的訪问顺序就不是固定的,訪问的时候同名action相应的class文件谁后被訪问到,谁将会真正的放入容器中。我们看到102行的循环遍历hashset,201行的主要逻辑代码例如以下
上面的52行解释了这里的道理。由于PackageConfig中封装了一个名为actionConfigs的hashmap, protected Map
比方上面这个类,是struts对于package的一个封装,并同一时候在内部提供了一个builder的静态内部类,同一时候将外部类的构造函数声明为protected这样避免外部随便定义对象,并通过内部类中创建一个外部类的对象。
这样的方式非常普遍。
那么总结下来。struts对于同样包,同样命名空间的,同名action,在项目启动的时候是不进行报错提示的,不知道算不算是个bug,这样对于注解方式的项目,假如眼下如今有一个类文件,当中定义了非常多action,这时候直接把该文件复制一下,然后改动名字后,在新文件里加入新的action,原先的action不删除的话,这样就存在两套了。假如仅仅改了一套逻辑。那么问题就会出现了。就会好出现我们这次遇到的问题。改动的代码可能不生效(当然这次我遇到的问题就是这么来的,之前的同事做这块的时候由于新增了一个与原先的页面相似的功能,可是会有额外的逻辑,于是便直接复制粘贴了一份,也没有进行又一次命名。这样便导致了后面的事情的发生,尽管定位到两个重名的action推測会是这个原因,可是详细的道理就非常难找了。仅仅能看源代码了,只是看源代码学会了非常多东西,窃喜),明确了struts用hashset存储之后进行的解析。也就是说明会存在偶然性,于是在自己的机器上实验。不停的重新启动測试重新启动測试重新启动測试,最终发现的确两套逻辑中的代码都可能进入的。到这里也就彻底大悟了。
总结:struts对于同样包,同样命名空间的,同名action。不会报错,而且用注解方式实现的话,详细用哪个类中的action具有偶然性,由于struts是用hashset存储后进行解析的。
没办法,问题还是要解决。在确认了发版环境的确是最新代码之后,回自己座位找原因。
这次我用action名称全局搜索项目project,尼玛发现两个重名action,当然我仅仅改动了当中一个文件。还有一个文件的action中的代码逻辑没有改动,找到原因之后,发现两个action之前的逻辑一模一样,不同之处仅仅在于我刚改动的部分,以防万一。先把这个没有改动的文件里新增我的逻辑代码。再次提交公布,ok。
那么问题来了。两个重名的action。会导致什么问题呢,为何在我电脑上改动之后就生效。
打版发tag版本号后就没实用呢?百思不得其解。上网搜了一下action文件名反复的问题,结果网上全是struts怎样避免struts反复,无非就是说struts通过文件名及命名空间包名称来避免action反复。标记唯一。可是action文件名反复会导致什么问题却没有搜到相关技术文章。
然后问了项目的相关技术人员。也不知道根本原因。没办法。自己动手,丰衣足食。仅仅能自己推測是struts可能在自己搜索action名称相应的action的时候具有随机性质吧。
那么详细什么原因呢。回家后下载struts源代码看看看呗。
以下把看到的代码解说一下为何在我的电脑上能够,公布后不能够。
1.首先我们都知道,struts项目中,我们都会在web.xml中配置struts封装的过滤器,为何呢。由于struts为我们封装了非常多现成的东西。这样我们才干通过action来请求相关,而不必用传统的servlet方式请求。
既然我们配置的StrutsPrepareAndExecuteFilter是一个过滤器。那么我们在项目启动的时候必定会运行过滤器的init方法。
public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter { protected PrepareOperations prepare; protected ExecuteOperations execute; protected List<Pattern> excludedPatterns = null; public void init(FilterConfig filterConfig) throws ServletException { InitOperations init = new InitOperations(); Dispatcher dispatcher = null; try { FilterHostConfig config = new FilterHostConfig(filterConfig); init.initLogging(config); dispatcher = init.initDispatcher(config); init.initStaticContentLoader(config, dispatcher); prepare = new PrepareOperations(dispatcher); execute = new ExecuteOperations(dispatcher); this.excludedPatterns = init.buildExcludedPatternsList(dispatcher); postInit(dispatcher, filterConfig); } finally { if (dispatcher != null) { dispatcher.cleanUpAfterInit(); } init.cleanup(); } }
看上面源代码会明确,第10行会把web.xml配置的一些基本配置给创建进来。12行通过这些配置创建dispatcher,当然。非常多核心的功能都是环绕着dispaatcher进行的,创建这个dispatcher会发生什么呢,我们看initDispatcher的源代码
2.以下是关于第12行InitOperations中创建dispatcher的源代码
public class InitOperations { public InitOperations() { } /** * Creates and initializes the dispatcher */ public Dispatcher initDispatcher( HostConfig filterConfig ) { Dispatcher dispatcher = createDispatcher(filterConfig); dispatcher.init(); return dispatcher; } }
看上面第10行创建了dispatcher之后。11行立刻进行了初始化,那么都干了些什么呢,继续往下深入。
3.以下是关于dispatcher中init方法的实现
/** * Load configurations, including both XML and zero-configuration strategies, * and update optional settings, including whether to reload configurations and resource files. */ public void init() { if (configurationManager == null) { configurationManager = createConfigurationManager(DefaultBeanSelectionProvider.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); } } errorHandler.init(servletContext); } catch (Exception ex) { if (LOG.isErrorEnabled()) LOG.error("Dispatcher initialization failed", ex); throw new StrutsException(ex); } }
从上面的12-18行我们看到这些代码都是一些主要的初始化操作,无非就是讲struts默认封装的一些properties文件,默认格式的struts.xml,struts-plugin.xml之类的文件的解析类放入配置管理器中。通过这几部之后将以下几个重要的DefaultPropertiesProvider(默认有个jboss),DefaultPropertiesProvider,StrutsXmlConfigurationProvider(默认三个。各自是解析struts默认扫描文件struts-default.xml,struts-plugin.xml及struts.xml,假设在web.xml中配置filter时传入config配置时,会默认扫描config的配置,而不会扫面上面这三个配置了)全部加入到配置管理器中的containerProviders属性中。真正核心的代码部分在20行。这里面包括了解析的全过程。以下看代码。
4.
private Container init_PreloadConfiguration() { Container container = getContainer(); boolean reloadi18n = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_I18N_RELOAD)); LocalizedTextUtil.setReloadBundles(reloadi18n); boolean devMode = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_DEVMODE)); LocalizedTextUtil.setDevMode(devMode); return container; } /** * Expose the dependency injection container. * @return Our dependency injection container */ public Container getContainer() { if (ContainerHolder.get() != null) { return ContainerHolder.get(); } ConfigurationManager mgr = getConfigurationManager(); if (mgr == null) { throw new IllegalStateException("The configuration manager shouldn't be null"); } else { Configuration config = mgr.getConfiguration(); if (config == null) { throw new IllegalStateException("Unable to load configuration"); } else { Container container = config.getContainer(); ContainerHolder.store(container); return container; } } }
上面源代码中第2行。首先获取container,当然也是用的ThreadLocal模式。不懂得能够百度,这里不说了,直接说重点22行在获取容器container的过程中。首先创建配置管理器ConfigurationManager,通过配置管理器获取容器Configuration。那么26行到底干了什么呢,继续往下看。
5.
public class ConfigurationManager { protected static final Logger LOG = LoggerFactory.getLogger(ConfigurationManager.class); protected Configuration configuration; protected Lock providerLock = new ReentrantLock(); private List<ContainerProvider> containerProviders = new CopyOnWriteArrayList<ContainerProvider>(); private List<PackageProvider> packageProviders = new CopyOnWriteArrayList<PackageProvider>(); protected String defaultFrameworkBeanName; private boolean providersChanged = false; private boolean reloadConfigs = true; // for the first time public ConfigurationManager() { this("xwork"); } public ConfigurationManager(String name) { this.defaultFrameworkBeanName = name; } /** * Get the current XWork configuration object. By default an instance of DefaultConfiguration will be returned * * @see com.opensymphony.xwork2.config.impl.DefaultConfiguration */ public synchronized Configuration getConfiguration() { if (configuration == null) { setConfiguration(createConfiguration(defaultFrameworkBeanName)); try { configuration.reloadContainer(getContainerProviders()); } catch (ConfigurationException e) { setConfiguration(null); throw new ConfigurationException("Unable to load configuration.", e); } } else { conditionalReload(); } return configuration; } protected Configuration createConfiguration(String beanName) { return new DefaultConfiguration(beanName); }
27行没有configuration先创建一个,创建的是42行的默认配置DefaultConfiguration,然后在29行调用了reloadContainer方法,通过名称应该能明确。意思为又一次载入容器。这种方法应该是统一封装的。个人推測开发改动代码后又一次载入不用重新启动服务应该也是调用的这种方法,这种方法的參数为containerProviders,也就是在3中的那些解析处理类,那么该方法都有什么东东呢,继续。。
6.
public class DefaultConfiguration implements Configuration { /** * Calls the ConfigurationProviderFactory.getConfig() to tell it to reload the configuration and then calls * buildRuntimeConfiguration(). * * @throws ConfigurationException */ public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException { packageContexts.clear(); loadedFileNames.clear(); List<PackageProvider> packageProviders = new ArrayList<PackageProvider>(); ContainerProperties props = new ContainerProperties(); ContainerBuilder builder = new ContainerBuilder(); Container bootstrap = createBootstrapContainer(providers); for (final ContainerProvider containerProvider : providers) { bootstrap.inject(containerProvider); containerProvider.init(this); containerProvider.register(builder, props); } props.setConstants(builder); builder.factory(Configuration.class, new Factory<Configuration>() { public Configuration create(Context context) throws Exception { return DefaultConfiguration.this; } }); ActionContext oldContext = ActionContext.getContext(); try { // Set the bootstrap container for the purposes of factory creation setContext(bootstrap); container = builder.create(false); setContext(container); objectFactory = container.getInstance(ObjectFactory.class); // Process the configuration providers first for (final ContainerProvider containerProvider : providers) { if (containerProvider instanceof PackageProvider) { container.inject(containerProvider); ((PackageProvider)containerProvider).loadPackages(); packageProviders.add((PackageProvider)containerProvider); } } // Then process any package providers from the plugins Set<String> packageProviderNames = container.getInstanceNames(PackageProvider.class); for (String name : packageProviderNames) { PackageProvider provider = container.getInstance(PackageProvider.class, name); provider.init(this); provider.loadPackages(); packageProviders.add(provider); } rebuildRuntimeConfiguration(); } finally { if (oldContext == null) { ActionContext.setContext(null); } } return packageProviders; } }
上面这块代码的核心部分为40-46。以及50-56部分,40-46行部分代码中心逻辑是遍历之前放入configurationManager中的containerProviders,当然struts的三个默认配置文件或者自己在filter中配置的文件会在这里解析,而50行-56行的部分便是struts针对相关插件进行的处理。比方我们公司项目用注解的方式进行配置,那么就须要在项目中加入struts-convention.jar的jar包,因此这里先分析注解方式下的action解析。核心重点在54行
7.那么54行的provider的详细实现类是什么呢?我们能够在struts-convention的jar包中找到实现了PackageProvider的ClasspathPackageProvider类,该类包括了ActionConfigBuilder的一个引用。而ActionConfigBuilder的实现类为PackageBasedActionConfigBuilder,源代码例如以下
/** * <p> * This class is a configuration provider for the XWork configuration * system. This is really the only way to truly handle loading of the * packages, actions and results correctly. This doesn't contain any * logic and instead delegates to the configured instance of the * {@link ActionConfigBuilder} interface. * </p> */ public class ClasspathPackageProvider implements PackageProvider { private ActionConfigBuilder actionConfigBuilder; @Inject public ClasspathPackageProvider(Container container) { this.actionConfigBuilder = container.getInstance(ActionConfigBuilder.class, container.getInstance(String.class, ConventionConstants.CONVENTION_ACTION_CONFIG_BUILDER)); } public void init(Configuration configuration) throws ConfigurationException { } public boolean needsReload() { return actionConfigBuilder.needsReload(); } public void loadPackages() throws ConfigurationException { actionConfigBuilder.buildActionConfigs(); } }
public interface ActionConfigBuilder { /** * Builds all the action configurations and stores them into the XWork configuration instance * via XWork dependency injetion. */ void buildActionConfigs(); boolean needsReload(); void destroy(); }
public class PackageBasedActionConfigBuilder implements ActionConfigBuilder { /** * Builds the action configurations by loading all classes in the packages specified by the * property <b>struts.convention.action.packages</b> and then figuring out which classes implement Action * or have Action in their name. Next, if this class is in a Java package that hasn't been * inspected a new PackageConfig (XWork) is created for that Java package using the Java package * name. This will contain all the ActionConfigs for all the Action classes that are discovered * within that Java package. Next, each class is inspected for the {@link ParentPackage} * annotation which is used to control the parent package for a specific action. Lastly, the * {@link ResultMapBuilder} is used to create ResultConfig instances of the action. */ public void buildActionConfigs() { //setup reload class loader based on dev settings initReloadClassLoader(); if (!disableActionScanning) { if (actionPackages == null && packageLocators == null) { throw new ConfigurationException("At least a list of action packages or action package locators " + "must be given using one of the properties [struts.convention.action.packages] or " + "[struts.convention.package.locators]"); } if (LOG.isTraceEnabled()) { LOG.trace("Loading action configurations"); if (actionPackages != null) LOG.trace("Actions being loaded from action packages " + Arrays.asList(actionPackages)); if (packageLocators != null) LOG.trace("Actions being loaded using package locators " + Arrays.asList(packageLocators)); if (excludePackages != null) LOG.trace("Excluding actions from packages " + Arrays.asList(excludePackages)); } Set<Class> classes = findActions(); buildConfiguration(classes); } } @SuppressWarnings("unchecked") protected Set<Class> findActions() { Set<Class> classes = new HashSet<Class>(); try { if (actionPackages != null || (packageLocators != null && !disablePackageLocatorsScanning)) { // By default, ClassFinder scans EVERY class in the specified // url set, which can produce spurious warnings for non-action // classes that can't be loaded. We pass a package filter that // only considers classes that match the action packages // specified by the user Test<String> classPackageTest = getClassPackageTest(); List<URL> urls = readUrls(); ClassFinder finder = new ClassFinder(getClassLoaderInterface(), urls, EXTRACT_BASE_INTERFACES, fileProtocols, classPackageTest); Test<ClassFinder.ClassInfo> test = getActionClassTest(); classes.addAll(finder.findClasses(test)); } } catch (Exception ex) { if (LOG.isErrorEnabled()) LOG.error("Unable to scan named packages", ex); } return classes; } /** * Construct a {@link Test} Object that determines if a specified class * should be included in the package scan based on the full {@link ClassInfo} * of the class. At this point, the class has been loaded, so it's ok to * perform tests such as checking annotations or looking at interfaces or * super-classes of the specified class. * * @return a {@link Test} object that returns true if the specified class * should be included in the package scan */ protected Test<ClassFinder.ClassInfo> getActionClassTest() { return new Test<ClassFinder.ClassInfo>() { public boolean test(ClassFinder.ClassInfo classInfo) { // Why do we call includeClassNameInActionScan here, when it's // already been called to in the initial call to ClassFinder? // When some action class passes our package filter in that step, // ClassFinder automatically includes parent classes of that action, // such as com.opensymphony.xwork2.ActionSupport. We repeat the // package filter here to filter out such results. boolean inPackage = includeClassNameInActionScan(classInfo.getName()); boolean nameMatches = classInfo.getName().endsWith(actionSuffix); try { return inPackage && (nameMatches || (checkImplementsAction && com.opensymphony.xwork2.Action.class.isAssignableFrom(classInfo.get()))); } catch (ClassNotFoundException ex) { if (LOG.isErrorEnabled()) LOG.error("Unable to load class [#0]", ex, classInfo.getName()); return false; } } }; } @SuppressWarnings("unchecked") protected void buildConfiguration(Set<Class> classes) { Map<String, PackageConfig.Builder> packageConfigs = new HashMap<String, PackageConfig.Builder>(); for (Class<?> actionClass : classes) { Actions actionsAnnotation = actionClass.getAnnotation(Actions.class); Action actionAnnotation = actionClass.getAnnotation(Action.class); // Skip classes that can't be instantiated if (cannotInstantiate(actionClass)) { if (LOG.isTraceEnabled()) LOG.trace("Class [#0] did not pass the instantiation test and will be ignored", actionClass.getName()); continue; } if (eagerLoading) { // Tell the ObjectFactory about this class try { objectFactory.getClassInstance(actionClass.getName()); } catch (ClassNotFoundException e) { if (LOG.isErrorEnabled()) LOG.error("Object Factory was unable to load class [#0]", e, actionClass.getName()); throw new StrutsException("Object Factory was unable to load class " + actionClass.getName(), e); } } // Determine the action package String actionPackage = actionClass.getPackage().getName(); if (LOG.isDebugEnabled()) { LOG.debug("Processing class [#0] in package [#1]", actionClass.getName(), actionPackage); } // Determine the default namespace and action name List<String> namespaces = determineActionNamespace(actionClass); for (String namespace : namespaces) { String defaultActionName = determineActionName(actionClass); PackageConfig.Builder defaultPackageConfig = getPackageConfig(packageConfigs, namespace, actionPackage, actionClass, null); // Verify that the annotations have no errors and also determine if the default action // configuration should still be built or not. Map<String, List<Action>> map = getActionAnnotations(actionClass); Set<String> actionNames = new HashSet<String>(); boolean hasDefaultMethod = ReflectionTools.containsMethod(actionClass, DEFAULT_METHOD); if (!map.containsKey(DEFAULT_METHOD) && hasDefaultMethod && actionAnnotation == null && actionsAnnotation == null && (alwaysMapExecute || map.isEmpty())) { boolean found = false; for (String method : map.keySet()) { List<Action> actions = map.get(method); for (Action action : actions) { // Check if there are duplicate action names in the annotations. String actionName = action.value().equals(Action.DEFAULT_VALUE) ? defaultActionName : action.value(); if (actionNames.contains(actionName)) { throw new ConfigurationException("The action class [" + actionClass + "] contains two methods with an action name annotation whose value " + "is the same (they both might be empty as well)."); } else { actionNames.add(actionName); } // Check this annotation is the default action if (action.value().equals(Action.DEFAULT_VALUE)) { found = true; } } } // Build the default if (!found) { createActionConfig(defaultPackageConfig, actionClass, defaultActionName, DEFAULT_METHOD, null); } } // Build the actions for the annotations for (String method : map.keySet()) { List<Action> actions = map.get(method); for (Action action : actions) { PackageConfig.Builder pkgCfg = defaultPackageConfig; if (action.value().contains("/") && !slashesInActionNames) { pkgCfg = getPackageConfig(packageConfigs, namespace, actionPackage, actionClass, action); } createActionConfig(pkgCfg, actionClass, defaultActionName, method, action); } } // some actions will not have any @Action or a default method, like the rest actions // where the action mapper is the one that finds the right method at runtime if (map.isEmpty() && mapAllMatches && actionAnnotation == null && actionsAnnotation == null) { createActionConfig(defaultPackageConfig, actionClass, defaultActionName, null, actionAnnotation); } //if there are @Actions or @Action at the class level, create the mappings for them String methodName = hasDefaultMethod ? DEFAULT_METHOD : null; if (actionsAnnotation != null) { List<Action> actionAnnotations = checkActionsAnnotation(actionsAnnotation); for (Action actionAnnotation2 : actionAnnotations) createActionConfig(defaultPackageConfig, actionClass, defaultActionName, methodName, actionAnnotation2); } else if (actionAnnotation != null) createActionConfig(defaultPackageConfig, actionClass, defaultActionName, methodName, actionAnnotation); } } buildIndexActions(packageConfigs); // Add the new actions to the configuration Set<String> packageNames = packageConfigs.keySet(); for (String packageName : packageNames) { configuration.addPackageConfig(packageName, packageConfigs.get(packageName).build()); } }
那么我们看上面的代码buildActionConfigs方法中核心代码33行中的findActions,该方法的意思是找出jar包及class文件里全部匹配指定格式的文件列表,然后过滤有action。actions,struts,struts2的全部类文件,54行就是匹配符合条件的action。也就是我们普通情况下写在action包中的那些全部的class文件列表,从这里能够看出findClasses文件返回的是一个列表,然后将列表放入一个hashset中,我们知道,hashset的一个特点就是不保证我们存放的顺序,看到这里我们也就恍然大悟了,全部在符合action,actions,struts,struts2路径下的定义的文件。都会在hashset中存储。那么存放的地址是不确定的,这样在后面34行的解析全部的action时候的訪问顺序就不是固定的,訪问的时候同名action相应的class文件谁后被訪问到,谁将会真正的放入容器中。我们看到102行的循环遍历hashset,201行的主要逻辑代码例如以下
/** * Creates a single ActionConfig object. * * @param pkgCfg The package the action configuration instance will belong to. * @param actionClass The action class. * @param actionName The name of the action. * @param actionMethod The method that the annotation was on (if the annotation is not null) or * the default method (execute). * @param annotation The ActionName annotation that might override the action name and possibly */ protected void createActionConfig(PackageConfig.Builder pkgCfg, Class<?> actionClass, String actionName, String actionMethod, Action annotation) { String className = actionClass.getName(); if (annotation != null) { actionName = annotation.value() != null && annotation.value().equals(Action.DEFAULT_VALUE) ? actionName : annotation.value(); actionName = StringUtils.contains(actionName, "/") && !slashesInActionNames ? StringUtils.substringAfterLast(actionName, "/") : actionName; if(!Action.DEFAULT_VALUE.equals(annotation.className())){ className = annotation.className(); } } ActionConfig.Builder actionConfig = new ActionConfig.Builder(pkgCfg.getName(), actionName, className); actionConfig.methodName(actionMethod); if (LOG.isDebugEnabled()) { LOG.debug("Creating action config for class [#0], name [#1] and package name [#2] in namespace [#3]", actionClass.toString(), actionName, pkgCfg.getName(), pkgCfg.getNamespace()); } //build interceptors List<InterceptorMapping> interceptors = interceptorMapBuilder.build(actionClass, pkgCfg, actionName, annotation); actionConfig.addInterceptors(interceptors); //build results Map<String, ResultConfig> results = resultMapBuilder.build(actionClass, annotation, actionName, pkgCfg.build()); actionConfig.addResultConfigs(results); //add params if (annotation != null) actionConfig.addParams(StringTools.createParameterMap(annotation.params())); //add exception mappings from annotation if (annotation != null && annotation.exceptionMappings() != null) actionConfig.addExceptionMappings(buildExceptionMappings(annotation.exceptionMappings(), actionName)); //add exception mapping from class ExceptionMappings exceptionMappings = actionClass.getAnnotation(ExceptionMappings.class); if (exceptionMappings != null) actionConfig.addExceptionMappings(buildExceptionMappings(exceptionMappings.value(), actionName)); //add pkgCfg.addActionConfig(actionName, actionConfig.build()); //check if an action with the same name exists on that package (from XML config probably) PackageConfig existingPkg = configuration.getPackageConfig(pkgCfg.getName()); if (existingPkg != null) { // there is a package already with that name, check action ActionConfig existingActionConfig = existingPkg.getActionConfigs().get(actionName); if (existingActionConfig != null && LOG.isWarnEnabled()) LOG.warn("Duplicated action definition in package [#0] with name [#1].", pkgCfg.getName(), actionName); } //watch class file if (isReloadEnabled()) { URL classFile = actionClass.getResource(actionClass.getSimpleName() + ".class"); fileManager.monitorFile(classFile); loadedFileUrls.add(classFile.toString()); } }
上面的52行解释了这里的道理。由于PackageConfig中封装了一个名为actionConfigs的hashmap, protected Map
/* * Copyright 2002-2006,2009 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.opensymphony.xwork2.config.entities; import com.opensymphony.xwork2.util.location.Located; import com.opensymphony.xwork2.util.location.Location; import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Configuration for Package. * <p/> * In the xml configuration file this is defined as the <code>package</code> tag. * * @author Rainer Hermanns * @version $Revision$ */ public class PackageConfig extends Located implements Comparable, Serializable, InterceptorLocator { private static final Logger LOG = LoggerFactory.getLogger(PackageConfig.class); protected Map<String, ActionConfig> actionConfigs; protected Map<String, ResultConfig> globalResultConfigs; protected Map<String, Object> interceptorConfigs; protected Map<String, ResultTypeConfig> resultTypeConfigs; protected List<ExceptionMappingConfig> globalExceptionMappingConfigs; protected List<PackageConfig> parents; protected String defaultInterceptorRef; protected String defaultActionRef; protected String defaultResultType; protected String defaultClassRef; protected String name; protected String namespace = ""; protected boolean isAbstract = false; protected boolean needsRefresh; protected PackageConfig(String name) { this.name = name; actionConfigs = new LinkedHashMap<String, ActionConfig>(); globalResultConfigs = new LinkedHashMap<String, ResultConfig>(); interceptorConfigs = new LinkedHashMap<String, Object>(); resultTypeConfigs = new LinkedHashMap<String, ResultTypeConfig>(); globalExceptionMappingConfigs = new ArrayList<ExceptionMappingConfig>(); parents = new ArrayList<PackageConfig>(); } protected PackageConfig(PackageConfig orig) { this.defaultInterceptorRef = orig.defaultInterceptorRef; this.defaultActionRef = orig.defaultActionRef; this.defaultResultType = orig.defaultResultType; this.defaultClassRef = orig.defaultClassRef; this.name = orig.name; this.namespace = orig.namespace; this.isAbstract = orig.isAbstract; this.needsRefresh = orig.needsRefresh; this.actionConfigs = new LinkedHashMap<String, ActionConfig>(orig.actionConfigs); this.globalResultConfigs = new LinkedHashMap<String, ResultConfig>(orig.globalResultConfigs); this.interceptorConfigs = new LinkedHashMap<String, Object>(orig.interceptorConfigs); this.resultTypeConfigs = new LinkedHashMap<String, ResultTypeConfig>(orig.resultTypeConfigs); this.globalExceptionMappingConfigs = new ArrayList<ExceptionMappingConfig>(orig.globalExceptionMappingConfigs); this.parents = new ArrayList<PackageConfig>(orig.parents); this.location = orig.location; } public boolean isAbstract() { return isAbstract; } public Map<String, ActionConfig> getActionConfigs() { return actionConfigs; } /** * returns the Map of all the ActionConfigs available in the current package. * ActionConfigs defined in ancestor packages will be included in this Map. * * @return a Map of ActionConfig Objects with the action name as the key * @see ActionConfig */ public Map<String, ActionConfig> getAllActionConfigs() { Map<String, ActionConfig> retMap = new LinkedHashMap<String, ActionConfig>(); if (!parents.isEmpty()) { for (PackageConfig parent : parents) { retMap.putAll(parent.getAllActionConfigs()); } } retMap.putAll(getActionConfigs()); return retMap; } /** * returns the Map of all the global ResultConfigs available in the current package. * Global ResultConfigs defined in ancestor packages will be included in this Map. * * @return a Map of Result Objects with the result name as the key * @see ResultConfig */ public Map<String, ResultConfig> getAllGlobalResults() { Map<String, ResultConfig> retMap = new LinkedHashMap<String, ResultConfig>(); if (!parents.isEmpty()) { for (PackageConfig parentConfig : parents) { retMap.putAll(parentConfig.getAllGlobalResults()); } } retMap.putAll(getGlobalResultConfigs()); return retMap; } /** * returns the Map of all InterceptorConfigs and InterceptorStackConfigs available in the current package. * InterceptorConfigs defined in ancestor packages will be included in this Map. * * @return a Map of InterceptorConfig and InterceptorStackConfig Objects with the ref-name as the key * @see InterceptorConfig * @see InterceptorStackConfig */ public Map<String, Object> getAllInterceptorConfigs() { Map<String, Object> retMap = new LinkedHashMap<String, Object>(); if (!parents.isEmpty()) { for (PackageConfig parentContext : parents) { retMap.putAll(parentContext.getAllInterceptorConfigs()); } } retMap.putAll(getInterceptorConfigs()); return retMap; } /** * returns the Map of all the ResultTypeConfigs available in the current package. * ResultTypeConfigs defined in ancestor packages will be included in this Map. * * @return a Map of ResultTypeConfig Objects with the result type name as the key * @see ResultTypeConfig */ public Map<String, ResultTypeConfig> getAllResultTypeConfigs() { Map<String, ResultTypeConfig> retMap = new LinkedHashMap<String, ResultTypeConfig>(); if (!parents.isEmpty()) { for (PackageConfig parentContext : parents) { retMap.putAll(parentContext.getAllResultTypeConfigs()); } } retMap.putAll(getResultTypeConfigs()); return retMap; } /** * returns the List of all the ExceptionMappingConfigs available in the current package. * ExceptionMappingConfigs defined in ancestor packages will be included in this list. * * @return a List of ExceptionMappingConfigs Objects with the result type name as the key * @see ExceptionMappingConfig */ public List<ExceptionMappingConfig> getAllExceptionMappingConfigs() { List<ExceptionMappingConfig> allExceptionMappings = new ArrayList<ExceptionMappingConfig>(); if (!parents.isEmpty()) { for (PackageConfig parentContext : parents) { allExceptionMappings.addAll(parentContext.getAllExceptionMappingConfigs()); } } allExceptionMappings.addAll(getGlobalExceptionMappingConfigs()); return allExceptionMappings; } public String getDefaultInterceptorRef() { return defaultInterceptorRef; } public String getDefaultActionRef() { return defaultActionRef; } public String getDefaultClassRef() { if ((defaultClassRef == null) && !parents.isEmpty()) { for (PackageConfig parent : parents) { String parentDefault = parent.getDefaultClassRef(); if (parentDefault != null) { return parentDefault; } } } return defaultClassRef; } /** * Returns the default result type for this package. */ public String getDefaultResultType() { return defaultResultType; } /** * gets the default interceptor-ref name. If this is not set on this PackageConfig, it searches the parent * PackageConfigs in order until it finds one. */ public String getFullDefaultInterceptorRef() { if ((defaultInterceptorRef == null) && !parents.isEmpty()) { for (PackageConfig parent : parents) { String parentDefault = parent.getFullDefaultInterceptorRef(); if (parentDefault != null) { return parentDefault; } } } return defaultInterceptorRef; } /** * gets the default action-ref name. If this is not set on this PackageConfig, it searches the parent * PackageConfigs in order until it finds one. */ public String getFullDefaultActionRef() { if ((defaultActionRef == null) && !parents.isEmpty()) { for (PackageConfig parent : parents) { String parentDefault = parent.getFullDefaultActionRef(); if (parentDefault != null) { return parentDefault; } } } return defaultActionRef; } /** * Returns the default result type for this package. * <p/> * If there is no default result type, but this package has parents - we will try to * look up the default result type of a parent. */ public String getFullDefaultResultType() { if ((defaultResultType == null) && !parents.isEmpty()) { for (PackageConfig parent : parents) { String parentDefault = parent.getFullDefaultResultType(); if (parentDefault != null) { return parentDefault; } } } return defaultResultType; } /** * gets the global ResultConfigs local to this package * * @return a Map of ResultConfig objects keyed by result name * @see ResultConfig */ public Map<String, ResultConfig> getGlobalResultConfigs() { return globalResultConfigs; } /** * gets the InterceptorConfigs and InterceptorStackConfigs local to this package * * @return a Map of InterceptorConfig and InterceptorStackConfig objects keyed by ref-name * @see InterceptorConfig * @see InterceptorStackConfig */ public Map<String, Object> getInterceptorConfigs() { return interceptorConfigs; } public String getName() { return name; } public String getNamespace() { return namespace; } public List<PackageConfig> getParents() { return new ArrayList<PackageConfig>(parents); } /** * gets the ResultTypeConfigs local to this package * * @return a Map of ResultTypeConfig objects keyed by result name * @see ResultTypeConfig */ public Map<String, ResultTypeConfig> getResultTypeConfigs() { return resultTypeConfigs; } public boolean isNeedsRefresh() { return needsRefresh; } /** * gets the ExceptionMappingConfigs local to this package * * @return a Map of ExceptionMappingConfig objects keyed by result name * @see ExceptionMappingConfig */ public List<ExceptionMappingConfig> getGlobalExceptionMappingConfigs() { return globalExceptionMappingConfigs; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PackageConfig)) { return false; } final PackageConfig packageConfig = (PackageConfig) o; if (isAbstract != packageConfig.isAbstract) { return false; } if ((actionConfigs != null) ? (!actionConfigs.equals(packageConfig.actionConfigs)) : (packageConfig.actionConfigs != null)) { return false; } if ((defaultResultType != null) ? (!defaultResultType.equals(packageConfig.defaultResultType)) : (packageConfig.defaultResultType != null)) { return false; } if ((defaultClassRef != null) ? (!defaultClassRef.equals(packageConfig.defaultClassRef)) : (packageConfig.defaultClassRef != null)) { return false; } if ((globalResultConfigs != null) ? (!globalResultConfigs.equals(packageConfig.globalResultConfigs)) : (packageConfig.globalResultConfigs != null)) { return false; } if ((interceptorConfigs != null) ? (!interceptorConfigs.equals(packageConfig.interceptorConfigs)) : (packageConfig.interceptorConfigs != null)) { return false; } if ((name != null) ? (!name.equals(packageConfig.name)) : (packageConfig.name != null)) { return false; } if ((namespace != null) ? (!namespace.equals(packageConfig.namespace)) : (packageConfig.namespace != null)) { return false; } if ((parents != null) ? (!parents.equals(packageConfig.parents)) : (packageConfig.parents != null)) { return false; } if ((resultTypeConfigs != null) ? (!resultTypeConfigs.equals(packageConfig.resultTypeConfigs)) : (packageConfig.resultTypeConfigs != null)) { return false; } if ((globalExceptionMappingConfigs != null) ? (!globalExceptionMappingConfigs.equals(packageConfig.globalExceptionMappingConfigs)) : (packageConfig.globalExceptionMappingConfigs != null)) { return false; } return true; } @Override public int hashCode() { int result; result = ((name != null) ? name.hashCode() : 0); result = (29 * result) + ((parents != null) ? parents.hashCode() : 0); result = (29 * result) + ((actionConfigs != null) ? actionConfigs.hashCode() : 0); result = (29 * result) + ((globalResultConfigs != null) ? globalResultConfigs.hashCode() : 0); result = (29 * result) + ((interceptorConfigs != null) ? interceptorConfigs.hashCode() : 0); result = (29 * result) + ((resultTypeConfigs != null) ? resultTypeConfigs.hashCode() : 0); result = (29 * result) + ((globalExceptionMappingConfigs != null) ? globalExceptionMappingConfigs.hashCode() : 0); result = (29 * result) + ((defaultResultType != null) ? defaultResultType.hashCode() : 0); result = (29 * result) + ((defaultClassRef != null) ? defaultClassRef.hashCode() : 0); result = (29 * result) + ((namespace != null) ? namespace.hashCode() : 0); result = (29 * result) + (isAbstract ? 1 : 0); return result; } @Override public String toString() { return "PackageConfig: [" + name + "] for namespace [" + namespace + "] with parents [" + parents + "]"; } public int compareTo(Object o) { PackageConfig other = (PackageConfig) o; String full = namespace + "!" + name; String otherFull = other.namespace + "!" + other.name; // note, this isn't perfect (could come from different parents), but it is "good enough" return full.compareTo(otherFull); } public Object getInterceptorConfig(String name) { return getAllInterceptorConfigs().get(name); } /** * The builder for this object. An instance of this object is the only way to construct a new instance. The * purpose is to enforce the immutability of the object. The methods are structured in a way to support chaining. * After setting any values you need, call the {@link #build()} method to create the object. */ public static class Builder implements InterceptorLocator { protected PackageConfig target; private boolean strictDMI; public Builder(String name) { target = new PackageConfig(name); } public Builder(PackageConfig config) { target = new PackageConfig(config); } public Builder name(String name) { target.name = name; return this; } public Builder isAbstract(boolean isAbstract) { target.isAbstract = isAbstract; return this; } public Builder defaultInterceptorRef(String name) { target.defaultInterceptorRef = name; return this; } public Builder defaultActionRef(String name) { target.defaultActionRef = name; return this; } public Builder defaultClassRef(String defaultClassRef) { target.defaultClassRef = defaultClassRef; return this; } /** * sets the default Result type for this package * * @param defaultResultType */ public Builder defaultResultType(String defaultResultType) { target.defaultResultType = defaultResultType; return this; } public Builder namespace(String namespace) { if (namespace == null) { target.namespace = ""; } else { target.namespace = namespace; } return this; } public Builder needsRefresh(boolean needsRefresh) { target.needsRefresh = needsRefresh; return this; } public Builder addActionConfig(String name, ActionConfig action) { target.actionConfigs.put(name, action); return this; } public Builder addParents(List<PackageConfig> parents) { for (PackageConfig config : parents) { addParent(config); } return this; } public Builder addGlobalResultConfig(ResultConfig resultConfig) { target.globalResultConfigs.put(resultConfig.getName(), resultConfig); return this; } public Builder addGlobalResultConfigs(Map<String, ResultConfig> resultConfigs) { target.globalResultConfigs.putAll(resultConfigs); return this; } public Builder addExceptionMappingConfig(ExceptionMappingConfig exceptionMappingConfig) { target.globalExceptionMappingConfigs.add(exceptionMappingConfig); return this; } public Builder addGlobalExceptionMappingConfigs(List<ExceptionMappingConfig> exceptionMappingConfigs) { target.globalExceptionMappingConfigs.addAll(exceptionMappingConfigs); return this; } public Builder addInterceptorConfig(InterceptorConfig config) { target.interceptorConfigs.put(config.getName(), config); return this; } public Builder addInterceptorStackConfig(InterceptorStackConfig config) { target.interceptorConfigs.put(config.getName(), config); return this; } public Builder addParent(PackageConfig parent) { target.parents.add(0, parent); return this; } public Builder addResultTypeConfig(ResultTypeConfig config) { target.resultTypeConfigs.put(config.getName(), config); return this; } public Builder location(Location loc) { target.location = loc; return this; } public boolean isNeedsRefresh() { return target.needsRefresh; } public String getDefaultClassRef() { return target.defaultClassRef; } public String getName() { return target.name; } public String getNamespace() { return target.namespace; } public String getFullDefaultResultType() { return target.getFullDefaultResultType(); } public ResultTypeConfig getResultType(String type) { return target.getAllResultTypeConfigs().get(type); } public Object getInterceptorConfig(String name) { return target.getAllInterceptorConfigs().get(name); } public Builder strictMethodInvocation(boolean strict) { strictDMI = strict; return this; } public boolean isStrictMethodInvocation() { return strictDMI; } public PackageConfig build() { embalmTarget(); PackageConfig result = target; target = new PackageConfig(result); return result; } protected void embalmTarget() { target.actionConfigs = Collections.unmodifiableMap(target.actionConfigs); target.globalResultConfigs = Collections.unmodifiableMap(target.globalResultConfigs); target.interceptorConfigs = Collections.unmodifiableMap(target.interceptorConfigs); target.resultTypeConfigs = Collections.unmodifiableMap(target.resultTypeConfigs); target.globalExceptionMappingConfigs = Collections.unmodifiableList(target.globalExceptionMappingConfigs); target.parents = Collections.unmodifiableList(target.parents); } @Override public String toString() { return "[BUILDER] " + target.toString(); } } }
比方上面这个类,是struts对于package的一个封装,并同一时候在内部提供了一个builder的静态内部类,同一时候将外部类的构造函数声明为protected这样避免外部随便定义对象,并通过内部类中创建一个外部类的对象。
这样的方式非常普遍。
那么总结下来。struts对于同样包,同样命名空间的,同名action,在项目启动的时候是不进行报错提示的,不知道算不算是个bug,这样对于注解方式的项目,假如眼下如今有一个类文件,当中定义了非常多action,这时候直接把该文件复制一下,然后改动名字后,在新文件里加入新的action,原先的action不删除的话,这样就存在两套了。假如仅仅改了一套逻辑。那么问题就会出现了。就会好出现我们这次遇到的问题。改动的代码可能不生效(当然这次我遇到的问题就是这么来的,之前的同事做这块的时候由于新增了一个与原先的页面相似的功能,可是会有额外的逻辑,于是便直接复制粘贴了一份,也没有进行又一次命名。这样便导致了后面的事情的发生,尽管定位到两个重名的action推測会是这个原因,可是详细的道理就非常难找了。仅仅能看源代码了,只是看源代码学会了非常多东西,窃喜),明确了struts用hashset存储之后进行的解析。也就是说明会存在偶然性,于是在自己的机器上实验。不停的重新启动測试重新启动測试重新启动測试,最终发现的确两套逻辑中的代码都可能进入的。到这里也就彻底大悟了。
总结:struts对于同样包,同样命名空间的,同名action。不会报错,而且用注解方式实现的话,详细用哪个类中的action具有偶然性,由于struts是用hashset存储后进行解析的。
相关文章推荐
- struts中action名称重复导致的奇异事件
- struts中action名称重复导致的奇异事件
- jfinal的绝对路径和action请求路径添加文件夹名称而导致"404 not found"的问题
- 快速移动鼠标导致js 的mouseover,mouseout,mouseenter,mouseleave等事件(触发动画)反复叠加的处理
- Struts 2 配置拦截器导致Action POJO 无值处理
- Struts1.2 Action 和ActionForm 导致页面出不来
- Struts 2在Action中获取Spring容器中所有Bean实例名称的方法
- Struts Action 属性初始化 注入Spring DAO导致java.lang.NoClassDefFoundError异常
- 路径名称和struts.xml配置不一致导致struts2报404
- 解决struts验证失败后导致其他Action方法不能执行
- 在struts-config.xml中配置validator-plugin导致404 Servlet action is not available
- Struts 2 项目打包名称不是项目名称之“灵异事件 ”
- spring托管struts,出现javaruntimeexception,导致相应action无效
- 【Struts 2.x】调用addActionError导致异常
- struts拦截所有action的低级错误,继承BASEACTION导致报错
- Struts 传智Struts2笔记(三)Action名称的搜索顺序
- Struts 配置拦截器导致 验证的某某Action-vationdation.xml文件不好使的问题
- 关于struts2种的action运行两次,或多次,或反复运行的bug
- struts.xml不在默认位置,导致出现HTTP Status 404 - There is no Action mapped for namespace [/] and action name [
- (Struts)Action类及其相关类