Java MVC 1.0 入门手册(三)
七、深入的 Java MVC:第二部分
在这一章中,我们将继续深入探讨 Java MVC。我们将讨论一些与上一章讨论的主题相比你会遇到的不太频繁的主题,但是根据具体情况,这些主题可能对你的项目很重要。这包括 bean 验证、可注入上下文、部分页面更新和观察者类。我们还加深了对状态处理的了解,并且包括了一些配置主题。
添加 Bean 验证
JSR 380 规范描述了 Bean 验证(版本 2.0)。完整规格可从 https://jcp.org/en/jsr/detail?id=380 下载。
这项技术是关于由注释定义的约束。您可以添加检查来确定字段或方法参数是否为空,数字是否超过某个下限或上限,字符串的大小是否在某个范围内,日期是在过去还是未来,等等。您甚至可以定义自己的自定义注释来检查某些参数或字段。
我们不讨论 bean 验证的所有可能性——规范和互联网上的许多教程可以告诉你更多。我们将讨论 bean 验证在 Java MVC 中的位置,我们将介绍一些您经常使用的内置约束,以及一些自定义约束。
在 Java MVC 中,你可以很容易地在表单旁边使用 bean 验证,并在控制器内部查询参数。如果您有约束,比如@ CONSTRAINT1、@ CONSTRAINT2等等(我们很快会谈到可能的值和约束参数),您可以使用以下任何一个:
public class SomeController {
// constraints for fields:
@MvcBinding @FormParam("name")
@CONSTRAINT1
@CONSTRAINT2
...
private String formParam; // or other type
// or, for query parameters:
@MvcBinding @QueryParam("name")
@CONSTRAINT1
@CONSTRAINT2
...
private String queryParam; // or other type
// or, in controller action:
@POST
@Path("/xyz")
public Response someMethod(
@MvcBinding @FormParam("name")
@CONSTRAINT1
@CONSTRAINT2
...
String name )
{
...
}
// or, for query parameters:
@GET
@Path("/xyz")
public Response someMethod(
@MvcBinding @QueryParam("name")
@CONSTRAINT1
@CONSTRAINT2
...
String name )
{
...
}
}
任何违反都将作为错误被转发到注入的BindingResult:
@Controller
@Path("/xyz")
public class SomeController {
@Inject BindingResult br;
...
}
例如,如果我们希望将表单参数字符串限制为多于两个但少于十个字符,我们编写以下代码:
@Controller
@Path("/xyz")
public class SomeController {
@Inject BindingResult br;
@MvcBinding @FormParam("name")
@Size(min=3,max=10)
private String formParam;
...
}
表 7-1 中定义了最有趣的内置 bean 验证约束。
表 7-1
内置 Bean 验证约束
|名字
|
描述
|
| --- | --- |
| @Null | 检查该值是否为null。 |
| @NotNull | 检查该值是否不是null。 |
| @AssertTrue | 检查布尔值是否为true。 |
| @AssertFalse | 检查布尔值是否为false。 |
| @Min(min) | 检查数值(short、int、long、BigDecimal或BigInteger)是否大于或等于提供的参数。 |
| @Max(max) | 检查数值(short、int、long、BigDecimal或BigInteger)是否小于或等于提供的参数。 |
| @Negative | 检查数值(short、int、long、BigDecimal或BigInteger)是否小于零。 |
| @NegativeOrZero | 检查数值(short、int、long、BigDecimal或BigInteger)是否小于或等于零。 |
| @Positive | 检查数值(short、int、long、BigDecimal、BigInteger)是否大于零。 |
| @PositiveOrZero | 检查数值(short、int、long、BigDecimal、BigInteger)是否大于或等于零。 |
| @Size(min=minSize, max=maxSize) | 检查字符串值的长度是否在指定的界限之间。两个边界都是可选的;如果省略,则假定为0或Integer.MAX_VALUE。例子:@Size(max=10)表示十号或十号以下。 |
| @NotEmpty | 检查该值是否不为空。对于字符串,这意味着字符串长度必须大于0。 |
| @NotBlank | 检查字符串值是否包含至少一个非空白字符。 |
| @Pattern(regexp=regExp,``flags={f1,f2,...}) | 检查字符串值是否与给定的正则表达式匹配。可选的flags参数可以是控制匹配的javax.validation.constraints.Pattern.Flag.*常量列表,例如不区分大小写。对于注释来说,如果列表中只有一个元素,可以省略{ }。 |
| @Email(regexp=regExp,``flags={f1,f2,...}) | 检查字符串值是否表示电子邮件地址。可选的regexp和flags参数指定了一个附加模式,与@Pattern约束的含义相同。 |
您可以看到,对于 float 或 double 值,没有 min 或 max 检查。这些是故意漏掉的。由于可能的精度误差,这些类型的检查不能可靠地执行。
也可以定义自己的 bean 验证器。例如,由于缺少双值边界检查,您可能想要定义一个双精度(浮点)范围验证器(包括一些精度宽限)。对于这样的注释,您应该编写以下内容:
package book.javamvc.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
@Constraint(validatedBy = FloatRangeValidator.class)
@Target({ PARAMETER, FIELD })
@Retention(RUNTIME)
public @interface FloatRange {
String message() default
"Value out of range [{min},{max}]";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value() default { };
double min() default -Double.MAX_VALUE;
double max() default Double.MAX_VALUE;
double precision() default 0.0;
}
重要部分如下:
-
validatedBy = FloatRangeValidator.class实现类,请参见下一个代码部分。
-
@Target我们希望允许对字段和方法参数进行这种注释。
-
@Retention(RUNTIME)RUNTIME在这里很重要,所以注释不会在编译过程中丢失。 -
message()验证失败时显示的消息,带有参数的占位符。
-
value()如果没有命名参数,这是默认参数。我们想要引入三个命名参数——
min、max和precision——所以我们不使用默认参数。 -
min(), max(), precision()作为方法的三个命名参数。
-
groups(), payload()这里不用。
实现类检查代码,如下所示:
package book.javamvc.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FloatRangeValidator implements
ConstraintValidator<FloatRange, Number> {
private double min;
private double max;
private double precision;
@Override
public void initialize(FloatRange constraint) {
min = constraint.min();
max = constraint.max();
precision = constraint.precision();
}
@Override
public boolean isValid(Number value,
ConstraintValidatorContext context) {
return value.doubleValue() >=
(min == -Double.MAX_VALUE ? min :
min - precision)
&& value.doubleValue() <= (max == Double.MAX_VALUE ?
max : max + precision);
}
}
被覆盖的isValid()方法执行实际的验证。在这种情况下,我们必须确保精度宽限不会应用于默认值+/- Double.MAX_VALUE。
为了向 Java MVC 控制器添加新的约束,我们使用了与内置约束相同的方法:
...
import book.javamvc.validation.FloatRange;
...
@Path("/abc")
@Controller
public class SomeController {
@MvcBinding @FormParam("theDouble")
@FloatRange(min=1.0, max=2.0, precision = 0.000001)
private double theDouble;
...
}
作为另一个使用value注释缺省参数的定制 bean 验证器,考虑一个只允许特定集合中的字符串值的检查。我们称之为StringEnum,其代码如下:
package book.javamvc.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
@Constraint(validatedBy = StringEnumValidator.class)
@Target({ PARAMETER, FIELD })
@Retention(RUNTIME)
public @interface StringEnum {
String message() default
"String '${validatedValue}' not inside {value}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value() default { };
}
这一次,没有引入命名参数,只有默认的value属性。实现如下所示:
package book.javamvc.validation;
import java.util.Arrays;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class StringEnumValidator implements
ConstraintValidator<StringEnum, String> {
private String[] val;
@Override
public void initialize(StringEnum constraint) {
this.val = constraint.value();
}
@Override
public boolean isValid(String value,
ConstraintValidatorContext context) {
return Arrays.asList(val).contains(value);
}
}
因为只有一个默认参数,所以我们不需要使用它的名称:
...
import book.javamvc.validation.StringEnum;
...
@Path("/abc")
@Controller
public class SomeController {
@MvcBinding @FormParam("fruit")
@StringEnum({"grape", "apple", "banana"})
private String fruit;
...
}
到目前为止,对于验证失败消息,我们已经看到了用于注释默认值的形式为{paramName}或{value}的命名参数占位符,以及用于检查值的表达式语言结构${validatedValue}。在国际化的应用中,如果我们能够添加对本地化消息文件的引用,那就更好了。这是可能的,捆绑文件的名字是ValidationMessages.properties。本地化的属性文件具有以下名称:
ValidationMessages.properties (default)
ValidationMessages_en.properties (English)
ValidationMessages_fr.properties (French)
ValidationMessages_de.properties (German)
...
在 Gradle 项目布局中,你应该把它们放在src/main/resources文件夹中。在属性文件中,您可以编写如下消息:
myapp.user.name.error = Invalid User Name: \
${validatedValue}
myapp.user.address.error = Invalid Address
...
在 bean 验证注释的 message 方法中,使用花括号和属性键名:
String message() default
"{myapp.user.name.error}";
Note
像这样的资源包属于 JRE 标准。使用Validation-Messages作为基本名称是 bean 验证技术的惯例。
可注射环境
在 Java MVC 控制器类中,我们可以使用几个上下文对象。基本上有两种方法可以访问它们。首先,我们可以在类实例级别使用 CDI 提供的@Inject注释,如下所示:
...
import javax.servlet.http.HttpSession;
import javax.mvc.MvcContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletContext;
import javax.mvc.binding.BindingResult;
import javax.ws.rs.core.Application;
import javax.enterprise.inject.spi.BeanManager;
...
@Controller
public class SomeController {
// Access to the session. You can use it to retrieve
// the session ID, the creation time, the last
// accessed time, and more.
@Inject private HttpSession httpSession;
// Access to the MVC context. This is a context
// object provided by Java MVC. You can use it to
// construct URIs given the simple controller name
// and method name, to retrieve the current
// request's locale, to look up the base URI, and
// more.
@Inject private MvcContext mvcContext;
// Access to the current servlet request. You can use
// it to get various HTTP request related properties,
// like headers, user information, and many more.
@Inject private HttpServletRequest httpServletRequest;
// Access to the servlet context. There you can for
// example get the URI of a resource file, or an
// info about the server (container), and more.
@Inject private ServletContext servletContext;
// Use this to fetch conversion and validation errors.
// Parameters (@FormParam or @QueryParam) must have
// been marked with @MvcBinding for this error
// fetching process to work.
@Inject private BindingResult bindingResult;
// Use this to access the application scope
// Application object. You can for example register
// and retrieve application-wide custom properties.
@Inject private Application application;
// In case you ever need to have programmatic access
// to CDI, you can inject the BeanManager. This can
// also be handy for diagnostic purposes.
@Inject private BeanManager beanManager;
...
}
其次,作为 Java MVC 的一个附加特性,也可以将javax.ws.rs.core.Request和javax.ws.rs.core.HttpHeaders直接注入到控制器方法中:
...
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Request;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
...
@Controller
public class SomeController {
...
@GET // or @POST
public String someMethod(
... query and post parameters...,
@Context HttpHeaders httpHeaders,
@Context Request request)
{
...
}
...
}
在方法的参数列表中的什么位置添加这样的@Context参数并不重要。然后,httpHeaders参数允许访问 HTTP 头值、语言、cookie 值等等。request参数为前提条件和变量提供了帮助器方法(在本书中我们不谈论前提条件和变量)。
关于这种注入类型的更多细节,请参考 API 文档(Jakarta EE、JAX RS 和 Java MVC)。
持续状态
如果您需要在几个请求之间保持状态,那么来自javax.servlet.http包的HttpSession类就是您的朋友。每当用户在浏览器上启动 web 应用时,就会创建一个HttpSession实例。一旦存在,只要满足以下所有条件,完全相同的会话对象将透明地分配给任何后续的 HTTP 请求/响应周期:
-
用户停留在同一服务器上的同一 web 应用中
-
用户使用相同的浏览器实例(浏览器没有重新启动)
-
由于超时,会话未被容器销毁
-
web 应用没有显式销毁会话
在您的 web 应用中,您通常不需要采取任何预防措施来使用会话。您所要做的就是注册会话范围的 CDI beans:
...
import javax.enterprise.context.SessionScoped;
...
@Named
@SessionScoped
public class UserData {
...
}
@Controller
public class SomeController {
@Inject UserData userData;
// <- same object inside a session
...
}
该容器自动确保在同一个浏览器会话中,每个会话范围的 CDI bean 只使用一个实例。
Note
服务器通过 cookies 透明地维护会话标识,在 URL 查询参数中自动添加会话 id,或者在表单中添加不可见字段。
我们已经知道,要以编程方式访问会话数据,我们可以将会话作为类实例字段注入:
...
import javax.servlet.http.HttpSession;
...
@Controller
public class SomeController {
@Inject private HttpSession httpSession;
...
}
这也是我们可以以编程方式要求会话 ID: httpSession.getId()(一个字符串)的地方。或者我们可以使一个会话无效:httpSession.invalidate()。
会话数据对于 web 应用正常工作可能很重要,但是请记住,对于许多并发工作的 web 用户来说,您也有许多并发活动的会话。因此,如果您在会话存储中存储了许多数据项,那么 web 应用的内存占用将会增加,可能会破坏应用的稳定。
处理页面片段
我们了解到,JSP 视图页面的逐字输出没有进行语法正确性检查。例如,这样一个文件:
<%@ page language="java"
contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix = "c"
uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt"
uri = "http://java.sun.com/jsp/jstl/fmt" %>
This is a JSP generated page. Hello ${userData.name}
实际上是一个正确的 JSP 页面,尽管它不产生有效的 HTML。输出符合text/plain媒体类型,因此相应的控制器方法可以读取以下内容:
...
import javax.mvc.Controller;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/abc")
@Controller
public class SomeController {
// Assuming the JSP is stored at
// WEB-INF/views/fragm1.jsp
@POST
@Path("/fragm1")
public Response fragm1(...) {
...
return Response.ok("fragm1.jsp").
type(MediaType.TEXT_PLAIN).build();
}
}
您甚至可以将这个响应发送到浏览器客户端,它通常会产生简单的文本输出(至少我所知道的大多数浏览器都是这样)。这样的页面既不包含任何格式指令,也不可能显示任何输入字段,所以问题是应用如何利用这些不完整的输出。
当 MVC 发明的时候,通常的范例是在任何用户提交之后重新加载整个页面,或者如果导航需要的话,加载一个 ?? 的新页面。Web 开发人员从一开始就感到不舒服,即使对结果页面进行非常小的更改,也会导致整个页面在网络上传递。这似乎只是不必要的网络资源浪费。出于这个原因,在 2000 年代中期,AJAX 开始变得越来越流行。AJAX(异步 JavaScript 和 XML)允许浏览器使用 JavaScript 从服务器请求数据,并再次使用 JavaScript 将结果写入页面。为了确保最大的前端可用性,这发生在后台(异步),用户可以在 AJAX 进程仍然活动的时候操作浏览器。
现代高度动态的 web 应用经常使用 AJAX,所以这就引出了一个问题:我们是否也可以在 Java MVC 内部使用 AJAX。
答案是肯定的,因为我们知道我们可以向服务器请求页面片段。所缺少的只是启动 AJAX 服务器请求的几个 JavaScript 函数,然后将来自服务器的结果处理成相应的页面部分。您可以使用普通的 JavaScript 来实现这个目的,但是使用像 jQuery 这样的 JavaScript 库可以方便地消除浏览器差异并简化 AJAX 处理。
例如,我们从第四章的中恢复了HelloWorld应用,并为 AJAX 请求添加了第二个表单和一个显示 AJAX 调用结果的区域。
首先我们添加 jQuery 库,你可以从 https://jquery.com/download/ 下载。任何像样的版本都可以(例子是用版本 3.5.1 测试的)。将文件移动到src/main/webapp/js。
Note
除了 AJAX 之外,jQuery 库还提供了更多的工具功能。您还可以获得查找 HTML 元素、遍历 DOM、操作 HTML 元素等功能。
接下来,我们更新index.jsp以包含 jQuery,并添加一个新表单和一个区域来接收 AJAX 响应:
...
<head>
...
<script type="text/javascript"
src="${mvc.basePath}/../js/jquery-3.5.1.min.js">
</script>
</head>
<body>
...
<form>
<script type="text/javascript">
function submitAge() {
var age = jQuery('#age').val();
var url = "${mvc.uriBuilder(
'HelloWorldController#ageAjax'). build()}";
jQuery.ajax({
url : url,
method: "POST",
data : { age: age },
dataType: 'text',
success: function(data, textStatus, jqXHR) {
jQuery('#ajax-response').html(data);
},
error: function (jqXHR, textStatus,
errorThrown) {
console.log(errorThrown);
}
});
return false;
}
</script>
Enter your age: <input type="text" id="age" />
<button onclick="return submitAge()">Submit</button>
</form>
<div>
<span>AJAX Response: </span>
<div id="ajax-response">
</div>
</div>
...
</body>
...
关于这个 JSP 代码,有几个重要的注意事项似乎是合适的:
-
<div id = "ajax-response">只是一个占位符。一旦 AJAX 调用返回数据,它就会被 JavaScript 填充。 -
JavaScript 函数内部的
${ ... }是一个表达式语言构造,只有当 JSP 引擎看到它时,它才会被正确处理。因此,如果没有进一步的预防措施,您无法将这段 JavaScript 代码导出到一个script.js文件中。在将代码导出到自己的文件之前,您可以将 URL 作为参数添加到函数:function submitAge( url ) { ... }。在onclick = ...事件处理程序声明中,你必须写下onclick = "return submitAge( '${ ... }' )"。 -
该表单从未被提交。这就是为什么它没有一个
action属性,并且onclick处理程序返回false。如果使用 AJAX,实际上并不需要<form>。为了清楚起见,我们在这里添加它。 -
要使用 jQuery 对象,通常要应用快捷符号
$(它与jQuery中的含义相同)。我们不能在 JSP 页面中这样做,因为,在那里,$开始一个 JSP 表达式。 -
为简单起见,AJAX 错误只是写入控制台。在实际应用中,您应该将错误消息放在用户可见的地方。
-
在
<head>脚本标签中,当然要参考你下载的 jQuery 版本。 -
dataType: 'text'指的是返回text/plain数据的 AJAX 调用。如果服务器返回不同的内容,例如 XML 或 JSON,您必须更改它。
您向控制器类添加了一个新的与 AJAX 相关的方法:
@POST
@Path("/ageAjax")
public Response ageAjax(
@MvcBinding @FormParam("age")
int age)
{
if(br.isFailed()) {
br.getAllErrors().stream().
forEach((ParamError pe) -> {
errorMessages.addMessage(
pe.getParamName() + ": " +
pe.getMessage());
});
}
userData.setAge(age);
return Response.ok("ageAjaxFragm.jsp").
type(MediaType.TEXT_PLAIN).build();
}
这里假设我们在控制器类中使用了一个private @Inject UserData userData;字段,并且UserData得到了一个新的age字段:
package book.javamvc.helloworld;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named
@RequestScoped
public class UserData {
private String name;
private int age;
// Getters and setters...
}
我们在第四章的一个练习中介绍了这个类。
src/main/webapp/-WEB-INF/views内的片段页ageAjaxFragm.jsp是从控制器类寻址的。因此,AJAX 请求如下所示:
<%@ page language="java"
contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix = "c"
uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt"
uri = "http://java.sun.com/jsp/jstl/fmt" %>
This is a JSP generated fragment. Your age is: ${userData. age}
观察者
借助于 CDI,Java MVC 提供了一个优雅的观察者机制,您可以使用它来处理横切关注点,比如日志记录、监控和性能测量,或者仅仅用于诊断目的。
您所要做的就是提供一个带有一个或多个方法的 CDI bean 类,这些方法带有一个来自javax.mvc.event包的事件类型的参数。应标有@Observes(在javax.enterprise.event包装内):
package book.javamvc.helloworld.event;
import java.io.Serializable;
import java.lang.reflect.Method;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.event.Observes;
import javax.mvc.event.AfterControllerEvent;
import javax.mvc.event.AfterProcessViewEvent;
import javax.mvc.event.BeforeControllerEvent;
import javax.mvc.event.BeforeProcessViewEvent;
import javax.mvc.event.ControllerRedirectEvent;
@SessionScoped
public class HelloWorldObserver implements Serializable {
private static final long serialVersionUID =
-2547124317706157382L;
public void update(@Observes BeforeControllerEvent
beforeController) {
Class<?> clazz = beforeController.getResourceInfo().
getResourceClass();
Method m = beforeController.getResourceInfo().
getResourceMethod();
System.err.println(this.toString() + ": " +
clazz + " - " + m);
}
public void update(@Observes AfterControllerEvent
afterController) {
System.err.println(this.toString() + ": " +
afterController);
}
public void update(@Observes ControllerRedirectEvent
controllerRedirect) {
System.err.println(this.toString() + ": " +
controllerRedirect);
}
public void update(@Observes BeforeProcessViewEvent
beforeProcessView) {
String view = beforeProcessView.getView();
System.err.println(this.toString() + ": " +
view);
}
public void update(@Observes AfterProcessViewEvent
afterProcessView) {
System.err.println(this.toString() + ": " +
afterProcessView);
}
}
仅此而已。Java MVC 在处理请求的过程中负责调用适当的 observer 方法。
用@SessionScoped标记观察者类并不是观察者类工作的必要条件。但是,如果您需要收集运行时间,如下所示:
package book.javamvc.helloworld.event;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.time.Instant;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.event.Observes;
import javax.mvc.event.AfterControllerEvent;
import javax.mvc.event.BeforeControllerEvent;
@SessionScoped
public class HelloWorldObserver implements Serializable {
private long controllerStarted;
public void update(@Observes BeforeControllerEvent
beforeController) {
controllerStarted = Instant.now().toEpochMilli();
...
}
public void update(@Observes AfterControllerEvent
afterController) {
long controllerElapseMillis =
Instant.now().toEpochMilli()
- controllerStarted;
...
}
...
}
重要的是,我们只有一个跨越多次调用的 observer 类实例,使用会话范围可以确保这一点。如果不需要,可以删除@SessionScoped注释(这与使用@Dependent范围注释是一样的)。
Note
Serializable标记接口是会话范围 CDI bean 正确工作所必需的。如果忽略它,您将得到一个运行时错误消息。
配置
由于 Java MVC 位于 JAX-RS 之上,我们可以使用一个继承自javax.ws.rs.core.Application的类来添加一个条目到 URL 上下文路径:
package any.project.package;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/mvc")
public class App extends Application {
}
这个类是故意空的。上下文路径元素/mvc由注释单独添加。由此产生的 URL 是一个依赖于服务器的路径,加上/mvc,再加上控制器的@Path注释中指定的任何内容。我们在本书中经常使用这种应用配置。
Note
对于 GlassFish,这个依赖于服务器的路径默认为 http://ser.ver.addr:8080/WarName/ ,其中WarName需要替换为部署的 WAR 文件的名称,减去.war文件后缀。
您可以在Application类中指定更多的配置项。这一次,我们重写了getProperties()方法,并编写了以下代码:
package any.project.package;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import javax.mvc.engine.ViewEngine;
import javax.mvc.security.Csrf;
...
@ApplicationPath("/mvc")
public class App extends Application {
@Override
public Map<String,Object>getProperties(){
final Map<String,Object> map = new HashMap<>();
// This setting makes sure view files
// will be looked up at some specified location
// (default is /WEB-INF/views)
map.put(ViewEngine.VIEW_FOLDER,"/jsp/");
// Set a CSRF (cross site request forgery)
// security mode. See Chapter 4 of the
// specification
map.put(Csrf.CSRF_PROTECTION, Csrf.CsrfOptions.OFF); // default
// ...or...
map.put(Csrf.CSRF_PROTECTION, Csrf.CsrfOptions.EXPLICIT);
// ...or...
map.put(Csrf.CSRF_PROTECTION, Csrf.CsrfOptions.IMPLICIT);
// Set CSRF header name. See Chapter 4 of the
// specification. Default is "X-CSRF-TOKEN".
map.put(Csrf.CSRF_HEADER_NAME,
"CSRF-HDR");
return map;
}
}
要添加一个欢迎文件(一个登录页面),再次避免使用web.xml XML 配置文件来简化开发,您可以使用 HTTP 过滤器,如下所示:
package any.project.package;
import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(urlPatterns = "/")
public class RootRedirector extends HttpFilter {
private static final long serialVersionUID =
7332909156163673868L;
@Override
protected void doFilter(final HttpServletRequest req,
final HttpServletResponse res,
final FilterChain chain) throws IOException {
res.sendRedirect("mvc/facelets");
}
}
如果以这种方式使用,一个 URL http://my.server:8080/TheWAR/ (在 GlassFish 上,这对应于/,因为这是基 URL)将发送一个REDIRECT到 http://my.server:8080/TheWAR/mvc/facelets ,这又应该触发例如一个 Java MVC 控制器的@GET注释方法。在这个例子中,来自控制器类的@Path加上来自控制器方法的@Path必须连接到/facelets(记住,前面的mvc/来自前面的应用配置)。
练习
-
练习 1:4 的
HelloWorld应用中的,首先确保使用了一个模型类UserData,然后添加一个名为age的新整数字段。更新视图中的表单,并添加一个名为“你的年龄是多少?”。更新控制器,并应用 bean 验证约束,确保用户输入的年龄大于零。添加错误处理,如第六章所述。还可以通过添加年龄来更新响应页面。 -
练习 2: 在第四章的
HelloWorld应用中,将会话注入控制器。在控制器的showIndex()方法中,将会话 ID 写入System.err。 -
练习 3: 在第四章的
HelloWorld应用中,将头注入到greeting()方法中。将所有请求头写入System.err。 -
练习 4: 在前一个练习中,将年龄输入字段提取到一个新的表单中,并使用 AJAX 响应来自该字段的用户输入(添加一个按钮)。使用 JSON 作为 AJAX 响应(
{"Text" : "Your age is ... " })编写一个页面片段,并让它将响应传递到一个区域<div id = "ajax-response"> </div>。使用 jQuery 作为 JavaScript AJAX 库。 -
练习 5: 在第四章的
HelloWorld应用中,编写一个计算控制器响应时间的观察器。将结果输出到System.err。
摘要
JSR 380 规范描述了 Bean 验证(版本 2.0)。这项技术是关于由注释定义的约束。您可以检查字段或方法参数是否为空,数字是否超过某个下限或上限,字符串的大小是否在某个范围内,日期是在过去还是未来,等等。您甚至可以定义自己的自定义注释来检查某些参数或字段。
在 Java MVC 中,你可以很容易地在表单旁边使用 bean 验证,并在控制器内部查询参数。如果您有约束,比如@ CONSTRAINT1、@ CONSTRAINT2等等,您可以将它们添加到控制器的字段和方法参数中。任何违反都将作为错误被转发到注入的BindingResult中。
我们可以在 Java MVC 控制器类中使用几个上下文对象。基本上有两种方法可以访问它们。首先,我们可以在类实例级别使用 CDI 提供的@Inject注释。第二,作为 Java MVC 的一个附加特性,也可以将javax.ws.rs.core.Request和javax.ws.rs.core.HttpHeaders直接注入到控制器方法中。在方法的参数列表中的什么地方添加这样的@Context参数并不重要。然后,httpHeaders参数允许访问 HTTP 头值、语言、cookie 值等等。request参数为前提条件和变量提供了 helper 方法(本书中我们不谈前提条件和变量)。
关于这种注入类型的更多细节,请参考 API 文档(Jakarta EE、JAX RS 和 Java MVC)。
如果您需要在几个请求之间保持状态,那么来自javax.servlet.http包的HttpSession类就是您的朋友。每当用户在浏览器上启动 web 应用时,就会创建一个HttpSession实例。一旦存在,只要满足以下所有条件,完全相同的会话对象将透明地分配给后续的 HTTP 请求/响应周期:
-
用户停留在同一服务器上的同一 web 应用中
-
用户使用相同的浏览器实例(浏览器没有重新启动)
-
由于超时,会话未被容器销毁
-
web 应用没有显式销毁会话
在您的 web 应用中,您通常不需要采取任何预防措施来使用会话。您所要做的就是通过@SessionScoped注释注册会话范围的 CDI beans。该容器自动确保在同一个浏览器会话中使用每个会话范围的 CDI bean 的恰好一个实例。
会话数据对于您的 web 应用正常工作可能很重要,但是请记住,对于许多并发工作的 web 用户来说,您也有许多并发活动的会话。因此,如果您在会话存储中存储许多数据项,您的 web 应用的内存占用将会增加,可能会使应用不稳定。
我们了解到,JSP 视图页面的逐字输出没有进行语法正确性检查。所以一个文件可以是一个正确的 JSP 页面,即使它不产生有效的 HTML。例如,如果输出符合text/plain媒体类型,相应的控制器方法返回可能如下所示:
return Response.ok("fragm1.jsp" ).type( MediaType.TEXT_PLAIN ).build();
您甚至可以将这个text/plain响应发送到浏览器客户端,这通常会产生简单的文本输出。这样的text/plain页面既不包含任何格式指令,也不可能显示任何输入字段,所以问题是应用如何利用这些不完整的输出。
当 MVC 发明的时候,通常的范例是在任何用户提交之后重新加载整个页面,或者如果导航需要的话,加载一个 ?? 的新页面。Web 开发人员从一开始就感到不舒服,即使对结果页面进行非常小的更改,也会导致整个页面在网络上传递。这似乎是对网络资源不必要的浪费。出于这个原因,在 21 世纪中期,AJAX 开始变得越来越流行。AJAX(异步 JavaScript 和 XML)允许浏览器使用 JavaScript 从服务器请求数据,并再次使用 JavaScript 将结果写入页面。为了确保最大的前端可用性,这发生在后台(异步),用户可以在 AJAX 进程仍然活动的时候操作浏览器。
现代高度动态的 web 应用经常使用 AJAX,所以这就引出了一个问题:我们是否也可以在 Java MVC 内部使用 AJAX。答案是肯定的,因为我们知道我们可以向服务器请求页面片段。所缺少的只是启动 AJAX 服务器请求的几个 JavaScript 函数,然后将来自服务器的结果处理成相应的页面部分。您可以使用普通的 JavaScript 来实现这个目的,但是使用像 jQuery 这样的 JavaScript 库可以方便地消除浏览器差异并简化 AJAX 处理。然后,向控制器类添加一个新的与 AJAX 相关的方法。
借助于 CDI,Java MVC 提供了一个优雅的观察者机制,您可以使用它来处理横切关注点,比如日志记录、监控和性能测量,或者仅仅用于诊断目的。您所要做的就是提供一个带有一个或多个方法的 CDI bean 类,这些方法带有一个来自javax.mvc.event包的事件类型的参数。必须标有@Observes(在javax.enterprise.event包装内)。然后,Java MVC 在处理请求的过程中负责调用适当的 observer 方法。
由于 Java MVC 位于 JAX-RS 之上,我们可以使用一个继承自javax.ws.rs.core.Application的类来添加一个条目到 URL 上下文路径。这个类是故意空的。上下文路径元素/mvc由注释单独添加。您可以在Application类中指定更多的配置项。例如,您可以覆盖getProperties()方法来添加属性。
要添加一个欢迎文件(一个登录页面),再次避免使用web.xml XML 配置文件来简化开发,您可以使用 HTTP 过滤器。
在下一章,我们将讨论 Java MVC 应用的国际化。
八、国际化
Java 通过资源包提供内置的国际化支持。可以在不同语言相关的属性文件中保存不同语言的文本片段。使用标记,还可以以特定于地区的格式输出数字和日期,Java MVC 可以根据地区处理用户输入。
语言资源
在标准 JSP 中,与语言相关的资源由fmt:setBundle和fmt:bundle标记以及fmt:message标记来处理,后者使用key属性来引用包中的文本。例如,您可以编写以下内容:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core"
prefix="c" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>JSTL Bundles</title>
</head>
<body>
<fmt:bundle
basename="book.javamvc.helloworld.messages.Messages">
<fmt:message key="msg.first"/><br/>
<fmt:message key="msg.second"/><br/>
<fmt:message key="msg.third"/><br/>
</fmt:bundle>
</body>
</html>
属性指定了语言文件在文件系统中的位置。
对于 Facelets,通常使用 JSF 方法来访问语言资源。在 JSF 配置文件faces-config.xml中,您编写以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd" version="2.0">
<application>
<resource-bundle>
<base-name>
book.javamvc.helloworld.messages.Messages
</base-name>
<var>msg</var>
</resource-bundle>
...
</application>
</faces-config>
在 JSF/Facelets 页面中,你可以简单地写${msg.MSG_KEY}来引用消息。
对于book.javamvc.helloworld.messages.Messages基础名称(以及 JSP 和 Facelets),在src/main/resources/book/javamvc/helloworld/messages文件夹中,您现在添加这些属性文件:Messages.properties(默认)、Messages_en.properties(英语)、Messages_en_US.properties(英语变体)、Messages_de.properties(德语),等等:
-- File 'Messages.properties':
msg.first = First Message
msg.second = Second Message
msg.third = Third Message
-- File 'Messages_en.properties':
msg.first = First Message
msg.second = Second Message
msg.third = Third Message
-- File 'Messages_de.properties':
msg.first = Erste Nachricht
msg.second = Zweite Nachricht
msg.third = Dritte Nachricht
这些方法可能适合您的需要,您可以自由使用它们。不过,这也有一些缺点:
-
对于 JSP,消息依赖于
fmt:标签库。我们不能编写类似于${msg.first}的东西来访问消息。 -
对于 JSP,您必须使用相当笨拙的语法
<input title = "<fmt:message key = "msg.first" />" />在属性中放置消息。带有语法突出显示的编辑器可能无法应付这种情况。 -
对于 JSP,视图需要知道一些内部的东西,比如语言属性文件的文件位置。通常,视图不应该处理这样的内部问题。
-
对于 Facelets,我们必须混合使用 JSF 和 Java MVC,因为架构范式不匹配,这是我们想要避免的。
在本章的下一节,我们将研究出另一种消息访问方法。
向会话添加本地化消息
如果我们可以编写${msg.KEY}来访问页面上任何地方的本地化消息就好了,对于 JSP 和 Facelets,而不需要进一步的 JSF 配置。为了实现这一点,我们让一个@WebFilter注册一个本地化的资源包作为会话属性:
package book.javamvc.i18n;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
@WebFilter("/*")
public class SetBundleFilter implements Filter {
@Override
public void init(FilterConfig filterConfig)
throws ServletException {
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
BundleForEL.setFor((HttpServletRequest) request);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
对于任何请求都会调用doFilter()方法("/*"是匹配任何请求的 URL 模式),它会将请求发送给BundleForEL类。
定制的 bundle 类从请求中提取会话和语言环境,并在会话的属性存储中注册自己。代码内容如下:
package book.javamvc.i18n;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.servlet.http.HttpServletRequest;
public class BundleForEL extends ResourceBundle {
// This is the variable name used in JSPs
private static final String TEXT_ATTRIBUTE_NAME =
"msg";
// This is the base name (including package) of
// the properties files:
// TEXT_BASE_NAME + ".properties" -> default
// TEXT_BASE_NAME + "_en.properties" -> English
// TEXT_BASE_NAME + "_en_US.properties"
// TEXT_BASE_NAME + "_fr.properties" -> Fench
// ...
private static final String TEXT_BASE_NAME =
"book.javamvc.helloworld.messages.Messages";
private BundleForEL(Locale locale) {
setLocale(locale);
}
public static void setFor(
HttpServletRequest request) {
if (request.getSession().
getAttribute(TEXT_ATTRIBUTE_NAME) == null) {
request.getSession().
setAttribute(TEXT_ATTRIBUTE_NAME,
new BundleForEL(request.getLocale()));
}
}
public static BundleForEL getCurrentInstance(
HttpServletRequest request) {
return (BundleForEL) request.getSession().
getAttribute(TEXT_ATTRIBUTE_NAME);
}
public void setLocale(Locale locale) {
if (parent == null ||
!parent.getLocale().equals(locale)) {
setParent(getBundle(TEXT_BASE_NAME, locale));
}
}
@Override
public Enumeration<String> getKeys() {
return parent.getKeys();
}
@Override
protected Object handleGetObject(String key) {
return parent.getObject(key);
}
}
ResourceBundle的 API 文档包含关于被覆盖方法的详细信息。对我们的目的很重要的是setFor()方法,它将本地化的包注册为会话属性。来自 JSTL 的 EL(和 Facelets)开箱即用,知道如何处理Resource-Bundle对象,因此我们可以编写以下代码:
${msg.MSG_KEY}
<someTag someAttr="${msg.MSG_KEY}" />
要从 JSP 或 Facelets 内部访问本地化的消息,请用属性文件内部使用的消息键替换MSG_KEY。
因为新开发人员很难理解msg指的是什么,所以您应该在每个 JSP 或 Facelets 页面中添加注释,描述msg的来源:
<%-- ${msg} is the localized bundle variable,
registered by class SetBundleFilter --%>
将此用于 Facelets:
<ui:remove> ${msg} is the localized bundle variable,
registered by class SetBundleFilter </ui:remove>
Note
这个<ui:remove> ... </ui:remove>乍一看很奇怪。然而,如果你使用 HTML 注释<!– –>,它们将写入输出。<ui:remove>标签实际上确保了里面的所有东西都将被丢弃用于渲染。
格式化视图中的数据
如果在一个视图页面上,你写下${dbl}并且dbl指的是一个双值数字,那么这个数字的toString()表示就会被打印出来。只有当您的前端用户希望数字采用英语区域设置时,这才是可接受的。为了确保所有其他用户根据他们的国家规则获得预期的数字格式,JSTL 提供了一个 http://java.sun.com/jsp/jstl/fmt 标签库,它使用地区信息收集用于对象格式化的标签。
这个标签库的完整规格可以在 https://docs.oracle.com/javaee/5/jstl/1.1/docs/tlddocs/fmt/tld-frame.html (一行)查看,但是最重要的两个标签是<fmt:formatNumber>和<fmt:formatDate>。在 JSP 页面中使用<fmt:formatNumber>,如下所示:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
...
<%-- Supposing ${dbl1} refers to a float or double --%>
<fmt:formatNumber value="${dbl1}" type="number" var="n1" />
<%-- <= Use Java's DecimalFormat class to format --%>
<%-- the number. Store as string in variable n1 --%>
<fmt:formatNumber value="${dbl1}" type="currency" var="n1" />
<%-- <= Format as currency --%>
<fmt:formatNumber value="${dbl1}" type="percent" var="n1" />
<%-- <= Format as percentage --%>
<fmt:formatNumber value="${dbl1}" type="number"
maxFractionDigits="6"
minFractionDigits="2"
var="n1" />
<%-- <= We can set the minimum and maximum --%>
<%-- number of fraction digits --%>
<fmt:formatNumber value="${dbl1}" type="number"
pattern="#,##0.00;(#,##0.00)"
var="n1" />
<%-- <= Set the pattern according to the --%>
<%-- DecimalFormat API documentation --%>
The number reads: ${n1}
...
</html>
在 Facelets 页面上,您编写了相同的代码,但是使用了不同的头(并且删除了<%ederts注释):
<!DOCTYPE html>
<html lang="en"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt">
...
<fmt:formatNumber value="${dbl1}" type="number"
var="n1" />
...
</html>
表 8-1 中解释了<fmt:formatNumber>的完整属性集。
表 8-1
格式编号标记
|属性
|
需要
|
描述
|
| --- | --- | --- |
| value | x | 价值。使用 EL 语法,如${someBean.someField} |
| type | - | 类型。number、currency或percent中的一种。默认为number。 |
| pattern | - | 格式化模式,如对DecimalFormat类.的描述 |
| currencyCode | - | ISO 4217 货币代码。只有当type = "currency"。 |
| currencySymbol | - | 货币符号。只有当type = "currency"。 |
| groupingUsed | - | 是否使用分组(例如,千位分隔符)。真或假。 |
| minFractionDigits | - | 小数位数的最小值。 |
| maxFractionDigits | - | 分数位数的最大值。 |
| minIntegerDigits | - | 最小整数位数。 |
| maxIntegerDigits | - | 最大整数位数。 |
| var | - | 格式化结果将被写入的变量的名称。如果使用该属性,将禁止直接输出数字。 |
| scope | - | 格式化结果将被写入的变量的范围。page(默认)、application、session或request中的一种。 |
使用fmt:formatDate,可以格式化一个java.util.Date对象。使用各种属性,可以只输出日期部分,或只输出时间部分,或两者都输出,给定一些模式:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
...
<%-- Supposing ${date1} refers to a java.util.Date --%>
<fmt:formatDate value="${date1}" type="date" var="d1" />
<%-- <= Use Java's DateFormat class to format a --%>
<%-- day (ignore the time-of-day) in the user's --%>
<%-- locale default format. --%>
<%-- Store the result in page scope variable "d1" --%>
<fmt:formatDate value="${date1}" type="date"
dateStyle="long"
var="d1" />
<%-- <= Use Java's DateFormat class to format a --%>
<%-- day in the user's locale "long" format --%>
<%-- Instead of "long" you can also write --%>
<%-- "default", "short", "medium", "long" or --%>
<%-- "full" --%>
<fmt:formatDate value="${date1}" type="time"
var="d1" />
<%-- <= Use Java's DateFormat class to format a --%>
<%-- time-of-day (ignore the day) in the user's --%>
<%-- locale default format. --%>
<%-- Store the result in page scope variable "d1" --%>
<fmt:formatDate value="${date1}" type="time" timeStyle="long"
var="d1" />
<%-- Time-of-day in long format. --%>
<%-- Instead of "long" you can also write --%>
<%-- "default", "short", "medium", "long" or --%>
<%-- "full" --%>
<fmt:formatDate value="${date1}" type="both"
var="d1" />
<%-- Write both day and time-of day. Use --%>
<%-- "dateStyle" and "timeStyle" to control the --%>
<%-- day and time-of-day styling as described --%>
<%-- above. --%>
<fmt:formatDate value="${date1}"
pattern="yyyy-MM-dd hh:mm:ss"
var="d1" />
<%-- Write day and/or time, as described by the --%>
<%-- pattern (see class SimpleDateFormat for a --%>
<%-- pattern description). --%>
The date reads: ${d1}
...
</html>
表 8-2 中描述了fmt:formatDate的完整概要
表 8-2
格式日期标签
|属性
|
需要
|
描述
|
| --- | --- | --- |
| value | x | 价值。使用 EL 语法,比如${someBean.someField}。 |
| type | - | 要格式化的部分。date、time或both中的一种。 |
| dateStyle | - | 指定日期样式。默认、short、medium、long或full中的一种。使用 Java 的DateFormat类来指定细节等级。类型必须是date或both。 |
| timeStyle | - | 指定时间样式。默认、short、medium、long或full中的一种。使用 Javaefa DateFormat类来指定细节等级。类型必须是date或both。 |
| pattern | - | 按照模式描述,写下日期和/或时间。模式描述见SimpleDateFormat类。 |
| timeZone | - | 设置时区。API 文档java.util.TimeZone,方法getTimeZone()中描述了格式。直接传递一个TimeZone对象也是可以的。 |
| var | - | 格式化结果将被写入的变量的名称。如果使用该属性,将禁止直接输出。 |
| scope | - | 格式化结果将被写入的变量的范围。page(默认)、application、session或request中的一种。 |
使用 JSF 进行格式化
如果您使用 Facelets 作为视图引擎,并决定忽略我关于混合 Java MVC 和 JSF 的警告,那么声明数字转换器的构造如下所示:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<body>
...
<h:outputText value="${someBean.someField}">
<f:convertNumber type="number"
maxIntegerDigits="5"
maxFractionDigits="5"
groupingUsed="false"/>
</h:outputText>
...
</body>
</html>
所以你必须把<f:convertNumber> 放在文本输出标签里面。
<f:convertNumber>的各种属性与 JSTL 等效物没有太大区别,如表 8-3 所示。
表 8-3
转换号码标签
|属性
|
需要
|
描述
|
| --- | --- | --- |
| type | - | 类型。number、currency或percent中的一种。默认为number。 |
| pattern | - | 格式化模式,如对DecimalFormat类的描述。 |
| currencyCode | - | ISO 4217 货币代码。只有当type = "currency"。 |
| currencySymbol | - | 货币符号。只有当type = "currency"。 |
| groupingUsed | - | 是否使用分组(例如,千位分隔符)。使用true或false。 |
| minFractionDigits | - | 小数位数的最小值。 |
| maxFractionDigits | - | 分数位数的最大值。 |
| minIntegerDigits | - | 最小整数位数。 |
| maxIntegerDigits | - | 最大整数位数。 |
| integerOnly | - | 如果true,小数被忽略。不是true就是false。 |
| locale | - | 用于显示数字的区域设置。要么直接是一个java.util.-Locale对象,要么是一个适合作为Locale构造函数的第一个参数的字符串。 |
为了将java.util.Date对象转换成字符串表示形式,您用 JSF 编写了以下代码:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<body>
...
<h:outputText value="${someBean.someField}">
<f:convertDateTime type="both"
dateStyle="full"
timeStyle="medium" />
</h:outputText>
...
</body>
</html>
毫不奇怪,f:convertDateTime的一组可能属性非常类似于 JSTL 对等词<fmt:formatDate>的属性;见表 8-4 。
表 8-4
convertdatetime tag 转换日期时间标记
|属性
|
需要
|
描述
|
| --- | --- | --- |
| type | - | 要格式化的部分。date、time、both、localDate、localTime、localDateTime、offsetTime、offset- DateTime或zonedDateTime中的一种。默认为date。 |
| dateStyle | - | 指定日期样式。default、short、medium、long或full中的一种。使用 Java 的DateFormat类来指定细节等级。 |
| timeStyle | - | 指定时间样式。default、short、medium、long或full中的一种。使用 Java 的DateFormat类来指定细节等级。 |
| pattern | - | 按照模式描述,写下日期和/或时间。模式描述见SimpleDateFormat类。 |
| timeZone | - | 设置时区。API 文档java.util.TimeZone,方法getTimeZone()中描述了格式。直接路过一个TimeZone对象也是可以的。 |
| locale | - | 用于显示日期/时间的区域设置。要么直接通过一个java.- util.Locale对象,要么通过一个字符串作为Locale构造函数的第一个参数。 |
本地化数据转换
我们已经在 MVC 控制器中为表单参数使用了非字符串类型:
@POST
@Path("/response")
public Response response(
@MvcBinding @FormParam("name") String name,
@MvcBinding @FormParam("userId") int userId) {
@MvcBinding @FormParam("rs") long timeStamp,
@MvcBinding @FormParam("rank") double rank) {
// Handle form input, set model data, ...,
// return response
}
参数类型可以选择String、int、long、float、double、BigDecimal、BigInteger、boolean ( true或false)。如果您选择除了string之外的任何类型,Java MVC 确保用户输入被适当地转换。这种转换以特定于地区的方式进行。在英语语言环境中,用户输入的0.45具有双精度参数的正确格式。例如,在德国地区,必须输入与0,45相同的数字。正确的转换发生在幕后。
目前没有可靠的方法来定义自定义转换器。此外,没有时间和日期的转换器。作为一种变通方法,您可以始终将值作为String传递给控制器,然后以编程方式执行转换。
@POST
@Path("/response")
public Response response(
@MvcBinding @FormParam("day") String day,
@Context HttpHeaders httpHeaders) {
Locale loc = httpHeaders.getLanguage();
// <- You could use this for locale specific
// conversion rules.
DateTimeFormatter formatter1 =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate ld = LocalDate.parse(day,
formatter1);
...
}
练习
-
练习 1: 在第四章的
HelloWorld应用中,将视图文件中的消息放入资源包,并使用“向会话添加本地化消息”一节中描述的方法来访问消息。将资源文件放在src/main/resources/book/javamvc/helloworld/-messages文件夹中。 -
练习 2: 继续上一个练习,使用
App类将常量从BundleForEL类移动到应用属性。将应用对象注入到SetBundleFilter中,并更新BundleForEL.setFor()以从应用对象接收一个捆绑变量名和一个捆绑资源包名。 -
练习 3: 继续上一个练习,向模型类
UserData添加一个双值rank字段。向index.jsp视图添加一个 Rank:标签输入字段,并向greeting.jsp添加相同值的格式化输出。更新控制器类,并向@POST方法添加一个名为double rank的方法参数。 -
练习 4: 继续上一个练习,向名为
UserData的模型类添加一个dateOfBirth字段。向index.jsp视图添加一个生日:标签输入字段,并向greeting.jsp添加一个相同值的格式化输出。更新控制器类,并向@POST方法添加一个名为String dateOfBirth的方法参数。
摘要
Java 通过资源包提供内置的国际化支持。可以在不同语言相关的属性文件中保存不同语言的文本片段。使用标记,还可以以特定于地区的格式输出数字和日期,Java MVC 可以根据地区处理用户输入。
在标准 JSP 中,与语言相关的资源由fmt:setBundle和fmt:bundle标记以及fmt:message标记来处理,后者使用key属性来引用包中的文本。
对于 Facelets,您通常使用 JSF 方法来访问语言资源,在 JSF 配置文件faces-config.xml中,您指定了一个资源包,从此可以在视图内部使用。
尽管这些方法可能符合您的需求,但也有一些缺点。通过使用 web 过滤器和ResourceBundle自定义类,可以提供对语言资源的简化访问。
如果在一个视图页面上你写了${dbl}并且dbl指的是一个双值数字,那么这个数字的toString()表示就会被打印出来。只有当您的前端用户希望数字采用英语区域设置时,这才是可接受的。为了确保所有其他用户根据他们的国家规则获得预期的数字格式,JSTL 提供了一个 http://java.sun.com/jsp/jstl/fmt 标签库,它使用地区信息收集用于对象格式化的标签。
在 Facelets 页面中,您可以编写与 JSP 相同的代码,但是使用不同的头。
我们已经在 MVC 控制器中为表单参数使用了非字符串类型。参数类型可以选择String、int、long、float、double、BigDecimal、BigInteger、boolean(对或错)。如果您选择除了string之外的任何类型,Java MVC 确保用户输入被适当地转换。这种转换以特定于地区的方式进行。在英语语言环境中,用户输入0.45具有双精度参数的正确格式。例如,在德语地区,必须输入与0,45相同的数字。正确的转换发生在幕后。
目前没有可靠的方法来定义自定义转换器。此外,没有时间和日期的转换器。作为一种变通方法,您可以始终将值作为String传递给控制器,然后以编程方式执行转换。
在下一章,我们将讨论 Java MVC 寻址 EJB,这是一种与后端组件通信的标准化方法。
九、Java MVC 和 EJB
企业 Java bean(EJB)是封装业务功能的类,每一个都有特定的种类。然而,与普通的 Java 类不同,EJB 运行在一个容器环境中,这意味着服务器向它们添加系统级服务。这些服务包括生命周期管理(实例化和销毁,何时和如何),事务性(构建逻辑的、原子的、支持回滚的工作单元),以及安全性(哪些用户可以调用哪些方法)。因为 Java MVC 运行在这样一个容器中,即 Jakarta EE,EJB 是 Java MVC 应用封装其业务功能的好方法。
EJB 技术包括会话bean 和消息驱动 bean。然而,后者超出了本书的范围,所以这里我们只讨论会话EJB。
关于会话 EJB
可以本地(在同一个应用中)、远程(通过网络、通过方法调用)或通过某些 web 服务接口(跨异构网络的分布式应用、HTML、XML 或 JSON 数据格式)访问会话 EJB。
关于会话 EJB 的创建和销毁,有三种类型的会话 EJB:
-
Singleton: 使用单例会话 EJB,容器只实例化一个实例,所有客户端共享这个单个实例。如果 EJB 没有歧视客户端的状态,并且并发访问没有造成问题,您可以这样做。
-
无状态:“无状态”类型的 EJB 不维护状态,所以一个特定的客户机可以将不同的实例分配给后续的 EJB 调用(容器处理这一点;客户不知道这个任务)。
-
**有状态:**有状态 EJB 维护一个状态,客户端可以确保它将从容器接收相同的会话 EJB 实例,以便后续使用相同的 EJB。您会经常听说有状态 EJB 客户端维护一个关于使用有状态 EJB 的会话状态。有状态会话 EJB 不能实现 web 服务,因为不允许 web 服务有状态,也不传递会话信息。
定义 EJB
要定义一个单独的 EJB、一个无状态的 EJB 或一个有状态的 EJB,您可以分别向 EJB 实现添加这些注释中的一个——分别是@Singleton、@Stateless或@Stateful。
考虑三个例子。一个名为Configuration的 EJB,用于封装对应用范围配置设置的访问。另一个 EJB 称为Invoice,它处理发票注册和给定发票 ID 的查询。第三个 EJB 调用了TicTacToe来实现一个简单的井字游戏。显然,对于配置 EJB,我们可以使用单一 EJB,因为本地状态和并发性都不重要。类似地,对于发票 EJB,我们可以使用无状态的 EJB,因为状态是通过 ID 来传递的,它不访问 EJB 状态,而是访问数据库状态。最后一个,井字游戏 EJB,需要为每个客户端维护游戏板,因此我们必须为它使用有状态的 EJB。
import javax.ejb.Singleton;
import javax.ejb.Stateless;
import javax.ejb.Stateful;
...
@Singleton
public class Configuration {
... configuration access methods
}
@Stateless
public class Invoice {
... invoice access methods
}
@Stateful
public class TicTacToe {
... tic-tac-toe methods
}
当然,所有这些类必须放在不同的文件中。我们将它们放在一起只是为了说明的目的。
关于它们从客户端代码的可访问性,会话 EJB 可以使用一种或三种方法的组合(所有显示的注释都来自javax.ejb包):
- **无接口:**如果你不想通过接口描述 EJB 访问,你可以使用这个方法。只有在同一应用中运行本地客户端时,这才是可能的。虽然将接口(描述在
interfaces 中完成的和实现(如何实现,在非抽象的classes 中实现)分开对于干净的代码来说通常是一个好主意,但是无接口视图对于简单的 EJB 来说是有意义的。对于无接口 EJB,您只需声明实现,如下所示:
@Stateless public class Invoice {
... implementation
}
当然,EJB 客户端只能直接访问实现类,而不需要中介接口。
- Local: 如果您想要定义对会话 EJB(在同一应用中运行的 EJB 和 EJB 客户端)的本地访问,并且想要为此使用接口视图,您可以用
@Local标记该接口,并让 EJB 实现类实现该接口:
@Local public interface InvoiceInterface {
... abstract interface methods
}
@Stateless public class Invoice
implements InvoiceInterface {
... implementation
}
或者在实现类中使用@Local注释:
public interface InvoiceInterface {
... abstract interface methods
}
@Stateless
@Local(InvoiceInterface.class)
public class Invoice implements InvoiceInterface {
... implementation
}
您甚至可以省略实现,如下所示:
public interface InvoiceInterface {
... abstract interface methods
}
@Stateless
@Local(InvoiceInterface.class)
public class Invoice {
... implementation
}
最后一种方法将进一步降低接口的耦合,尽管一般不建议这样做。
- @Remote: 使用
@Remote注释,以便可以从应用外部访问这个会话 EJB。您可以简单地将@Local替换为@Remote,所有关于本地访问和接口的内容对于远程访问都是真实的。例如,您可以编写以下内容:
public interface InvoiceInterface {
... abstract interface methods
}
@Stateless
@Remote(InvoiceInterface.class)
public Invoice
implements InvoiceInterface {
... implementation
}
EJB 可以有一个本地和一个远程接口;只需同时使用两种注释:
public interface InvoiceLocal {
... abstract interface methods
}
public interface InvoiceRemote {
... abstract interface methods
}
@Stateless
@Local(InvoiceLocal.class)
@Remote(InvoiceRemote.class)
public Invoice
implements InvoiceLocal,
InvoiceRemote {
... implementation
}
此外,没有人阻止我们使用相同的接口进行本地和远程访问:
public interface InvoiceInterface {
... abstract interface methods
}
@Stateless
@Local(InvoiceInterface.class)
@Remote(InvoiceInterface.class)
public Invoice implements InvoiceInterface {
... implementation
}
Caution
远程访问意味着方法调用中的参数是通过值传递的,而不是通过引用!因此,尽管本地和远程接口被声明为相互协同,但是在某些情况下,您必须小心使用方法参数。
访问 EJB
从 Java MVC 控制器访问本地 EJB 很容易:只需使用@EJB注入让 CDI 将实例访问分配给 EJB:
public class SomeController {
...
@EJB
private SomeEjbInterface theEjb;
// or, for no-interface EJBs
@EJB
private SomeEjbClass theEjb;
...
}
与本地访问 EJB 相比,寻址远程 EJB 要复杂得多。您必须设置一个 JNDI 上下文,然后使用它来查找远程实例:
...
String remoteServerHost = "localhost";
// or "192.168.1.111" or something
String remoteServerPort = "3700";
// Port 3700 is part of the GlassFish conf
Properties props = new Properties();
props.setProperty("java.naming.factory.initial",
"com.sun.enterprise.naming."+
"SerialInitContextFactory");
props.setProperty("java.naming.factory.url.pkgs",
"com.sun.enterprise.naming");
props.setProperty("java.naming.factory.state",
"com.sun.corba.ee.impl.presentation.rmi."+
"JNDIStateFactoryImpl");
props.setProperty("org.omg.CORBA.ORBInitialHost",
remoteServerHost);
props.setProperty("org.omg.CORBA.ORBInitialPort",
remoteServerPort);
try {
InitialContext ic = new InitialContext(props);
// Use this to see what EJBs are available
// and how to name them
//NamingEnumeration<NameClassPair> list =
// ic.list("");
//while (list.hasMore()) {
// System.out.println(list.next().getName());
//}
// Looking up a remote EJB
SomeEjbRemote testEJB = (SomeEjbRemote)
ic.lookup(
"book.jakarta8.testEjbServer.SomeEjbRemote");
// Invoking some EJB method
System.out.println(testEJB.tellMe());
}catch(Exception e) {
e.printStackTrace(System.err);
}
此示例假设,在远程服务器端,您创建了一个具有远程接口的会话 EJB:
package book.jakarta8.testEjbServer;
public interface SomeEjbRemote {
String tellMe();
}
还有这样一个实现:
package book.jakarta8.testEjbServer;
import javax.ejb.Remote;
import javax.ejb.Stateless;
@Stateless
@Remote(SomeEjbRemote.class)
public class SomeEjb implements SomeEjbRemote {
@Override
public String tellMe() {
return "Hello World";
}
}
显然,要做到这一点,Java MVC 应用必须能够访问编译后的远程接口。这意味着在 EJB 服务器构建中,您必须以某种方式包含一个从生成的类中提取接口的步骤。我们稍后会详细讨论这一点。
如果远程 EJB 服务器是 GlassFish 服务器,您还可以使用它的asadmin命令来查看哪些 EJB 适合远程访问以及它们是如何命名的:
cd [GLASSFISH_INST]
cd bin
./asadmin list-jndi-entries
其他 Java 企业版(JEE 或 Jakarta EE)应用服务器可能会对远程可访问的 EJB 应用其他命名方案。所以你必须查阅他们的文件和/或获得远程可见的 JNDI 条目列表。对于后者,您可以尝试编程访问(在前面的清单中被注释掉),或者使用为远程 EJB 服务器实现的一些管理特性。
EJB 项目
Jakarta EE 项目不一定是 web 项目;他们还可以向访问其远程 EJB 接口的客户端公开服务。与 REST 或 Web 服务接口一样,web 接口是与 web 浏览器和非 Jakarta EE 服务器进行互操作的首选。但是,对于具有不同网络节点的大型系统中的 Jakarta EE 参与者之间更快的通信,使用组件到 EJB 的通信可能是更好的选择。
Web 项目还可以向适当的客户端公开远程 EJB。如果您想要一个没有 web 功能的流线型项目,那么在 Eclipse 中这样做的过程将在下面的段落中描述。
启动一个新的 Gradle 项目,类似于我们到目前为止创建的 web 项目,但是将插件声明更改为以下内容:
plugins {
id 'java-library'
}
从这里开始,按照描述创建 EJB 及其远程接口,并附加以下约束:将 EJB 接口移动到它们自己的包中。例如:
book.javamvc.ejbproj.ejb <- Implementation
book.javamvc.ejbproj.ejb.interfaces <- Interfaces
在构建文件中,我们添加了一个自动生成 EJB 存根的任务:
task extractStubs (type: Jar, dependsOn:classes) {
archiveClassifier = 'ejb-stubs'
from "$buildDir/classes/java/main"
include "**/interfaces/*.class"
}
jar.finalizedBy(extractStubs)
这确保了在每个jar任务执行之后,存根被创建。然后,您可以运行jar任务来创建完整的 EJB jar 和存根。你可以在build/libs文件夹中找到这两个文件。您可能需要在该文件夹上按 F5 来更新视图。任何希望与 EJB 通信的客户机都必须包含接口 JAR 作为依赖项。当然,EJB 项目本身必须部署在服务器上,EJB 才能工作。
具有依赖性的 EJB
到目前为止,我们只开发了非常简单的 EJB,而不需要使用作为 jar 包含的库。一旦你需要添加库到 EJB,你会遇到麻烦。这是因为没有标准的方法可以将依赖项添加到独立的 EJB 模块中。如果需要添加库 jar,最好的方法是将 EJB 模块打包到企业归档(EAR)中。
ear 是捆绑了 EJB、web 应用(war)和库 jar 的归档。处理 ear 而不是孤立的 EJB 在某种程度上增加了管理活动的复杂性。但是将库 jar 添加到 ear 是包含与非 web 应用的依赖关系的最佳方式。
为了向 Eclipse 中的应用添加 EAR 功能,您基本上必须完成以下工作:
-
建立一个新的 Gradle 项目。去新➤其他...➤·格拉德➤·格拉德项目。
-
选择任何你喜欢的名字。名字后面加“
ear”是个好主意。 -
在
build.gradle内,将plugins { }部分改为plugins { id 'ear' }。 -
在
build.gradle内,用作dependencies { }部分: -
在项目根目录下创建
war和ejb1文件夹。 -
打开
settings.gradle文件并添加以下内容:
dependencies {
deploy project(path: ':war',
configuration: 'archives')
deploy project(path: ':ejb1',
configuration: 'archives')
earlib "org.apache.commons:"+
"commons-math3:3.6.1"
}
-
调用格拉德➤刷新格拉德项目。Eclipse 可能会抛出一条错误消息;你可以暂时忽略它。
-
两个子项目
war和ejb1出现在项目浏览器中。如果您正在使用工作集,您可能需要更新它。 -
将两个子项目转换为多面形式(选择配置➤转换为多面形式...),并在设置中,添加 Java 1.8 功能。
include 'war', 'ejb1'
我们现在有一个包含两个子项目的 EAR 项目。剩下要做的就是给每个子项目添加 Gradle 功能。WAR 项目需要一个构建文件,就像我们用于 Java MVC 项目的许多build.gradle文件中的一个。然而,不同的是,我们向兄弟 EJB 项目添加了一个依赖项:
dependencies {
implementation project(":ejb1")
// Other dependencies...
}
Note
这是为了格雷尔的从属关系。为了让 Eclipse 识别依赖项,您必须将 EJB 项目作为依赖项添加到 Java 构建路径中(选择项目设置➤ Java 构建路径➤项目选项卡)。
对于 EJB 项目,您可能会使用如下的build.gradle文件:
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
jcenter()
}
dependencies {
implementation 'javax:javaee-api:8.0'
// Add dependencies here...
}
如果您运行ear任务,子项目和 EAR 文件将被构建。后者可以在build/libs文件夹中找到。
异步 EJB 调用
EJB 客户端异步调用 EJB 方法。这意味着客户端调用一个标记为适合异步调用的 EJB 方法,立即重新获得对程序执行的控制,并在以后 EJB 调用的结果可用时处理它。
要将 EJB 方法标记为异步调用,可以将javax.ejb包中的@Asynchronous注释添加到方法中:
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Singleton;
@Singleton // Example only, all EJB types work!
public class SomeEjb {
@Asynchronous
public Future<String> tellMeLater() {
// Simulate some long running calculation
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return new AsyncResult<String>(
"Hi from tellMeLater()");
}
}
这个示例 EJB 使用无接口方法,但是异步调用也适用于本地和远程接口。AsyncResult是一个方便的类,允许轻松创建一个Future。这个Future对象不会暴露给客户端;它的主要目的是服从方法签名。返回给客户端的Future将由 EJB 容器透明地创建。
在 EJB 客户端,您像往常一样调用 EJB,并像在 JRE 并发 API 中一样处理从 EJB 调用中收到的Future:
...
@EJB
private SomeEjb someEjb;
...
Future<String> f = someEjb.tellMeLater();
try {
// Example only: block until the result
// is available:
String s = f.get();
System.err.println(s);
} catch (Exception e) {
e.printStackTrace(System.err);
}
计时器 EJB
EJB 可以配备计时器设施,例如用于延迟执行某些任务或重复自动方法调用。你有两种选择:自动定时器和程序定时器。
对于自动定时器,你可以添加一个@Schedule或者@Schedules注释(来自javax.ejb包)到任何一个void方法(可见性无关紧要),或者不带参数,或者带一个javax.ejb.Timer参数。@Schedule注释的参数描述了频率,如下所示:
@Stateless
public class SomeEjb {
@Schedule(minute="*", hour="0", persistent=false)
// every minute during the hour between 00:00 and 01:00
public void timeout1() {
System.err.println("Timeout-1 from " + getClass());
}
}
像“在服务器启动后十秒钟做一次事情”这样的延迟执行是不可能用自动定时器实现的。
以下是您可以在自动计时器中使用的一些示例计划列表:
@Schedule(second="10", minute="0", hour="0")
// <- at 00:00:10 every day
@Schedule(minute="30", hour="0",
dayOfWeek="Tue")
// <- at 00:30:00 on Tuesdays (second defaults to 00)
@Schedule(minute="11", hour="15",
dayOfWeek="Mon,Tue,Fri")
// <- at 15:11:00 on mondays, Tuesdays and Fridays
@Schedule(minute="*/10", hour="*")
// <- every 10 minutes, every hour
@Schedule(minute="25/10", hour="1")
// <- 01:25, 01:35, 01:45 and 01:55
@Schedule(hour="*", dayOfMonth="1,2,3")
// <- every hour at 1st, 2nd and 3rd each month
// (minute defaults to 00)
@Schedule(hour="*/10")
// <- every 10 hours
@Schedule(month="Feb,Aug")
// <- 00:00:00 each February and August
// (hour defaults to 00)
@Schedule(dayOfMonth="1", year="2020")
// <- 00:00:00 each 1st each month during 2020
@Schedule(dayOfMonth="1-10")
// <- 00:00:00 each 1st to 10th each month
@Schedules注释可用于将几个@Schedule规范应用于定时器回调:
@Schedules({
@Schedule(hour="*"),
@Schedule(hour="0", minute="30")
})
private void someMethod(Timer tm) {
...
}
这意味着每隔 x:00:00 (x = 00 到 23),但也包括 00:30:00。除非您也给@Schedule注释赋予一个persistent=false,否则计时器会在应用和服务器重启后继续存在。
计时器也可以通过编程来定义。这里也可以定义一次性拍摄,如下所示:
@Singleton
@Startup
public class Timer1 {
@Resource
private SessionContext context;
@PostConstruct
public void go() {
context.getTimerService().
createSingleActionTimer(5000, new TimerConfig());
}
@Timeout
public void timeout(Timer timer) {
System.err.println("Hello from " + getClass());
}
}
用@Timeout标注的方法在每次定时器触发时被调用。对于本例,这将是在 EJB 创建之后的 5000 毫秒,因为调用了createSingleActionTimer()。您通过context.getTimerService()获得的定时服务支持各种日程安排选项;有关详细信息,请参见 API 文档。
练习
练习 1
以下哪一项是正确的?
-
EJB 必须有一个本地接口和一个远程接口。
-
不提供接口意味着 EJB 被 EJB 容器(Jakarta EE 服务器中处理 EJB 的部分)自动分配给本地和远程接口。
-
远程 EJB 意味着可以从同一服务器上的其他应用访问 EJB。从其他 Jakarta EE 服务器访问是不可能的。
-
EJB 不能有状态。
-
如果客户端访问 EJB,服务器端会创建一个新的 EJB 实例。
-
要从客户端访问任何 EJB,您必须在 JNDI 上下文中使用“执行查找”。
-
为了从客户端使用 EJB,必须将 EJB 的接口及其实现导入到客户端项目中。
练习 2
创建四个项目:
-
一个 JSE 项目(没有 Jakarta EE 功能),有一个单独的
MyDateTime类和一个名为date( String format )的方法,该方法根据作为参数指定的格式字符串以字符串形式返回LocalDateTime。把它变成一个 Gradle 项目。 -
一个 EJB 项目,具有单个 EJB
MyDateTimeEjb以及本地和远程接口。让它使用上面的 JRE 项目生成的 JAR 文件。提示:您可以使用类似于implementation files( '../../- SimpleNoJEE/build/libs/SimpleNoJEE.jar' )的东西来指定一个本地依赖项。 -
一个 EAR 项目,包含 EJB 项目并添加了必要的 JAR 依赖项。
-
一个简单的 no-Jakarta-EE EJB 客户端项目,测试来自
MyDateTimeEjbEJB 的远程接口。提示:将 GlassFish 的lib文件夹中的gf-client.jar作为库依赖项包含进来。
摘要
企业 Java bean(EJB)是封装业务功能的类,每一个都有特定的种类。然而,与普通的 Java 类不同,EJB 运行在一个容器环境中,这意味着服务器向它们添加系统级服务。这些包括生命周期管理(实例化和销毁,何时和如何),事务性(构建逻辑的、原子的、支持回滚的工作单元),以及安全性(哪些用户可以调用哪些方法)。因为 Java MVC 运行在这样一个容器中,即 Jakarta EE,EJB 是 Java MVC 应用封装其业务功能的好方法。
EJB 技术包括会话bean 和消息驱动 bean。会话 EJB 可以本地访问(在同一个应用中),远程访问(通过网络,通过方法调用),或者通过一些 web 服务接口访问(跨异构网络的分布式应用,HTML,XML 或 JSON 数据格式)。
关于会话 EJB 的创建和销毁,有三种类型的会话 EJB。单体 EJB、无状态 EJB 和有状态 EJB。要定义它们中的任何一个,您可以向 EJB 实现添加适当的注释— @Singleton、@Stateless或@Stateful。
关于它们从客户机代码的可访问性,会话 EJB 可以使用一种或三种方法的组合:无接口访问、本地访问或远程访问。
从 Java MVC 控制器访问本地 EJB 很容易:只需使用@EJB注入让 CDI 将实例访问分配给 EJB: @EJB private SomeEjbInterface theEjb。
与本地访问 EJB 相比,寻址远程 EJB 要复杂得多。您必须设置一个 JNDI 上下文,然后使用它来查找远程实例。
为此,Java MVC 应用必须能够访问编译后的远程接口。这意味着,在 EJB 服务器构建中,您必须以某种方式包含一个从生成的类中提取接口的步骤。
Jakarta EE 项目不一定是 web 项目;他们还可以向访问其远程 EJB 接口的客户端公开服务。与 REST 或 Web 服务接口一样,web 接口是与 web 浏览器和非 Jakarta EE 服务器进行互操作的首选。对于具有不同网络节点的大型系统中的 Jakarta EE 参与者之间更快的通信,使用组件到 EJB 通信可能是更好的选择。Web 项目也可以向适当的客户端公开远程 EJB。
一旦需要向 EJB 添加库,最好的方法是将 EJB 模块打包到企业归档(EAR)中。ear 是捆绑了 EJB、web 应用(war)和库 jar 的归档。处理 ear 而不是孤立的 EJB 在某种程度上增加了管理活动的复杂性。但是一旦完成,如果您运行ear任务,子项目和 EAR 文件将被构建。后者可以在build/libs文件夹中找到。
EJB 客户端异步调用 EJB 方法。这意味着客户端调用一个标记为适合异步调用的 EJB 方法,立即重新获得对程序执行的控制,并在以后 EJB 调用的结果可用时处理它。
要为异步调用标记一个 EJB 方法,您可以将来自javax.ejb包的@Asynchronous注释添加到该方法中。
EJB 可以配备计时器设施,例如用于延迟执行某些任务或重复出现的自动方法调用。你有两种选择:自动定时器和程序定时器。
使用自动定时器,你可以添加一个@Schedule或者@Schedules注释(来自javax.ejb包)到任何一个void方法(可见性无关紧要),或者不带参数,或者带一个javax.ejb.Timer参数。@Schedule注释的参数描述了频率。
计时器也可以通过编程来定义。也可以定义一次性调用。
在下一章,我们将学习如何将 Java MVC 连接到数据库。