把系统从Struts2 迁移到 Spring MVC六大步总结

基于struts的系统迁移到SpringMVC架构上来,共分六部曲,让系统一部一部迁移过来,本文讲的知识点以Struts2 to Spring4,但是针对其他应用场景也是可以参考的。

Step 1: 替换基本的框架库。

Firstly while migrating from struts to spring we have to replace our struts related libraries with spring libraries in lib folder.

I have mentioned basic libraries of both struts and spring for your clarification.

Struts basic libraries :
  1. struts.jar
  2. struts-legacy.jar
  3. etc.. 

Have you ever use :   Javadoc comment in Java

Spring basic libraries :

  1. standard.jar
  2. org.springframework.asm-4.0.1.RELEASE-A.jar
  3. org.springframework.beans-4.0.1.RELEASE-A.jar
  4. org.springframework.context-4.0.1.RELEASE-A.jar
  5. org.springframework.core-4.0.1.RELEASE-A.jar
  6. org.springframework.expression-4.0.1.RELEASE-A.jar
  7. org.springframework.web.servlet-4.0.1.RELEASE-A.jar
  8. org.springframework.web-4.0.1.RELEASE-A.jar
  9. etc..

Step 2: 修改web.xml配置文件

In this step we have to remove Action filter dispatcher for the web.xml and add Spring dipatcher servlet as Front controller
Work on new technology  :  Create and manage cloud applications using Java

In Strut application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>Struts2MyFirstApp</display-name>  
  <filter>  
        <filter-name>struts2</filter-name>  
        <filter-class>  
            org.apache.struts2.dispatcher.FilterDispatcher  
        </filter-class>  
    </filter>  

<filter-mapping>  
        <filter-name>struts2</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <welcome-file-list>  
        <welcome-file>Login.jsp</welcome-file>  
    </welcome-file-list>  
</web-app>
In Spring application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>dispatcher</display-name>  
  <servlet>  
    <servlet-name>dispatcher</servlet-name>  
    <servlet-class>  
            org.springframework.web.servlet.DispatcherServlet  
        </servlet-class>  
    <load-on-startup>1</load-on-startup>  
  </servlet>  
  <servlet-mapping>  
    <servlet-name>dispatcher</servlet-name>  
    <url-pattern>/</url-pattern>  
  </servlet-mapping>  
</web-app>

Step 3: 替换Struts本身的配置文件

Now replace all struts configuration files to spring configuration file as follows

In Struts applivation struts configuration file-
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE struts PUBLIC  
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  
    "http://struts.apache.org/dtds/struts-2.0.dtd">  
   
<struts>  
    <constant name="struts.enable.DynamicMethodInvocation" value="false" />  
    <constant name="struts.devMode" value="false" />  
    <constant name="struts.custom.i18n.resources" value="myapp" />  
   
    <package name="default" extends="struts-default" namespace="/">  
        <action name="login" class="com.geekonjavaonjava.struts2.login.LoginAction">  
            <result name="success">Welcome.jsp</result>  
            <result name="error">Login.jsp</result>  
        </action>  
    </package>  
</struts>  
In Spring application spring configuration file as follows
<?xml version="1.0" encoding="UTF-8"?>    
<beans xmlns="http://www.springframework.org/schema/beans"    
     xmlns:context="http://www.springframework.org/schema/context"    
     xmlns:p="http://www.springframework.org/schema/p"      
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
     xsi:schemaLocation="http://www.springframework.org/schema/beans    
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd    
    http://www.springframework.org/schema/context    
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">    
        
     <context:component-scan base-package="com.geekonjavaonjava.spring.login.controller" />    
        
     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
        <property name="prefix">    
          <value>/WEB-INF/views/</value>    
        </property>    
        <property name="suffix">    
          <value>.jsp</value>    
        </property>    
      </bean>    
</beans>

Here, <context:component-scan> tag is used, so that spring will load all the components from given package i.e. " com.geekonjavaonjava.spring.login.controller".

Use this in Struts2 : Get value of struts property tag into jsp variable

We can use different view resolver, here I have used InternalResourceViewResolver. In which prefix and suffix are used to resolve the view by prefixing and suffixing values to the object returned by ModelAndView in action class.

Step 4: 修改JSP文件

While migration an application from struts to spring we need to change in jsp file as following

Firstly replace all tlds-
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>  
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>  
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %>  
<%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
Replace these with following spring taglib's :
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>  
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
In Struts :
<html:form action="/addLogin" method="post">
In Spring :
<form:form method="POST" commandName="loginForm" name="loginForm" action="login.do"> 

Here commandName is going to map with corresponding formbean for that jsp. Next we will see, how action is getting called with spring 4 annotations.

Step 5: 修改Action 类文件

Now following changes need to be done in action classes for struts to spring migration using annotations-

Struts Action:
package com.geekonjavaonjava.struts2.login;  
  
import com.opensymphony.xwork2.ActionSupport;  
  
/** 
 * @author geekonjava 
 * 
 */  
@SuppressWarnings("serial")  
public class LoginAction  extends ActionSupport{  
 private String username;  
    private String password;  
      
 public String execute() {  
     
        if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            return "success";  
        } else {  
         addActionError(getText("error.login"));  
            return "error";  
        }  
    }  
  
 public String getUsername() {  
  return username;  
 }  
  
 public void setUsername(String username) {  
  this.username = username;  
 }  
  
 public String getPassword() {  
  return password;  
 }  
  
 public void setPassword(String password) {  
  this.password = password;  
 }  
   
}
Spring action
package com.geekonjavaonjava.spring.login.controller;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.ModelMap;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
  
/** 
 * @author GeekOnJava 
 * 
 */  
@Controller  
public class LoginController {  
   
 @RequestMapping(value="/login.do", method = RequestMethod.GET)  
 public String doLogin(ModelMap model, LoginForm loginForn) {  
 if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            model.addAttribute("message", "Login Success");  
        } else {  
            model.addAttribute("message", "Login Failure");  
        }  
         return "home";  
   
 }  
}

Step 6: 修改前端验证机制

In struts JSP file validation changes as follows
<% ActionErrors actionErrors = (ActionErrors)request.getAttribute("org.apache.struts.action.ERROR"); %>
In Spring JSP file as follows-
<form:errors path="*" cssClass="error" /> 

参考链接


macOS Catalina(10.15.4)/IntelliJ IDEA 2018.3/Tomcat 9.0.33/Maven项目调试报错"Caused by: java.util.zip.ZipException: zip file is empty"

macOS Catalina(10.15.4)/IntelliJ IDEA 2018.3/Tomcat 9.0.33/Maven项目调试时报错,这个项目以前是可以正常调试的,一段时间之后,就不能正常调试之下了

继续阅读macOS Catalina(10.15.4)/IntelliJ IDEA 2018.3/Tomcat 9.0.33/Maven项目调试报错"Caused by: java.util.zip.ZipException: zip file is empty"

struts2升级之后报错“java.lang.NoSuchMethodError: org.apache.commons.lang3.reflect.MethodUtils.getAnnotation”

struts2 从如下版本:

<struts2.version>2.5.10.1</struts2.version>
<apache.commons.version>3.5</apache.commons.version>

升级到

<struts2.version>2.5.20</struts2.version>

之后报错如下:

四月 13, 2019 10:07:59 上午 org.apache.catalina.core.StandardWrapperValve invoke
严重: Servlet.service() for servlet [default] in context with path [] threw exception [Filter execution threw an exception] with root cause
java.lang.NoSuchMethodError: org.apache.commons.lang3.reflect.MethodUtils.getAnnotation(Ljava/lang/reflect/Method;Ljava/lang/Class;ZZ)Ljava/lang/annotation/Annotation;
	at org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor.doIntercept(AnnotationValidationInterceptor.java:44)
	at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:99)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor.doIntercept(ConversionErrorInterceptor.java:142)
	at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:99)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:137)
	at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:99)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:137)
	at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:99)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:201)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.MultiselectInterceptor.intercept(MultiselectInterceptor.java:67)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.DateTextFieldInterceptor.intercept(DateTextFieldInterceptor.java:133)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:89)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:243)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:101)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:142)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:160)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:175)
	at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:99)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:121)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:167)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:203)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:196)
	at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:249)
	at org.apache.struts2.factory.StrutsActionProxy.execute(StrutsActionProxy.java:48)
	at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:574)
	at org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:79)
	at org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:141)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:494)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
	at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:1025)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445)
	at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1137)
	at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
	at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:317)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:745)

解决方法为升级

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>${apache.commons.version}</version>
</dependency>

到更高的版本,如下:

<struts2.version>2.5.20</struts2.version>
<apache.commons.version>3.8.1</apache.commons.version>

参考链接


java.lang.NoSuchMethodError: org.apache.commons.lang3.math.NumberUtils.isCreatable(Ljava/lang/String

CVE-2017-5638:Struts 2 远程代码执行漏洞

背景介绍

Struts2的使用范围及其广泛,国内外均有大量厂商使用该框架。

Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2Struts的下一代产品,是在struts 1WebWork的技术基础上进行了合并的全新的Struts 2框架。

继续阅读CVE-2017-5638:Struts 2 远程代码执行漏洞

升级Struts2之后报告HTTP Status 500 - java.lang.ClassNotFoundException: org.apache.jsp.index_jsp以及org.apache.jasper.JasperException: Unable to compile class for JSP

升级Struts22.3.20.1版本升级到2.5.5版本后可能报告如下错误:

HTTP Status 500 - Unable to compile class for JSP:

type Exception report

message Unable to compile class for JSP:

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.apache.jasper.JasperException: Unable to compile class for JSP:

An error occurred at line: [38] in the generated java file: [/var/lib/tomcat7/work/Catalina/localhost/Tools/org/apache/jsp/index_jsp.java]
The method getJspApplicationContext(ServletContext) is undefined for the type JspFactory

Stacktrace:
	org.apache.jasper.compiler.DefaultErrorHandler.javacError(DefaultErrorHandler.java:103)
	org.apache.jasper.compiler.ErrorDispatcher.javacError(ErrorDispatcher.java:366)
	org.apache.jasper.compiler.JDTCompiler.generateClass(JDTCompiler.java:468)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:378)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:353)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:340)
	org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:657)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:357)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
	org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:110)
note The full stack trace of the root cause is available in the Apache Tomcat/7.0.52 (Ubuntu) logs.

也有可能发生如下错误信息:

HTTP Status 500 - java.lang.ClassNotFoundException: org.apache.jsp.index_jsp

具体信息如下图:

1422265899_66497

比较诡异的是,在Tomcat 8的环境下,是可以正常运行的,但是在Tomcat 7环境下却会报错。造成这个现象的原因就是在引入的Jar包中包含了jsp-api.jar这个Jar包,只要在最后生成的war包中排除这个文件即可。

Struts2从2.3.20.1升级到2.5.5版本后报错:ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter

以前项目一直使用Struts22.3.20.1版本,这个版本是IntelliJ Idea新建项目的时候默认指定的版本,但是这个版本存在漏洞,必须进行升级,干脆一不做二不休,直接升级到最新的2.5.5版本,但是运行的时候报告如下错误信息:

严重: Exception starting filter struts2
java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1892)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1735)
	at org.apache.catalina.core.DefaultInstanceManager.loadClass(DefaultInstanceManager.java:504)
	at org.apache.catalina.core.DefaultInstanceManager.loadClassMaybePrivileged(DefaultInstanceManager.java:486)
	at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:113)
	at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:258)
	at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:105)
	at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4958)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5652)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:145)
	at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:899)
	at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:875)
	at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:652)
	at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1863)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:301)
	at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
	at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
	at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:618)
	at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:565)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:301)
	at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
	at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
	at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1487)
	at javax.management.remote.rmi.RMIConnectionImpl.access$300(RMIConnectionImpl.java:97)
	at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1328)
	at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1420)
	at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:848)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:322)
	at sun.rmi.transport.Transport$2.run(Transport.java:202)
	at sun.rmi.transport.Transport$2.run(Transport.java:199)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:745)

分析Struts2-2.5.5的源代码发现

org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter

被更改了目录,变成了

org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter

只要如此修改即可。

IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE,sockjs-1.1.1,stomp-1.2搭建基于Tomcat-7.0.68的WebSocket应用

接着上文IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE搭建基于Tomcat-7.0.68的WebSocket应用

上文的最后我们说到,WebSocket是需要定时心跳的,否则会在一段时间后自动断开连接,而更重要的是,不是所有的浏览器都支持WebSocket,早期的IE 10之前的版本就是不支持的,而这一部分的设备其实是不算少的,而sockjs的出现,恰恰好来解决了这个问题。对于不支持WebSocket的浏览器,sockjs使用了多种方式来兼容这种情况,包括使用长轮询等方式,Spring更是内建支持这种方式。

下面我们看如何在上篇文章的基础上,增加对于sockjs的支持。

首先是STOMP的文档官网地址 http://stomp.github.io/
代码的地址为https://github.com/jmesnil/stomp-websocket,项目下面的lib/stomp.js就是我们想要的文件。也可以本站下载stomp.js.zip

接下来sockjs的代码地址https://github.com/sockjs/sockjs-client,项目下面的dist/sockjs-1.1.1.js就是我们想要的文件。也可以本站下载sockjs-1.1.1.js.zip

接下来我们把下载到的文件放到我们工程目录下面的web->resources->javascript目录下面,如下图:

stomp-websockjs-resources

接下来,添加我们需要的com.fasterxml.jackson.core:jackson-annotations:2.8.1,com.fasterxml.jackson.core:jackson-core:2.8.1,com.fasterxml.jackson.core:jackson-databind:2.8.1这三个jar包,增加的方式参照上一篇中对于javax.servlet:javax.servlet-api:3.1.0的操作方法。与上一篇的操作不同的是,这次添加的三个jar包,都要放到编译完成后的War包中。最后的结果如下图:
ToolsJacksonMaven

ToolsJacksonMavenWar

下面,我们开始进行代码的操作,我们在上篇文章中的src->Tools->WebSocket中新增两个源代码文件SockJsController.java,WebJsSocketConfig.java.如下图:

NewJavaSourcesForSockjs

其中的代码如下:
SockJsController.java

package Tools.WebSocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class SockJsController {

        @MessageMapping("/hello")
        @SendTo("/hello/subscribe") /*貌似这个名字可以随意的,主要用在stomp.subscribe时候的名字*/
        public String Hello(String message) throws Exception {
            return new String("Hello");
        }
}

WebJsSocketConfig.java

package Tools.WebSocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration
@EnableWebSocketMessageBroker
public class WebJsSocketConfig  extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/webSocketServer");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();
    }
}

然后修改WebSocket.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
    <style type="text/css">
        #connect-container {
            float: left;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }

        #console p {
            padding: 0;
            margin: 0;
        }
    </style>

    <script src="./resources/javascript/sockjs-1.1.1.js"></script>
    <script src="./resources/javascript/stomp.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('echo').disabled = !connected;
        }

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('sockjs') != -1) ?
                    new SockJS(url,undefined, {transports: transports}) : new WebSocket(url);
            if((url.indexOf('sockjs') != -1)) {
                stompClient = Stomp.over(ws);
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    log('Connected: ' + frame);
                    stompClient.subscribe('/hello/subscribe', function(message){
                        log(message.body);
                    });
                });
            }else {
                ws.onopen = function () {
                    setConnected(true);
                    log('Info: connection opened.');
                };

                ws.onmessage = function (event) {
                    log('Received: ' + event.data);
                };

                ws.onclose = function (event) {
                    setConnected(false);
                    log('Info: connection closed.');
                    log(event);
                };
            }
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
            stompClient = null;
            setConnected(false);
        }

        function echo() {
            if(stompClient != null){
                var message = document.getElementById('message').value;
                stompClient.send("/webSocketServer/hello", {}, message);
                log('Sent: ' + message);
            }else {
                if (ws != null) {
                    var message = document.getElementById('message').value;
                    log('Sent: ' + message);
                    ws.send(message);
                } else {
                    alert('connection not established, please connect.');
                }
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('sockjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
                if (window.location.protocol == 'http:') {
                    url = 'ws://' + window.location.host + urlPath;
                } else {
                    url = 'wss://' + window.location.host + urlPath;
                }
                document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
            transports = (transport == 'all') ?  [] : [transport];
        }

        function log(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        }
    </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets
    rely on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/webSocketServer');">
        <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/webSocketServer/sockjs/hello');">
        <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
                <option value="all">all</option>
                <option value="websocket">websocket</option>
                <option value="xhr-polling">xhr-polling</option>
                <option value="jsonp-polling">jsonp-polling</option>
                <option value="xhr-streaming">xhr-streaming</option>
                <option value="iframe-eventsource">iframe-eventsource</option>
                <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </div>
        <div>
            <button id="connect" onclick="connect();">Connect</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Here is a message!</textarea>
        </div>
        <div>
            <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>

</body>
</html>

最后,我们修改web->WEB-INF->web.xml,在其中增加

<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/webSocketServer/sockjs/*</url-pattern>
</servlet-mapping>

修改后的最终结果如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>Tools.Filter.StrutsPrepareAndExecuteFilterEx</filter-class>
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/sockjs/*</url-pattern>
    </servlet-mapping>
</web-app>

参考链接


 

Struts2 中读取静态资源文件

If it is placed in the webapp's classpath, then just use:

InputStream input = servletContext.getResourceAsStream("file.txt");

If it is placed in the global classpath, then use:

InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream("file.txt");

If it is placed in the webcontent, then just use:

InputStream input = new FileInputStream(servletContext.getRealPath("file.txt"));

The examples assumes that they're placed in the root. You can of course use relative path as opposed to the classpath root or webcontent root, e.g. path/to/file.txt. You can get the ServletContextin Struts by ServletActionContext#getServletContext().

Struts2使用execAndWait在Action中调用getText报告java.lang.NullPointerException

使用 Struts2 编写页面,遇到一个要长时间运行的接口,因此增加了一个execAndWait ,结果在 Action 中调用 getText的时候报告异常

java.lang.NullPointerException
at com.opensymphony.xwork2.util.LocalizedTextUtil.findText(LocalizedTextUtil.java:361)
at com.opensymphony.xwork2.TextProviderSupport.getText(TextProviderSupport.java:208)
at com.opensymphony.xwork2.TextProviderSupport.getText(TextProviderSupport.java:123)
at com.opensymphony.xwork2.ActionSupport.getText(ActionSupport.java:103)
at com.infy.action.LoginAction.execute(LoginAction.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:450)
at com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:289)
at org.apache.struts2.interceptor.BackgroundProcess$1.run(BackgroundProcess.java:57)
at java.lang.Thread.run(Thread.java:662)

查询了很多评论,最终找到原因跟解决方案,具体解释在 http://stackoverflow.com/questions/16692658/execandwait-interceptor-not-redirecting-to-success-page-after-waiting

  1. execAndWait causes the action to be executed in a new thread.
  2. Since ActionContext is ThreadLocal, the new thread will not get the values stored in the parent thread version of ActionContext. Every thread has a unique version of ActionContext
  3. getText() will throw a NPE when it tries to execute in the new thread because it depends onActionContext

简单解释一下,就是说

execAndWait 会导致执行的Action 在另外一个线程中被执行,而getText 依赖 ActionContext ,他从 ActionContext 中获得当前的Locale 从而根据语言的不同加载不同的文字,可是,由于ActionContext 是ThreadLocal 的,而execAndWait 新开线程的时候并没有把父线程的ActionContext 传递给子线程 结果导致在新开的子线程中的ActionContext中的数据都是null ,因此出现异常信息就不足为怪了。

解决方法为

To fix this, you need to copy the parent threads ActionContext into the execAndWait thread. You can do this by extending the BackgroundProcess class, implementing the beforeInvocation()and afterInvocation() methods, and extending ExecuteAndWaitInterceptor, implementing the getNewBackgroundProcess() method.

代码例子如下,注意,原文中作者的代码存在多线程同步问题,具体体现在

beforeInvocation

被调用的时候,得到的 context 为null ,导致注入失败。

因此需要重载两个类,来解决这个问题

import org.apache.struts2.interceptor.BackgroundProcess;
import org.apache.struts2.interceptor.ExecuteAndWaitInterceptor;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;

public class ExecAndWaitInterceptorEx extends ExecuteAndWaitInterceptor {

	private static final long serialVersionUID = -4456744368791451159L;
	  /**
     * {@inheritDoc}
     */
    @Override
    protected BackgroundProcess getNewBackgroundProcess(String arg0, ActionInvocation arg1, int arg2) {
    	ActionInvocationEx aActionInvocationEx = new ActionInvocationEx(arg1,ActionContext.getContext());
        return new BackgroundProcessEx(arg0, aActionInvocationEx, arg2);
    }

    private class BackgroundProcessEx extends BackgroundProcess {
		public BackgroundProcessEx(String threadName,
				ActionInvocation invocation, int threadPriority) {
			super(threadName, invocation, threadPriority);
		}

		private static final long serialVersionUID = -9069896828432838638L;
        /**
         * {@inheritDoc}
         * @throws InterruptedException 
         */
        @Override
        protected void beforeInvocation() throws InterruptedException {
        	ActionInvocationEx aActionInvocationEx = (ActionInvocationEx)this.invocation;
        	ActionContext context = aActionInvocationEx.getContext();
            ActionContext.setContext(context);
        }

        /**
         * {@inheritDoc}
         */
       @Override
        protected void afterInvocation() {
            ActionContext.setContext(null);
        }

    }
}
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionEventListener;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.ValueStack;

public class ActionInvocationEx implements ActionInvocation {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private final ActionInvocation mActionInvocation;

	private final ActionContext context;

	public ActionInvocationEx(ActionInvocation aActionInvocation,ActionContext aContext)
	{
		mActionInvocation = aActionInvocation;
		context = aContext;
	}

	public Object getAction() {
		return mActionInvocation.getAction();
	}

	public boolean isExecuted() {
		return mActionInvocation.isExecuted();
	}

	public ActionContext getInvocationContext() {
		return mActionInvocation.getInvocationContext();
	}

	public ActionProxy getProxy() {
		return mActionInvocation.getProxy();
	}

	public Result getResult() throws Exception {
		return mActionInvocation.getResult();
	}

	public String getResultCode() {
		return mActionInvocation.getResultCode();
	}

	public void setResultCode(String resultCode) {
		mActionInvocation.setResultCode(resultCode);
	}

	public ValueStack getStack() {
		return mActionInvocation.getStack();
	}

	public void addPreResultListener(PreResultListener listener) {
		mActionInvocation.addPreResultListener(listener);
	}

	public String invoke() throws Exception {
		return mActionInvocation.invoke();
	}

	public String invokeActionOnly() throws Exception {
		return mActionInvocation.invokeActionOnly();
	}

	public void setActionEventListener(ActionEventListener listener) {
		mActionInvocation.setActionEventListener(listener);
	}

	public void init(ActionProxy proxy) {
		mActionInvocation.init(proxy);
	}

	public ActionInvocation serialize() {
		return mActionInvocation.serialize();
	}

	public ActionInvocation deserialize(ActionContext actionContext) {
		return mActionInvocation.deserialize(actionContext);
	}

	/**
	 * @return the context
	 */
	public ActionContext getContext() {
		return context;
	}
}

写完之后,在struts.xml 中配置一下拦截器,覆盖掉默认的拦截器,下面是我的配置例子

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
	<constant name="struts.i18n.encoding" value="utf-8"></constant>
	<constant name="struts.multipart.maxSize" value="20971520"/>
    <constant name="struts.devMode" value="true" />

	<package name="FaeSupport" namespace="/" extends="struts-default">
	    <!-- 安装自定义的 execAndWait 拦截器,覆盖掉系统默认的,目的是解决在 execAndWait 出现的时候,Action调用 getText 报java.lang.NullPointerException-->
	    <interceptors > 
            <interceptor name="execAndWait" class="com.FaeSupport.Interceptor.ExecAndWaitInterceptorEx"/> 
        </interceptors > 
		<action name="faesupport" class="com.FaeSupport.Action.FaeSelAction" method="SelParametersCheck">
        <interceptor-ref name="defaultStack"/>
        <interceptor-ref name="execAndWait">
            <param name="delay">1500</param>
        </interceptor-ref>
        <result name="wait">/build_wait.jsp</result>			
		<result name="success">/build_wait.jsp</result>
		</action>
	</package>

</struts>

为Struts 2应用程序创建进度条(等待页面)

Struts 2模拟进度条的原理

对于一些需要较长时间才能完成的任务,在Web开发中,会由HTTP协议会因为超时而断开而面临许多风险,这是在桌面开发不曾遇到的。Struts 2提供的execAndWait拦截器就是为了处理和应付这种情况而设计的。注意,该拦截器不在"defaultStack"中,所以必须在使用它的动作里声明它,并且必须放在拦截器栈的最后一个。

使用了该拦截器后,动作依然正常执行,只是该拦截器会分配一个后台线程处理动作的运行,并在动作完成之前把用户带到一个"等待"页面。,该页面每隔一段时间刷新一次,直到那个后台线程执行完毕为止。如果用户随后又触发了同一个动作,但顶一个动作尚未执行完毕,这个拦截器将继续向用户发送"等待"结果;如果他已经执行完毕,用户会看到该动作的最终结果。
"等待"结果的行为与"dispatcher"结果的行为很相似,但是要注意的是,"等待"结果对应的视图带有如下的meta标签:

<meta http-equiv="refresh" content="5;url=/Struts2/default_progressbar.action"/>

该标签的作用就每隔多少秒就重新加载一次同样的URL。"5"是5秒,"url=/Struts2/default_progressbar.action"表示要加载的URL。
Struts 2是一个灵活强大的框架,如果你不喜欢Struts 2提供的默认"等待页面",你也可以自己设计自己的等待页面,若在动作声明中,没有找到"等待"结果,将使用默认值。

execAndWait拦截器

execAndWait拦截器 可以接收以下参数:

  • threadPriority:分配给相关线程的优先级,默认值为Thread.NORM_PRIORITY。
  • delay:向用户发送"等待"结果前的毫秒数,默认值为0。如果你不想立刻发送"等待"结果,可以将该参数设置为一个值。例如,你想让动作超过2秒还未完成时才发送"等待"结果,需要将其值设置为2000.
  • delaySleepInterval:每隔多少毫秒唤醒主线程(处理动作的后台线程)去检查后台线程是否已经处理完成,默认值是100。这个值设为0时无效。

示例:使用默认视图与自定义视图

创建一个动作类,该动作类的工作为挂起30秒:

public class ProgressbarAction extends ActionSupport
{
    private static final long serialVersionUID = 7441785390598480063L;
    private int  complete = 0;

    // 获取进度值
    public int getComplete()
    {
        complete += 10;
        return complete;
    }

    @Override
    public String execute()
    {
        try
        {
            Thread.sleep(30000);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return SUCCESS;
    }
}

配置struts.xml文件:

<package name="progressbar" extends="struts-default">
    <action name="default_progressbar" class="struts2.suxiaolei.progressbar.action.ProgressbarAction">
        <interceptor-ref name="defaultStack"></interceptor-ref>
        <interceptor-ref name="execAndWait">
            <param name="delay">1500</param>
        </interceptor-ref>
        <result name="success">/state_ok.jsp</result>
    </action>

    <action name="customer_progressbar" class="struts2.suxiaolei.progressbar.action.ProgressbarAction">
        <interceptor-ref name="defaultStack"></interceptor-ref>
        <interceptor-ref name="execAndWait">
            <param name="delay">1500</param>
        </interceptor-ref>
        <result name="wait">/customer_wait.jsp</result>
        <result name="success">/state_ok.jsp</result>
    </action>
</package>

测试页面:

<body>
    <s:a href="/Struts2/default_progressbar.action">default_view</s:a>
    <br />
    <s:a href="/Struts2/customer_progressbar.action">customer_view</s:a>
</body>

自定义等待页面:

<html>
  <head>
    <base href="<%=basePath%>">

    <title>My JSP 'customer_wait.jsp' starting page</title>

    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">

    <!-- 下面的meta元素才是重点,其他的没什么影响,是IDE自带的  -->
    <meta http-equiv="refresh" content="3;url=/Struts2/customer_progressbar.action">
    <!--
    <link rel="stylesheet" type="text/css" href="styles.css">
    -->

  </head>

  <body>
    <div>
        Please wait...(<s:property value="complete"/>)% complete
    </div>
  </body>
</html>

最终结果页面:

<body>
    OK!
</body>

在浏览器中输入:http://localhost:8081/Struts2/test.jsp,获得如下页面2011110117040626

首先点击,"default_view"链接:
2011110117061560
查看它的源代码:

<html>
    <head>
        <meta http-equiv="refresh" content="5;url=/Struts2/default_progressbar.action"/>
    </head>
    <body>
        Please wait while we process your request...
        <p/>

        This page will reload automatically and display your request when it is completed.
    </body>
</html>

这次点击"customer_view"链接:

2011110117104414

2011110117105659

这是自定义界面,最后动作执行完毕后,都会获得最终页面

2011110117121914

引用自:http://www.blogjava.net/athrunwang/archive/2011/11/18/364200.html