近期,想将整条产品线的产品代码进行重构,希望将其组件化。这样对于Struts2希望能够在代价很小的前提下实现组件化。首先定义struts-parent.xml配置文件:
<struts>
<package name="framework-default" extends="embeddedjsp-default">
<result-types>
<result-type name="embeddedJspExt" class="com.mycompany.framework.util.EmbeddedJSPResultExt"/>
</result-types>
<interceptors>
<interceptor name="authority" class="com.mycompany.framework.security.LoginInterceptor"/>
<interceptor-stack name="frameworkdefaultStack">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="authority" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="frameworkdefaultStack" />
<global-results>
<result name="info">/info.jsp</result>
<result name="error" type="redirect">/error.jsp</result>
<result name="login" type="redirect">/index.action</result>
</global-results>
</package>
<package name="framework-jar" extends="framework-default">
<result-types>
<result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="false"/>
<result-type name="embeddedJspExt" class="com.mycompany.framework.util.EmbeddedJSPResultExt" default="true"/>
</result-types>
</package>
<package name="framework-codebehind" extends="framework-default">
<result-types>
<result-type name="dispatcher" class="com.mycompany.framework.util.EmbeddedJSPResult4CodeBehind" default="true"/>
</result-types>
</package>
<constant name="struts.codebehind.defaultPackage" value="framework-codebehind"/>
<constant name="struts.configuration.classpath.defaultParentPackage" value="framework-codebehind"/>
<bean type="com.opensymphony.xwork2.UnknownHandler" name="CodebehindUnknownHandler4Jar" class="com.mycompany.framework.util.CodebehindUnknownHandlerExt" />
</struts>
先定义了一个framework-default包,所有的二次开发定制action都将直接隶属于此包。它主要定了一个登录拦截器,和一些公共信息;然后定义了一个framework-jar包,所有的产品action都直接隶属于此包,它也是从framework-default中派生的,主要将默认的result换成了embeddedJspExt,这样默认就是支持JSP IN JAR的;然后又定义了一个framework-codebehind包,此包是不直接使用的,而是赋给了struts.codebehind.defaultPackage和struts.configuration.classpath.defaultParentPackage常量,它将dispatcher修改成了EmbeddedJSPResult4CodeBehind,使得codebehind能够支持JSP IN JAR;最后定义了一个com.opensymphony.xwork2.UnknownHandler类型的Bean CodebehindUnknownHandlerExt,目的是扩展异常处理。
相关代码如下:
public class LoginInterceptor extends AbstractInterceptor {
@Override
public void init() {
super.init();
mapSkipLoginUrlChache.put("/index.action", Boolean.TRUE);
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
boolean bDontNeedLogin = UserUtil.isLogged();
String strContextPath = ServletActionContext.getRequest().getContextPath();
String strActionUri = ServletActionContext.getRequest().getRequestURI();
if(strActionUri!=null && strContextPath!=null && strActionUri.length()>strContextPath.length()) {
strActionUri = strActionUri.substring(strContextPath.length());
}
if(!bDontNeedLogin && !Utility.isEmpty(strActionUri)) {
Boolean bCache = mapSkipLoginUrlChache.get(strActionUri);
if(bCache != null) {
bDontNeedLogin = bCache;
} else if(ApplicationParameter.SKIPLOGINURIPREFIXS != null) {
for(String strSkipLoginUrlPrefix : ApplicationParameter.SKIPLOGINURIPREFIXS) {
if(strActionUri.startsWith(strSkipLoginUrlPrefix)) {
bDontNeedLogin = true;
break;
}
}
mapSkipLoginUrlChache.put(strActionUri, Boolean.valueOf(bDontNeedLogin));
}
}
if(bDontNeedLogin) {
long iStart = System.currentTimeMillis();
String strRet = invocation.invoke();
log.debug("Total Used " + (System.currentTimeMillis()-iStart) + "ms to Solve URI " + strActionUri + ".");
return strRet;
}
log.error("You Should Login before Visit URI " + strActionUri + "!");
invocation.getInvocationContext().put("tip", "尚未登录");
return Action.LOGIN;
}
protected static final Map<String, Boolean> mapSkipLoginUrlChache = new Hashtable<String, Boolean>();
private static final Logger log = LoggerFactory.getLogger(LoginInterceptor.class);
}
public class EmbeddedJSPResult4CodeBehind extends EmbeddedJSPResultExt {
@Override
protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
ServletContext servletContext = ServletActionContext.getServletContext();
if(new File(servletContext.getRealPath(finalLocation)).exists()) {
servletDispatcherResult.doExecute(finalLocation, invocation);
} else {
super.doExecute(finalLocation, invocation);
}
}
}
public class EmbeddedJSPResultExt extends EmbeddedJSPResult {
@Inject
public void setUrlHelper(UrlHelper urlHelper) {
servletDispatcherResult.setUrlHelper(urlHelper);
}
@Override
protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
long iStart = System.currentTimeMillis();
ServletContext servletContext = ServletActionContext.getServletContext();
String strCheckedFinalLocation = finalLocation;
if(!finalLocation.startsWith("/")) {
String strServletPath = ServletActionContext.getRequest().getServletPath();
int iNamespaceLastIndex = strServletPath.lastIndexOf("/");
if(iNamespaceLastIndex > 0) {
strCheckedFinalLocation = strServletPath.substring(0, iNamespaceLastIndex+1) + finalLocation;
}
}
String strClassPath = strBasePathLocation + strClassPathPrefix
+ (strCheckedFinalLocation.charAt(0)=='/' ? strCheckedFinalLocation.substring(1) : strCheckedFinalLocation);
String strRelativePath = strCheckedFinalLocation.toLowerCase().startsWith(strBasePathLocation.toLowerCase())
? strCheckedFinalLocation.substring(strBasePathLocation.length()) : strCheckedFinalLocation;
String strOverridePath = strBasePathLocation + strOverridePathPrefix
+ (strRelativePath.charAt(0)=='/' ? strRelativePath.substring(1) : strRelativePath);
Integer iDispatchKind = mapCache.get(strCheckedFinalLocation);
if(iDispatchKind==null && new File(servletContext.getRealPath(strOverridePath)).exists()
|| iDispatchKind!=null && iDispatchKind==1) {
if(iDispatchKind == null) {
mapCache.put(strCheckedFinalLocation, 1);
}
dispatchResultByMerge(strOverridePath, invocation);
} else if(iDispatchKind==null && new File(servletContext.getRealPath(strClassPath)).exists()
|| iDispatchKind!=null && iDispatchKind==2) {
if(iDispatchKind == null) {
mapCache.put(strCheckedFinalLocation, 2);
}
dispatchResultByMerge(strClassPath, invocation);
} else if(iDispatchKind==null || iDispatchKind==3) {
try {
if(ApplicationParameter.IF_USEPLUGIN) {
super.doExecute(strCheckedFinalLocation, invocation);
} else {
dispatchResultByMerge(strCheckedFinalLocation, invocation);
}
if(iDispatchKind == null) {
mapCache.put(strCheckedFinalLocation, 3);
}
} catch (Exception e) {
mapCache.put(strCheckedFinalLocation, 4);
e.printStackTrace();
prepareFileFromJar(strCheckedFinalLocation);
dispatchResultByMerge(strCheckedFinalLocation, invocation);
}
} else {
dispatchResultByMerge(strCheckedFinalLocation, invocation);
}
log.debug("Total Used " + (System.currentTimeMillis()-iStart) + "ms to Render Jsp " + strCheckedFinalLocation + ".");
}
protected void dispatchResultByMerge(String finalLocation, ActionInvocation invocation) {
boolean bErrorOccur = false;
Boolean bDispatchNeedSync = false;
while (!bErrorOccur) {
try {
servletDispatcherResult.setLocation(getLocation());
servletDispatcherResult.doExecute(finalLocation, invocation);
break;
} catch (Exception e) {
int iStartIndex = e.getMessage().indexOf("File \"") + 6;
int iEndIndex = e.getMessage().indexOf("\" not found");
if(iStartIndex==-1 || iEndIndex==-1) {
if(!bDispatchNeedSync) {
e.printStackTrace();
bErrorOccur = true;
} else {
try {
Thread.sleep(5000);
bDispatchNeedSync = false;
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
} else {
bDispatchNeedSync = true;
String strRealFileName = e.getMessage().substring(iStartIndex, iEndIndex);
String strDest = ServletActionContext.getServletContext().getRealPath(strRealFileName);
File fileDest = new File(strDest);
if(prepareSubPath(strDest)) {
String strRelativePath = strRealFileName.toLowerCase().startsWith(strBasePathLocation.toLowerCase())
? strRealFileName.substring(strBasePathLocation.length()) : strRealFileName;
String strOverrideSrc = ServletActionContext.getServletContext().getRealPath(strBasePathLocation
+ strOverridePathPrefix + (strRelativePath.charAt(0)=='/' ? strRelativePath.substring(1) : strRelativePath));
if(!copyFile(strOverrideSrc, strDest, fileDest)) {
if(!getFileFromJar(strRealFileName, fileDest)) {
String strClassPathSrc = ServletActionContext.getServletContext().getRealPath(strBasePathLocation
+ strClassPathPrefix + (strRealFileName.charAt(0)=='/' ? strRealFileName.substring(1) : strRealFileName));
copyFile(strClassPathSrc, strDest, fileDest);
}
}
} else {
bErrorOccur = true;
}
}
}
}
}
protected boolean copyFile(String strSrc, String strDest, File fileDest) {
File fileSrc = new File(strSrc);
if(fileSrc.exists()) {
FileUtil.copyFile(fileSrc, fileDest);
mapFileNeedSync.put(strSrc, strDest);
synchronized (mapFileNeedSync) {
if(!bSyncStarted) {
bSyncStarted = true;
thdFileSync = new Thread(new Runnable() {
@Override
public void run() {
while(bSyncStarted) {
try {
Thread.sleep(iSyncTick);
for(String strSrc : mapFileNeedSync.keySet()) {
File fileSrc = new File(strSrc);
File fileDest = new File(mapFileNeedSync.get(strSrc));
if(fileSrc.lastModified() > fileDest.lastModified()) {
FileUtil.copyFile(fileSrc, fileDest);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
thdFileSync.start();
}
}
return true;
}
return false;
}
protected void prepareFileFromJar(String finalLocation) {
String strDest = ServletActionContext.getServletContext().getRealPath(finalLocation);
File fileDest = new File(strDest);
if(!fileDest.exists() && prepareSubPath(strDest)) {
getFileFromJar(finalLocation, fileDest);
}
}
protected boolean getFileFromJar(String finalLocation, File fileDest) {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + finalLocation;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
if(resources!=null && resources.length>0) {
InputStream fis = resources[0].getInputStream();
OutputStream fos = new FileOutputStream(fileDest);
byte[] bArrBuffer = new byte[10240];
int iCount = 0;
while ((iCount = fis.read(bArrBuffer, 0, bArrBuffer.length)) != -1) {
fos.write(bArrBuffer, 0, iCount);
}
fis.close();
fos.close();
return true;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return false;
}
protected boolean prepareSubPath(String strDest) {
try {
String strSubPath = strDest.substring(0, strDest.lastIndexOf(File.separator));
FileUtil.creatDirs("", strSubPath);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
protected ServletDispatcherResult servletDispatcherResult = new ServletDispatcherResult();
protected static volatile boolean bSyncStarted = false;
protected static Thread thdFileSync = null;
protected static final int iSyncTick = 1000;
private static final Map<String, Integer> mapCache = new Hashtable<String, Integer>();
private static final Map<String, String> mapFileNeedSync = new Hashtable<String, String>();
private static Logger log = LoggerFactory.getLogger(EmbeddedJSPResultExt.class);
private static final String strOverridePathPrefix = "jsp-override/";
private static final String strBasePathLocation = "/WEB-INF/";
private static final String strClassPathPrefix = "classes/";
}
public class CodebehindUnknownHandlerExt extends CodebehindUnknownHandler {
@Inject
public CodebehindUnknownHandlerExt(@Inject("struts.codebehind.defaultPackage") String defaultPackage,
@Inject Configuration configuration) {
super(defaultPackage, configuration);
}
@Override
public ActionConfig handleUnknownAction(String namespace, String actionName) throws XWorkException {
ActionConfig actionConfig = super.handleUnknownAction(namespace, actionName);
if(actionConfig == null) {
String pathPrefix = determinePath(templatePathPrefix, namespace);
for (String ext : resultsByExtension.keySet()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Trying to locate unknown action template with extension ."+ext+" in directory "+pathPrefix);
}
String path = string(pathPrefix, actionName, "." , ext);
if (isTemplateInJar(path)) {
actionConfig = buildActionConfig(path, namespace, actionName, resultsByExtension.get(ext));
break;
}
}
}
return actionConfig;
}
@Override
public Result handleUnknownResult(ActionContext actionContext, String actionName,
ActionConfig actionConfig, String resultCode) throws XWorkException {
Result result = super.handleUnknownResult(actionContext, actionName, actionConfig, resultCode);
if(result == null) {
PackageConfig pkg = configuration.getPackageConfig(actionConfig.getPackageName());
String ns = pkg.getNamespace();
String pathPrefix = determinePath(templatePathPrefix, ns);
for (String ext : resultsByExtension.keySet()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Trying to locate result with extension ."+ext+" in directory "+pathPrefix);
}
String path = string(pathPrefix, actionName, "-", resultCode, "." , ext);
if (isTemplateInJar(path)) {
result = buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
break;
}
path = string(pathPrefix, actionName, "." , ext);
if (isTemplateInJar(path)) {
result = buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
break;
}
}
}
return result;
}
protected boolean isTemplateInJar(String path) {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + path;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
return resources!=null && resources.length>0;
} catch (IOException e) {
}
return false;
}
}
对于FilterDispacher的扩展:
public class FilterDispatcherForFramework extends FilterDispatcherExt {
// 增强并规范config属性
@Override
protected void solveStrutsConfig(Map<String, String> m) {
if(ApplicationParameter.IF_COPYSTRUTS2XML) {
m.put("config", "classpath:struts-default.xml,classpath:struts2-plugin/**/struts-plugin.xml,classpath:struts-parent.xml"
+ ",classpath:struts2/**/*.xml,classpath:struts2-override/**/*.xml,classpath:struts2-local.xml");
} else {
m.put("config", "struts-default.xml,struts-plugin.xml,struts-parent.xml"
+ ",classpath*:struts2/**/*.xml,classpath*:struts2-override/**/*.xml,classpath*:struts2-local.xml");
}
super.solveStrutsConfig(m);
}
}
public class FilterDispatcherExt extends FilterDispatcher {
@Override
protected Dispatcher createDispatcher(FilterConfig filterConfig) {
Map<String, String> params = new HashMap<String, String>();
for (Enumeration e = filterConfig.getInitParameterNames(); e
.hasMoreElements();) {
String name = (String) e.nextElement();
String value = filterConfig.getInitParameter(name);
params.put(name, value);
}
solveStrutsConfig(params);
return new Dispatcher(filterConfig.getServletContext(), params);
}
// 增强并规范config属性
protected void solveStrutsConfig(Map<String, String> m) {
String strConfigKey = "config";
String[] strSystemXml = new String[]{"struts-default.xml", "struts-plugin.xml", "struts.xml"};
String strOldPath = m.get(strConfigKey);
String strNewPath = null;
if(Utility.isEmpty(strOldPath)) {
strNewPath = chgArray2String(strSystemXml);
} else {
String[] arrPath = strOldPath.split(",");
List<String> lstConfigXml = new LinkedList<String>();
for(String strPath : arrPath) {
lstConfigXml.add(strPath);
}
if(!ApplicationParameter.IF_COPYSTRUTS2XML) {
if(lstConfigXml.indexOf(strSystemXml[0]) == -1) {
lstConfigXml.add(0, strSystemXml[0]);
}
if(lstConfigXml.indexOf(strSystemXml[1]) == -1) {
lstConfigXml.add(1, strSystemXml[1]);
}
}
for(int i=(ApplicationParameter.IF_COPYSTRUTS2XML?0:2); i<lstConfigXml.size();) {
String strConfigXml = lstConfigXml.get(i);
if(strConfigXml.indexOf("*")!=-1 || strConfigXml.indexOf("?")!=-1
|| strConfigXml.indexOf("classpath")!=-1) {
lstConfigXml.remove(i);
scanAllMatchFile(strConfigXml, lstConfigXml);
} else {
++i;
}
}
arrPath = new String[lstConfigXml.size()];
strNewPath = chgArray2String(lstConfigXml.toArray(arrPath));
}
m.put(strConfigKey, strNewPath);
}
private void scanAllMatchFile(String strConfigXml, List<String> lstConfigXml) {
try {
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(strConfigXml);
if(resources != null) {
for(Resource resource : resources) {
String strFullPath = resource.getURL().getPath();
int iIndex = strFullPath.indexOf("!");
if(iIndex != -1) {
if(strFullPath.charAt(iIndex+1) == '/') {
++iIndex;
}
strFullPath = strFullPath.substring(iIndex+1);
}
lstConfigXml.add(strFullPath.toString());
}
}
} catch (IOException e) {
}
}
private String chgArray2String(String[] keys) {
if(keys==null || keys.length==0) {
return null;
}
String strRet = keys[0];
for(int i=1; i<keys.length; ++i) {
strRet += "," + keys[i];
}
return strRet;
}
}
这里需要注意ApplicationParameter.IF_USEPLUGIN和ApplicationParameter.IF_COPYSTRUTS2XML两个常量,前者表示是否使用JSP IN JAR插件,如果不使用则需要在Tomcat的最后一个Lisenter中将JSP从JAR包中拷贝到本地来;后者表示是否拷贝Struts2的配置文件,如果拷贝则需要在Tomcat的最后一个Lisenter中将Struts的配置文件从JAR包中拷贝到本地来。对于stuts2-plugin.xml这一配置文件可能有多个,需要特殊处理。
如果项目使用的是高版本struts,则:
public class FilterConfigAdapter extends FilterDispatcherForFramework implements FilterConfig {
public FilterConfigAdapter (FilterConfig filterConfig) {
this.filterConfig = filterConfig;
this.params = new HashMap<String, String>();
for (Enumeration e = filterConfig.getInitParameterNames(); e
.hasMoreElements();) {
String name = (String) e.nextElement();
String value = filterConfig.getInitParameter(name);
params.put(name, value);
}
solveStrutsConfig(params);
}
@Override
public String getInitParameter(String name) {
String mappedValue = params.get(name);
if(mappedValue != null) {
return mappedValue;
}
return filterConfig.getInitParameter(name);
}
@Override
public Enumeration getInitParameterNames() {
return Collections.enumeration(params.keySet());
}
@Override
public String getFilterName() {
return filterConfig.getFilterName();
}
@Override
public ServletContext getServletContext() {
return filterConfig.getServletContext();
}
private FilterConfig filterConfig;
private Map<String, String> params;
}
public class StrutsPrepareAndExecuteFilterExt extends StrutsPrepareAndExecuteFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
FilterConfig filterConfigAdapter = new FilterConfigAdapter(filterConfig);
super.init(filterConfigAdapter);
}
}
public class StrutsPrepareFilterExt extends StrutsPrepareFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
FilterConfig filterConfigAdapter = new FilterConfigAdapter(filterConfig);
super.init(filterConfigAdapter);
}
}
public class StrutsExecuteFilterExt extends StrutsExecuteFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
FilterConfig filterConfigAdapter = new FilterConfigAdapter(filterConfig);
super.init(filterConfigAdapter);
}
}
后记:如果项目要定制action配置,如增加result,这个需求struts2本来就支持的,只要重新写个action即可覆盖原先的action定义。注意,重新定义的action所在的package名字必须和原来的不同,struts2会忽略同名的package,但会覆盖同名的action,甚是奇怪~
如果想实现自己的一套action配置的覆盖机制,如假定struts2-override中的bean不是覆盖已有的同名bean,而是和它合并,他中没有定义的result添加进去,有的不管或者覆盖,可以从org.apache.struts2.config.DefaultConfiguration中派生一个类,覆盖掉buildRuntimeConfiguration方法,再覆盖常量struts.configuration的值为定义的派生类即可。buildRuntimeConfiguration方法主要是根据packageContexts保存的所有配置文件信息来生成namespaceActionConfigs,namespaceActionConfigs就是全局的struts2配置信息。只要扩展这一机制即可。