[Struts2官方指南的个人学习和翻译] Struts2自带例子MailReader的学习

 漫谈 Struts 2 MailReader 应用开发过程

  该文章通过讲述一个简单但功能齐全的应用开发过程来指导Struts2的初学者. 文章中包含了使用到的代码段, 但你最好在自己搭建一个服务器来运行MailReader应用. 


该教程默认读者有一定的Java,JavaBeans,JSP,web应用开发的基础知识. 想了解底层的实现技术,请浏览 Key Technologies Primer.



需要的知道的是,该MailReaer只是该应用的第一次迭代开发. 该版本提供的功能是让用户登录并维护多个不同邮箱服务器的账号. 完全完成后, 该应用能让用户从他们的账号中读取邮件.

该MailReader应用演示了注册,登录,维护一些主记录和子记录。文章概述了开发过程中需要做的事,包括jsp,java类,配置文件的编写。



JAAS - Note that for compatibility and ease of deployment, the MailReader uses "application-based" authorization. However, use of the standard Java Authentication and Authorization Service (JAAS) is recommended for most applications. (See the Key Technologies Primer for more about authentication technologies.)


首先来了解一个初始的欢迎页面是如何呈现的, 然后如何登录应用和修改订阅信息. 请注意该文章不是教你如何写一个简单的hello wrold程序,而是通过丰富的实践经验和“最佳实践”来开发一个应用。 你应该调整状态来仔细的阅读这29页长的文章。

Welcome Page

一个通常的web应用, 可以指定一些主页面. 当你使用这个web应用没有指定一个特殊的页面时候,服务器会返回一个默认的主页。

web.xml

当一个web应用被载入时, 容器会读取和解析 "Web应用部署文件"或 "web.xml" 文件. Struts2通入一个过滤器来嵌入一个web应用中. 跟其他过滤器一样, "struts2" 的过滤器filter部署在"web.xml"中.


web.xml - The Web Application Deployment Descriptor




  Struts 2 MailReader

  
    struts2
    
      org.apache.struts2.dispatcher.FilterDispatcher
    
   

  
    struts2
    /*
  

  
    
      org.springframework.web.context.ContextLoaderListener
    
  

  
  
    
      mailreader2.ApplicationListener
    
  

  
    index.html
  

  

注意在web.xml中没有指定actions的后缀名. Struts 2 默认的后缀名是 ".action", 但可以在 struts.properties 文件中替换. 为了与之前的版本相容, MailReader使用.do作为后缀名


struts.properties
struts.action.extension = do

web.xml中为应用指定了一个主页面.当一个请求不是指定具体页面而是一个目录, 容器会使用默认的主页作为返回.

但是,大多数Struts2应用不会指向一个具体的真实的页面, 而是一个虚拟的资源 - actions. Actions 指定了一段你需要在返回一个页面或者其他资源之前执行的代码. 一个被普遍认可的做法是不直接链接到服务页面, 而是仅仅通过action的映射. By linking to actions, developers can often "rewire" an application without editing the server pages.


最佳实践:

"Link actions not pages."


actions 被列在一个或多个 XML配置文件中, 默认的配置文件是 "struts.xml". 在载入一个web应用时, struts.xml和其他包含在内的配置文件会被解析, 然后框架会创建出一些配置对象. 主要的工作是将一个请求和某些action,某些页面进行关联。

可以在web.xml中设置0或多个"Welcome" 页面.除非你在使用JAVA1.5,actions不能被指定为一个 Welcome page. 所以在这种情况下,如何履行"Link actions not pages."的最佳实践呢?

一种解决方案是用一个页面来引导actions. 我们可以用一个"index.html" 作为主页面然后将它重定向至 "Welcome" action.


MailReader's index.html


  
  
  
    

Loading ...


我们也可以使用jsp页面通过struts标签进行重定向,但是一个简单的HTML就足够解决了。

Welcome.do

当客户端请求"Welcome.do", 这个请求将进入 "struts2" 通过 FilterDispatcher (之前在web.xml中配置的过滤器).FilterDispatcher 从配置中寻找合适的action映射. 如果我们只需要跳转至Welcome页面, 只需要进行简单的配置.


A simple "forward thru" action element

  /pages/Welcome.jsp

如果请求 Welcome action ("Welcome.do"),  "/page/Welcome.jsp" 页面会作为响应返回.客户端不知道也不需要知道返回资源的路径其实是"/pages/Welcome.jsp",客户端仅知道自己请求的资源是"Welcome.do".

如果你看了MailReader的配置文件,会发现Welcome action的配置其实有更多内容.


The Welcome action element
class="mailreader2.Welcome">
    /pages/Welcome.jsp
    
    

任何时候请求Welcome acton,Welcome 这个JAVA类都会被执行. 当他执行完,会选择一个"result" . 默认的result 名是"success".另外一个有效的result"error", 定义在一个全局域中.


关键概念:

Action不需要知道结果 "success" 或 "error"的返回类型,只需要返回一个result的名字, 不必了解它的具体实现.


所以所有的result的细节,包括返回页面的路径,都只需要在配置文件中定义一次. 将实现细节耦合在一起而不是在应用中到处分散。


关键概念:

Struts配置文件让我们可以分离关注点,并且只作一次定义,帮助我们规范地开发应用。


为什么welcome action需要选择 "success" 或 "error"?

Welcome Action

MailReader应用保存了一些用户和他们的邮箱账号在数据库中. 如果无法连接到数据库, 应用则无法进行工作. 所以在显示welcome页面之前, welcome类 会检查数据是否可用.

MailReader也是一个国际化的应用. 所以Welcome类也会先检查信息资源的有效性. 如果两个资源都是有效的,class 才会返回结果 "success" . 此外, class会返回结果"error",则页面无法正常显示。


The Welcome Action class
package mailreader2;
public class Welcome extends MailreaderSupport {

  public String execute() {

    // Confirm message resources loaded
    String message = getText(Constants.ERROR_DATABASE_MISSING);
    if (Constants.ERROR_DATABASE_MISSING.equals(message)) {
      addActionError(Constants.ERROR_MESSAGES_NOT_LOADED);
    }

    // Confirm database loaded
    if (null==getDatabase()) {
      addActionError(Constants.ERROR_DATABASE_NOT_LOADED);
    }

    if (hasErrors()) {
      return ERROR;
    }
    else {
      return SUCCESS;
    }
  }
}

几个常用的result名字被预先定义好了, 包括 ERROR, SUCCESS, LOGIN, NONE, 和 INPUT, 可以在 Struts2中任意使用.

Globol Result

之前提到, "error" 定义在全局范围. 其他的action可能也会碰上数据库无法连接的问题, 或者其他错误发生. MailReader 定义 "error" result 为 Global Result, 所以任意的action可以使用它.


MailReader's global-result element
 
  "error">/pages/Error.jsp
  /pages/Error.jsp
  Login_input

当然, 如果一个自定义的action mapping 包含了自己的 "error" result, 则局部的result会被使用。

ApplicationListener.java

数据库被当作一个对象保存在应用中. 数据库对象是实现于一个接口的.应用不需要重启就可以载入不用的数据库接口的实现类. But how is the database object loaded in the first place?

我们在"web.xml"配置了一个自定义的监听器用于创建数据库对象.


mailreader2.ApplicationListener
 
  
    mailreader2.ApplicationListener
  

默认的, 我们的ApplicationListener 会载入一个 MemoryDatabase, UserDatabase的实现类. MemoryDatabase stores the database content as a XML document, which is parsed and loaded as a set of nested hashtables. The outer table is the list of user objects, each of which has its own inner hashtable of subscriptions. When you register, a user object is stored in this hashtable. When you login, the user object is stored within the session context.

数据库已经被创建好并有一个示例用户. 如果你检查"/src/main/resources"路径下的"database.xml" 文件 , 你可以看见实例用户在其中的描述.


The "seed" user element from the MailReader database.xml

     autoConnect="false"
      password="bar" type="pop3" username="user1234">
    
    
    

The "seed" user element creates a registration record for "John Q. User", with the subscription detail for his hotmail and yahoo accounts.

Message Resource

MailReader是一个国际化的应用. 在Struts 2中, 在Action类在进行处理时,信息资源是关联在其中的. 检查源码,会看见一个语言资源包MailreaderSupport. MailreaderSupport 是MailReader应用中的所有action的基本类. Since 因为所有的Action都继承自MailreaderSupport, 则共用着相同的语言资源包.


Message Resource entries used by the Welcome page
index.heading=MailReader Application Options
index.login=Log on to the MailReader Application
index.registration=Register with the MailReader Application
index.title=MailReader Demonstration Application
index.tour=A Walking Tour of the MailReader Demonstration Application

如果你在资源中更改一条信息,然后重载应用,这个更改就生效了. 如果你为其他地域提供信息资源包,则可以本地化你的应用. MailReader提供了 English, Russian, 和Japanese的资源.

Welcome Page

确定完必要的资源存在后,Welcome action 将跳转至 Welcome page.


Welcome.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
  
  
    
      
      <strong><s:text name="index.title"/></strong>
      " rel="stylesheet"
      type="text/css"/>
    

    
      

Language Options

  • en English
  • ja Japanese
  • ru Russian

" alt=""/>

">


在上面的Welcome page中, 使用了 Struts 2 标签库,它们用红色来标记了,分别使用了Struts JSP标签的 "text", "url", and "i18n".

(在Struts 2 MailReader 应用中使用了"s:"前缀, 你也可以使用任何你想使用的前缀在你的应用中.)

text 标签中插入了一条应用默认的信息资源中的信息.如果用户的地区设置被更改了, text标签会使用新的本地的资源来替代.

url 标签可以指向一个action或者其他web资源, applying "URL encoding" to the hyperlinks as needed. Java's URL encoding feature lets your application maintain client state without requiring cookies.


Tip:

Cookies - If you turn cookies off in your browser, and then reload your browser and this page, you will see the links with the Java session id information attached. (If you are using Internet Explorer and try this, be sure you reset cookies for the appropriate security zone, and that you disallow "per-session" cookies.)


i18n 标签提供访问多个资源包. MailReader application uses a second set of message resources for non-text elements. When these are needed, we use the "i18n" tag to specify a different bundle.

The alternate bundle is stored in the {{/src/main/resources}} folder, so that it ends up under "classes", which is on the application's class path.

In the span of a single request for the Welcome page, the framework has done quite a bit already:

  • Confirmed that required resources were loaded during initialization.
  • Written all the page headings and labels from internationalized message resources.
  • Automatically URL-encoded paths as needed.

成功跳转后, Welcome页面会有两个选项: 登录和注册. 下面先介绍登录的实现。

Login

如果选择了登录, 且一切正常, Login action 会跳转至 Login 页面.

Login Page

Login 页面显示了一个输入用户名和密码的表单.你可以使用默认的用户名和密码登录.试试用不同错误的方式来登录,看看应用会给出什么响应. 注意用户名和密码都是大小写敏感的.


Login.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
  <%@ taglib prefix="s" uri="http://struts.apache.org/tags"  %>
  
  
  
    <s:text name="login.title"/>
      " rel="stylesheet"
        type="text/css"/>
  
  
    
    
      
      
      
      
      action="Login_cancel" onclick="form.onsubmit=null"
        key="button.cancel"/>
    
    
  

在welcome页面上我们已经看过一些struts2标签,现在再来看几个新的标签.

login.jsp中第一遇见的新标签是actionerrors. 每一个属性都可能发生各种验证错误. 如果你没有输入用户名, struts会在该标记处显示一个错误信息来提示你. 但也有跟属性无关的错误,如数据库可能无法连接了. 如果action返回一个"Action Error", 和 "Field Error"不同的是, 错误会显示在 "actionerror" 标签.无论是Action Errors还会是Field Errors错误的提示文本, 都应该指向资源包,这样便于管理和实现国际化。

第二个新标签是form. 对应HTML中的form标签. "validate=true" 设置开启了客户端检验, 所以form 在发送至服务前会通过Javascript来进行验证. 为了确保安全,struts还是会进行验证, 但是开启客户端验证会为服务器减少开销。

在form标签中, 可以看见更多的新标签: "textfield", "password", "submit" 和 "reset". 还可以看到 "submit" 利用action属性完成两个不同的功能.

当我们在form中添加一个控件的时候,我们还需要使用HTML来完成需求. 通常, 我们只是需要一个简单的 "input type=text" 标签. 我们还会为它添加一个label,可能还会想要一个tooltip.当然, 应该还要有用来显示验证结果的.

Struts标签提供了模板和样式,则使用一个简单的Struts标签就可以完成一系列HTML的编写.例如, 这一个标签:


    textfield key="username"/>

会生成如下HTML标记.



  
    
  
  
    
  

如果你不喜欢由Struts标签生成的标记, 它们都可以被替换. 每个标签都是由一个可以被修改的模板控制的. 例如,这是ActionErrors标签默认的生成HTML代码的模板:


<#if (actionErrors?exists && actionErrors?size > 0)>
  
    <#list actionErrors as error>
  • ${error}

如果你想要ActionErrors用表格替换列表的方式显示, 你可以复制下面的文件,将它保存在"template/simple/actionerror.ftl", 且将它放在你的应用的classpath之下.


<#if (actionErrors?exists && actionErrors?size > 0)>
  
    <#list actionErrors as error>
      
${error}

Under the covers, Struts 使用Freemarker作为它的标准的模板语言. FreeMarker类似于Velocity,但它提供了更好的错误反馈和其他额外的特性. 如果你愿意, Velocity和JSP模板可应用来创建你自己的标签.

password 标签对应了一个"input type=password" 标签, along with the usual template/theme markup. 默认的, password 不会保留出入的信息在提交失败时. 如果用户名是错的, 客户端会要求再次输入密码. (如果你希望在验证失败时保留密码, 你可以将标签的"showPassword" 属性为true.)

显然, submitreset标签对应的是它们对应的类型的button.

The second submit button is more interesting.

  action="Login_cancel" onclick="form.onsubmit=null"
    key="button.cancel"/>

这里我们在form中创建了一个 Cancel button. 这个button 的属性 action="Login_cancel"告诉框架去使用Login的"cancel" 方法替代"execute"方法.οnclick="form.οnsubmit=null" 脚本使客户端验证失效. 在服务器端, "cancel" 是一个用来跳过验证的方法, 所以请求会直接调用 Action 的cancel方法. Another entry on the special-case list is the "input" method.


Tip:

The Struts Tags have options and capabilities beyond what we have shown here. For more see, the Struts Tag documentation.


但是标签为何知道两个属性都是必要的呢?怎么知道当属性为空的时候显示什么错误信息?

为了得到答案,我们需要看下另一种配置文件 : "validation" 文件.

Login-validation.xml

当然可以很容易的在Action类中编写一段验证数据的代码, 但Struts提供了一种更为简单的方式.

这种验证通过XML文件来配置, Login-validation.xml.


Validation file for Login Action


  
    
    
  
  
  
    
    
    
  


需要注意 DTD引用的是 "XWork"中的. Open Symphony XWork 脱离web容器的通用的基于命令模式的框架.本质上, Struts 2 是将XWork拓展成一个web框架.

The field elements correspond to the ActionForm properties. The username and password field elements say that each field depends on the "requiredstring" validator. If the username is blank or absent, validation will fail and an error message is generated. The messages would be based on the "error.username.required" or "error.password.required" message templates from the resource bundle.

Login Action

如果验证通过了, 框架将调用Login Action的"execute"方法. 实际上 Login Action 非常简短, 因为大部分都继承自一个基本类,MailreaderSupport.


Login.java
package mailreader2;
import org.apache.struts.apps.mailreader.dao.User;
public final class Login extends MailreaderSupport {
public String execute() throws ExpiredPasswordException {
  User user = findUser(getUsername(), getPassword());
  if (user != null) {
    setUser(user);
  }
  if (hasErrors()) {
    return INPUT;
  }
    return SUCCESS;
  }
}

Login 实现了如何认证一个user.试用提供的信息来寻到对应的user.如果user找到了,则将其缓存.如果没找到,则返回 "input" 让客户端重新尝试.否则,返回 "success",则客户端可以访问应用上更多的内容.

MailreaderSupport.java

我们来看看MailreaderSupport 和另一个基本类ActionSupport的成员变量和方法, "getUsername", "getPassword", "findUser", "setUser", 和 "hasErrors".

Struts希望你直接在Action中定义JavaBean properties . 任何JavaBean 属性都可以被使用. 当一个请求到来, 任何Action 类上的共有属性会与请求参数匹配.如果名字相匹配, 请求参数的值会被写进 JavaBean 的属性. Struts会尽力将数据进行转换,如果需要还会报告发生的错误.

UsernamePassword属性并没有什么特别之处, 仅仅是标准的JavaBean属性.


MailreaderSupport.getUsername() and getPassword()
private String username = null;
public String getUsername() {
  return this.username;
}
public void setUsername(String username) {
  this.username = username;
}

private String password = null;
public String getPassword() {
  return this.password;
}
public void setPassword(String password) {
  this.password = password;
}

我们用这些属性来保存客户端传来的值, 通过它们来执行 findUser 方法.


MailreaderSupport.findUser
public User findUser(String username, String password)
  throws ExpiredPasswordException {
  User user = getDatabase().findUser(username);
  if ((user != null) && !user.getPassword().equals(password)) {
    user = null;
  }
  if (user == null) {
    this.addFieldError("password", getText("error.password.mismatch"));
  }
  return user;
}

"findUser" 方法进入了MailReader的 Data Access Object 层, which is represented by the Database property. DAO 层的代码被分离成一个组件. MailReader应用 导入了DAO JAR,但是没有管理任何DAO源码的职责. Keeping the data access layer at "arms-length" is a very good habit. It encourages a style of development where the data access layer can be tested and developed independently of a specific end-user application.实际上, 我们有几个不同版本的MailReader应用, 而它们都共用了一个MailReader DAO JAR!


Best Practice:

"Strongly separate data access and business logic from the rest of the application."


当"findUser" 方法返回,  Login Action查看是否返回了一个有效的user. 一个有效的user会被放在User property. 尽管它还是一个 JavaBean 属性, the User property is not implemented in quite the same way as Username and Password.


MailreaderSupport.setUser
public User getUser() {
  return (User) getSession().get(Constants.USER_KEY);
}
public void setUser(User user) {
  getSession().put(Constants.USER_KEY, user);
}

用于替代使用一个属性来存放值, "setUser" 通过Session.


MailreaderSupport.getSession() and setSession()
private Map session;
public Map getSession() {
  return session;

public void setSession(Map value) {
  session = value;
}

查看 MailreaderSupport 类, 你也许会认为 Session 属性是一个简单古老的 Map. 实际上, Session属性是在运行期用来支持servlet session对象的配适器. MailreaderSupport类不需要具体了解过程. 可以随时使用Session就像一个Map. 我们还可以通过一些MAP的其他实现类来测试MailreaderSupport类。 进行测试来看看 MailreaderSupport 会因为一个“假”的 Session对象产生什么变化.

但是, 当MailreaderSupport运行在一个web应用中, 它如何获取一个servlet session?

如何仔细查看 MailreaderSupport 类, 你会发现没有一行代码用来设置 session property. 是的,当我们运行这个类时, session 属性是空的.

这种在运行期为Session属性提供一个值的方式称为 "dependency injection"(依赖注入).  MailreaderSupport 类实现了SessionAware接口.SessionAware 是在Struts内部的, 它定义了一个为 Session 属性设值的方法.

public void setSession(Map session);

在Struts内部还有一个对象 ServletConfigInterceptor. 当ServletConfigInterceptor 看见一个实现了SessionAware接口的 Action ,它会自动地在session属性里传值.

if (action instanceof SessionAware) {
  ((SessionAware) action).setSession(context.getSession());
}

Struts使用一些 "Interceptor" 类为每一个在应用中定义的action创建一个front controller. 每个Interceptor 会被调用来在Action执行之前处理请求, 在action执行完后再次调用. (如果你了解 Servlet Filters, 你会理解这个模式.但是不同于Filters, Interceptors 不依赖于HTTP. Interceptors 可以在web容器之外测试、开发.)

你可以为你的action设置相同的interceptor, 或者为指定的action定义一些自己的 Interceptors, 还可以为不用action类型定义不同的Interceptors. Struts带有一些默认的 interceptor, 在没有指定interceptor时使用, 你也可以在配置文件中指定一个自己的默认的interceptor.

许多Interceptor提供了一些实用的功能,如设置session属性,如 ValidationInterceptor, 可以改变一个action的流程。 Interceptor 是Struts的核心特性.

如果没有发现有效的 User,或者密码不匹配,"findUser" 方法会调用addFieldError方法来记录这个错误. 当 "findUser" 返回时,Login Action 检查有没有错误, 然后返回结果 INPUT 或者 SUCCESS.

"addFieldError" 方法来自Struts自带的ActionSupport类. INPUT 和 SUCCESS也是在 ActionSupport类中. ActionSupport类提供了许多有用的方法,但不需要一定得选择它作为基类. 任何Java类都可以当作一个action.

良好的做法是为你的应用中的action提供一个带有实用功能的基类. Struts为我们提供了 ActionSupport, 而在 MailReader 应用则使用了MailreaderSupport 类.


最佳实践:

"Use a base class to define common functionality."


当Login 返回一个INPUT而不是SUCCESS. Struts如何确定下一步该怎么做?

我们需要在配置文件中查看Login的配置。

Login Configuration

Login action 配置描述了流程的操作, 包括了返回"input"时下一步该怎么做, 或者默认的"success".


mailreader-support.xml Login

  /pages/Login.jsp
  Welcome
  MainMenu
  ChangePassword
  <exception-mapping
    exception="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException"
  result="expired"/>
  

你会注意到这个action配置的名字不是 "Login" 而是 "Login_*". 这个星号是一个通配符用来匹配任意的字符. 在方法属性中, "{1}" 代表了前面*中代表的内容. When we cite actions like "Login_cancel" or "Login_input", the framework matches "cancel" or "input" with the wildcard and fills in the blanks.

The "trailing bang" notation was hardwired into WebWork 2. To provide backward compatibility, the notation is supported by Struts 2.0. If you prefer to use wildcards to emulate the same notation, as the Mailreader does, you should disable the old notation in the Struts properties file.


struts.properties
struts.enable.DynamicMethodInvocation = false

Using wildcards with a exclamation point (or "bang") is not the only way we can use wilcards to invoke methods. If we wanted to use actions like "inputLogin", we could move the asterisk and use an action name like "*Login".

Within the Login action element, the first result element is named "input". If validation or authentification fail, the Action class will return "input" and the framework will transfer control to the "Login.jsp" page.

The second result element is named cancel. If someone presses the cancel button on the Login page, the Action class will return "cancel", this result will be selected, and the framework will issue a redirect to the Welcome action.

The third result has no name, so it will be called if the default success token is returned. So, if the Login succeeds, control will transfer to the MainMenu action.

The MailReader DAO exposes a "ExpiredPasswordException". If the DAO throws this exception when the User logs in, the framework will process the exception-mapping and transfer control to the "ChangePassword" action.

Just in case any other Exceptions are thrown, the MailReader application also defines a global handler.


mailreader-default.xml exception-mapping

  

如果一个没有预料到的异常被抛出, exception-mapping 会将控制传给 action的 "error" result, 或者全局的 "error" result. MailReader 定义了一个全局 "error" result 将其跳转至 "Error.jsp" 页面来显示错误信息.


Error.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>
  
  
  
    Unexpected Error
  
  
    

An unexpected error has occured

Please report this error to your system administrator or appropriate technical support personnel. Thank you for your cooperation.


Error Message


Technical Details


Error 页面使用了property标签来显示异常信息.

在最后, Login action 指定了一个 InterceptorStack 为 defaultStack. 如果之前使用过 Struts 2 或 WebWork 2 , 你会感觉奇怪, 因为 "defaultStack" 是初始的默认值.

在 MailReader应用中,大多数action只对通过身份验证的用户提供服务,出了 Welcome, Login, 和 Register action是向任何人提供的。 为了验证客户端请求的合法性, MailReader 使用了一个自定义的Interceptor 和 Interceptor stack.


mailreader2.AuthenticationInterceptor
package mailreader2;
import com.opensymphony.xwork2.interceptor.Interceptor;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.Action;
import java.util.Map;
import org.apache.struts.apps.mailreader.dao.User;

public class AuthenticationInterceptor implements Interceptor {
  public void destroy () {}
  public void init() {}
  public String intercept(ActionInvocation actionInvocation) throws Exception {
    Map session = actionInvocation.getInvocationContext().getSession();
    User user = (User) session.get(Constants.USER_KEY);
    boolean isAuthenticated = (null!=user) && (null!=user.getDatabase());
    if (isAuthenticated) {
      return actionInvocation.invoke();
    }
    else {
      return Action.LOGIN;
    }
  }
}

AuthenticationInterceptor 查找user对象是否被保存在客户端对应的session中. 如果是,则正常返回,下一个interceptor会被调用.如果user对象不存在,则返回 "login". Struts会在全局的result中匹配"login" 然后跳转至 Login action.

MailReader 定义了三个自定义的Interceptor stacks: "user", "user-submit", 和 "guest".


mailreader-default.xml interceptors

  
  
      
      
  
  
      
      
  
  
      
  

<default-interceptor-ref name="user"/>

user stacks 要求客户端通过了认证. 就是说, User 对象应该在session中.action使用了guest stack 则可以被任意客户端访问. The -submit versions of each can be used with actions with forms, to guard against double submits.

Double Submits

一个常见的web应用中的问题是用户时常没有耐心.有时候, 用户会多按一次提交按钮,则浏览器会再一次提交请求,以至于提交了两次做同样事情的请求. 比如在注册用户时,如果多点了一次提交按钮, 又赶上巧合,则会显示该用户已经被注册了. (在第一次提交时.) 实际中这可能不会发生,但是在一个长时间的处理过程中, 如检查购物车,就容易发生二次提交.

为了防止二次提交,和后退按钮导致的重新提交, Struts会生成一个嵌入在form里面的token保存在session里.如果token的值不一样, 则表明出现了异常, 即一个form被提交了多次.

Token Session Interceptor 也会尝试提供智能的fail-over 在多次使用相同session的请求中。它会阻挡后来的请求直到之前的请求处理完毕, 然后返回 "invalid.token" , 它会尝试显示相同的原始的有效的action响应.

因为默认的 interceptor stack 会认证客户端, 我们需要指定一个标准的 "defaultStack" 为那些 "guest actions", Welcome, Login, 和Register.要求通过默认的验证是好的做法,因为它意味着我们不会忘记为一个新的action进行拦截. 同时,那些讨厌的用户不会被拒绝使用一些 "guest" 服务.

MainMenu

成功登录后,将会显示Main Menu页面. 如果你使用了默认的账号登录,页面的标题应该是 "Main Menu Options for John Q. User". 下面会有两个连接:

  • Edit your user registration profile
  • Log off MailReader Demonstration Application

我们来看下 "MainMenu" action 映射和 "MainMenu.jsp" 的源码.


Action mapping element for MainMenu

    /pages/MainMenu.jsp
    
MainMenu.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="http://struts.apache.org/tags"  %>

  
  
    <s:text name="mainMenu.title"/>
      " rel="stylesheet"
      type="text/css"/>
  

  
  


"MainMenu.jsp" 中包含了一个新的标签 property, 用来为用户生成不同的页面,根据user中的"fullName" 属性.

MainMenu action能够显示user的fullname是因为继承自MailreaderSupport类.  MailreaderSupport 类有一个User属性让text标签来访问. 如果没有MailreaderSupport, property 标签无法找到User对象来读取fullname.

MainMenu 页面中有两个连接.一个是 "Edit your user registration profile". 另一个是"Logout the MailReader Demonstration Application".

Registraion Page

如果你点击了 "Edit your user registration profile" , 我们最终会来到 MailReader 应用的关键功能: Registration 或者 "Profile" 页面. 这个页面显示了MailReader中你的资料, 其中利用了一些有趣的技术.

为了完成两个任务 "Create" 和 "Edit" , "Registration.jsp"对test标签进行拓展,让它看起来是两个不同的页面.


Registration.jsp - head element

  
    <s:text name="registration.title.create"/>
  
  
    <s:text name="registration.title.edit"/>
  
  " rel="stylesheet"
    type="text/css"/>

例如, 如果客户端要是要进行编辑(task == 'Edit'), 页面会根据user对象插入username. 如果是注册 (task == 'Create'), 页面会有一个空的输入框.


Note:

Presention Logic - "test" 标签是一个在你页面中加入逻辑表达式的便捷方式. Customized pages帮助防止用户误操作,动态生成页面减少服务器需要维护的页面,以及其他好处。


页面中也使用了一些逻辑标签来显示订阅信息给用户. 如果RegistrationForm 的task是 "Edit", 那么页面下方将会显示订阅列表.


"task == 'Edit'">
  


否则, 页面中只包含了用户注册的输入表单。

iterator

除了"if"之外还有一些控制标签用来排序、过滤、迭代所有数据.Registration页面包含了一个使用iterator标签来显示用户的所有订阅信息的例子.


订阅信息被保存在 hashtable 对象中, 而该对象在user对象中保存.所以为了显示每条订阅,我们需要先找到user对象,然后依次读取其中的订阅信息.使用iterator标签,你可以非常快地实现它.


Using iterator to list the Subscriptions

  
    
      
    
    
       
   
  
      
  
  
     
  
  
    Subscription_delete">">
      
     
    Subscription_edit">">
      
     
   
 

当迭代完成后,会生成一个用户的订阅信息表格.


Current Subscriptions

Host Name User Name Server Type Auto Action
mail.hotmail.com user1234 pop3 false Delete   Edit
mail.yahoo.com jquser imap false Delete   Edit
Add

现在再来看看生成这块使用的代码。

注意这样做的好处了吗?

使用将标记写在iterator 标签是不是比为表格的每一行都写代码来得方便?

取代使用一个完整的引用如"value=user.subscription[0].host", 我们使用尽可能简单的 "value=host". 我们没有必要去定义一个局部变量, 然后在这个循环块中引用. 列表中每个项的引用都是自动解决的,不用麻烦.

实现该功能是通过value stack. 不算Interceptor的话, value stack 是struts中最酷的东西. 为了解释value stack, 我们从最初的地方开始讲.

WEB应用的一个关键功能就是将动态数据融入进静态页面中. Java API 有一个机制让你将对象放进servlet作用范围(page, request, session, 或 application), 然后使用JSP脚本来取出和使用它. 如果将对象直接放置到一种作用范围里, JSP标签或scriptlet在通过按顺序搜索page、request、session、application范围来找到这个对象.

value stack 工作原理类似但是进行了改良. 当你在 value stack 放进一个对象, 该对象的公共属性为了这个stack的 first-class properties. 这个对象的属性成为了stack的属性. 如果另一个在stack中的对象有一个相同名字的属性,选择最后进入stack的对象. (Last-In, First-Out.)

当iterator标签在一个集合中遍历时,它将集合的每个元素都压入stack中. 元素的属性则成为了stack的属性. 在订阅信息的例子中, 如果Subscription有一个公共的Host属性,在迭代的时候, stack 可以访问同样的属性.

当然,完成每次迭代后,会将每个元素从stack中移出. 如果我们在之后页面中试图访问Host属性,将不会存在.

当一个Action被调用,这个Action类会进入value stack. 因为 Action在value stack之中,我们的标签可以访问任何action的属性就像它是页面的属性. 标签不需要直接访问这个action. 如果一个textfield标签需要引用"Username"属性,它会去value stack中寻问"Username"的值, 然后 value stack 返回在所有action中找到的第一个属性名相同的.

Validators 也使用的是这个stack. 当一个属性验证失败了, 该属性的值会进入value stack. As a result, if the client enters text into an Integer field, the framework can still redisplay whatever was entered. An invalid input value is not stored in the field (even if it could be). The invalid input is pushed onto the stack for the scope of the request.

订阅列表中有一个新的标签: param标签. As tags go, "param" 中只有非常少的参数: 只有 "name" 和 "value", 且都是必要的. 尽管看似简单, "param" 是Struts提供的最强大的标签之一. 不是因为它能做什么, 而是因为"param" 允许其他标签做什么.

本质上, "param" 标签为其他标签提供了参数. 如 "text"标签可能会需要一些参数来组成一条信息. 无论需要多少参数,无论参数的名字是什么,你都可以使用"param" 标签来获得任何你需要的.

pager.legend = Displaying {current} of {count} items matching {criteria}.
...

    param name="current" value="42" />
    param name="count" value="314" />
    param name="criteria" value="Life, the Universe, and Everything" />

例如在"url"标签中, 我们可以使用 "param" 来创建查询字符串. A statement like this:


  ">

可以生成下面这样的超链接:


  Edit

如果需要更多的参数,你可以使用"param" 来增加你需要的参数.


Subscription


如果在Registration页面点击任何订阅信息条中的"Edit", 会来到Subscriptions页面, 显示了订阅信息的细节的输入表单.先来看一下Subscription的配置.


mailreader-support.xml Subscription element

  /pages/Subscription.jsp
  Registration_input

The Edit link specified the Subscription action, but also includes the qualifier _edit. The wildcard notation tells the framework to use any characters given after "Subscription_" as the name of a method to invoke on the Action class, instead of the default execute method. The "alternate" execute methods are called alias methods.


Subscription edit alias
public String edit() {
  setTask(Constants.EDIT);>
  return find();
}

public String find() {
  org.apache.struts.apps.mailreader.dao.Subscription
    sub = findSubscription();
   if (sub == null) {
       return ERROR;
   }
   setSubscription(sub);
   return INPUT;
}

"edit" 有两个职责. 首先, 将Task属性设为"Edit".  Subscription 页面会根据Task的值生成不同的显示. 然后, "edit" 需要找到对应的Subscription然后将它放进Subscription属性. 如果一起正常, "edit"会返回INPUT, 然后input" result 会被调用.

正常情况下,Subscription 总是可以找到.因为我们的选择是根据系统生成的列表.如果Subscription没有找到, 可能是因为数据库无法连接或请求是通过伪造的, edit 会返回全局的 "error" result, 因为这是非预期的异常.

"edit" 的业务逻辑封装在MailReader DAO 类中.


MailreaderSupport findSubscription()
public Subscription findSubscription() {
    return findSubscription(getHost());
}

public Subscription findSubscription(String host) {
    Subscription subscription;
    subscription = getUser().findSubscription(host);
    return subscription;
}

代码非常简单而且没有看到进行错误处理的逻辑. 但是没关系. 因为页面是从我们生成的连接跳转而来的,我们期望一切都是正常. 但是如果出错了,定义在配置文件中的全局的异常处理会帮助我们处理.

同样, AuthentificationInterceptor会确保只有通过认证的用户才有资格来访问.如果session过期了,客户端会自动跳转至Login页面.

作为最后一层保护措施,我们也为Subscription配置了一个验证, 确定Host参数是合法的.

Subscription-validation.xml


  
    
        
    
  

通过将常规的保护措施与action分离,使得所有的action变得更小更容易维护.

在将对应的Subscription对象存入Subscription属性后,Struts将转至Subscription页面.


Subscription.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="http://struts.apache.org/tags" %>


  
    
        <s:text name="subscription.title.create"/>
    
    
        <s:text name="subscription.title.edit"/>
    
    
        <s:text name="subscription.title.delete"/>
    
    " rel="stylesheet"
          type="text/css"/>
  
  

    
    action="Subscription_save" validate="true">
      
      
      

      
        
      
      
        
        
      

      
        
        
        
        
        
      
      
        
        
        
        
        
        
      

      
  

  
  

又出现了几个新的标签: "token", "hidden", "label", "select", 和 "checkbox".

token 标签和 Token Session Interceptor配合使用来防止两次提交. 该标签会生成一个key嵌入form中存在session里. 没有这个标签,Token Session Interceptor就无法工作了.

hidden 标签 将Task属性嵌入在form中.当form被提交, Subscription_save action 会根据Task属性来决定是进行插入还是更新操作.

label 将属性显示在表单中以只读的形式. 在 Edit或者 Delete模式, 我们希望Host属性是不可变的 . Delete模式所有属性都是不可变的.


比较关键的标签是 "select" 和 "checkbox".

显然,select标签用来进行选择,且不需要很多繁琐的标记.

list="types" />

 "select" 标记中有一个有趣的属性"list",在上例中的值为"types". 仔细查看Subscription action,会发现它实现了Preparable 接口并且在"prepare"方法中为types属性赋值.


Subscription-validation.xml
public class Subscription extends MailreaderSupport
  implements Preparable {

  private Map types = null;
  public Map getTypes() {
    return types;
   }

   public void prepare() {
     Map m = new LinkedHashMap();
       m.put("imap", "IMAP Protocol");
       m.put("pop3", "POP3 Protocol");
       types = m;
       setHost(getSubscriptionHost());
    }

    // ... 

默认的拦截器栈中包含了 PrepareInterceptor, 用来对应Preparable接口.


PrepareInterceptor
public class PrepareInterceptor extends AroundInterceptor {

  protected void after(ActionInvocation dispatcher, String result) throws Exception {
  }

  protected void before(ActionInvocation invocation) throws Exception {
    Object action = invocation.getAction();
     if (action instanceof Preparable) {
        ((Preparable) action).prepare();
    }
  }
}

PrepareInterceptor 确保"prepare" 方法总会在action执行之前调用.我们使用"prepare"来确定list如何显示. 还将Subscription对象中的host属性进行局部化保存,使之更容易管理.

Subscription.java

如同许多的应用,MailReader大部分使用的是String类型的属性. 除了Subscription对象中的AutoConnect 属性.在HTML表单中, AutoConnect 属性通过一个单选框来表示.

在开发一个web应用时,checkbox 会是一个繁琐的东西. Subscription 对象有一个布尔类型的属性AutoConnect,通过用一个checkbox来简单的代表它的状态.问题是如果checkbox为空,浏览器不会提交任何信息就像checkbox是不存在的.HTTP 协议没有表示 "false"的方式. 如果控件丢失了,我们需要知道它是没有被点击.

在Struts 1, 我们使用reset方发解决checkbox问题.在 Struts 2, checkbox状态是自动处理的.Struts2会检测checkbox标签是否被传回,如果没有就会为checkbox赋值为"false" .

当按下SAVE按钮,表单会被提交至 Subscription_save action. 因为 save 方法需要一些额外的验证,我们可以增加一个验证文件.


Subscription-Subscription_save-validation.xml


  
    
        
    
  

The validators follow the same type of inheritance path as the classes. SubscriptionSave extends Subscription, so when Subscription_save is validated, the Host property specified by "Subscription-validation.xml" will also be required.

If validation succeeds, the save method of Subscription will fire.


Subscription
public String save() throws Exception {

  if (Constants.DELETE.equals(getTask())) {
   removeSubscription();
  }

  if (Constants.CREATE.equals(getTask())) {
    copySubscription(getHost());
  }

  saveUser();
  return SUCCESS;
}

save 方法使用Task属性来选择进行创建还是删除,然后在user对象中进行更新.

removeSubscription 方法 calls the DAO facade, and then updates the application state.


removeSubscription
public void removeSubscription() throws Exception {
  getUser().removeSubscription(getSubscription());
  getSession().remove(Constants.SUBSCRIPTION_KEY);
}

copySubscription method is a bit more interesting. The MailReader DAO layer API includes some immutable fields that can't be set once the object is created. Because key fields are immutable, we can't just create a Subscription, let the framework populate all the fields, and then save it when we are done -- because some fields can't be populated, except at construction.

One workaround would be to declare properties on the Action for all the properties we need to pass to the Subscription or User objects. When we are ready to create the object, we could pass the new object values from the Action properties.

Another workaround is to declare only the immutable properties on the Action, and then use what we can from the domain object.

This implementation of the MailReader utilizes the second alternative. We define User and Subscription objects on our base Action, and add other properties only as needed.

To add a new Subscription or User, we create a blank object to capture whatever fields we can. When this "input" object returns, we create a new object, setting the immutable fields to appropriate values, and copy over the rest of the properties.


copySubscription
public void copySubscription(String host) {
  Subscription input = getSubscription();
  Subscription sub = createSubscription(host);
  if (null != sub) {
    BeanUtils.setValues(sub, input, null);
    setSubscription(sub);
    setHost(sub.getHost());
  }
}

当然这不是一个最好的解决方法,但是一个简单的可行方法.

Subscription Submit

当我们按下SAVE 按钮后, 这里有一个步骤被我们忽视了. Mailreader 应用使用了一个 "double submit" 防护来防止SAVE 按钮被多次点击和提交.

为了增加the double-submit防护,我们可以改变action的默认处理stack为user-submit. 但是, 我们不想仅仅复制粘贴别的action的设置来修改Subscription action. 我们只需要subscription actions 放入我们自己的package中, 这样他们就可以共享result types.


mailreader-support.xml





    
        /Subscription.jsp
        Registration_input
    

    
        
    

    





    
        /{1}.jsp
    


}

完成了成功的save后 , Subscription Action 会返回 "success", Struts会跳转至 Registration input.

Summary

we've booted the application, logged on, reviewed a Registration record, and edited a Subscription. Of course, there's more, but from here on, it is mostly more of the same. The full source code for MailReader is available online and in the distribution.

Enjoy!

你可能感兴趣的:(Struts2)