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.platformjakarta.mvcorg.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"按钮:

403错误

当控制器受到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


原始标题:Introduction to Jakarta EE MVC / Eclipse Krazo