2015年10月13日 星期二

JSP賦值給 javascript

最近在公司寫了如下這一行
var xxx = <%=variable%>;

此時瀏覽器直器當機,看來是語法的錯誤,可是當時居然找不到,靜下心來後,發現原來要這樣子用

var xxx = '<%=variable%>';
        或是
var xxx = "<%=variable%>";

因為javascript原本就可以用單或雙引號,在此做個筆記,以免忘記

泛型方法

※基本

public static void main(String[] args) {
    new Test().<String>xxx("");
    new Test().xxx("");
}
    
private <T> void xxx(T t) {}
    
private <T extends Xxx, U extends String, V extends Animal &Ooo & Oooo> void xxx(T t, U u, V v) {}

※宣告時,寫在回傳值的左邊,而T是隨便打的,反正就是一個型態,型態是什麼由呼叫者決定

※宣告後可以不用,不會出現錯誤或警告,此例使用在參數

※可以在呼叫方法的左邊寫泛型,可以確定是什麼型態,但不寫也不會出現錯誤或警告

※&只能寫在泛型類別和泛型方法,像 List 是不行的
在 & 的右邊只能放 interface,這句話表示 V 必需是 Animal 的子類且還需 implements Ooo 和 Oooo 兩個 interface
.「,」隔開每一個自定的泛型名稱,不能重覆用,如 第一個已經用 T 了,後面就不能用 T 了



※集合

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    
    new Test().<Integer>xxx(list);
    new Test().xxx(list);
}
    
private <T> void xxx(List<T> t) {
    for(T tt:t) {
        System.out.println(tt);
    }
}

※呼叫時的泛型寫的不是 List,而是 List 裡的型態,因為泛型方法的參數 T 在 List 裡,也就是說它只管泛型的部分

2015年10月9日 星期五

JFreeChart (Struts2.3.x 十三)

首先先去這個網站下載
下載後打開jfreechart-1.x.x-demo.jar

打開後,找一個自己喜歡的,然後在source資料夾搜尋你選到的檔名,就有原始檔可看,直接複製到Eclipse,即可得到和畫面一樣的結果

這裡模擬一個較簡單的
DefaultPieDataset dataset = new DefaultPieDataset();
dataset.setValue("Samsung", new Double(27.8));
dataset.setValue("Others", new Double(55.3));
dataset.setValue("Nokia", new Double(16.8));
dataset.setValue("Apple", new Double(17.1));
JFreeChart jfc = ChartFactory.createPieChart("Phone Person", dataset);
try {
    ChartUtilities.saveChartAsJPEG(new File("D:/test.jpg"), jfc, 800, 600);
    System.out.println("成功!");
} catch (IOException e) {
    e.printStackTrace();
    System.err.println("失敗!");
}

setValue(Comparable, Number)
Comparable的兒子包括8個基本型別的Wrapper類別,還有String、Date都是

以上設定如果有中文,也不會出錯,但中文會變成「□」,所以要增加了以下的方法:
DefaultPieDataset dataset = new DefaultPieDataset();
dataset.setValue("象棋", 12);
dataset.setValue("跳棋", 25);
dataset.setValue("五子棋", 30);
dataset.setValue("孔明棋", 5);

// 中文標題
JFreeChart jfc = ChartFactory.createPieChart("棋類使用率圖", dataset);
jfc.getTitle().setFont(new Font("標楷體", Font.BOLD, 28));

// 圖裡的中文
PiePlot plot = (PiePlot) jfc.getPlot();
plot.setLabelFont(new Font("微軟正黑體", Font.PLAIN, 16));

// 圖底的中文
jfc.getLegend().setItemFont(new Font("新細明體", Font.ITALIC, 20));
try {
    ChartUtilities.saveChartAsJPEG(new File("D:/test.jpg"), jfc, 800, 600);
    System.out.println("成功!");
} catch (IOException e) {
    e.printStackTrace();
    System.err.println("失敗!");
}


如果本機沒有對應的字體也不會報錯,看起來是有預設的字體

※在Struts2裡使用JFreeChart

1.首先要有jfreechart-1.x.x.jar,還有Struts2合併JFreeChart的struts2-jfreechart-plugin-2.x.x.jar

2.struts.xml
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.devMode" value="true" />

<package name="basicstruts2" extends="struts-default"
    namespace="/">
    <result-types>
        <result-type name="chart"
            class="org.apache.struts2.dispatcher.ChartResult">
            <param name="height">200</param>
            <param name="width">200</param>
        </result-type>
    </result-types>
</package>

3.JFreeChartAction.java
@ParentPackage("basicstruts2")
@Namespace("/struts2")
@Action("picture")
@Results({ @Result(name = "success", type = "chart") })
public class JFreeChartAction extends ActionSupport {
    private JFreeChart chart;

    public JFreeChart getChart() {
        return chart;
    }

    public String productPic() {
        DefaultPieDataset dataset = new DefaultPieDataset();
        dataset.setValue("象棋", 12);
        dataset.setValue("跳棋", 25);
        dataset.setValue("五子棋", 30);
        dataset.setValue("孔明棋", 5);

        // 中文標題
        chart = ChartFactory.createPieChart("棋類使用率圖", dataset);
        chart.getTitle().setFont(new Font("標楷體", Font.BOLD, 28));

        // 圖裡的中文
        PiePlot plot = (PiePlot) chart.getPlot();
        plot.setLabelFont(new Font("微軟正黑體", Font.PLAIN, 16));

        // 圖底的中文
        chart.getLegend().setItemFont(new Font("新細明體", Font.ITALIC, 20));
        try {
            ChartUtilities.saveChartAsJPEG(new File("D:/test.jpg"), chart, 200,
                    200);
            System.out.println("成功!");
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("失敗!");
        }
        return SUCCESS;
    }
}

在瀏覽器輸入http://localhost:8080/Struts2Demo/struts2/picture!productPic.action即可看到畫面
struts.xml裡的寬高是瀏覽器看到的寬高
JFreeChartAction.java的寬高是輸出檔案的寬高
我試的結果,名字一定要叫chart,不然會報錯

官方也有提供做法,但有地方要修改如下:
struts.xml不用動
ViewModerationChartAction.java因為沒有RandomUtils,所以改成以下:
Random random = new Random();
for (int i = 0; i <= 100; i++) {
    dataSeries.add(i, random.nextInt());
}

return SUCCESS即可,不用super,這樣就能產生報表了

其他更深入的寫法,就看JFreeChart官方網站了,它的教學指南是要花錢買的

Annotation (Struts2.3.x 十二)

首先必需要有struts2-convention-plugin.jar
Struts2.x(七)為例,可改成以下這個樣子
CRUDAction.java
@Namespaces(value = { 
                @Namespace("/"), 
                @Namespace("/struts2") 
            })
@Actions(value = { 
            @Action(value = "crud1"), 
            @Action(value = "crud2") 
        })
@Results(value = {
            @Result(name = "success", location = "/struts2/success.jsp"),
            @Result(name = "input", location = "/struts2/fail.jsp") 
        })
public class CRUDAction extends ActionSupport {
    @Action(value = "crudI")
    public String insert() {
        System.out.println("增");
        return null;
    }

    @Action(value = "crudD")
    public void delete() {
        System.out.println("刪");
    }

    @Action(value = "crudU")
    public void update() {
        System.out.println("改");
    }

    @Action(value = "crudQ")
    public List<String> query() {
        System.out.println("查");
        return null;
    }
}

@裡面的「value=」,可以不寫,不管它裡面有多少field,不打就是value,可以參考我這篇文章
※測試:
開啟伺服器後,網址列會出現http://localhost:8080/Struts2Demo/開頭,Struts2Demo是我的專案名稱
1.在後面加crudI.acion或struts2/crudI.action都會成功,因為@Namespace有設定兩個路徑,其他刪改查都是一樣的意思

2.在後面加crud1.action或struts2/crud1.action(crud1和crud2都可以)也都會跳轉到成功頁

3.@Namespaces、@Actions、@Results 最後都有個「s」,所以裡面都是沒s的,其實如果只要設定一個,就不需要外層的s,只是我舉這個例子會較容易懂

4.@Action裡面有個results屬性,意思和@Result一樣,看你喜歡寫哪裡都行

5.在後面加crud1!insert.action或struts2/crud1!insert.action(crud1和crud2都可以),還是要在struts.xml或struts.properties設定struts.enable.DynamicMethodInvocation是true才可以

6.@ParentPackage
這個Annotation是繼承另外一個package用的
譬如在xml是設定<package extends="">,Annotation就是用這個

7.@InterceptorRefs
攔截器一樣可以設定,@Action也有interceptorRefs屬性,兩者一樣,看你喜歡寫哪裡都行

8.@ExceptionMappings
@Action也有exceptionMappings屬性,兩者一樣,看你喜歡寫哪裡都行
譬如:
@Action(
    value = "crudI", 
    exceptionMappings = 
        @ExceptionMapping(
            exception = "java.lang.NullPointerException", 
            result = "success", 
            params = {
                "key", "value" 
            }
        ))
public String insert() {
    System.out.println("增");
    return null;
}

當發生NullPointerException,會導到@Result是success的網頁,而且傳參數過去

其他的Annotation,可以參考這個網頁

2015年10月4日 星期日

jQuery 和 javascript 混用問題

最近發現非$(document)開頭的,如$(location).href,裡面是location可以和javascript的location混用
而$(document)就不可以混用,真是太奇怪了
$(document).ready()有三種寫法,其中一種不建議使用,看官網,所以下面的$(function(){}),function(){}被$()包起來,也屬於$(document)

jQuery本身是集合,所以在轉換成DOM時,必需取出其中一項才能轉換
譬如用jQuery的get()或javascript陣列,這時只能用DOM方法(下面的程式第一段)
如果一定要用jQuery的方法,可以用$()包起來(下面的程式第二段)
而jQuery只能用jQuery的方法,以下是個範例(下面的程式第二段)
最後一個each是改良後較佳的寫法,比較不會有混用的情形(下面的程式第三段)
而javascript迴圈的continue和break
在jQuery就是return和return false

<html lang="en">
<head>
    <meta charset="utf-8">
    <title>get demo</title>
    <script src="https://code.jquery.com/jquery-1.10.2.js"></script>
    <script>
        $(function(){
            var aaa = $('[name="aaa"]');
            alert(aaa.get(0).value);
            alert(aaa[0].value);
            alert(aaa.eq(0)[0].value);
            
            aaa.each(function(i){
                alert($(aaa.get(i)).val());
                alert($(aaa[i]).val());
                alert($(aaa.eq(i)[0]).val());
            });
            
            $('[name="aaa"]').each(function(){
                alert($(this).val());
                alert(this.value);
            });
        });
    </script>
</head>
<body>
    <input type="text" value="1" name="aaa" />
    <input type="text" value="2" name="aaa" />
    <input type="text" value="3" name="aaa" />
</body>
</html>

上傳 (Struts2.3.x 十一)

上傳首先要有兩個jar,看這裡
先寫個type="file",看看Action抓到的情形
upload.jsp
<form action="upload!upload.action" method="post" enctype="multipart/form-data">
    <input type="file" name="uploadFile" /><br /> 
    <input type="submit" />
</form>


struts.xml,因為action沒有result,待會會出錯,但目前只要看控制台的訊息就好
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.custom.i18n.resources" value="Message" />
<constant name="struts.devMode" value="true" />

<package name="basicstruts2" extends="struts-default" namespace="/struts2">
    <action name="upload" class="action.UploadAction"></action>
</package>


UploadAction.java,只要繼承ActinSupport
public class UploadAction extends ActionSupport {
    private File uploadFile;
    //setter即可

    public String upload() {
        System.out.println("uploadFile=" + uploadFile);
        return null;
    }
}


抓到的檔案副檔名為tmp,而且控制台部分訊息出現「'uploadFileContentType' on 'class action.UploadAction」和「'uploadFileFileName' on 'class action.UploadAction」的訊息,表示要增加這兩個屬性,所以Action最後的結果要改成如下的方式:

public class UploadAction extends ActionSupport {
    private File uploadFile;
    private String uploadFileContentType;
    private String uploadFileFileName;
    //三個屬性都給setter即可

    public String upload() {
        System.out.println("uploadFile=" + uploadFile);
        System.out.println("uploadFileContentType=" + uploadFileContentType);
        System.out.println("uploadFileFileName=" + uploadFileFileName);
        return null;
    }
}

這裡也有說明

知道這三個屬性的關係後,重新再寫一次

upload.jsp
<%@ page isELIgnored="false"%>

${uploadMsg}
<form action="upload!upload.action" method="post" enctype="multipart/form-data">
    <input type="file" name="uploadFile" /><br /> 
    <input type="submit" />
</form>

struts.xml,constant可以在src下建立一個struts.properties,也是一樣,目前這隻saveDir還沒有作用
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.custom.i18n.resources" value="Message" />
<constant name="struts.devMode" value="true" />
<constant name="struts.multipart.saveDir" value="D:\testStruts2" />
<constant name="struts.multipart.maxSize" value="2097152" />

<package name="basicstruts2" extends="struts-default" namespace="/struts2">
    <action name="upload" class="action.UploadAction">
        <result name="success">/struts2/upload.jsp</result>
    </action>
</package>
struts2-core-2.x.x.jar\org.apache.strtus2\default.properties可以找到constant的所有資訊
2097152單位是bit,除兩次1024就是2MB

UploadAction.java
private File uploadFile;
private String uploadFileContentType;
private String uploadFileFileName;
//三個屬性都給setter即可

public String upload() {
    HttpServletRequest request = ServletActionContext.getRequest();

    //可以到「tomcat目錄\conf\web.xml」可看到很多xxx/xxx
    List<String> list = Arrays.asList(new String[] { "image/jpeg",
            "image/bmp", "image/tiff" });

    if (list.contains(this.uploadFileContentType)) {
        ServletContext context = ServletActionContext.getServletContext();
        StringBuffer path = new StringBuffer();
        // 取個資料夾名稱放上傳的檔案
        path.append(context.getRealPath("/uploadPlace"));
        // 用唯一值命名主檔名
        path.append(File.separator + UUID.randomUUID() + ".");
        // 取原本的副檔名
        path.append(this.uploadFileFileName
                .substring(this.uploadFileFileName.lastIndexOf(".") + 1));

        FileControl.saveFile(this.uploadFile, path.toString());
        request.setAttribute("uploadMsg", "上傳成功!");
    } else {
        request.setAttribute("uploadMsg", "上傳非圖片格式!");
    }
    return SUCCESS;
}

FileControl.java,裡面只有兩個靜態方法
public static boolean saveFile(File file, String path) {
    File outFile = new File(path);
    if (!outFile.getParentFile().exists()) {
        outFile.getParentFile().mkdirs();
    }
    InputStream input = null;
    OutputStream output = null;
    try {
        input = new FileInputStream(file);
        output = new FileOutputStream(outFile);
        byte[] data = new byte[1024];
        int temp = 0;
        while ((temp = input.read(data)) != -1) {
            output.write(data, 0, temp);
        }
        input.close();
        output.close();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

public static boolean deleteFile(String path) {
    File file = new File(path);
    if (file.exists()) {
        file.delete();
        return true;
    }
    return false;
}
這個class就隨個人發揮了

以上雖然完成了功能,但是是屬於自己寫的苦功,所以stuts.xml的saveDir(儲存路徑)沒有效果,當然路徑也可以用Action裡的ServletActionContext.getServletContext().getRealPath("")取得檔案內容,然後進一步取得路徑

※用struts2的攔截器來達成上傳的功能

upload.jsp
${fieldErrors},<br />
${fieldErrors['uploadFile']},<br />
${fieldErrors['uploadFile'][0]}<br />
<form action="upload!upload.action" method="post" enctype="multipart/form-data">
    <input type="file" name="uploadFile" /><br /> 
    <input type="submit" />
</form>

struts.xml,saveDir是暫存目錄,不加會在control有警告「Unable to find 'struts.multipart.saveDir' property setting. Defaulting to javax.servlet.context.tempdir」,但不影響上傳功能,而這個資料夾,上傳完成後,我也沒看到有什麼檔案在裡面
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<constant name="struts.custom.i18n.resources" value="Message" />
<constant name="struts.devMode" value="false" />
<constant name="struts.multipart.saveDir" value="D:/testStruts2" />

<package name="basicstruts2" extends="struts-default"
        namespace="/struts2">
    <action name="upload" class="action.UploadAction">
        <interceptor-ref name="fileUpload">
            <param name="allowedTypes">
                image/jpeg,image/bmp,image/tiff,image/png,image/gif
            </param>
            <param name="maximumSize">1048576</param>
        </interceptor-ref>
        <interceptor-ref name="basicStack"/>
        <result name="success">/struts2/upload.jsp</result>
    </action>
</package>

UploadAction
private File uploadFile;
private String uploadFileContentType;
private String uploadFileFileName;
//都給setter即可

public String upload() {
    ServletContext context = ServletActionContext.getServletContext();
    StringBuffer path = new StringBuffer();
    path.append(context.getRealPath("/uploadPlace"));
    path.append(File.separator + this.uploadFileFileName);
    System.out.println("path=" + path);
    FileControl.saveFile(this.uploadFile, path.toString());
    return SUCCESS;
}

Message.properties,顯示錯誤訊息用,不加也有預設的錯誤訊息
struts.messages.error.uploading=aaa
struts.messages.error.file.too.large=bbb
struts.messages.error.content.type.not.allowed=ccc
struts.messages.error.file.extension.not.allowed=ddd

※多檔案上傳

以上個檔案為例,其實只要變成陣列的型式就可以了
upload.jsp
<form action="upload!upload.action" method="post" enctype="multipart/form-data">
    <input type="file" name="uploadFile" />${fieldErrors['uploadFile'][0]}<br /> 
    <input type="file" name="uploadFile" />${fieldErrors['uploadFile'][1]}<br /> 
    <input type="file" name="uploadFile" />${fieldErrors['uploadFile'][2]}<br /> 
    <input type="submit" />
</form>

UploadAction.java
private File[] uploadFile;
private String[] uploadFileContentType;
private String[] uploadFileFileName;
//都給setter即可

public String upload() {
    ServletContext context = ServletActionContext.getServletContext();
    for (int i = 0; i < uploadFile.length; i++) {
        StringBuffer path = new StringBuffer();
        path.append(context.getRealPath("/uploadPlace"));
        path.append(File.separator + this.uploadFileFileName[i]);
        System.out.println("path=" + path);
        FileControl.saveFile(this.uploadFile[i], path.toString());
    }
    return SUCCESS;
}


2015年9月29日 星期二

攔截器 (Struts2.3.x 十)

※系統內鍵的攔截器

HelloInterceptor.java
public class HelloInterceptor extends ActionSupport{
    public void Hello(){
        System.out.println("Hello Interceptor!");
    }
}
和一般action一樣
struts.xml
<action name="hello" class="action.HelloInterceptor">
    <interceptor-ref name="timer" />
</action>
因為package有extends="struts-default",這個檔案在struts2-core-2.x.x.jar裡, 所以name可以是timer,還有很多,可以參考這裡
測試時,在瀏覽器的網址列最後打上hello!Hello.action, 記得struts.xml要加上<constant name="struts.enable.DynamicMethodInvocation" value="true" />

※自訂攔截器

TestInterceptor.java
public class TestInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        System.out.println("攔截成功!");
        return invocation.invoke();
    }
}
繼承一個叫AbstractInterceptor的抽象類別,並覆寫方法,invocation.invoke()表示繼續執行

struts.xml
<interceptors>
    <interceptor name="testICR" class="interceptor.TestInterceptor" />
</interceptors>
<action name="hello" class="action.HelloInterceptor">
    <interceptor-ref name="timer" />
    <interceptor-ref name="testICR" />
</action>
可以有很多攔截器,這個例子有兩個,要注意interceptors要寫在action上面,錯誤會有提示

※多個攔截器設定

也就是上面的struts.xml可以改成以下的寫法

<interceptors>
    <!-- interceptor可以很多 -->
    <interceptor name="testICR" class="interceptor.TestInterceptor" />
    
    <interceptor-stack name="xxx">
        <interceptor-ref name="timer" />
        <interceptor-ref name="testICR" />
    </interceptor-stack>
</interceptors>

<action name="hello" class="action.HelloInterceptor">
    <interceptor-ref name="xxx" />
</action>
把interceptor-ref搬上去後,用interceptor-stack包起來,然後取個名字,下面就使用新的名字即可

※全域攔截器設定

<interceptors>
    <interceptor name="testICR" class="interceptor.TestInterceptor" />
    
    <interceptor-stack name="xxx">
        <interceptor-ref name="timer" />
        <interceptor-ref name="testICR" />
    </interceptor-stack>
    <default-interceptor-ref name="xxx" />
</interceptors>

<action name="hello1" class="action.HelloInterceptor1" />
<action name="hello2" class="action.HelloInterceptor2" />
<action name="hello3" class="action.HelloInterceptor3" />

default-interceptor-ref設定完,所有的actin在執行之前都會攔截

※defaultStack

攔截器有個defaultStack,主要是執行struts2的方法
<interceptor-ref name="defaultStack" />
不加時,會執行setContainer()-->execute()
加上後,會執行setContainer()-->validate()-->hasErrors()-->execute()
所以如果想用validate方法,就要設這一行

※模擬一個登入攔截

登入時,如果帳號或密碼沒輸入,就再原網頁不動

login.jsp
<form action="login.action" method="post">
    <input type="text" name="user.userName" /><br /> 
    <input type="password" name="user.userPassword" /><br /> 
    <input type="reset" />
    <input type="submit" />
</form>

struts.xml
<interceptors>
    <interceptor name="login" class="interceptor.LoginInterceptor" />
    <interceptor-stack name="stack">
        <interceptor-ref name="login" />
    </interceptor-stack>
</interceptors>

<action name="login" class="login.LoginAction" method="execute">
    <interceptor-ref name="stack" />
    <result name="login">/struts2/login.jsp</result>
    
    <!-- 方法一 -->
    <result name="success">/struts2/success.jsp</result>
    <!-- 方法二,只是測試redirectAction -->
    <result name="success" type="redirectAction">xxx.action</result>
</action>
<!-- 此action方法二才需要 -->
<action name="xxx" class="login.LoginAction" method="success">
    <result name="success">/struts2/success.jsp</result>
</action>

LoginInterceptor.java的intercept
HttpServletRequest req = ServletActionContext.getRequest();
User user = new User();
user.setUserName(req.getParameter("user.userName"));
user.setUserPassword(req.getParameter("user.userPassword"));

// 方法一
Map<String, Object> map = invocation.getInvocationContext()
        .getSession();
// 方法二
// Map<String, Object> map = ActionContext.getContext().getSession();
if (user.getUserName().isEmpty() || user.getUserPassword().isEmpty()) {
    //方法一
    HttpServletResponse res = ServletActionContext.getResponse();
    res.sendRedirect("/Struts2Demo/struts2/login.jsp");
    return null;
    //方法二
    // return ActionSupport.LOGIN;
} else {
    map.put("user", user);
    return invocation.invoke();
}

LoginAction.java,記得繼承ActionSupport
private User user;
//setter/getter
@Override
public String execute() throws Exception {
    return SUCCESS;
}
//此方法為struts.xml的方法二才需要
public String success() {
    return SUCCESS;
}

User.java
private String userName;
private String userPassword;
//setter/getter

success.jsp
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ page isELIgnored="false"%>

OGNL:
<ul>
    <li>帳號:<s:property value="#session.user.userName"/>
    <li>密碼:<s:property value="#session.user.userPassword"/>
</ul>

EL:
<ul>
    <li>帳號:${user.userName}
    <li>密碼:${user.userPassword}
</ul>

※用攔截器防止頁面重覆提交

以登入的例子修改struts.xml
<constant name="struts.custom.i18n.resources" value="Message" />
<interceptors>
    <interceptor name="login" class="interceptor.LoginInterceptor" />
    
    <interceptor-stack name="stack">
        <interceptor-ref name="login" />
        <interceptor-ref name="basicStack" />
        <interceptor-ref name="token" />
    </interceptor-stack>
</interceptors>
<action name="login" class="login.LoginAction" method="execute">
    <interceptor-ref name="stack" />
    <result name="login">/struts2/login.jsp</result>
    <result name="success">/struts2/success.jsp</result>
    <result name="invalid.token">/struts2/login.jsp</result>
</action>
注意invalid.token要設定

login.jsp增加兩行
<form action="login.action" method="post">
    <s:token name="ooo" />
    <s:actionerror />
    <input type="text" name="user.userName" /><br /> 
    <input type="password" name="user.userPassword" /><br /> 
    <input type="reset" />
    <input type="submit" />
</form>
s:token的name可以隨便打,網頁原始碼可看出會變成兩行hidden
s:actionerror為針測到重覆提交時所產生的訊息

Message.properties 訊息定義在這個檔裡,struts.xml要設定這個檔名
struts.messages.invalid.token=\u8868\u55AE\u91CD\u8986\u63D0\u4EA4\uFF01

可參考這裡
如果把jsp的s:token放在form外面,不用重覆提交,一次就出現Message.properties的訊息了,而且在控制台會出「Could not find token name in params.」的錯