1. Introduction
模型-视图-控制器(MVC)是构建Web应用的流行设计模式。多年来,它一直是构建现代Web应用的事实标准。
本教程将带你了解如何使用Jakarta EE MVC 2.0构建一个包含Web页面和REST API的Web应用。
2. JSR-371
Jakarta MVC 2.0(前身为JSR 371 MVC 1.0)是一个基于动作的Web框架,构建于Jakarta RESTful Web Services(即JAX-RS)之上。 JSR-371通过额外的注解补充了JAX-RS,让Web应用开发更便捷。
JSR 371(即Jakarta MVC)标准化了Java Web应用的开发方式。主要目标是利用现有的CDI(上下文与依赖注入)和Bean Validation,并支持JSP和Facelets作为视图技术。
目前,Jakarta MVC 2.1规范正在制定中,预计会随Jakarta EE 10一同发布。
3. JSR-371 注解
除了JAX-RS的注解外,JSR-371还定义了一些额外注解。所有这些注解都位于jakarta.mvc.*
包中。
3.1. jakarta.mvc.Controller
@Controller
注解将资源标记为MVC控制器。当用于类时,该类中所有资源方法都成为控制器。同样,在资源方法上使用此注解会使该方法成为控制器。通常,当需要在同一个类中定义MVC控制器和REST API时,在方法上定义@Controller
会很有用。
例如,定义一个控制器:
@Path("user")
public class UserController {
@GET
@Produces("text/html")
@Controller
public String showUserForm(){
return "user.jsp";
}
@GET
@Produces("application/json")
public String getUserDetails(){
return getUserDetails();
}
}
这个类包含一个渲染用户表单的@Controller
方法(showUserForm
)和一个返回用户详情JSON的REST API(getUserDetails
)。
3.2. jakarta.mvc.View
与@Controller
类似,我们可以用@View
注解标记资源类或资源方法。通常,返回void
的资源方法应该使用@View
。带有@View
的类表示类中返回void
的控制器的默认视图。
例如,定义一个带有@View
的控制器:
@Controller
@Path("user")
@View("defaultModal.jsp")
public class UserController {
@GET
@Path("void")
@View("userForm.jsp")
@Produces("text/html")
public void showForm() {
getInitFormData();
}
@GET
@Path("string")
@Produces("text/html")
public void showModal() {
getModalData();
}
}
这里,资源类和资源方法都有@View
注解。控制器showForm
渲染视图userForm.jsp
,而控制器showModal
渲染资源类上定义的defaultModal.jsp
。
3.3. jakarta.mvc.binding.MvcBinding
Jakarta RESTful Web Services会拒绝存在绑定和验证错误的请求。但对Web页面用户来说,这种处理可能不太友好。幸运的是,Jakarta MVC即使发生绑定和验证错误也会调用控制器。通常,用户需要清楚地知道数据绑定错误。
控制器注入BindingResult
以便向用户展示人类可读的验证和绑定错误信息。例如,定义一个带有@MvcBinding
的控制器:
@Controller
@Path("user")
public class UserController {
@MvcBinding
@FormParam("age")
@Min(18)
private int age;
@Inject
private BindingResult bindingResult;
@Inject
private Models models;
@POST
public String processForm() {
if (bindingResult.isFailed()) {
models.put("errors", bindingResult.getAllMessages());
return "user.jsp";
}
}
}
这里,如果用户输入的年龄小于18,用户将被重定向到同一页面并显示绑定错误。user.jsp
页面可以使用表达式语言(EL)获取请求属性errors
并显示在页面上。
3.4. jakarta.mvc.RedirectScoped
考虑一个表单,用户填写并提交数据(HTTP POST)。服务器处理数据后,将用户重定向到成功页面(HTTP GET)。这种模式被广泛称为PRG(Post-Redirect-Get)模式。在某些场景下,我们希望在POST和GET之间保留数据。这时,模型/Bean的作用域需要超出单个请求。
当Bean使用@RedirectScoped
注解时,其状态会超出单个请求。但该状态在POST、重定向和GET完成后销毁。标记为@RedirectScoped
的Bean在POST、重定向和GET完成后被销毁。
例如,假设User
Bean有@RedirectScoped
注解:
@RedirectScoped
public class User
{
private String id;
private String name;
// getters and setters
}
在控制器中注入该Bean:
@Controller
@Path("user")
public class UserController {
@Inject
private User user;
@POST
public String post() {
user.setName("John Doe");
return "redirect:/submit";
}
@GET
public String get() {
return "success.jsp";
}
}
这里,User
Bean在POST和后续的重定向及GET请求中均可用。因此,success.jsp
可以通过EL访问Bean的name
属性。
3.5. jakarta.mvc.UriRef
@UriRef
注解只能用于资源方法。它允许我们为资源方法指定一个名称。在视图中,我们可以使用这些名称来调用控制器,而不是控制器路径URI。
假设有一个用户表单,其中包含一个href
:
<a href="/app/user">Click Here</a>
点击"Click Here"会调用映射到GET /app/user
的控制器。
使用@UriRef
后:
@GET
@UriRef("user-details")
public String getUserDetails(String userId) {
userService.getUserDetails(userId);
}
这里,我们将控制器命名为"user-details"。现在,在视图中我们可以引用这个名称,而不是URI:
<a href="${mvc.uri('user-details')}">Click Here</a>
3.6. jakarta.mvc.security.CsrfProtected
该注解要求调用资源方法时必须进行CSRF验证。如果CSRF令牌无效,客户端将收到ForbiddenException
(HTTP 403)异常。只有资源方法可以使用此注解。
考虑一个控制器:
@POST
@Path("user")
@CsrfProtected
public String saveUser(User user) {
service.saveUser(user);
}
由于控制器有@CsrfProtected
注解,请求只有在包含有效CSRF令牌时才能到达控制器。
4. 构建MVC应用
接下来,我们将构建一个包含REST API和控制器的Web应用。最后,将应用部署到最新版的Eclipse Glassfish。
4.1. 生成项目
首先,使用Maven的archetype:generate
生成Jakarta MVC 2.0项目:
mvn archetype:generate
-DarchetypeGroupId=org.eclipse.krazo
-DarchetypeArtifactId=krazo-jakartaee9-archetype
-DarchetypeVersion=2.0.0 -DgroupId=com.example
-DartifactId=krazo -DkrazoImpl=jersey
上述原型生成了一个包含所需构件的Maven项目,结构如下:
生成的pom.xml
包含jakarta.platform
、jakarta.mvc
和org.eclipse.krazo
依赖:
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.mvc</groupId>
<artifactId>jakarta.mvc-api</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.krazo</groupId>
<artifactId>krazo-jersey</artifactId>
<version>2.0.0</version>
</dependency>
4.2. 控制器
定义控制器用于显示表单、保存用户详情以及获取用户详情的API。首先,定义应用路径:
@ApplicationPath("/app")
public class UserApplication extends Application {
}
应用路径定义为/app
。然后,定义控制器,将用户转发到用户详情表单:
@Path("users")
public class UserController {
@GET
@Controller
public String showForm() {
return "user.jsp";
}
}
在WEB-INF/views
下创建视图user.jsp
,然后构建并部署应用:
mvn clean install glassfish:deploy
这个Glassfish Maven插件会构建、部署并在8080端口运行。成功部署后,在浏览器中访问URL:
http://localhost:8080/mvc-2.0/app/users
接下来,定义一个处理表单提交的HTTP POST方法:
@POST
@Controller
public String saveUser(@Valid @BeanParam User user) {
return "redirect:users/success";
}
现在,当用户点击"Create"按钮时,控制器处理POST请求并将用户重定向到成功页面:
利用Jakarta验证、CDI和@MvcBinding
提供表单验证:
@Named("user")
public class User implements Serializable {
@MvcBinding
@Null
private String id;
@MvcBinding
@NotNull
@Size(min = 1, message = "Name cannot be blank")
@FormParam("name")
private String name;
// other validations with getters and setters
}
有了表单验证后,检查绑定错误。如果有绑定错误,需要向用户展示验证消息。为此,注入BindingResult
处理无效的表单参数。更新saveUser
方法:
@Inject
private BindingResult bindingResult;
public String saveUser(@Valid @BeanParam User user) {
if (bindingResult.isFailed()) {
models.put("errors", bindingResult.getAllErrors());
return "user.jsp";
}
return "redirect:users/success";
}
验证就绪后,如果用户提交表单时未填写必填参数,会显示验证错误:
接下来,使用@CsrfProtected
保护POST方法免受CSRF攻击。给saveUser
方法添加@CsrfProtected
:
@POST
@Controller
@CsrfProtected
public String saveUser(@Valid @BeanParam User user) {
}
尝试点击"Create"按钮:
当控制器受到CSRF保护时,客户端必须传递CSRF令牌。因此,在user.jsp
中添加一个隐藏字段,每次请求时添加CSRF令牌:
<input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}"/>
类似地,开发一个REST API:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> getUsers() {
return users;
}
这个HTTP GET API返回用户列表。
5. 结论
本文介绍了Jakarta MVC 2.0,以及如何使用Eclipse Krazo开发Web应用和REST API。我们了解了MVC 2.0如何标准化Java中构建基于MVC的Web应用的方式。
完整源代码可在GitHub上获取:https://github.com/example/jakarta-mvc-demo