最近由于项目需求变更,需要本人对其中的某个业务功能进行修改。本人按照前台页面找action,根据action找代码的逻辑进行了修改(公司项目是ssh框架,struts配置全部是通过注解的方式进行,配置简单方便)。当然测试人员也成功的进行了测试,发现没有任何问题,成功发版。奇葩事情来了,在发版环境中,修改的代码总是没用!
没办法,问题还是要解决,在确认了发版环境的确是最新代码之后,回自己座位找原因。这次我用action名称全局搜索项目工程,尼玛发现两个重名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方法。
1 public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter { 2 protected PrepareOperations prepare; 3 protected ExecuteOperations execute; 4 protected List<Pattern> excludedPatterns = null; 5 6 public void init(FilterConfig filterConfig) throws ServletException { 7 InitOperations init = new InitOperations(); 8 Dispatcher dispatcher = null; 9 try { 10 FilterHostConfig config = new FilterHostConfig(filterConfig); 11 init.initLogging(config); 12 dispatcher = init.initDispatcher(config); 13 init.initStaticContentLoader(config, dispatcher); 14 15 prepare = new PrepareOperations(dispatcher); 16 execute = new ExecuteOperations(dispatcher); 17 this.excludedPatterns = init.buildExcludedPatternsList(dispatcher); 18 19 postInit(dispatcher, filterConfig); 20 } finally { 21 if (dispatcher != null) { 22 dispatcher.cleanUpAfterInit(); 23 } 24 init.cleanup(); 25 } 26 }
看上面源代码会明白,第10行会把web.xml配置的一些基本配置给创建进来,12行通过这些配置创建dispatcher,当然,很多核心的功能都是围绕着dispaatcher进行的,创建这个dispatcher会发生什么呢,我们看initDispatcher的源码
2.下面是关于第12行InitOperations中创建dispatcher的源码
1 public class InitOperations { 2 3 public InitOperations() { 4 } 5 6 /** 7 * Creates and initializes the dispatcher 8 */ 9 public Dispatcher initDispatcher( HostConfig filterConfig ) { 10 Dispatcher dispatcher = createDispatcher(filterConfig); 11 dispatcher.init(); 12 return dispatcher; 13 } 14 }
看上面第10行创建了dispatcher之后,11行立马进行了初始化,那么都干了些什么呢,继续往下深入。
3.下面是关于dispatcher中init方法的实现
1 /** 2 * Load configurations, including both XML and zero-configuration strategies, 3 * and update optional settings, including whether to reload configurations and resource files. 4 */ 5 public void init() { 6 7 if (configurationManager == null) { 8 configurationManager = createConfigurationManager(DefaultBeanSelectionProvider.DEFAULT_BEAN_NAME); 9 } 10 11 try { 12 init_FileManager(); 13 init_DefaultProperties(); // [1] 14 init_TraditionalXmlConfigurations(); // [2] 15 init_LegacyStrutsProperties(); // [3] 16 init_CustomConfigurationProviders(); // [5] 17 init_FilterInitParameters() ; // [6] 18 init_AliasStandardObjects() ; // [7] 19 20 Container container = init_PreloadConfiguration(); 21 container.inject(this); 22 init_CheckWebLogicWorkaround(container); 23 24 if (!dispatcherListeners.isEmpty()) { 25 for (DispatcherListener l : dispatcherListeners) { 26 l.dispatcherInitialized(this); 27 } 28 } 29 errorHandler.init(servletContext); 30 31 } catch (Exception ex) { 32 if (LOG.isErrorEnabled()) 33 LOG.error("Dispatcher initialization failed", ex); 34 throw new StrutsException(ex); 35 } 36 }
从上面的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.
1 private Container init_PreloadConfiguration() { 2 Container container = getContainer(); 3 4 boolean reloadi18n = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_I18N_RELOAD)); 5 LocalizedTextUtil.setReloadBundles(reloadi18n); 6 7 boolean devMode = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_DEVMODE)); 8 LocalizedTextUtil.setDevMode(devMode); 9 10 return container; 11 } 12 13 14 /** 15 * Expose the dependency injection container. 16 * @return Our dependency injection container 17 */ 18 public Container getContainer() { 19 if (ContainerHolder.get() != null) { 20 return ContainerHolder.get(); 21 } 22 ConfigurationManager mgr = getConfigurationManager(); 23 if (mgr == null) { 24 throw new IllegalStateException("The configuration manager shouldn't be null"); 25 } else { 26 Configuration config = mgr.getConfiguration(); 27 if (config == null) { 28 throw new IllegalStateException("Unable to load configuration"); 29 } else { 30 Container container = config.getContainer(); 31 ContainerHolder.store(container); 32 return container; 33 } 34 } 35 }
上面源代码中第2行,首先获取container,当然也是用的ThreadLocal模式,不懂得可以百度,这里不说了,直接说重点22行在获取容器container的过程中,首先创建配置管理器ConfigurationManager,通过配置管理器获取容器Configuration,那么26行究竟干了什么呢,继续往下看。
5.
1 public class ConfigurationManager { 2 3 protected static final Logger LOG = LoggerFactory.getLogger(ConfigurationManager.class); 4 protected Configuration configuration; 5 protected Lock providerLock = new ReentrantLock(); 6 private List<ContainerProvider> containerProviders = new CopyOnWriteArrayList<ContainerProvider>(); 7 private List<PackageProvider> packageProviders = new CopyOnWriteArrayList<PackageProvider>(); 8 protected String defaultFrameworkBeanName; 9 private boolean providersChanged = false; 10 private boolean reloadConfigs = true; // for the first time 11 12 public ConfigurationManager() { 13 this("xwork"); 14 } 15 16 public ConfigurationManager(String name) { 17 this.defaultFrameworkBeanName = name; 18 } 19 20 /** 21 * Get the current XWork configuration object. By default an instance of DefaultConfiguration will be returned 22 * 23 * @see com.opensymphony.xwork2.config.impl.DefaultConfiguration 24 */ 25 public synchronized Configuration getConfiguration() { 26 if (configuration == null) { 27 setConfiguration(createConfiguration(defaultFrameworkBeanName)); 28 try { 29 configuration.reloadContainer(getContainerProviders()); 30 } catch (ConfigurationException e) { 31 setConfiguration(null); 32 throw new ConfigurationException("Unable to load configuration.", e); 33 } 34 } else { 35 conditionalReload(); 36 } 37 38 return configuration; 39 } 40 41 protected Configuration createConfiguration(String beanName) { 42 return new DefaultConfiguration(beanName); 43 }
27行没有configuration先创建一个,创建的是42行的默认配置DefaultConfiguration,然后在29行调用了reloadContainer方法,通过名称应该能明白,意思为重新加载容器,这个方法应该是统一封装的,个人猜测开发修改代码后重新加载不用重启服务应该也是调用的这个方法,这个方法的参数为containerProviders,也就是在3中的那些解析处理类,那么该方法都有什么东东呢,继续。。
6.
1 public class DefaultConfiguration implements Configuration { 2 /** 3 * Calls the ConfigurationProviderFactory.getConfig() to tell it to reload the configuration and then calls 4 * buildRuntimeConfiguration(). 5 * 6 * @throws ConfigurationException 7 */ 8 public synchronized List<PackageProvider> reloadContainer(List<ContainerProvider> providers) throws ConfigurationException { 9 packageContexts.clear(); 10 loadedFileNames.clear(); 11 List<PackageProvider> packageProviders = new ArrayList<PackageProvider>(); 12 13 ContainerProperties props = new ContainerProperties(); 14 ContainerBuilder builder = new ContainerBuilder(); 15 Container bootstrap = createBootstrapContainer(providers); 16 for (final ContainerProvider containerProvider : providers) 17 { 18 bootstrap.inject(containerProvider); 19 containerProvider.init(this); 20 containerProvider.register(builder, props); 21 } 22 props.setConstants(builder); 23 24 builder.factory(Configuration.class, new Factory<Configuration>() { 25 public Configuration create(Context context) throws Exception { 26 return DefaultConfiguration.this; 27 } 28 }); 29 30 ActionContext oldContext = ActionContext.getContext(); 31 try { 32 // Set the bootstrap container for the purposes of factory creation 33 34 setContext(bootstrap); 35 container = builder.create(false); 36 setContext(container); 37 objectFactory = container.getInstance(ObjectFactory.class); 38 39 // Process the configuration providers first 40 for (final ContainerProvider containerProvider : providers) 41 { 42 if (containerProvider instanceof PackageProvider) { 43 container.inject(containerProvider); 44 ((PackageProvider)containerProvider).loadPackages(); 45 packageProviders.add((PackageProvider)containerProvider); 46 } 47 } 48 49 // Then process any package providers from the plugins 50 Set<String> packageProviderNames = container.getInstanceNames(PackageProvider.class); 51 for (String name : packageProviderNames) { 52 PackageProvider provider = container.getInstance(PackageProvider.class, name); 53 provider.init(this); 54 provider.loadPackages(); 55 packageProviders.add(provider); 56 } 57 58 rebuildRuntimeConfiguration(); 59 } finally { 60 if (oldContext == null) { 61 ActionContext.setContext(null); 62 } 63 } 64 return packageProviders; 65 } 66 }
上面这块代码的核心部分为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(); } }
1 public interface ActionConfigBuilder { 2 /** 3 * Builds all the action configurations and stores them into the XWork configuration instance 4 * via XWork dependency injetion. 5 */ 6 void buildActionConfigs(); 7 8 boolean needsReload(); 9 10 void destroy(); 11 }
1 public class PackageBasedActionConfigBuilder implements ActionConfigBuilder { 2 /** 3 * Builds the action configurations by loading all classes in the packages specified by the 4 * property <b>struts.convention.action.packages</b> and then figuring out which classes implement Action 5 * or have Action in their name. Next, if this class is in a Java package that hasn't been 6 * inspected a new PackageConfig (XWork) is created for that Java package using the Java package 7 * name. This will contain all the ActionConfigs for all the Action classes that are discovered 8 * within that Java package. Next, each class is inspected for the {@link ParentPackage} 9 * annotation which is used to control the parent package for a specific action. Lastly, the 10 * {@link ResultMapBuilder} is used to create ResultConfig instances of the action. 11 */ 12 public void buildActionConfigs() { 13 //setup reload class loader based on dev settings 14 initReloadClassLoader(); 15 16 if (!disableActionScanning) { 17 if (actionPackages == null && packageLocators == null) { 18 throw new ConfigurationException("At least a list of action packages or action package locators " + 19 "must be given using one of the properties [struts.convention.action.packages] or " + 20 "[struts.convention.package.locators]"); 21 } 22 23 if (LOG.isTraceEnabled()) { 24 LOG.trace("Loading action configurations"); 25 if (actionPackages != null) 26 LOG.trace("Actions being loaded from action packages " + Arrays.asList(actionPackages)); 27 if (packageLocators != null) 28 LOG.trace("Actions being loaded using package locators " + Arrays.asList(packageLocators)); 29 if (excludePackages != null) 30 LOG.trace("Excluding actions from packages " + Arrays.asList(excludePackages)); 31 } 32 33 Set<Class> classes = findActions(); 34 buildConfiguration(classes); 35 } 36 } 37 38 @SuppressWarnings("unchecked") 39 protected Set<Class> findActions() { 40 Set<Class> classes = new HashSet<Class>(); 41 try { 42 if (actionPackages != null || (packageLocators != null && !disablePackageLocatorsScanning)) { 43 44 // By default, ClassFinder scans EVERY class in the specified 45 // url set, which can produce spurious warnings for non-action 46 // classes that can't be loaded. We pass a package filter that 47 // only considers classes that match the action packages 48 // specified by the user 49 Test<String> classPackageTest = getClassPackageTest(); 50 List<URL> urls = readUrls(); 51 ClassFinder finder = new ClassFinder(getClassLoaderInterface(), urls, EXTRACT_BASE_INTERFACES, fileProtocols, classPackageTest); 52 53 Test<ClassFinder.ClassInfo> test = getActionClassTest(); 54 classes.addAll(finder.findClasses(test)); 55 } 56 } catch (Exception ex) { 57 if (LOG.isErrorEnabled()) 58 LOG.error("Unable to scan named packages", ex); 59 } 60 61 return classes; 62 } 63 64 65 /** 66 * Construct a {@link Test} Object that determines if a specified class 67 * should be included in the package scan based on the full {@link ClassInfo} 68 * of the class. At this point, the class has been loaded, so it's ok to 69 * perform tests such as checking annotations or looking at interfaces or 70 * super-classes of the specified class. 71 * 72 * @return a {@link Test} object that returns true if the specified class 73 * should be included in the package scan 74 */ 75 protected Test<ClassFinder.ClassInfo> getActionClassTest() { 76 return new Test<ClassFinder.ClassInfo>() { 77 public boolean test(ClassFinder.ClassInfo classInfo) { 78 79 // Why do we call includeClassNameInActionScan here, when it's 80 // already been called to in the initial call to ClassFinder? 81 // When some action class passes our package filter in that step, 82 // ClassFinder automatically includes parent classes of that action, 83 // such as com.opensymphony.xwork2.ActionSupport. We repeat the 84 // package filter here to filter out such results. 85 boolean inPackage = includeClassNameInActionScan(classInfo.getName()); 86 boolean nameMatches = classInfo.getName().endsWith(actionSuffix); 87 88 try { 89 return inPackage && (nameMatches || (checkImplementsAction && com.opensymphony.xwork2.Action.class.isAssignableFrom(classInfo.get()))); 90 } catch (ClassNotFoundException ex) { 91 if (LOG.isErrorEnabled()) 92 LOG.error("Unable to load class [#0]", ex, classInfo.getName()); 93 return false; 94 } 95 } 96 }; 97 } 98 @SuppressWarnings("unchecked") 99 protected void buildConfiguration(Set<Class> classes) { 100 Map<String, PackageConfig.Builder> packageConfigs = new HashMap<String, PackageConfig.Builder>(); 101 102 for (Class<?> actionClass : classes) { 103 Actions actionsAnnotation = actionClass.getAnnotation(Actions.class); 104 Action actionAnnotation = actionClass.getAnnotation(Action.class); 105 106 // Skip classes that can't be instantiated 107 if (cannotInstantiate(actionClass)) { 108 if (LOG.isTraceEnabled()) 109 LOG.trace("Class [#0] did not pass the instantiation test and will be ignored", actionClass.getName()); 110 continue; 111 } 112 113 if (eagerLoading) { 114 // Tell the ObjectFactory about this class 115 try { 116 objectFactory.getClassInstance(actionClass.getName()); 117 } catch (ClassNotFoundException e) { 118 if (LOG.isErrorEnabled()) 119 LOG.error("Object Factory was unable to load class [#0]", e, actionClass.getName()); 120 throw new StrutsException("Object Factory was unable to load class " + actionClass.getName(), e); 121 } 122 } 123 124 // Determine the action package 125 String actionPackage = actionClass.getPackage().getName(); 126 if (LOG.isDebugEnabled()) { 127 LOG.debug("Processing class [#0] in package [#1]", actionClass.getName(), actionPackage); 128 } 129 130 // Determine the default namespace and action name 131 List<String> namespaces = determineActionNamespace(actionClass); 132 for (String namespace : namespaces) { 133 String defaultActionName = determineActionName(actionClass); 134 PackageConfig.Builder defaultPackageConfig = getPackageConfig(packageConfigs, namespace, 135 actionPackage, actionClass, null); 136 137 // Verify that the annotations have no errors and also determine if the default action 138 // configuration should still be built or not. 139 Map<String, List<Action>> map = getActionAnnotations(actionClass); 140 Set<String> actionNames = new HashSet<String>(); 141 boolean hasDefaultMethod = ReflectionTools.containsMethod(actionClass, DEFAULT_METHOD); 142 if (!map.containsKey(DEFAULT_METHOD) 143 && hasDefaultMethod 144 && actionAnnotation == null && actionsAnnotation == null 145 && (alwaysMapExecute || map.isEmpty())) { 146 boolean found = false; 147 for (String method : map.keySet()) { 148 List<Action> actions = map.get(method); 149 for (Action action : actions) { 150 151 // Check if there are duplicate action names in the annotations. 152 String actionName = action.value().equals(Action.DEFAULT_VALUE) ? defaultActionName : action.value(); 153 if (actionNames.contains(actionName)) { 154 throw new ConfigurationException("The action class [" + actionClass + 155 "] contains two methods with an action name annotation whose value " + 156 "is the same (they both might be empty as well)."); 157 } else { 158 actionNames.add(actionName); 159 } 160 161 // Check this annotation is the default action 162 if (action.value().equals(Action.DEFAULT_VALUE)) { 163 found = true; 164 } 165 } 166 } 167 168 // Build the default 169 if (!found) { 170 createActionConfig(defaultPackageConfig, actionClass, defaultActionName, DEFAULT_METHOD, null); 171 } 172 } 173 174 // Build the actions for the annotations 175 for (String method : map.keySet()) { 176 List<Action> actions = map.get(method); 177 for (Action action : actions) { 178 PackageConfig.Builder pkgCfg = defaultPackageConfig; 179 if (action.value().contains("/") && !slashesInActionNames) { 180 pkgCfg = getPackageConfig(packageConfigs, namespace, actionPackage, 181 actionClass, action); 182 } 183 184 createActionConfig(pkgCfg, actionClass, defaultActionName, method, action); 185 } 186 } 187 188 // some actions will not have any @Action or a default method, like the rest actions 189 // where the action mapper is the one that finds the right method at runtime 190 if (map.isEmpty() && mapAllMatches && actionAnnotation == null && actionsAnnotation == null) { 191 createActionConfig(defaultPackageConfig, actionClass, defaultActionName, null, actionAnnotation); 192 } 193 194 //if there are @Actions or @Action at the class level, create the mappings for them 195 String methodName = hasDefaultMethod ? DEFAULT_METHOD : null; 196 if (actionsAnnotation != null) { 197 List<Action> actionAnnotations = checkActionsAnnotation(actionsAnnotation); 198 for (Action actionAnnotation2 : actionAnnotations) 199 createActionConfig(defaultPackageConfig, actionClass, defaultActionName, methodName, actionAnnotation2); 200 } else if (actionAnnotation != null) 201 createActionConfig(defaultPackageConfig, actionClass, defaultActionName, methodName, actionAnnotation); 202 } 203 } 204 205 buildIndexActions(packageConfigs); 206 207 // Add the new actions to the configuration 208 Set<String> packageNames = packageConfigs.keySet(); 209 for (String packageName : packageNames) { 210 configuration.addPackageConfig(packageName, packageConfigs.get(packageName).build()); 211 } 212 }
那么我们看上面的代码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行的主要逻辑代码如下
1 /** 2 * Creates a single ActionConfig object. 3 * 4 * @param pkgCfg The package the action configuration instance will belong to. 5 * @param actionClass The action class. 6 * @param actionName The name of the action. 7 * @param actionMethod The method that the annotation was on (if the annotation is not null) or 8 * the default method (execute). 9 * @param annotation The ActionName annotation that might override the action name and possibly 10 */ 11 protected void createActionConfig(PackageConfig.Builder pkgCfg, Class<?> actionClass, String actionName, 12 String actionMethod, Action annotation) { 13 String className = actionClass.getName(); 14 if (annotation != null) { 15 actionName = annotation.value() != null && annotation.value().equals(Action.DEFAULT_VALUE) ? actionName : annotation.value(); 16 actionName = StringUtils.contains(actionName, "/") && !slashesInActionNames ? StringUtils.substringAfterLast(actionName, "/") : actionName; 17 if(!Action.DEFAULT_VALUE.equals(annotation.className())){ 18 className = annotation.className(); 19 } 20 } 21 22 ActionConfig.Builder actionConfig = new ActionConfig.Builder(pkgCfg.getName(), actionName, className); 23 actionConfig.methodName(actionMethod); 24 25 if (LOG.isDebugEnabled()) { 26 LOG.debug("Creating action config for class [#0], name [#1] and package name [#2] in namespace [#3]", 27 actionClass.toString(), actionName, pkgCfg.getName(), pkgCfg.getNamespace()); 28 } 29 30 //build interceptors 31 List<InterceptorMapping> interceptors = interceptorMapBuilder.build(actionClass, pkgCfg, actionName, annotation); 32 actionConfig.addInterceptors(interceptors); 33 34 //build results 35 Map<String, ResultConfig> results = resultMapBuilder.build(actionClass, annotation, actionName, pkgCfg.build()); 36 actionConfig.addResultConfigs(results); 37 38 //add params 39 if (annotation != null) 40 actionConfig.addParams(StringTools.createParameterMap(annotation.params())); 41 42 //add exception mappings from annotation 43 if (annotation != null && annotation.exceptionMappings() != null) 44 actionConfig.addExceptionMappings(buildExceptionMappings(annotation.exceptionMappings(), actionName)); 45 46 //add exception mapping from class 47 ExceptionMappings exceptionMappings = actionClass.getAnnotation(ExceptionMappings.class); 48 if (exceptionMappings != null) 49 actionConfig.addExceptionMappings(buildExceptionMappings(exceptionMappings.value(), actionName)); 50 51 //add 52 pkgCfg.addActionConfig(actionName, actionConfig.build()); 53 54 //check if an action with the same name exists on that package (from XML config probably) 55 PackageConfig existingPkg = configuration.getPackageConfig(pkgCfg.getName()); 56 if (existingPkg != null) { 57 // there is a package already with that name, check action 58 ActionConfig existingActionConfig = existingPkg.getActionConfigs().get(actionName); 59 if (existingActionConfig != null && LOG.isWarnEnabled()) 60 LOG.warn("Duplicated action definition in package [#0] with name [#1].", pkgCfg.getName(), actionName); 61 } 62 63 //watch class file 64 if (isReloadEnabled()) { 65 URL classFile = actionClass.getResource(actionClass.getSimpleName() + ".class"); 66 fileManager.monitorFile(classFile); 67 loadedFileUrls.add(classFile.toString()); 68 } 69 }
上面的52行解释了这里的道理,因为PackageConfig中封装了一个名为actionConfigs的hashmap, protected Map<String, ActionConfig> actionConfigs;,key为action的名字,value为class的名字,这样谁后被访问,后面的class将起到作用。
另外说一点关于struts中很多类似于下面的构造
1 /* 2 * Copyright 2002-2006,2009 The Apache Software Foundation. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.opensymphony.xwork2.config.entities; 17 18 import com.opensymphony.xwork2.util.location.Located; 19 import com.opensymphony.xwork2.util.location.Location; 20 import com.opensymphony.xwork2.util.logging.Logger; 21 import com.opensymphony.xwork2.util.logging.LoggerFactory; 22 23 import java.io.Serializable; 24 import java.util.ArrayList; 25 import java.util.Collections; 26 import java.util.LinkedHashMap; 27 import java.util.List; 28 import java.util.Map; 29 30 31 /** 32 * Configuration for Package. 33 * <p/> 34 * In the xml configuration file this is defined as the <code>package</code> tag. 35 * 36 * @author Rainer Hermanns 37 * @version $Revision$ 38 */ 39 public class PackageConfig extends Located implements Comparable, Serializable, InterceptorLocator { 40 41 private static final Logger LOG = LoggerFactory.getLogger(PackageConfig.class); 42 43 protected Map<String, ActionConfig> actionConfigs; 44 protected Map<String, ResultConfig> globalResultConfigs; 45 protected Map<String, Object> interceptorConfigs; 46 protected Map<String, ResultTypeConfig> resultTypeConfigs; 47 protected List<ExceptionMappingConfig> globalExceptionMappingConfigs; 48 protected List<PackageConfig> parents; 49 protected String defaultInterceptorRef; 50 protected String defaultActionRef; 51 protected String defaultResultType; 52 protected String defaultClassRef; 53 protected String name; 54 protected String namespace = ""; 55 protected boolean isAbstract = false; 56 protected boolean needsRefresh; 57 58 protected PackageConfig(String name) { 59 this.name = name; 60 actionConfigs = new LinkedHashMap<String, ActionConfig>(); 61 globalResultConfigs = new LinkedHashMap<String, ResultConfig>(); 62 interceptorConfigs = new LinkedHashMap<String, Object>(); 63 resultTypeConfigs = new LinkedHashMap<String, ResultTypeConfig>(); 64 globalExceptionMappingConfigs = new ArrayList<ExceptionMappingConfig>(); 65 parents = new ArrayList<PackageConfig>(); 66 } 67 68 protected PackageConfig(PackageConfig orig) { 69 this.defaultInterceptorRef = orig.defaultInterceptorRef; 70 this.defaultActionRef = orig.defaultActionRef; 71 this.defaultResultType = orig.defaultResultType; 72 this.defaultClassRef = orig.defaultClassRef; 73 this.name = orig.name; 74 this.namespace = orig.namespace; 75 this.isAbstract = orig.isAbstract; 76 this.needsRefresh = orig.needsRefresh; 77 this.actionConfigs = new LinkedHashMap<String, ActionConfig>(orig.actionConfigs); 78 this.globalResultConfigs = new LinkedHashMap<String, ResultConfig>(orig.globalResultConfigs); 79 this.interceptorConfigs = new LinkedHashMap<String, Object>(orig.interceptorConfigs); 80 this.resultTypeConfigs = new LinkedHashMap<String, ResultTypeConfig>(orig.resultTypeConfigs); 81 this.globalExceptionMappingConfigs = new ArrayList<ExceptionMappingConfig>(orig.globalExceptionMappingConfigs); 82 this.parents = new ArrayList<PackageConfig>(orig.parents); 83 this.location = orig.location; 84 } 85 86 public boolean isAbstract() { 87 return isAbstract; 88 } 89 90 public Map<String, ActionConfig> getActionConfigs() { 91 return actionConfigs; 92 } 93 94 /** 95 * returns the Map of all the ActionConfigs available in the current package. 96 * ActionConfigs defined in ancestor packages will be included in this Map. 97 * 98 * @return a Map of ActionConfig Objects with the action name as the key 99 * @see ActionConfig 100 */ 101 public Map<String, ActionConfig> getAllActionConfigs() { 102 Map<String, ActionConfig> retMap = new LinkedHashMap<String, ActionConfig>(); 103 104 if (!parents.isEmpty()) { 105 for (PackageConfig parent : parents) { 106 retMap.putAll(parent.getAllActionConfigs()); 107 } 108 } 109 110 retMap.putAll(getActionConfigs()); 111 112 return retMap; 113 } 114 115 /** 116 * returns the Map of all the global ResultConfigs available in the current package. 117 * Global ResultConfigs defined in ancestor packages will be included in this Map. 118 * 119 * @return a Map of Result Objects with the result name as the key 120 * @see ResultConfig 121 */ 122 public Map<String, ResultConfig> getAllGlobalResults() { 123 Map<String, ResultConfig> retMap = new LinkedHashMap<String, ResultConfig>(); 124 125 if (!parents.isEmpty()) { 126 for (PackageConfig parentConfig : parents) { 127 retMap.putAll(parentConfig.getAllGlobalResults()); 128 } 129 } 130 131 retMap.putAll(getGlobalResultConfigs()); 132 133 return retMap; 134 } 135 136 /** 137 * returns the Map of all InterceptorConfigs and InterceptorStackConfigs available in the current package. 138 * InterceptorConfigs defined in ancestor packages will be included in this Map. 139 * 140 * @return a Map of InterceptorConfig and InterceptorStackConfig Objects with the ref-name as the key 141 * @see InterceptorConfig 142 * @see InterceptorStackConfig 143 */ 144 public Map<String, Object> getAllInterceptorConfigs() { 145 Map<String, Object> retMap = new LinkedHashMap<String, Object>(); 146 147 if (!parents.isEmpty()) { 148 for (PackageConfig parentContext : parents) { 149 retMap.putAll(parentContext.getAllInterceptorConfigs()); 150 } 151 } 152 153 retMap.putAll(getInterceptorConfigs()); 154 155 return retMap; 156 } 157 158 /** 159 * returns the Map of all the ResultTypeConfigs available in the current package. 160 * ResultTypeConfigs defined in ancestor packages will be included in this Map. 161 * 162 * @return a Map of ResultTypeConfig Objects with the result type name as the key 163 * @see ResultTypeConfig 164 */ 165 public Map<String, ResultTypeConfig> getAllResultTypeConfigs() { 166 Map<String, ResultTypeConfig> retMap = new LinkedHashMap<String, ResultTypeConfig>(); 167 168 if (!parents.isEmpty()) { 169 for (PackageConfig parentContext : parents) { 170 retMap.putAll(parentContext.getAllResultTypeConfigs()); 171 } 172 } 173 174 retMap.putAll(getResultTypeConfigs()); 175 176 return retMap; 177 } 178 179 /** 180 * returns the List of all the ExceptionMappingConfigs available in the current package. 181 * ExceptionMappingConfigs defined in ancestor packages will be included in this list. 182 * 183 * @return a List of ExceptionMappingConfigs Objects with the result type name as the key 184 * @see ExceptionMappingConfig 185 */ 186 public List<ExceptionMappingConfig> getAllExceptionMappingConfigs() { 187 List<ExceptionMappingConfig> allExceptionMappings = new ArrayList<ExceptionMappingConfig>(); 188 189 if (!parents.isEmpty()) { 190 for (PackageConfig parentContext : parents) { 191 allExceptionMappings.addAll(parentContext.getAllExceptionMappingConfigs()); 192 } 193 } 194 195 allExceptionMappings.addAll(getGlobalExceptionMappingConfigs()); 196 197 return allExceptionMappings; 198 } 199 200 201 public String getDefaultInterceptorRef() { 202 return defaultInterceptorRef; 203 } 204 205 public String getDefaultActionRef() { 206 return defaultActionRef; 207 } 208 209 public String getDefaultClassRef() { 210 if ((defaultClassRef == null) && !parents.isEmpty()) { 211 for (PackageConfig parent : parents) { 212 String parentDefault = parent.getDefaultClassRef(); 213 if (parentDefault != null) { 214 return parentDefault; 215 } 216 } 217 } 218 return defaultClassRef; 219 } 220 221 /** 222 * Returns the default result type for this package. 223 */ 224 public String getDefaultResultType() { 225 return defaultResultType; 226 } 227 228 /** 229 * gets the default interceptor-ref name. If this is not set on this PackageConfig, it searches the parent 230 * PackageConfigs in order until it finds one. 231 */ 232 public String getFullDefaultInterceptorRef() { 233 if ((defaultInterceptorRef == null) && !parents.isEmpty()) { 234 for (PackageConfig parent : parents) { 235 String parentDefault = parent.getFullDefaultInterceptorRef(); 236 237 if (parentDefault != null) { 238 return parentDefault; 239 } 240 } 241 } 242 243 return defaultInterceptorRef; 244 } 245 246 /** 247 * gets the default action-ref name. If this is not set on this PackageConfig, it searches the parent 248 * PackageConfigs in order until it finds one. 249 */ 250 public String getFullDefaultActionRef() { 251 if ((defaultActionRef == null) && !parents.isEmpty()) { 252 for (PackageConfig parent : parents) { 253 String parentDefault = parent.getFullDefaultActionRef(); 254 255 if (parentDefault != null) { 256 return parentDefault; 257 } 258 } 259 } 260 return defaultActionRef; 261 } 262 263 /** 264 * Returns the default result type for this package. 265 * <p/> 266 * If there is no default result type, but this package has parents - we will try to 267 * look up the default result type of a parent. 268 */ 269 public String getFullDefaultResultType() { 270 if ((defaultResultType == null) && !parents.isEmpty()) { 271 for (PackageConfig parent : parents) { 272 String parentDefault = parent.getFullDefaultResultType(); 273 274 if (parentDefault != null) { 275 return parentDefault; 276 } 277 } 278 } 279 280 return defaultResultType; 281 } 282 283 /** 284 * gets the global ResultConfigs local to this package 285 * 286 * @return a Map of ResultConfig objects keyed by result name 287 * @see ResultConfig 288 */ 289 public Map<String, ResultConfig> getGlobalResultConfigs() { 290 return globalResultConfigs; 291 } 292 293 /** 294 * gets the InterceptorConfigs and InterceptorStackConfigs local to this package 295 * 296 * @return a Map of InterceptorConfig and InterceptorStackConfig objects keyed by ref-name 297 * @see InterceptorConfig 298 * @see InterceptorStackConfig 299 */ 300 public Map<String, Object> getInterceptorConfigs() { 301 return interceptorConfigs; 302 } 303 304 public String getName() { 305 return name; 306 } 307 308 public String getNamespace() { 309 return namespace; 310 } 311 312 public List<PackageConfig> getParents() { 313 return new ArrayList<PackageConfig>(parents); 314 } 315 316 /** 317 * gets the ResultTypeConfigs local to this package 318 * 319 * @return a Map of ResultTypeConfig objects keyed by result name 320 * @see ResultTypeConfig 321 */ 322 public Map<String, ResultTypeConfig> getResultTypeConfigs() { 323 return resultTypeConfigs; 324 } 325 326 327 public boolean isNeedsRefresh() { 328 return needsRefresh; 329 } 330 331 /** 332 * gets the ExceptionMappingConfigs local to this package 333 * 334 * @return a Map of ExceptionMappingConfig objects keyed by result name 335 * @see ExceptionMappingConfig 336 */ 337 public List<ExceptionMappingConfig> getGlobalExceptionMappingConfigs() { 338 return globalExceptionMappingConfigs; 339 } 340 341 @Override 342 public boolean equals(Object o) { 343 if (this == o) { 344 return true; 345 } 346 347 if (!(o instanceof PackageConfig)) { 348 return false; 349 } 350 351 final PackageConfig packageConfig = (PackageConfig) o; 352 353 if (isAbstract != packageConfig.isAbstract) { 354 return false; 355 } 356 357 if ((actionConfigs != null) ? (!actionConfigs.equals(packageConfig.actionConfigs)) : (packageConfig.actionConfigs != null)) { 358 return false; 359 } 360 361 if ((defaultResultType != null) ? (!defaultResultType.equals(packageConfig.defaultResultType)) : (packageConfig.defaultResultType != null)) { 362 return false; 363 } 364 365 if ((defaultClassRef != null) ? (!defaultClassRef.equals(packageConfig.defaultClassRef)) : (packageConfig.defaultClassRef != null)) { 366 return false; 367 } 368 369 if ((globalResultConfigs != null) ? (!globalResultConfigs.equals(packageConfig.globalResultConfigs)) : (packageConfig.globalResultConfigs != null)) { 370 return false; 371 } 372 373 if ((interceptorConfigs != null) ? (!interceptorConfigs.equals(packageConfig.interceptorConfigs)) : (packageConfig.interceptorConfigs != null)) { 374 return false; 375 } 376 377 if ((name != null) ? (!name.equals(packageConfig.name)) : (packageConfig.name != null)) { 378 return false; 379 } 380 381 if ((namespace != null) ? (!namespace.equals(packageConfig.namespace)) : (packageConfig.namespace != null)) { 382 return false; 383 } 384 385 if ((parents != null) ? (!parents.equals(packageConfig.parents)) : (packageConfig.parents != null)) { 386 return false; 387 } 388 389 if ((resultTypeConfigs != null) ? (!resultTypeConfigs.equals(packageConfig.resultTypeConfigs)) : (packageConfig.resultTypeConfigs != null)) { 390 return false; 391 } 392 393 if ((globalExceptionMappingConfigs != null) ? (!globalExceptionMappingConfigs.equals(packageConfig.globalExceptionMappingConfigs)) : (packageConfig.globalExceptionMappingConfigs != null)) { 394 return false; 395 } 396 397 return true; 398 } 399 400 @Override 401 public int hashCode() { 402 int result; 403 result = ((name != null) ? name.hashCode() : 0); 404 result = (29 * result) + ((parents != null) ? parents.hashCode() : 0); 405 result = (29 * result) + ((actionConfigs != null) ? actionConfigs.hashCode() : 0); 406 result = (29 * result) + ((globalResultConfigs != null) ? globalResultConfigs.hashCode() : 0); 407 result = (29 * result) + ((interceptorConfigs != null) ? interceptorConfigs.hashCode() : 0); 408 result = (29 * result) + ((resultTypeConfigs != null) ? resultTypeConfigs.hashCode() : 0); 409 result = (29 * result) + ((globalExceptionMappingConfigs != null) ? globalExceptionMappingConfigs.hashCode() : 0); 410 result = (29 * result) + ((defaultResultType != null) ? defaultResultType.hashCode() : 0); 411 result = (29 * result) + ((defaultClassRef != null) ? defaultClassRef.hashCode() : 0); 412 result = (29 * result) + ((namespace != null) ? namespace.hashCode() : 0); 413 result = (29 * result) + (isAbstract ? 1 : 0); 414 415 return result; 416 } 417 418 @Override 419 public String toString() { 420 return "PackageConfig: [" + name + "] for namespace [" + namespace + "] with parents [" + parents + "]"; 421 } 422 423 public int compareTo(Object o) { 424 PackageConfig other = (PackageConfig) o; 425 String full = namespace + "!" + name; 426 String otherFull = other.namespace + "!" + other.name; 427 428 // note, this isn't perfect (could come from different parents), but it is "good enough" 429 return full.compareTo(otherFull); 430 } 431 432 public Object getInterceptorConfig(String name) { 433 return getAllInterceptorConfigs().get(name); 434 } 435 436 /** 437 * The builder for this object. An instance of this object is the only way to construct a new instance. The 438 * purpose is to enforce the immutability of the object. The methods are structured in a way to support chaining. 439 * After setting any values you need, call the {@link #build()} method to create the object. 440 */ 441 public static class Builder implements InterceptorLocator { 442 443 protected PackageConfig target; 444 private boolean strictDMI; 445 446 public Builder(String name) { 447 target = new PackageConfig(name); 448 } 449 450 public Builder(PackageConfig config) { 451 target = new PackageConfig(config); 452 } 453 454 public Builder name(String name) { 455 target.name = name; 456 return this; 457 } 458 459 public Builder isAbstract(boolean isAbstract) { 460 target.isAbstract = isAbstract; 461 return this; 462 } 463 464 public Builder defaultInterceptorRef(String name) { 465 target.defaultInterceptorRef = name; 466 return this; 467 } 468 469 public Builder defaultActionRef(String name) { 470 target.defaultActionRef = name; 471 return this; 472 } 473 474 public Builder defaultClassRef(String defaultClassRef) { 475 target.defaultClassRef = defaultClassRef; 476 return this; 477 } 478 479 /** 480 * sets the default Result type for this package 481 * 482 * @param defaultResultType 483 */ 484 public Builder defaultResultType(String defaultResultType) { 485 target.defaultResultType = defaultResultType; 486 return this; 487 } 488 489 public Builder namespace(String namespace) { 490 if (namespace == null) { 491 target.namespace = ""; 492 } else { 493 target.namespace = namespace; 494 } 495 return this; 496 } 497 498 public Builder needsRefresh(boolean needsRefresh) { 499 target.needsRefresh = needsRefresh; 500 return this; 501 } 502 503 public Builder addActionConfig(String name, ActionConfig action) { 504 target.actionConfigs.put(name, action); 505 return this; 506 } 507 508 public Builder addParents(List<PackageConfig> parents) { 509 for (PackageConfig config : parents) { 510 addParent(config); 511 } 512 return this; 513 } 514 515 public Builder addGlobalResultConfig(ResultConfig resultConfig) { 516 target.globalResultConfigs.put(resultConfig.getName(), resultConfig); 517 return this; 518 } 519 520 public Builder addGlobalResultConfigs(Map<String, ResultConfig> resultConfigs) { 521 target.globalResultConfigs.putAll(resultConfigs); 522 return this; 523 } 524 525 public Builder addExceptionMappingConfig(ExceptionMappingConfig exceptionMappingConfig) { 526 target.globalExceptionMappingConfigs.add(exceptionMappingConfig); 527 return this; 528 } 529 530 public Builder addGlobalExceptionMappingConfigs(List<ExceptionMappingConfig> exceptionMappingConfigs) { 531 target.globalExceptionMappingConfigs.addAll(exceptionMappingConfigs); 532 return this; 533 } 534 535 public Builder addInterceptorConfig(InterceptorConfig config) { 536 target.interceptorConfigs.put(config.getName(), config); 537 return this; 538 } 539 540 public Builder addInterceptorStackConfig(InterceptorStackConfig config) { 541 target.interceptorConfigs.put(config.getName(), config); 542 return this; 543 } 544 545 public Builder addParent(PackageConfig parent) { 546 target.parents.add(0, parent); 547 return this; 548 } 549 550 public Builder addResultTypeConfig(ResultTypeConfig config) { 551 target.resultTypeConfigs.put(config.getName(), config); 552 return this; 553 } 554 555 public Builder location(Location loc) { 556 target.location = loc; 557 return this; 558 } 559 560 public boolean isNeedsRefresh() { 561 return target.needsRefresh; 562 } 563 564 public String getDefaultClassRef() { 565 return target.defaultClassRef; 566 } 567 568 public String getName() { 569 return target.name; 570 } 571 572 public String getNamespace() { 573 return target.namespace; 574 } 575 576 public String getFullDefaultResultType() { 577 return target.getFullDefaultResultType(); 578 } 579 580 public ResultTypeConfig getResultType(String type) { 581 return target.getAllResultTypeConfigs().get(type); 582 } 583 584 public Object getInterceptorConfig(String name) { 585 return target.getAllInterceptorConfigs().get(name); 586 } 587 588 public Builder strictMethodInvocation(boolean strict) { 589 strictDMI = strict; 590 return this; 591 } 592 593 public boolean isStrictMethodInvocation() { 594 return strictDMI; 595 } 596 597 public PackageConfig build() { 598 embalmTarget(); 599 PackageConfig result = target; 600 target = new PackageConfig(result); 601 return result; 602 } 603 604 protected void embalmTarget() { 605 target.actionConfigs = Collections.unmodifiableMap(target.actionConfigs); 606 target.globalResultConfigs = Collections.unmodifiableMap(target.globalResultConfigs); 607 target.interceptorConfigs = Collections.unmodifiableMap(target.interceptorConfigs); 608 target.resultTypeConfigs = Collections.unmodifiableMap(target.resultTypeConfigs); 609 target.globalExceptionMappingConfigs = Collections.unmodifiableList(target.globalExceptionMappingConfigs); 610 target.parents = Collections.unmodifiableList(target.parents); 611 } 612 613 @Override 614 public String toString() { 615 return "[BUILDER] " + target.toString(); 616 } 617 } 618 619 }
比如上面这个类,是struts对于package的一个封装,并同时在内部提供了一个builder的静态内部类,同时将外部类的构造函数声明为protected这样避免外部随便定义对象,并通过内部类中创建一个外部类的对象。这种方式很普遍。
那么总结下来,struts对于相同包,相同命名空间的,同名action,在项目启动的时候是不进行报错提示的,不知道算不算是个bug,这样对于注解方式的项目,假如目前现在有一个类文件,其中定义了很多action,这时候直接把该文件复制一下,然后修改名字后,在新文件中添加新的action,原先的action不删除的话,这样就存在两套了,假如只改了一套逻辑,那么问题就会出现了,就会好出现我们这次遇到的问题,修改的代码可能不生效(当然这次我遇到的问题就是这么来的,之前的同事做这块的时候由于新增了一个与原先的页面类似的功能,但是会有额外的逻辑,于是便直接复制粘贴了一份,也没有进行重新命名,这样便导致了后面的事情的发生,虽然定位到两个重名的action猜测会是这个原因,但是具体的道理就很难找了,只能看源码了,不过看源码学会了很多东西,窃喜),明白了struts用hashset存储之后进行的解析,也就是说明会存在偶然性,于是在自己的机器上实验,不停的重启测试重启测试重启测试,终于发现的确两套逻辑中的代码都可能进入的。到这里也就彻底大悟了。
总结:struts对于相同包,相同命名空间的,同名action,不会报错,并且用注解方式实现的话,具体用哪个类中的action具有偶然性,因为struts是用hashset存储后进行解析的。