使用 Click 框架/页面进行 Java Web 应用程序开发
页面是 Web 应用程序的核心。在 Click 中,页面封装了对 HTML 请求的处理和对 HTML 响应的渲染。本节讨论 Click 页面并涵盖以下主题
在 Click 中,逻辑页面由一个 Java 类和一个 Velocity 模板组成,这些组件在 click.xml 文件的页面元素中定义。
<page path="search.htm" classname="com.mycorp.page.Search"/>
path 属性指定页面 Velocity 模板的位置,classname 属性指定页面 Java 类名。
或者,您也可以将 Click 配置为使用 JSP 页面进行渲染。
<page path="search.jsp" classname="com.mycorp.page.Search"/>
类
[edit | edit source]所有自定义 Click 页面都必须是 [click-api/net/sf/click/Page.html 页面] 基类的子类。页面类及其关联的辅助类 Context 和 Control 在下图 1 中描述。
图 1. 页面类图 - 由 Enterprise Architect 创建,由 Sparx Systems 提供
页面类提供了一个模型属性,该属性用于保存页面 Velocity 模板中渲染的所有对象。模型还可以包含 Control 对象,这些对象在页面上提供用户界面控件。
页面还具有关联的 Context 对象,该对象引用与请求关联的所有 javax.servlet 对象。在使用 Click 进行编程时,您可以使用 Context 对象访问 HttpServletRequest 属性、参数和 HttpSession 对象。
页面类提供了一些空处理方法,子类可以覆盖这些方法以提供功能
- onSecurityCheck()
- onInit()
- onGet()
- onPost()
- onRender()
- onDestroy()
ClickServlet 依赖于使用公共无参数构造函数实例化页面,因此在创建页面子类时,您必须确保不添加不兼容的构造函数。
执行
[edit | edit source]页面 GET 请求的执行顺序总结如下,如图 2 所示。
图 2. GET 请求顺序图 - 由 Enterprise Architect 创建,由 Sparx Systems 提供。
逐步执行此 GET 请求顺序,将创建一个新的页面实例,并设置页面的属性(上下文、格式、标头、路径)。接下来,将请求参数值绑定到任何匹配的公共页面字段。
然后onSecurityCheck()处理程序。此方法可用于确保用户有权访问页面,并在必要时可以中止任何进一步的处理。
接下来调用的方法是onInit(),这是您放置任何构造函数后初始化代码的地方。
下一步是处理页面的控件。ClickSerlvet 从页面获取控件列表,然后遍历列表,调用onProcess()。如果任何控件的onProcess()方法返回 false,则中止后续控件的处理和页面的onGet()方法。
如果一切正常执行,则页面的onGet()方法现在被调用。
下一步是渲染页面模板以生成显示的 HTML。ClickServlet 从页面获取模型(地图),然后将以下对象添加到模型中
- 使用字段的名称进行任何公共页面字段
- context - Servlet 上下文路径,例如 /mycorp
- cssImports - 页面标题中要包含的 CSS 导入和样式块。
- format - 用于格式化对象显示的 Format 对象。
- imports - 页面标题中要包含的 CSS 和 JavaScript 导入。
- jsImports - 页面页脚中要包含的 JavaScript 导入和脚本块。
- messages - 页面 getMessage() 方法的消息映射适配器
- path - 要渲染的页面模板的路径
- request - 页面的 HttpServletRequest 对象
- response - 页面的 HttpServletResponse 对象
- session - 用户 HttpSession 的 SessionMap 适配器
然后,它将模板与页面模型合并,并将结果写入 HttpServletResponse。当模型与模板合并时,模型中的任何控件都可以使用其toString()方法进行渲染。
此顺序中的最后一步是调用页面的onDestroy()方法。此方法可用于在页面被垃圾回收之前清理与页面关联的资源。即使在前面的步骤中发生异常,也保证会调用onDestroy()方法。
POST 请求的执行顺序几乎相同,只是onPost()方法在onGet()上被调用。请参阅 [../images/post-sequence-diagram.png POST 请求顺序图]。
下面的活动图说明了页面的另一种执行流程视图。
图 3. 页面执行活动图 - 由 Enterprise Architect 创建,由 Sparx Systems 提供。
请求参数自动绑定
[edit | edit source]Click 会自动将任何请求参数值绑定到具有相同名称的公共页面字段。在绑定这些值时,它还会尝试将它们转换为正确的类型。
理解这一点的最佳方法是逐步浏览一个示例。我们的应用程序接收一个 GET 请求
https://127.0.0.1:8080/mycorp/customer-details.htm?customerId=7203
此请求会由我们的CustomerDetails页面自动处理
package com.mycorp.page;
public class CustomerDetails extends Page
{
public Integer customerId;
}
创建 CustomerDetails 页面后,"customerId" 请求参数值 "7023" 将被转换为 Integer 并分配给公共页面字段customerId.
Click 的另一个特性是,任何公共页面字段都会在页面渲染之前自动添加到页面的模型中。这将使这些值在页面模板中可用以显示。在我们的示例中,公共customerId字段将被添加到页面模型中,并且将在页面模板中可用以进行渲染
我们的 customer-details.htm 页面模板包含
<html>
<body>
Customer ID: $customerId
</body>
</html>
处理完请求后,我们的页面将渲染为
- 客户 ID:7203
自定义自动绑定
[edit | edit source]自动绑定支持将请求字符串参数转换为 Java 类:Integer、Double、Boolean、Byte、Character、Short、Long、Float、BigInteger、BigDecimal、String 和各种 Date 类。
默认情况下,类型转换由 [click-api/net/sf/click/util/RequestTypeConverter.html RequestTypeConverter] 类执行,该类由 ClickServlet 方法 [click-api/net/sf/click/ClickServlet.html#getTypeConverter() getTypeConverter()] 使用。
如果您需要添加对其他类型的支持,则需要编写自己的类型转换器类并对 ClickSerlvet 进行子类化以使用自定义转换器。
例如,如果我们想从数据库中自动加载一个客户对象,当指定客户 ID 请求参数时,您可以编写自己的类型转换器
public class CustomTypeConverter extends RequestTypeConverter
{
private CustomerService customerService = new CustomerService();
/**
* @see RequestTypeConverter#convertValue(Object, Class)
*/
protected Object convertValue(Object value, Class toType)
{
if (toType == Customer.class)
{
return customerService.getCustomerForId(value);
}
else
{
return super.convertValue(value, toType);
}
}
}
此类型转换器将处理以下请求
https://127.0.0.1:8080/mycorp/customer-details.htm?customer=7203
使用给定的 "7203" 客户 ID 值从数据库中加载客户对象。然后,ClickServlet 会将此客户对象分配给匹配的页面字段
package com.mycorp.page;
public class CustomerDetails extends Page
{
public Customer customer;
}
要使自定义类型转换器可用,您需要对 ClickServlet 进行子类化并覆盖getTypeConverter()方法。例如
public class CustomClickServlet extends ClickServlet
{
/**
* @see ClickServlet#getTypeConverter()
*/
protected TypeConverter getTypeConverter()
{
if (typeConverter == null)
{
typeConverter = new CustomTypeConverter();
}
return typeConverter;
}
}
导航
[edit | edit source]使用转发、重定向和设置页面模板路径来实现页面之间的导航。
转发
[edit | edit source]要使用 servlet RequestDispatcher 转发到另一个页面,请设置页面的转发属性。例如,要转发到路径为 index.htm 的页面
/**
* @see Page#onPost()
*/
public void onPost()
{
// Process form post
..
setForward("index.htm");
}
这将调用映射到路径 index.htm 的新页面类实例。请注意,当请求转发到另一个页面时,第二个页面上的控件将不会被处理。这可以防止混淆和错误,例如第二个页面上的表单尝试处理来自第一个页面的 POST 请求。
转发参数传递
[edit | edit source]当您转发到另一个页面时,请求参数将被保留。这是一种通过请求传递状态信息的方法。例如,您可以添加一个客户对象作为请求参数,该参数在转发页面的模板中显示。
public boolean onViewClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
getContext().setRequestAttribute("customer", customer);
setForward("view-customer.htm");
return false;
}
转发到页面模板 view-customer.htm
<html>
<head>
<title>Customer Details</title>
</head>
<body>
<h1>Customer Details</h1>
<pre>
Full Name: $customer.fullName
Email: $customer.email
Telephone: $customer.telephone
</pre>
</body>
</html>
请求属性会自动添加到 Velocity Context 对象中,因此在页面模板中可用。
页面转发
[edit | edit source]页面转发是传递页面之间信息的另一种方式。在这种情况下,您使用 Context createPage() 方法创建要转发的页面,然后直接在页面上设置属性。最后,将此页面设置为将请求转发到的页面。例如
public boolean onEditClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
EditPage editPage = (EditPage) getContext().createPage("/edit-customer.htm");
editPage.setCustomer(customer);
setForward(editPage);
return false;
}
使用createPage()方法创建页面时,请确保在页面路径前添加"/"字符。
您还可以使用其类来指定目标页面,只要页面具有唯一的路径。使用此技术,上面的代码将变为
public boolean onEditClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
EditPage editPage = (EditPage) getContext().createPage(EditPage.class);
editPage.setCustomer(customer);
setForward(editPage);
return false;
}
此页面转发技术是最佳实践,因为它为您提供编译时安全性,并让您不必在代码中指定页面路径。
请始终使用 ContextcreatePage()方法以允许 Click 注入页面依赖项。
转发到新页面的另一种方法是简单地将路径设置为要呈现的新页面模板。使用这种方法,要呈现的页面模板必须具有它需要的一切,而无需创建其关联的页面对象。我们修改后的示例将是
public boolean onViewClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
addModel("customer", customer);
setPath("view-customer.htm");
return false;
}
请注意如何将customer对象通过页面模型传递到模板中。当您转发到另一个页面时,这种使用页面模型的方法不可用,因为第一个页面对象会在创建第二个页面对象之前被“销毁”,任何模型值都会丢失。
重定向是导航页面之间的一种非常有用的方法。
重定向的优点是,它们在用户的浏览器中提供了与他们正在查看的页面匹配的干净 URL。这对于用户想要将页面加入书签时非常重要。重定向的缺点是,它们涉及与用户浏览器进行通信往返,浏览器请求新页面。这不仅需要时间,而且意味着所有页面和请求信息都会丢失。
下面提供了一个重定向到 logout.htm 页面的示例
public boolean onLogoutClick()
{
setRedirect("/logout.htm");
return false;
}
如果重定向位置以“/”字符开头,则重定向位置将以 Web 应用程序的上下文路径为前缀。
例如,如果应用程序部署到上下文“mycorp”,则调用setRedirect("/customer/details.htm")将请求重定向到:"/mycorp/customer/details.htm"
您还可以通过目标页面的类获取重定向路径。例如
public boolean onLogoutClick()
{
String path = getContext().getPagePath(Logout.class);
setRedirect(path);
return false;
}
请注意,使用此重定向方法时,目标页面类必须具有唯一的路径。
重定向的简便方法是在重定向方法中简单地指定目标页面类。例如
public boolean onLogoutClick()
{
setRedirect(Logout.class);
return false;
}
您可以使用 URL 请求参数在重定向的页面之间传递信息。ClickServlet 将使用 HttpServletResponse.encodeRedirectURL(url) 方法为您对 URL 进行编码。
在下面的示例中,用户将点击“确定”按钮以确认付款。该onOkClick()按钮处理程序处理付款,获取付款交易 ID,然后将交易 ID 编码到 URL 中并重定向到 trans-complete.htm 页面。
public class Payment extends Page
{
..
public boolean onOkClick()
{
if (form.isValid())
{
// Process payment
..
// Get transaction id
Long transId = OrderDAO.purchase(order);
setRedirect("trans-complete.htm?transId=" transId);
return false;
}
return true;
}
}
trans-complete.htm 页面的页面类可以通过请求参数获取交易 ID"transId":
public class TransComplete extends Page
{
/**
* @see Page#onInit()
*/
public void onInit()
{
String transId = getContext().getRequest().getParameter("transId");
if (transId != null)
{
// Get order details
Order order = OrderDAO.findOrderByPK(new Long(transId));
if (order != null)
{
addModel("order", order);
}
}
}
}
上面的参数传递示例也是 Post 重定向的示例。Post 重定向技术是防止用户通过点击刷新按钮两次提交表单的非常有用的方法。
Click 支持页面模板(在 Struts 中也称为Tiles),使您可以为您的 Web 应用程序创建标准的外观,并大大减少您需要维护的 HTML 数量。
要实现模板,请定义一个边界模板基本页面,内容页面应扩展该页面。模板基本页面类覆盖 Page getTemplate() 方法,返回要呈现的边界模板的路径。例如
public class BorderedPage extends Page
{
/**
* @see Page#getTemplate()
*/
public String getTemplate()
{
return "/border.htm";
}
}
BorderedPage 模板 border.htm
<html>
<head>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
</head>
<body>
<h2 class="title">$title</h2>
#parse($path)
</body>
</html>
其他页面使用 Velocity #parse 指令将它们的内容插入到此模板中,并将它们的内容页面的路径传递给它。$path 值由 ClickServlet 自动添加到 VelocityContext 中。
下面提供了一个带边框的主页示例
<page path="home.htm" classname="Home"/>
public class Home extends BorderedPage
{
public String title = "Home";
}
主页的内容 home.htm
<b>Welcome</b> to Home page your starting point for the application.
当请求主页(home.htm)时,Velocity 将合并 border.htm 页面和 home.htm 页面,返回
<html>
<head>
<title>Home</title>
<link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
</head>
<body>
<h2 class="title">Home</h2>
<b>Welcome</b> to Home page your application starting point.
</body>
</html>
这可能会被渲染为
主页
欢迎使用您的应用程序的起点主页。
请注意,主页类如何定义一个title模型值,该模型值在border.htm模板中被引用为$title。每个带边框的页面都可以定义自己的标题,该标题在此模板中呈现。
使用相同模式还支持使用 JSP 页面进行模板化。请参阅 Click 示例应用程序以了解演示。
页面提供了一个 onSecurityCheck 事件处理程序,应用程序页面可以覆盖该处理程序以实现编程安全模型。
请注意,您通常不需要使用此功能,并且在可能的情况下,您应该使用声明性 JEE 安全模型。
应用程序可以使用onSecurityCheck()方法来实现自己的安全模型。下面的示例类提供了一个基本的安全页面类,其他页面可以扩展该类以确保用户已登录。在此示例中,登录页面在用户成功验证后创建会话。然后,此安全页面检查用户是否具有会话,否则请求将重定向到登录页面。
public class Secure extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().hasSession())
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
或者,您也可以使用 JEE Servlet Container 提供的安全服务。例如,为了确保用户已通过 Serlvet Container 验证,您可以使用一个安全页面
public class Secure extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().getRequest().getRemoteUser() != null)
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
Servlet Container 还提供设施来强制实施基于角色的访问控制(授权)。下面的示例是一个基本页面,用于确保只有“admin”角色中的用户才能访问该页面,否则用户将被重定向到登录页面。应用程序管理页面将扩展此安全页面以提供其功能。
public class AdminPage extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().getRequest().isUserInRole("admin"))
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
要使用应用程序或基于容器的安全模型注销,您只需使会话失效即可。
public class Logout extends Page
{
/**
* @see Page#onInit()
*/
public void onInit()
{
getContext().getSession().invalidate();
}
}
Click 支持有状态页面,其中页面的状态在用户请求之间保存。有状态页面在许多场景中很有用,包括
- 搜索页面和编辑页面交互,在这种情况下,您从一个可能已应用筛选条件的有状态搜索页面导航到一个对象编辑页面。在编辑页面上完成对象更新后,用户将被重定向到搜索页面,并且有状态筛选条件仍然适用。
- 具有多个表单或表格的复杂页面,需要在交互之间维护其状态。
要使页面有状态,您只需将页面的 [click-api/net/sf/click/Page.html#stateful stateful] 属性设置为 true,并让页面实现Serializable接口。例如
package com.mycorp.page;
import java.io.Serializable;
import net.sf.click.Page;
public class SearchPage extends Page implements Serializable
{
public SearchPage()
{
setStateful(true);
..
}
}
有状态页面实例存储在用户的 HttpSession 中,使用页面的类名作为键。在上面的示例中,页面将使用类名存储在用户的会话中。com.mycorp.page.SearchPage
对于有状态页面,它们只创建一次,之后从会话中检索。但是,页面事件处理程序会为每个请求调用,包括onInit()方法进行渲染。
当您创建有状态页面时,通常会将所有控件创建代码放在 Pages 构造函数中,以便只调用一次,并且不要将此代码放在onInit()方法中,该方法将随每个请求调用。
如果您有动态控件创建代码,通常会将此代码放在onInit()方法中,但您需要确保页面中不存在控件或模型。
默认的 Click 页面执行模型是线程安全的,因为每个请求和线程都会创建一个新的 Page 实例。对于有状态页面,用户将拥有单个页面实例,该实例在多个请求和线程中被重用。为了确保页面执行是线程安全的,用户的页面实例是同步的,因此一次只有一个请求线程可以执行页面实例。
在正常页面实例执行后,它们会解除引用并由 JVM 回收。但是,对于有状态页面,它们存储在用户的HttpSession中,因此需要注意不要在有状态页面实例中存储太多对象,这可能会导致内存和性能问题。
当页面完成执行后,所有 Page 的控件onDestroy()方法都会被调用,然后 Page 的onDestroy()方法会被调用。这是您解除引用任何大型集合或图的机会。例如,Table 控件默认情况下会在其 onDestroy() 方法中解除引用其 rowList。
如果在处理 Page 对象或渲染模板时发生异常,错误将委托给注册的处理程序。默认的 Click 错误处理程序是 ErrorPage,它被自动配置为
<page path="click/error.htm" classname="net.sf.click.util.ErrorPage"/>
要注册替代的错误处理程序,您必须子类化 ErrorPage 并使用路径“click/error.htm”定义您的页面。例如
<page path="click/error.htm" classname="com.mycorp.page.ErrorPage"/>
当 ClickSevlet 启动时,它会检查 click Web 子目录中是否存在 error.htm 模板。如果找不到页面,ClickServlet 将自动部署一个。您可以根据自己的喜好调整 click/error.htm 模板,ClickServlet 不会覆盖它。
当应用程序处于开发或调试模式时,默认的错误模板将显示大量调试信息。示例错误页面显示包括
- NullPointerException - 在页面方法中
- ParseErrorException - 在页面模板中
当应用程序处于生产模式时,只会显示简单的错误消息。
如果 ClickServlet 在click.xml配置文件中找不到请求的页面,它将使用注册的 not-found.htm 页面。
Click 找不到页面被自动配置为
<page path="click/not-found.htm" classname="net.sf.click.Page"/>
您可以覆盖默认配置并指定自己的类,但不能更改路径。
当 ClickSevlet 启动时,它会检查 click Web 子目录中是否存在 not-found.htm 模板。如果找不到页面,ClickServlet 将自动部署一个。
您可以根据自己的需要调整 click/not-found.htm 模板。此页面模板可以访问通常的 Click 对象。
Page 类提供了一个 messages 属性,它是一个 MessagesMap,包含该页面的本地化消息。当页面以键messages渲染时,这些消息在 VelocityContext 中可用。例如,如果您有一个页面标题消息,您将在页面模板中访问它,如下所示。
<h1> $messages.title </h1>
此消息映射从页面类的属性包加载。例如,如果您有一个页面类com.mycorp.page.CustomerList,您可以拥有一个包含页面本地化消息的关联属性文件
/com/mycorp/page/CustomerList.properties
您还可以定义一个应用程序全局页面消息属性文件
/click-page.properties
在此文件中定义的消息将通过您的应用程序中的所有页面提供。请注意,在页面类属性文件中定义的消息将覆盖应用程序全局页面属性文件中定义的任何消息。
页面消息也可用于覆盖控件消息。