vlambda博客
学习文章列表

读书笔记《digital-java-ee-7-web-application-development》Java EE MVC框架

第 9 章 Java EE MVC 框架

 

“与手机或电视的发展相比,Web 的发展速度非常快。”

 
  --Sir Tim Berners-Lee, inventor of the World Wide Web

在过去的几章中,我们从客户端的角度回顾了 Web 应用服务。对于最后一章,我们将回到主要在服务器端编写的数字应用程序。我们将在 Java EE 保护伞下研究一个全新的规范。它是称为Model-View-ControllerMVC),属于 Java EE 8 版本(2017 年 3 月)和 JSR 371 (https://jcp.org/en/jsr/detail?id=371)。在编写本书时,已经有一个 Java EE MVC 的早期草案版本,它演示了 运行名为 Ozark 的参考实现 (https://ozark. java.net/index.html)。

MVC 框架基于在 Smalltalk 编程语言和环境中发明的设计模式,这在早期的用户界面应用程序中特别常见。这个想法是模型是指存储应用程序数据的组件,例如值对象或数据传输对象。 View 是呈现或负责将应用程序数据的表示传递给用户的组件,而 Controller 是包含处理前两个组件(视图和模型)之间的输入和输出的逻辑的组件。设计模式非常流行,因为它包含关注点分离,这是良好实用的面向对象设计中的关键概念。

2014 年,Oracle 向更广泛的 Java EE 社区发布了一项公开调查并收集了结果(https://java.net/downloads/javaee-spec/JavaEE8_Community_Survey_Results.pdf)。数以千计的技术人员对这项调查做出了回应。调查中的一个关键问题是,Java EE 是否应该与 JSF 一起提供对 MVC 的支持? 60.8% 的受访者赞成,赞成票,20% 的人没有投票,19.2% 的人不确定。这足以让 MVC 1.0 规范成为 Java EE 8 的一部分。

Java EE 8 MVC


在我们继续编程之前,我应该警告您这里的信息可能会发生变化,因为 MVC 正在我们眼前发展。作为一个狂热的读者,你应该至少验证一下API是当前的还是最终的 规范。

综上所述,MVC 框架——即使在这个早期阶段——肯定会成为未来许多年未来数字 Web 开发框架的领先规范,而不仅仅是因为它现在正式成为 Java EE 伞式传动系统的一部分。 MVC 利用 JAX-RS(用于 RESTful 服务的 Java)API,目前与其他 Java EE 技术集成,包括 CDI 和 Bean Validation。

专家组决定在 JAX-RS 之上而不是旧的 Java servlet API 之上进行分层,因为 JAX-RS 符合现代编程实践以使用 HTTP 映射功能的完整语义。他们还认为,采用 servlet 会使开发人员接触到已经在 J​​AX-RS 规范中复制的低级编程接口。

从数字开发人员和现代 Web 实践的角度来看,在 JAX-RS 之上分层 MVC 确实是一个很好的采用。 servlet 规范已被 Play 框架和其他人严厉批评为广泛而厚实的上下文映射抽象(Eric Evans 的域驱动设计)和对 Web 和 HTTP 使用的自然设计的阻碍。

我们将使用名为 Ozark 的 Java EE 8 MVC 参考实现。在撰写本文时,Ozark 仍在进行中。但是,里程碑版本包含 MVC 应用程序的基本组件和接口。

MVC 控制器


javax.mvc 下有一个新的 包结构为 MVC 保留。 @javac.mvc.Controller 注解将类类型或方法声明为控制器组件。以下是它在方法中的使用示例:

@Controller
public String greet()
{
  return "Greetings, Earthling";
}

此控制器方法缺少 HTTP 语义,这就是 JAX-RS 注释的帮助。从 MVC 的角度来看,它也是无用的,因为存在与模型或视图组件的关联。

因此,首先让我们将方法转换为适当的 RESTful 资源,从模型对象开始:

package uk.co.xenonique.basic.mvc;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value="user")
@RequestScoped
public class User {
  private String name;
  public User() {}
  public String getName() {return name; }
  public void setName(String name) {this.name = name; }
}

User 组件 用作我们的模型组件。它有一个属性:我们礼貌地问候的人的名字。因此,我们可以编写一个 MVC 资源端点并将这个模型实例注入其中。

这是我们控制器的初始版本:

package uk.co.xenonique.basic.mvc;
/* ... */
import javax.mvc.Controller;
import javax.mvc.Viewable;
import javax.ws.rs.*;

@Path("/hello")
@Stateless
public class AlienGreetings {
  @Inject User user;

  @GET
  @Controller
  @Path("simple1")
  @Produces("text/html")
  public String simple1( @QueryParam("name") String name )
  {
    user.setName(name);
    return "/hello.jsp";
  }
}

我们将向我们的 AlienGreetings 类注解一个 JAX RS @Path 元素,以声明它是一个资源端点。尽管我们的类型被定义为 EJB 无状态会话 bean,但 MVC 有望与 CDI 范围(例如 @ApplicationScoped@SessionScoped 。在我写这篇文章时,参考实现正在发生变化。

我们将 simple1() 方法注释为 MVC @Controller 注释。此方法接受一个参数名称为 @QueryParam。我们将添加其他 JAX-RS 注释以定义 HTTP 方法协议 @GET、相对 URI @Path 和MIME 内容类型 @Produces。该方法在 User 实例中设置 name 属性,并返回一个引用字符串,即视图的名称:/hello.jsp

MVC 控制器 方法可以返回一个字符串,这意味着 servlet 容器接管了最终视图的呈现。但是,由于可扩展的实现,MVC 也可以呈现不同的视图。稍后我们会看到更多。在幕后,MVC 将字符串转换为视图类型。 Java 接口 javax.mvc.Viewable 表示视图技术的抽象。 Viewable 是 javax.mvc.ViewEnginejavax.mvc.Models 之间的关联。

抽象类类型 javax.mvc.Engine 负责将模型呈现给技术选择。工程师可以开发或添加这个引擎来渲染视图。目前,Ozark 支持多种渲染风格,从 Apache Velocity 到 AsciiDoc。

Java 接口 javax.mvc.Models 表示从视图传递到渲染引擎的键值存储。模型的类型是请求范围的地图集合。

所以,让我们用更多的方法来扩展 AlienGreeting 控制器。下面的 simple2() 方法返回一个 Viewable 实例:

  @GET
  @Controller
  @Path("simple2")
  @Produces("text/html")
  public Viewable simple2( @QueryParam("name") String name )
  {
    user.setName(name);
    return new Viewable("/hello.jsp");
  }

如您所见,simple2() 方法是 simple1() 的变体,MVC 相当灵活。它甚至支持不返回类型的 void 方法。我们将使用 @javax.mvc.View 注释后续的 simple3() 方法,以便声明下一个视图:

  @GET
  @Controller
  @Path("simple3")
  @Produces("text/html")
  @View("/hello.jsp")
  public void simple3( @QueryParam("name") String name )
  {
    user.setName(name);
  }

到目前为止,这三种方法都是 HTTP GET 请求。由于 MVC 在 JAX-RS 之上分层,我们也可以使用其他协议方法。编写一个处理 HTTP POST 请求的 HTML 表单处理程序 很简单。

以下是 helloWebForm() 方法的代码摘录:

  @POST
  @Controller
  @Path("webform")
  @Produces("text/html")
  public Viewable helloWebForm( @FormParam("name") String name )
  {
    user.setName(name);
    return new Viewable("/hello.jsp");
  }

前面的控制器方法 helloWebForm() 接受一个名为参数的 HTML 表单。它设置模型对象并返回一个视图实例。在 HTML5 标准中,form 元素正式支持 HTTP GET 和 POST 请求。流行的 Web 浏览器往往只能通过 JavaScript 访问 HTTP PUT 和 DELETE 协议请求。但是,此限制不会阻止使用 @PUT@DELETE 注释 MVC 控制器。

MVC 控制器可以访问 JAX-RS 端点可用的全部 URI 空间。以下示例说明了路径参数:

  @GET
  @Controller
  @Path("view/{name}")
  @Produces("text/html")
  public Viewable helloWebPath( @PathParam("name") String name )
  {
    user.setName(name);
    return new Viewable("/hello.jsp");
  }

前面的控制器方法 helloWebPath() 接受路径参数作为用户名。 @PathParam 注释建立在相对 URI 中使用的参数标记。 URI 由 @Path 注释定义。完整的 URL 将是 http://localhost:8080/javaee-basic-mvc/rest/hello/view/Curtis

MVC 页面视图和模板

MVC 规范下,一个 view 实例可以被视为一个 模板技术参考。到目前为止,我们只看到了 JavaServer Page 视图。视图可以是开发人员可以想象的任何东西,只要有一个相应的 ViewEngine 实例确切地知道如何处理(渲染)来自关联模型的视图和另一个来自控制器的结果。

让我们看看示例中的第一个基本JSP 视图index.jsp 本书源代码项目(ch09/basic-javaee-mvc):

<%@ page import="uk.co.xenonique.basic.mvc.AlienGreetings" %>
<!DOCTYPE html>
<html>
<head> ...
  <link href="${pageContext.request.contextPath}/styles/bootstrap.css" rel="stylesheet">
  <link href="${pageContext.request.contextPath}/styles/main.css" rel="stylesheet">
</head>
<body>
  <div class="main-content"> ...
    <div class="other-content">
      <h2>Simple MVC Controller</h2>
      <p>
        HTTP GET Request with query parameter and invocation of 
        <code><%= AlienGreetings.class.getSimpleName() %>
        </code> with HTML Form:<br>
        <a href="${pageContext.request.contextPath}
          /rest/hello/simple1?name=James" 
          class="btn btn-primary"> Person called James
        </a>
      </p>
      <p>...
        <a href="${pageContext.request.contextPath}
          /rest/hello/simple2?name=Patricia" 
          class="btn btn-primary"> Person called Patricia
        </a>
      </p>
      <p>...
        <a href="${pageContext.request.contextPath}
          /rest/hello/simple3?name=Aliiyah" 
          class="btn btn-primary"> Person called Aliiyah
        </a>
      </p>
      <p>      
        <form action="${pageContext.request.contextPath}
          /rest/hello/webform"method="POST" >
          <div class="form-group">
            <label for="elementName">Name</label>
            <input type="text" class="form-control" 
              id="elementName" name="name" placeholder="Jane Doe">
          </div>
        </form>
      </p>
      ...
    </div>
  </body>
</html>

在这个 JSP 视图中,我们 将利用 EL 来生成 一个 URL Web 应用程序的上下文路径,即 pageContext.request.contextPath。调用 AlienGreeting 控制器方法的三个 HTML 锚元素:simple1()simple2 ()simple3()。 HTML 表单调用 helloWebForm() 方法。

名为 hello.jsp 的页面提取 JSP 视图如下所示:

<div class="main-content">
  <div class="page-header">
    <h1> Java EE 8 MVC - Hello</h1>
    <p> Model View Controller </p>
  </div>
  <div class="other-content">
    <div class="jumbotron">
      <p> Hello ${user.name} </p>
    </div>
    <p> How are you?</p>
  </div>
</div>

视图模板非常简单;我们将使用请求范围的用户实例来提供名称。

在撰写本文时,Ozark 在 beta 参考实现中支持以下视图再现技术,如下表所示:

模板名称

描述

ASCII文档

这是一个文本文档格式来写笔记、文档、文章、书籍、e - 书籍、幻灯片、手册页和博客。

https://github.com/asciidoctor/asciidoctorj

自由标记

这是一个 Java 模板引擎,可生成 HTML、RTF 和 源代码。

http://freemarker.org/

车把

这是对原始 Mustache 模板规范的多语言和平台扩展,带有有用的实用程序令牌。

JavaScript 版本:http://handlebarsjs .com/ 和 Java 端口:https://github.com/ jknack/handlebars.java/

JSR 223

这是 MVC 框架的 扩展,支持用于动态脚本的 JSR 223 Groovy 和 Jython 等语言。

https://www.jcp.org/en/jsr/详细信息?id=223

胡子

这是一种 简单的Web 模板语言,它将表示与业务视图逻辑分开。它适用于多种平台和语言。

https://github.com/spullara/mustache.java

百里香叶

这是一个 基于 Java 的 HTML5 模板库,适用于 Web 和非 Web 环境。它与字符串框架紧密关联

http://www.thymeleaf.org/

速度

Apache Velocity 是 一套模板工具。 Velocity 引擎是提供模板的组件库。它是最早为 服务器端 Java 编写的 Web 模板框架之一。

http://velocity.apache.org/

小费

MVC 是一种 服务器端模板解决方案。不要将客户端模板的 世界与后端种类混淆。

MVC 模型

MVC 规范 支持两种形式的模型。第一种基于 javax.mvc.Models 实例,第二种形式利用 CDI @Named bean。 Models 接口类型将键名映射到映射集合中的值。所有视图引擎都必须强制支持 Models。视图引擎可以选择支持 CDI。规范建议视图引擎实现者提供 CDI bean 支持。

Models 接口的默认实现是一个请求范围的 bean:com.oracle.ozark.core.ModelsImpl。此类委托给 java.util.Map 集合。开发人员通常从不实例化这种类型,而是更喜欢注入 Models 类型。正如您稍后将看到的,有时需要为特定的视图引擎创建一个实例。

让我们在控制器类的第二个版本中演示 Models 接口的实际使用:

@Path("/hello2")
@Stateless
public class PhobosGreetings {
  @Inject
  Models models;

  @GET
  @Controller
  @Path("simple2")
  @Produces("text/html")
  public Viewable simple1(  @QueryParam("name") String name )
  {
    models.put("users", new User(name));
    return new Viewable("/hello2.jsp");
  }
}

PhobosGreetings 中,我们将 User 类型的 CDI 注入替换为 Models 实例。我们将 创建一个 User 实例并将其存储在属性键 用户 。方法返回后,框架将检索 Models 实例中的所有属性,并将它们放入 HttpServlerResponse 的属性集合中.因此,JSP 模板视图可以通过 EL 或内联脚本访问数据。

响应和重定向

MVC 还 支持返回 JAX RS 响应类型实例的控制器。这对于将 Grunt 渲染工作推到客户端的站点特别有用。高度可扩展的 Web 应用程序可能会选择发送数兆字节的 JSON 响应,而不是通过模板在服务器上呈现。

我们现在将检查另一个名为 RedirectController 的 MVC 控制器:

@Path("redirect")
@Stateless
@Controller
public class RedirectController {
  @GET
  @Path("string")
  public String getString() {
      return "redirect:redirect/here";
  }

  @GET
  @Path("response1")
  public Response getResponse1() {
    return Response.seeOther(
      URI.create("redirect/here")).build();
  }

  @GET
  @Path("deliberateError1")
  public Response getErrorResponse1() {
    return Response.status(Response.Status.BAD_REQUEST)
      .entity("/error.jsp").build();
  }

  @GET
  @Path("here")
  @Produces("text/html")
  public String getRedirectedView() {
    return "redirected.jsp";
  }
}

我们将使用 JAX-RS @Path 注释 RedirectController 并且我们必须特别注意基值

getString() 方法使用特殊前缀运算符redirect 执行URI 重定向。这个 方法有一个唯一的相对URI redirect/string。 MVC 在内部检测前缀并构建一个 JAX-RS 响应作为重定向请求。返回的 URI 是对 getRedirectedView() 控制器的引用,该控制器具有相对路径 URI 重定向/这里

我们可以直接自己构建响应,如 getResponse() 方法所示。使用 URI 调用静态 Response.seeOther() 方法等效于实现 HTTP 重定向响应。该方法有自己唯一的 redirect/response1 相对 URI。

getRedirectedView() 方法只是导航到视图模板,redirected.jsp。此控制器方法是其他控制器方法的最终目标:getString()getResponse()

小费

在撰写本文时,MVC 规范正在设计 HTTP 重定向方法。有一个关于为 MVC 应用程序提供 Flash 范围或等效的 JSF View 范围 bean 以便在多个请求范围之间保存数据的问题。我强烈建议您检查规范的更新。

最后,MVC 控制器还可以返回 HTTP 错误响应。 getErrorResponse() 方法的相对 URI 为 redirect/deliberateError1 并返回带有 HTTP Bad Request 错误代码 ( 401)。控制器还通知 MVC 框架为视图 ID error.jsp 下的视图模板提供服务。 RedirectController 表示 MVC 控制器的最简单形式 。我们可以通过 Models 或其他 CDI bean 的注入 来丰富它。我们还可以使用 Viewable 实例而不是哑字符串作为响应构建器中的实体。

让我们继续讨论不同的模板技术,这是迄今为止 MVC 的独特卖点。

重新配置视图根

在工作的 MVC 早期草稿版本中,视图根默认设置为 WEB-INF/views。以编程方式,这可以在 ViewEngine.DEFAULT_VIEW_ENGINE 的静态属性中找到。

对于具有相对 URI 的数字工程师来说,这可能 很不方便,尤其是在页面视图的重定向中。幸运的是,可以从 Web XML 描述符 (web.xml) 重新配置 Ozark 实现,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" ...>
  ...
  <servlet>
    <servlet-name>javax.ws.rs.core.Application</servlet-name>
    <init-param>
      <param-name>javax.mvc.engine.ViewEngine.viewFolder</param-name>
      <param-value>/</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>...
</web-app>

我们将覆盖网页根目录中的 javax.mvc.engine.ViewEngine.viewFolder 属性,以实现我们想要的行为。

车把 Java


Handlebars 框架 是一个用于开发 Web 应用程序的模板库。在它的 JavaScript 咒语(http://handlebarsjs.com/)中,您可能已经听说过数字界面开发人员对此赞不绝口。在本章的其余部分,我们将使用 Handlebars Java 端口。

Handlebars 框架允许开发人员和设计人员一起编写语义模板。它基于 一个稍旧的模板框架,称为 Mustache (https://mustache.github.io/)。这两个都是指男性的 面部毛发,如果您眯着眼睛看旋转90度的花括号,可以看到这种毛发。

模板框架尽可能地将关注点分离和减少在呈现的页面视图中混合业务逻辑。 Handlebars 框架从 Mustache 借用了双花括号符号。对于我们的男性读者来说,这是一种刻意的文字游戏和计算机编程的特质。 Handlebars 框架具有明显的优势,即在服务器端利用同样的模板引擎,该模板引擎也适用于客户端。

编译的内联模板 servlet

我们将从开始以一个servlet 示例作为介绍。我们将创建一个只有一个依赖项的 Java servlet,即 Handlebars Java 实现。我们还没有考虑 MVC 框架。我们的 servlet 将实例化模板框架,调用内联模板脚本,然后将输出作为直接的直接响应返回。

这是我们的 servlet:

package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import javax.servlet.*;
/* ... other imports omitted */

@WebServlet("/compiled")
public class CompileInlineServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
  {
    final Handlebars handlebars = new Handlebars();
    final Template template = handlebars.compileInline(
         "Hello {{this}}!");
    final String text = template.apply("Handlebars.java");
    response.setContentType("text/plain");
    response.getOutputStream().println(text);
  }
}

CompileInlineServlet 类说明了如何在 Java 代码中编译一个基本的处理程序模板示例。我们将实例化一个Handlebars引擎模板,然后内联编译模板,如下文所示:

Hello {{this}}!

编译完成后,我们会得到一个包含内容的Template实例。然后,我们将使用一个简单的字符串调用 apply() 方法,这将作为模板的上下文。 {{this}} 占位符指的是上下文对象。然后我们将检索文本表示并将响应发送回 Web 浏览器。我们将看到以下纯文本输出:

Hello Handlebars.java!

Handlebars Java 中没有将框架绑定到 Java EE 的依赖项。因此,它也可以在 Java SE 中用作独立的 Java 可执行文件。

Handlebars 中的模板表达式

这是 另一个具有不同上下文的 Handlebars 模板示例:

<div class="trade-entry">
  <h1>{{title}}</h1>
  <div class="trade-detail">
    {{detail}}
  </div>
</div>

模板在 DIV 层中呈现,并使用嵌入表达式的令牌名称替换。在双大括号之间保留并激活一个表达式。它为交易系统的域呈现输出。在 Handlebars 和 Mustache 表示法中,变量名周围的双花括号表示占位符条目。模板中的占位符条目意味着它可以替换为动态内容。因此,在模板渲染过程中,{{title}}占位符被替换为交易条目标题和{{detail}}< /code> 被替换为交易细节。表达式值可以是文字字符串,也可以是嵌入的 HTML5 标记。

让我们编写另一个使用 Handlebars 框架呈现此视图的 Java servlet:

package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import com.github.jknack.handlebars.io.*;
/* ... imports omitted */

@WebServlet("/tradeinfo")
public class TradeInfoServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest request,
     HttpServletResponse response)
      throws ServletException, IOException
  {
    final TemplateLoader loader = 
      new ServletContextTemplateLoader(
         request.getServletContext());
    final Handlebars handlebars = new Handlebars(loader);
    final Template template = handlebars.compile("trade");
    final Map<String,String> context = 
        new HashMap<String,String>() {{
      put("title", "12345FUND-EURO-FUTURE6789");
      put("detail", "Nominal 1.950250M EUR");
      put("trade-date", "23-07-2015");
      put("maturity-date", "23-07-2018");
    }};
    final String text = template.apply(context);
    response.setContentType("text/html");
    response.getOutputStream().println(text);
  }
}

这个TradeInfoServlet servlet和上一个几乎一样,CompileInlineServlet;但这一次,我们将使用 ServletContextTemplateLoader 类。这是一个加载器类,它通过 servlet 引擎从 Java EE Web 上下文中检索视图模板。我们将创建加载器并在构造 期间将此 作为参数传递车把实例。然后,我们将使用参考名称 trade 编译模板。框架调用加载器来检索视图模板,默认情况下后缀为 trade.hbs

我们构建了键和值的文字映射集合。它们将作为我们视图中的占位符集合,我们会将它们应用到模板中。使用 http://localhost:8080/handlebars-javaee-mvc-1.0-SNAPSHOT/tradeinfo 导航到 Web 浏览器应显示以下屏幕截图:

读书笔记《digital-java-ee-7-web-application-development》Java EE MVC框架

TradeInfoServlet 的屏幕截图

除了 直接替换,Handlebars 支持 BlockPartialsHelper 表达式。 Handlebars 框架提供了标准的构建块;但是,工程师可以注册自定义助手。在 JavaScript 实现中,开发人员通常会在页面中定义已编译的脚本模板,以便重用内容。 Handlebars.js 倾向于与一个或另一个 JavaScript 框架(例如 RequireJS 或 EmberJS)一起使用,以提供可重用的内容。

我们将继续使用 Handlebars Java 编写一个 CRUD 示例。我们的应用程序将允许用户操作基本的产品目录。我们需要一个欢迎页面,所以让我们使用模板框架创建它。

欢迎控制器

在 Java 端口中,我们不必注册或编译 模板,因为页面内容是从服务器端传递的。这个项目被称为 handlebars-javaee-mvc。首先要做的是在初始 JSP 中引起 HTTP 重定向,如下所示:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<meta http-equiv="refresh" content="0; 
  url=${pageContext.request.contextPath}/rest/welcome" />
</body>
</html>

上述代码将立即重定向 Web 浏览器以向 URI handler-javeee-mvc-1.0/rest/welcome 发送 HTTP GET 请求。在这个 URI 路径中,我们已经有一个 MVC 控制器在等待这个调用:

@Path("/welcome")
@Stateless
public class WelcomeController {
  @Inject Models models;

  @GET
  @Controller
  @Produces("text/html")
  public Viewable welcome()
  {
    models.put("pageTitle", "Handlebars.java Java EE 8 MVC" );
    models.put("title", "Welcome");
    return new Viewable("/welcome.hbs");
  }
}

在模型实例中填充几个属性后,WelcomeController 将响应推进到 Handlebars Java 模板 /welcomee.hbs。带有正斜杠字符的绝对 URI 确保 MVC 框架搜索来自 Web 上下文根的页面模板。后缀扩展 *.hbs 通常为客户端和服务器版本中的 Handlebars 视图模板保留。

让我们看看下面的 Handlebars 模板(welcome.hbs):

<!DOCTYPE html>
<html>
{{> header }}
<body>
  {{> navbar }}

  <div class="main-content">
    ...
  </div>

  {{> footer}}
</body>
{{> bottom }}
</html>

车把支持 Partials 的概念。部分是在另一个模板中使用的模板。它们对于页面组合非常有用。语法以 {{> NAME }},其中 NAME 指的是另一个模板。在 JavaScript 堆栈中,必须事先注册部分;但是,Java 端口知道如何通过从 servlet 容器加载部分模板来找到它们。因此,部分模板引用,{{> header }},指示 Handlebars 加载 header.hbs 视图。

welcome.hbs 视图中有 四个部分模板:header .hbsnavbar.hbsfooter.hbs底部。 hbs。您可以在本书的代码分发中查看这些模板的源代码。

自定义视图引擎

Handlebars Java 带有几个模板视图引擎。不幸的是,默认的扩展视图引擎并没有提供我们想要使用的所有功能。一方面,Handlebars 不能轻易地呈现十进制数字,因此我们必须注册自己的函数。幸运的是,使用 CDI 编写视图引擎扩展是合理的,如下所示:

package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import com.github.jknack.handlebars.io.*;
import com.oracle.ozark.engine.ViewEngineBase;
// ... other imports ommitted

@ApplicationScoped
public class MyHandlebarsViewEngine extends ViewEngineBase {
  @Inject private ServletContext servletContext;

  @Override
  public boolean supports(String view) {
    return view.endsWith(".hbs") || view.endsWith(".handlebars");
  }

  @Override
  public void processView(ViewEngineContext context) throws ViewEngineException {
    final Models models = context.getModels();
    final String viewName = context.getView();

    try (PrintWriter writer = context.getResponse().getWriter();
         InputStream resourceAsStream = 
         servletContext.getResourceAsStream(resolveView(context));
         InputStreamReader in = 
            new InputStreamReader(resourceAsStream, "UTF-8");
       BufferedReader buffer = new BufferedReader(in);) {
      final String viewContent = buffer.lines()
        .collect(Collectors.joining());
      final TemplateLoader loader = 
        new ServletContextTemplateLoader(servletContext);
      final Handlebars handlebars = new Handlebars(loader);
      models.put("webContextPath", 
         context.getRequest().getContextPath());
      models.put("page", context.getRequest().getRequestURI());
      models.put("viewName", viewName);
      models.put("request", context.getRequest());
      models.put("response", context.getResponse());
      handlebars.registerHelper("formatDecimal", new Helper<BigDecimal>() {
          @Override
          public CharSequence apply(BigDecimal number, Options options) throws IOException {
            final DecimalFormat formatter = 
               new DecimalFormat("###0.##");
            return formatter.format(number.doubleValue());
          }
      });
      Template template = handlebars.compileInline(viewContent);
      template.apply(models, writer);
    } catch (IOException e) {
        throw new ViewEngineException(e);
    }
  }
}

我们将 注释我们的 MyHandlebarsViewEngine 类作为应用程序范围的 bean,并确保它是 ViewEngineBase 来自 Ozark。我们将 ServletContext 注入到这个类中,因为我们 需要从中检索某些属性,例如网络上下文小路。

我们将重写 supports() 方法以建立对 Handlebars 文件的支持。 MVC 视图类型作为单个参数传递。

真正的工作发生在 processView() 方法中,我们完全负责渲染 Handlebars View 模板。 MVC 框架提供 javax.mvc.engine.ViewEngineContext,它提供对当前 View模型 实例。我们可以建立我们需要检索的视图模板的名称。从这里开始,我们可以创建 ServletContextTemplateLoaderHandlebars 实例来加载我们在前面的 TradeInfoServlet 类。然后我们必须通过读取缓冲区中当前视图的内容来导航一些棘手的水域。 ViewEngineBase 提供了一个 resolve() 方法,对我们帮助很大,并返回 InputStream .顺便说一句,try-catch 语句的 Java 7 获取/资源语法减少了样板代码。在方法结束时,我们可以内联编译视图,因为我们在缓冲区中有内容。

我们将在 MyHandlebarsViewEngine 中添加一些有用的功能。首先,我们将向 Models 实例添加额外的属性。我们将网络上下文路径以及请求和响应对象添加到 Models 实例。其次,我们将注册一个 Handlebars 助手,以便更好地呈现 BigDecimal 类型。

当我们的应用部署完成后,Ozark依赖CDI来找到ViewEngineBase 类型。 Ozark 扫描 JAR 文件和类的类路径以查找 ViewEengineBase 对象的类型。它构建可用渲染的内部列表。 MyHandlebarsViewEngine 是目前您可以在渲染阶段添加其他帮助程序和实用程序的地方。密切关注 MVC 规范,看看是否有任何这些接口暴露在有意义的公共可访问 API 中。

我们将继续我们的控制器和产品列表视图。

产品控制器

我们的域 对象是不起眼的 Product 实体,它具有 类似于以下形式的大纲:

@Entity
@NamedQueries({
  @NamedQuery(name="Product.findAll",
    query = "select p from Product p order by p.name"),
  @NamedQuery(name="Product.findById",
    query = "select p from Product p where p.id = :id"),
})
public class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;
  @NotEmpty @Size(max=64) private String name;
  @NotEmpty @Size(max=256) private String description;
  @NotNull @DecimalMin("0.010") private BigDecimal price;

  public Product() { this(null, null, null); }

  public Product(String name, String description, BigDecimal price) {
    this.name = name;
    this.description = description;
    this.price = price;
  }
  /* ... */
}

请注意,Product 充分利用了 Bean Validation 注解。特别是,它使用 BigDecimal 类型来获取准确的价格,而 @DecimalMin 注释可防止在有界上下文中存储负价格和零价格。

给定我们的 实体对象,我们将需要 ProductController 将域连接到表示视图:

@Path("/products")
@Stateless
public class ProductController {
  @Inject Models models;
  @Inject ValidatorFactory validatorFactory;
  @Inject FormErrorMessage formError;
  @EJB ProductService productService;

  private void defineCommonModelProperties(String title ) {
    models.put("pageTitle", "Handlebars.java Java EE 8 MVC" );
    models.put("title", title);
  }

  private void retrieveAll() {
    final List<Product> products = productService.findAll();
    models.put("products", products );
    models.put("title", "Products");
  }

  @GET
  @Controller
  @Path("list")
  @Produces("text/html")
  public Viewable listProducts()
  {
    retrieveAll();
    defineCommonModelProperties("Products");
    return new Viewable("/products.hbs");
  }
  /* ... */
}

像往常一样,我们将使用 JAX-RS 注释对 ProductController 进行注释,以便引入 RESTful 服务空间。我们将使用 Bean Validation 来验证输入对象的状态,因此,我们将从 Java EE 容器中注入 ValidatorFactory。我们将为 MVC 操作注入 Models 实例。我们还有一个自定义的 FormErrorBean POJO 来捕获错误消息,最后,还有一个 EJB,ProductService,用于持久化记录到数据库。

listProducts() 控制器方法委托给共享方法:retrieveAll()defineCommonModelProperties( ) retreieveAll() 方法使用 EJB 从数据库中提取所有产品。它将列表集合保存在 Models 中的已知属性键下。 defineCommonModelProperties() 方法 将标题保存为同一 模型实例。 事实证明,许多控制器方法都需要相同的功能,因此我们对其进行了重构。我们将检索到的产品集合放在名为 products 的 Models 键属性中。最后,listProducts() 将其转发到 Handlebars Java 视图模板 product.hbs。在我们的控制器方法返回后,Ozark 最终将委托给我们的自定义视图引擎:MyHandlebarsViewEngine

块表达式

我们可以 查看视图并了解有用的块表达式。以下是视图模板的摘录,/products.hbs

<div class="other-content">
  <div class="jumbotron">
      <p> {{title}} </p>
  </div>

  <p>
    <a href="{{webContextPath}}/rest/products/preview-create" 
      class="btn-primary btn">Add New Product</a>
  </p>

  <table class="product-container-table table table-bordered table-responsive table-striped">
    <tr>
      <th> Product Name </th>
      <th> Description </th>
      <th> Unit Price </th>
      <th> Action </th>
    </tr>

    {{#each products}}
      <tr>
        <td> {{this.name}} </td>
        <td> {{this.description}} </td>
        <td> {{formatDecimal this.price}} </td>
        <td>
          <a class="btn" href="{{webContextPath}}/rest/products/view/{{this.id}}" ><i class="glyphicon glyphicon-edit"></i></a>
          <a class="btn" href="{{webContextPath}}/rest/products/preview-delete/{{this.id}}" ><i class="glyphicon glyphicon-trash"></i></a>
        </td>
      </tr>
    {{/each}}
  </table>
</div>

如您 所见,我们实际上在这个视图模板中使用了 {{title}} 表达式,它在 defineCommonModelProperties() 方法中设置。 {{webContextPath}} 占位符的值在我们的扩展类 MyHandlebarsViewEngine 中设置。

有两个新的表达式:{{#each}}。这些是允许我们循环上下文元素的内置块表达式。在这种情况下,我们将迭代产品。循环中心的产品元素可以在 {{this}} 占位符下访问。

为了正确打印 BigDecimal 价格,我们将调用自定义视图引擎中定义的 {{formatDecimal}} 辅助函数。一个辅助函数可以接受多个参数。结果是带有名称、描述和价格的产品表格,带有用于编辑或删除项目的锚链接。

这是视图模板的屏幕截图,products.jsp

读书笔记《digital-java-ee-7-web-application-development》Java EE MVC框架

Handlebars 渲染的产品列表视图模板

检索和编辑操作

一旦客户选择了要编辑的产品,我们就需要从数据库中检索数据并将它们推送到不同的视图模板。这就是控制器中预览编辑方法的目的,如下:

  @GET
  @Controller
  @Path("view/{id}")
  @Produces("text/html")
  public Viewable retrieveProduct( @PathParam("id") int id )
  {
      final List<Product> products = productService.findById(id);
      models.put("product", products.get(0) );
      defineCommonModelProperties("Product");
      return new Viewable("/edit-product.hbs");
  }

我们将使用 @PathParam 注释此控制器方法 retrieveProduct() 以检索产品 ID。使用标识符,我们将简单地查找 Product 实体并将结果放入请求范围的 Models 属性中。显然,对于生产 应用程序,我们在检查标识符是否有效时可能会非常谨慎。该方法将响应最终传递到视图模板 /edit-products.hbs。视图模板的源代码可通过本书的源代码分发获得。

当用户在此页面视图模板上提交 HTML 表单时,我们将继续进行下一部分。如果客户提交了表单,那么他们的旅程将调用 ProductController 中的下一个控制器方法,称为 editProduct()

@POST
@Controller
@Path("edit/{id}")
@Produces("text/html")
public Response editProduct( 
  @PathParam("id") int id,
  @FormParam("action") String action,
  @FormParam("name") String name,
  @FormParam("description") String description,
  @FormParam("price") BigDecimal price)
{
  defineCommonModelProperties("Edit Product");
  if ("Save".equalsIgnoreCase(action)) {
    final List<Product> products = 
      productService.findById(id);
    final Product product2 = new Product(
      name, description, price );
    final Set<ConstraintViolation<Product>> set = 
      validatorFactory.getValidator().validate(product2);
    if (!set.isEmpty()) {
      final ConstraintViolation<?> cv = set.iterator().next();
      final String property = cv.getPropertyPath().toString();
      formError.setProperty(
        property.substring(property.lastIndexOf('.') + 1));
      formError.setValue(cv.getInvalidValue());
      formError.setMessage(cv.getMessage());
      models.put("formError",formError);
      return Response.status(BAD_REQUEST).
         entity("error.hbs").build();
    }
    final Product product = products.get(0);
    product.setName(name);
    product.setDescription(description);
    product.setPrice(price);
    productService.saveProduct(product);
    models.put("product", product);
  }
  retrieveAll();
  return Response.status(OK).entity("/products.hbs").build();
}

我们的控制器 方法在提交 HTML 表单时被调用,因此我们将使用必要的 @PathParam @FormParam 声明,以便接收产品 ID 和属性。我们的控制器需要一个带有 name 的表单参数,action。客户可以取消操作,如果取消,则操作与 Save 操作不匹配。因此,什么都不会发生,并且该方法将响应推进到产品列表视图。

通过 Save 动作,我们的方法将利用注入的 ValidatorFactory 实例来手动验证表单参数。由于实体产品具有验证注释,我们将使用表单参数构造一个临时实例,然后创建一个验证器来检查它。我们此时不直接更改持久化实体,因为数据可能无效。如果是这种情况,那么 Java EE 容器将在控制器方法退出后引发 javax.persistence.RollbackException,因为控制线程通过了事务屏障。验证临时实例后,返回 ConstraintViolation 元素的集合。

假设表单数据无效。我们将从第一次违规中检索信息并在请求范围 FormErrorMessage bean 中填充详细信息。可以通过属性键 formError 在视图模板中访问表单错误。然后,控制器方法使用 HTTP Bad Request 错误代码构建响应,然后转发到视图模板 /error.hbs

另一方面,如果表单根据 Bean Validation 检查是有效的,那么我们将继续通过产品 ID 从数据库中检索 Product 实体。我们将从表单属性中更新 Product 实体,然后将其保存在数据库中。由于我们已经手动检查了数据,因此保存数据时应该不会出错。我们将构建一个 HTTP OK 响应并转发到视图模板 /products.hbs

小费

在撰写本文时,MVC 规范负责人仍在开发 MVC 早期草案发布后的参数验证和绑定。值得注意的是,如果控制器注入 javax.mvc.BindingResult 实例,则可以在精确和狭窄的用户故事中处理表单验证,而不是全局处理,如在直接的 JAX-RS API 的情况。

为了完成图片,这里是 FormErrorMessage bean 的精简版:

@Named("error")
@RequestScoped
public class FormErrorMessage {
  private String property;
  private Object value;
  private String message;

  /* ... getters and setters omitted */
}

这是一个 请求范围的bean,我们将在Handlebars 视图模板error.hbs:

<div class="page-header">
  <h1> {{pageTitle}} </h1>
</div>
<div class="other-content"> ...
  <div class="main-content" >
    <table class="table table-stripped table-bordered table-responsive">
      <tr>
        <th>Name</th><th>Value</th>
      </tr>
      <tr>
        <td>Property</td> <td>{{formError.property}}</td>
      </tr>
      <tr>
        <td>Message</td> <td>{{formError.message}}</td>
      </tr>
      <tr>
        <td>Value</td> <td>{{formError.value}}</td>
      </tr>
    </table>
  </div> ...
</div>

此视图在 FormErrorMessage bean 中呈现错误消息。您可能想知道为什么我们将表单验证错误发送到单独的视图。答案很简单:婴儿步。在专业的数字应用程序中,我们将在客户端利用 AJAX 验证和 JavaScript 框架(如 jQuery)。我们的客户端 JavaScript 模块将调用对 MVC 控制器方法的 HTTP POST 请求,以验证属性信息。此方法称为 validateCheck(),将检查临时实例并报告包含违反约束(如果有)的 JSON 响应。也许 JSR-371 专家组的成员将简化这部分数字开发的工作。

JAX-RS 全局验证

editProduct() 方法的问题是我们被迫使用手动验证步骤。 目前唯一的选择是依赖 JAX-RS 验证。

因此,让我们检查一个名为 altEditProduct() 的控制器方法的新版本:

@POST
@Controller
@Path("alt-edit/{id}")
@Produces("text/html")
public Response altEditProduct( 
  @PathParam("id") int id,
  @FormParam("action") String action,
  @NotNull @NotEmpty @FormParam("name") String name,
  @NotNull @NotEmpty @FormParam("description") String description,
  @NotNull @DecimalMin("0.0") @FormParam("price") BigDecimal price )
{
  defineCommonModelProperties("Edit Product");
  if ("Save".equalsIgnoreCase(action)) {
    final List<Product> products = productService.findById(id);
    final Product product = products.get(0);
    product.setName(name);
    product.setDescription(description);
    product.setPrice(price);
    productService.saveProduct(product);
    models.put("product", product);
  }
  retrieveAll();
  return Response.status(OK).entity("/products.hbs").build();
}

这一次,我们将直接在控制器方法 altEditProduct() 上写入 Bean Validation 注解。您可能会担心这是重复,因为注解已经在 Entity bean 上。你是对的,但让我们继续。 altEditMethod() 方法更短,这很棒。这种方法现在存在一个问题,即验证被全局委托给 JAX-RS。如果客户向 altEditMethod() 提交 HTML 表单,那么他们将直接从应用服务器发送 HTTP Bad Request 错误响应和用户不友好的错误消息。显然,UX 团队会大吃一惊!我们能做什么?

JAX-RS 规范允许应用程序为错误响应提供处理程序。实现这个目标的方法是通过CDI配置一个provider,如下:

package uk.co.xenonique.basic.mvc;
import javax.annotation.Priority;
import javax.mvc.*;
import javax.ws.rs.*;
/* ... other import omitted ... */
import static javax.ws.rs.core.Response.Status.*;

@Provider
@Priority(Priorities.USER)
public class ConstraintViolationExceptionMapper
  implements ExceptionMapper<ConstraintViolationException> {
  @Context HttpServletRequest request;

  @Override
  public Response toResponse(
    final ConstraintViolationException exception) {
    final Models models = new com.oracle.ozark.core.ModelsImpl();
    final ConstraintViolation<?> cv = 
      exception.getConstraintViolations().iterator().next();
    final String property = cv.getPropertyPath().toString();
    final FormErrorMessage formError = new FormErrorMessage();
    formError.setProperty(
      property.substring(property.lastIndexOf('.') + 1));
    formError.setValue(cv.getInvalidValue());
    formError.setMessage(cv.getMessage());
    models.put("formError",formError);
    request.setAttribute("formError", formError);
    final Viewable viewable = new Viewable("error.hbs", models);
    return Response.status(BAD_REQUEST).entity(viewable).build();
  }
}

这个 ConstraintViolationExceptionMapper 类是一个 JAX-RS 提供程序,因为我们使用 @javax.ws.rs.ext.Provider 对其进行了注释。该类通常被键入为 ConstraintViolationException,因此,它可以处理 Web 应用程序中的所有故障!这里没有余地。我们将注入 HttpServletRequest POJO,以便 访问网络上下文。 toResponse() 方法将违反约束转换为新的响应。我们将需要 Models 实例的实现,因此我们将在 Ozark 框架中实例化该类。我们将直接构建一个 FormErrorMessage POJO,并从 javax.validation.ConstraintViolation 类型的第一个实例填充它。我们将在 Models 实例和 servlet 请求范围内设置一个关键属性。从这里,我们将创建一个 Viewable 实例,其中包含视图模板引用、error.hbsModels 实例,然后构建并返回响应。

值得一看的是参考实现 Ozark 的一些内部结构。我们经常看到 ViewEngineBaseViewEngineContext。下面是一些 重要的内部类及其包的示意图:

读书笔记《digital-java-ee-7-web-application-development》Java EE MVC框架

MVC 绑定结果验证

MVC 规范有一个用于细粒度验证的 API,该 API 仍在确定中。在 javax.mvc.binding< 中有两种接口类型——BindingResultBindingError /代码>包。

BindingResult 在尝试验证 MVC 控制器方法的输入参数期间捕获约束违规,该方法使用 @FormParam 进行注释。规范描述了术语绑定,以反映表单参数和正在验证的属性的实际类型之间的关联,以及可能发生的可能的约束违规。因此,如果作为字符串的 HTML 表单参数无法以有意义的方式转换为数字,则无法绑定整数属性。 BindingResult的接口如下:

package javax.mvc.binding;
public interface BindingResult {
  boolean isFailed();
  List<String> getAllMessages();
  Set<BindingError> getAllBindingErrors();
  BindingError getBindingError(String param);
  Set<ConstraintViolation<?>> getViolations(String param);
  ConstraintViolation<?> getViolation(String param);
}

BindingResult 的有趣成员是 isFailed()getAllViolations()、和 getAllBindingErrors()

BindingError 类型旨在表示将参数绑定到 控制器方法时发生的单个错误。这是此类型的精简接口 API:

package javax.mvc.binding;
public interface BindingError {
  String getMessage();
  String getMessageTemplate();
  String getParamName();
}

BindingError 类型的工作方式与内插消息的 Bean Validation 规范类似。因此,它有助于国际化,因为可以从 java.util.ResourceBundle 中检索消息。

对于我们的最后一个示例,我们将使用 BindingResult 来验证我们的新 editMethod() 方法:

@Path("/products")
@Stateless
public class ProductController {
  @Inject BindingResult br;
  
  /* ... */

  @POST
  @Controller
  @Path("edit/{id}")
  @Produces("text/html")
  @ValidateOnExecution(type = ExecutableType.NONE)
  public Response editProduct( 
    @PathParam("id") int id,
    @FormParam("action") String action,
    @Valid @BeanParam Product incomingProduct)
  {
    defineCommonModelProperties("Edit Product");
    if ("Save".equalsIgnoreCase(action)) {
      Set<ConstraintViolation<?>> set = vr.getAllViolations();
      if (!set.isEmpty()) {
        final ConstraintViolation<?> cv = set.iterator().next();
        final String property = cv.getPropertyPath().toString();
        formError.setProperty(
            property.substring(property.lastIndexOf('.') + 1));
        formError.setValue(cv.getInvalidValue());
        formError.setMessage(cv.getMessage());
        models.put("formError",formError);
        return Response.status(BAD_REQUEST).
          entity("error.hbs").build();
      }
      final List<Product> products = productService.findById(id);
      final Product product = products.get(0);
      product.setName(incomingProduct.getName());
      product.setDescription(incomingProduct.getDescription());
      product.setPrice(incomingProduct.getPrice());
      productService.saveProduct(product);
      models.put("product", product);
    }
    retrieveAll();
    return Response.status(OK).entity("/products.hbs").build();
  }
}

为了获得 细粒度验证的好处,我们将@BindingResult 作为属性注入到<代码类="literal">ProductController。我们将更改 editProduct() 方法周围的注释。为了确保 JAX-RS 执行验证并且 CDI 和 Bean Validation 不会中止该过程,我们将注解 @ValidateOnExecution 并将 type 参数设置为 ExecutableType.NONE。根据 Bean Validation 规范,@ValidateOnExecution 注解用于选择性地启用和禁用违规。关闭验证允许 JAX RS 接管我们的控制器方法 editProduct()。我们还将使用 @Valid@BeanParam 来指导 MVC 提供者验证 Product 实体 bean。

当 MVC 注意到控制器类已注入 BindingResult 实例或具有接受 BindingResult 的 JavaBean setter 方法时,它会接管验证.在 editProduct() 方法中,我们将通过调用 isFailed() 方法检查验证的布尔状态。如果输入未能通过验证,我们将从 BindResult 结果中获取第一个约束违规,然后将 FormErrorMessage bean 填充为前。然后,我们将发送带有错误请求错误代码的 HTTP 响应,该错误代码转发到 error.hbs 视图模板。

请注意,我们将使用单独的命名变量参数 incomingProduct 编写 editProduct() 方法,以便保留一个临时持有者HTML 表单数据。我们将把这个变量的属性复制到从数据库中检索到的产品实体上并保存。当我们到达控制器方法的末尾时,实体 bean 必须是有效的。我们将检索产品列表并返回 OK 响应。使用 BindingResult,开发人员很清楚这种验证更容易使用 进行编程。需要考虑的代码更少。

设计注意事项


MVC 是一个非常有前途的规范,目前,视图技术解决方案对于数字开发人员来说很常见。关于处理 HTTP 重定向响应,尤其是保持表单状态的方法,还有一个故事有待开发。许多数字Web 开发人员已经熟悉设计模式,HTTP POST-REDIRECT-GET (https://en.wikipedia.org/wiki/Post/Redirect/Get),因此,他们会寻找用于 MVC 规范中的等效且安全的选项。

等式的另一端是关于HTML表单验证的问题。但是,这方面可能会有突发新闻。表单验证的故事与 HTTP 重定向请求有很多共同之处。开发人员希望利用实体上的 Bean 验证,但他们也希望在控制器中无缝调用验证并内省验证结果。事实上,JAX-RS 允许通过全局提供程序进行验证。但是,这种方法不提供细粒度的验证处理,并且它不能将违反约束的实例映射到单个 HTML 表单输入元素。尽管如此,早期草稿发布后的快照在这方面已经显示出早期的前景。

正如我们在 Handlebars Java 代码示例中看到的那样,许多数字架构师都有设计考虑和权衡。有多少表示逻辑驻留在客户端而不是服务器端?

大多数服务器端模板

考虑到主要是服务器端模板的选项,为什么技术架构师会选择这种模式?这种方法有很多原因。一个原因可能是团队对 JavaScript 编程有点害怕,并且希望最小的依赖关系和上市时间。这是技能、培训和维护的权衡。另一个原因可能是最终的终端客户端设备功率不足,例如物联网设备。数字恒温器不太可能必须渲染视网膜版图像。另一个原因可能是更容易迁移非常旧的遗留应用程序以使其更新:这就是数字化转型。 MVC 可以在这里提供广泛的视图模板支持。

大多数客户端模板

这种设计选择意味着客户端在设备上呈现大部分内容。据说,带有 JavaScript 和 HTML5 的智能手机 的计算能力比将人类送上月球的火箭飞船计算机的计算能力高出几个数量级。技术架构师可能希望将负载委托给客户端的一个原因是减少服务器应用程序的负载。这是服务器上可伸缩性的权衡。 Handlebars.js 是一个 JavaScript 视图模板实现,在 Internet 上的其他竞争 JavaScript 框架中完全足以满足此要求。这种模式下的 MVC 控制器成为真正将视图绑定到模型的架构的薄层。如果您的团队拥有非常强大的界面设计人员和开发人员,或者他们缺乏现代 Java EE 开发经验,这种模式也可能是合适的。在 UI 方面,可能有充分的理由让客户旅程映射到逐页导航。因此,该架构避免了单页应用程序,这可能会排除诸如 AngularJS 之类的框架。

共享模板

最终的设计选择是结合客户端和服务器端模板化并分担演示视图的责任。对于跨职能团队工作的数字技术架构师来说,这可能是一个受欢迎的策略。如果有强大的接口开发团队和服务器端 Java EE 团队,并且他们沟通良好,那么这是一个合理的做法。除了敏捷团队中的人力资源考虑之外,这种方法在技术上更倾向于使用共享模板和愿景来组织数字资产的架构。假设架构由服务器端的 Handlebars Java 和客户端的 Handlebars.js 决定;团队立即面临视图模板的组织。哪些模板在 JavaScript 前端编译,哪些模板在服务器端很重要?此设计选择的解决方案将导致在服务器端构建 MVC 控制器。

让我就这三个设计注意事项向您提出警告。如果你值得你的盐,你会考虑 UX 变化和突然的惊喜的影响。因此,数字技术架构师必须将设计更改的影响纳入他或她的计算中。

概括


在本书的最后一章中,我们从头开始介绍了新兴的 MVC 规范。我们涵盖了模型、视图和控制器的基本元素。我们看到 MVC 是 JAX-RS 之上的一个分层,它重用了相同的注解,包括 @GET@FormParam@Path@FormParam@POST

为了建立一个方法作为 MVC 控制器,我们用 @Controller 注释它们。我们编写了控制器来生成响应实例,或者,如果它们返回 void 类型,我们将使用 @View 注释该方法。

您了解了 MVC 参考规范 Ozark 支持的各种视图技术。我们使用 Handlebars Java 视图模板来构建 CRUD 示例的元素。我们还了解到,MVC 规范可能会在重定向和验证 API 方面发生变化。

练习


以下是本章的问题和练习:

  1. Java EE MVC 框架的组成部分是什么? MVC 试图解决什么问题?

  2. @Controller@Model@View有什么区别?

  3. Response 和 Viewable 类型有什么区别?

  4. 鉴于标准 HTML 表单元素不发送 HTTP PUT 或 DELETE 请求,您将如何使用 MVC 从数据库中删除记录?这对于已经拥有完整 RESTful 接口的企业意味着什么?

  5. 到本书出版时,MVC 框架将会更进一步,并且可能会有许多里程碑式的版本。更新你的知识。发生了什么变化?特别注意重定向和验证。

  6. 从本书的源代码存储库下载 handlebars-javaee-mvc-validation 项目并调整产品实例以包含 description (string)、制造商 (string)、manufacturedDate (java.util.Date) 属性。为了正确显示格式化的日期(例如,MMM-dd-yyyy 或 dd-MMM-yyyy)需要做什么?

  7. 从上一个练习开始,确保使用 MVC 采用适当的验证技术。您将如何验证用户输入?

  8. 如果您对 JavaScript 编程相当熟悉,请编写一个模块来通过 AJAX 调用验证。 (提示:您可能需要了解 JQuery REST 请求和响应。)

  9. 在本练习中,使用 Product 实体和 ProductController 控制器类,并以它们为基础来试验另一种视图模板技术Ozark 参考实现。有很多选择。为产品 CRUD 应用程序调整一个新的模板框架——试试 Thymeleaf 甚至 AsciiDoc。模板选择之间有什么区别?有什么好处?有什么缺点?

  10. 写一篇短文,向您当前的团队提出针对特定项目的 MVC 框架。 MVC 方法的设计考虑因素是什么?您的目标受众是否适合单页导航或逐页导航或这两种想法的混合? (提示:角色模型可能有用;您可以编写旨在说服团队中的技术负责人的提案,或者您可能是旨在说服团队成员的技术负责人。)