SpringMVC WebFlux 高级教程(四)
八、解析和实现视图
到目前为止,我们主要使用 JavaServer Pages (JSP)和 HTML 模板作为我们的视图技术;然而,Spring MVC 提供了一个非常强大和灵活的机制来解析和实现视图。你已经在第四章中简单的看了一下视图解析机制。本章着眼于不同的ViewResolver实现,并展示如何创建和使用我们自己的实现。您可以看到 Spring MVC 开箱即用地支持哪些视图技术。我们创建了一些定制的实现。然而,在我们深入内部之前,让我们回顾一下视图呈现过程和 API。
视图解析器和视图
第四章讨论了 dispatcher servlet 的请求处理工作流。解析和呈现视图是该过程的一部分。图 8-1 显示了视图渲染过程(参见 4 章节中的“渲染视图”部分)。
图 8-1
查看渲染过程
控制器可以返回一个org.springframework.web.servlet.View实现或者一个视图的引用(视图名)。在后一种情况下,会参考已配置的 ViewResolvers 来将引用转换为具体的实现。当实现可用时,它被指示呈现;否则,抛出javax.servlet.ServletException。
ViewResolver(见清单 8-1 )只有一个方法来解析视图。
package org.springframework.web.servlet;
import java.util.Locale;
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
Listing 8-1ViewResolver API
当一个视图被选中时,dispatcher servlet 调用视图实例上的 render 方法(参见清单 8-2 )。在View实例上调用getContentType()方法来确定内容的类型。该值设置响应的内容类型;它还被org.springframework.web.servlet.view.ContentNegotiatingViewResolver用来确定最佳匹配视图(更多信息见下一节)。
package org.springframework.web.servlet;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface View {
String getContentType();
void render(Map<String, ?> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
Listing 8-2View API
查看解析器
第四章展示了不同 ViewResolver 实现的层次结构。让我们仔细看看通用的可用实现,它们是如何工作的,以及它们是如何配置的。图 8-2 再次显示了不同的实现。特定于特定视图技术的实现将在本章后面的“视图技术”一节中解释。
图 8-2
视图解析器层次结构
BeanNameViewResolver
默认情况下,org.springframework.web.servlet.view. BeanNameViewResolver实现是最基本的可用和配置。它获取视图的名称,并在org.springframework.context.ApplicationContext中查看是否有同名的视图。如果有,解析器返回它;否则,它返回 null。这个视图解析器在小型应用中很有用;然而,它有一个很大的缺点:每个视图都需要在应用上下文中使用 bean 进行配置。它有一个可以配置的属性,这就是它被调用的顺序(见表 8-1 )。
表 8-1
BeanNameViewResolver属性
财产
|
目的
|
| --- | --- |
| Order | 此视图解析程序在链中的调用顺序。数字越大,在链中的顺序越低。 |
清单 8-3 显示了视图解析器如何服务和解析索引页面的配置。我们还需要添加一个View实例,因为我们使用了一个支持 JSTL 的 JSP,所以我们返回了org.springframework.web.servlet.view.JstlView。
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.JstlView;
// Other imports omitted
@Configuration
public class ViewConfiguration {
@Bean
public ViewResolver viewResolver() {
BeanNameViewResolver viewResolver = new BeanNameViewResolver();
viewResolver.setOrder(1);
return viewResolver;
}
@Bean
public View index() {
JstlView view = new JstlView();
view.setUrl("/WEB-INF/views/index.jsp");
return view;
}
}
Listing 8-3BeanNameViewResolver Configuration
UrlBasedViewResolver
org.springframework.web.servlet.view. UrlBasedViewResolver期望视图名称直接映射到 URL。它可以通过向视图名称添加前缀和/或后缀来选择性地修改 URL。一般来说,这个类是不同视图技术的基类,比如 JSP 和基于模板的视图技术(参见本章后面的“视图技术”一节)。表 8-2 描述了这种视图解析器的属性。
表 8-2
UrlBasedViewResolver属性
财产
|
目的
|
| --- | --- |
| staticAttributes | 此视图解析程序解析的每个视图中包含的属性。属性及其值通过方法setAttributes(Properties)和setAttributeMap(Map<String,?>)作为Properties或Map<String,?>实例提供。 |
| cacheUnresolved | 是否应该缓存未解析的视图?也就是说,如果一个视图已经解析为 null,是否应该将它放入缓存中?默认值为 true。(继承自AbstractCachingViewResolver。) |
| contentType | 设置内容类型 1 (text/HTML,application/JSON 等。)对于由该视图解析器解析的所有视图,除了那些自己确定或返回内容类型并忽略该属性的视图实现(如 JSP)。 |
| exposePathVariables | 路径变量(见第五章)是否应该添加到模型中?一般来说,视图自己决定;设置此属性可以重写该行为。 |
| Order | 此视图解析程序在链中的调用顺序。数字越大,在链中的顺序越低。 |
| Prefix | 添加到视图名称以生成 URL 的前缀。 |
| redirectContextRelative | 以/开头的重定向 URL 是否应该被解释为相对于 servlet 上下文?默认值为 true。当此属性设置为 false 时,URL 相对于当前 URL 进行解析。 |
| redirectHttp10Compatible | 重定向应该与 HTTP 1.0 兼容吗?当为真时,HTTP 状态代码 302 发出重定向;否则,一个 HTTP 状态代码 303 将被重定向。默认值为true。 |
| requestContextAttribute | 为所有视图设置org.springframework.web.servlet.support.RequestContext属性的名称。默认值为 null,这意味着您没有公开 RequestContext。当使用 useBean 等标准 JSP 标签或 Velocity 等无法访问请求的技术时,公开一个RequestContext会很有用。RequestContext是特定请求状态的上下文持有者。 |
| Suffix | 添加到视图名称以生成 URL 的后缀。 |
| viewClass | 要创建的视图的类型;这需要是org.springframework.web.servlet.view.AbstractUrlBasedView的子类。该属性是必需的。 |
| viewNames | 此视图解析程序可以处理的视图的名称。名称可以包含用于匹配名称的通配符*。默认值为 null,表示解析所有视图。 |
清单 8-4 是这个视图解析器的示例配置。我们需要指定视图类(必需的)。通常,还需要添加前缀和/或后缀来生成指向实际视图实现的 URL。使用UrlBasedViewResolver的优点是,在我们的配置中,我们不需要为每个View实例准备一个 bean。我们依靠UrlBasedViewResolver使用配置的属性和一个符号视图名来创建一个View。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
@Configuration
public class ViewConfiguration {
@Bean
public ViewResolver viewResolver() {
UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
viewResolver.setOrder(1);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
viewResolver.setViewClass(JstlView.class);
return viewResolver;
}
}
Listing 8-4UrlBasedViewResolver Configuration
InternalResourceViewResolver
这个对UrlBasedViewResolver的扩展是一个方便的子类,它将视图类预配置为org.springframework.web.servlet.view.InternalResourceView及其子类。清单 8-5 显示了org.springframework.web.servlet.view.?? 的示例配置。结果基本上与清单 8-4 中的相同。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted*
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
public class ViewConfiguration {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver;
viewResolver = new InternalResourceViewResolver();
viewResolver.setOrder(1);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
Listing 8-5InternalResourceViewResolver configuration
XsltViewResolver
org.springframework.web.servlet.web.view.xslt. XsltViewResolver可以将视图名称解析为 XSLT 样式表,从而将模型转换为向用户显示的内容。为了使用这个视图解析器和视图,我们需要一个 XSLT 模板来将模型转换成视图。返回的视图中,org.springframework.web.servlet.view.xslt.XsltView的一个实例检测要渲染哪个模型对象。它支持以下类型。
-
javax.xml.transform.Source -
org.w3c.dom.Document -
org.w3c.dom.Node -
java.io.Reader -
java.io.InputStream -
org.springframework.core.io.Resource
XsltView接受支持的类型并使用 XSLT 样式表转换它。尽管这种机制可能很强大,但我们认为这不是为 web 应用创建视图层的东西。一般来说,从控制器返回 XML(或 JSON)并在客户端用 JavaScript 直接处理更容易。
ContentNegotiatingViewResolver
org.springframework.web.servlet.view. ContentNegotiatingViewResolver是一个非常特殊的视图解析器;它可以通过名称和内容类型来解析视图。它首先确定所请求的内容类型。有三种方法可以做到。
表 8-3
ContentNegotiatingViewResolver 属性
|财产
|
目的
|
| --- | --- |
| contentNegotiationManager | bean 确定请求的媒体类型。 |
| cnmFactoryBean | ContentNegotiationManagerFactoryBean bean 创建一个ContentNegotiationManager实例。 |
| defaultViews | 设置要参考的默认视图。当找不到特定视图时使用。在使用封送视图或返回 JSON 时非常有用。 |
| useNotAcceptableStatusCode | 当找不到合适的视图时,我们是否应该向客户端发送 HTTP 响应代码 406?默认值为 false。 |
| viewResolvers | 要咨询的视图解析器列表。默认情况下,它检测应用上下文中的所有视图解析器。 |
| Order | 此视图解析程序在链中的调用顺序。数字越大,在链中的顺序越低。 |
-
检查文件扩展名。
-
检查
Accept割台。 -
默认勾选一个名为
format的请求参数(参数名称可配置;参见表 8-3 。
在确定了内容类型之后,解析器会咨询所有已配置的视图解析器,以便按名称收集候选视图。最后,它通过检查是否支持所请求的内容类型来选择最佳匹配的视图。表 8-3 显示了视图解析器的可配置属性。
当使用多个视图解析器时,
ContentNegotiatingViewResolver必须具有最高的顺序才能正常工作。默认情况下已经设置好了,但是如果你改变了顺序,请记住这一点。
实现自己的 ViewResolver
本节解释了如何实现我们自己的视图解析器。我们创建了一个简单的实现,它从配置视图的映射中解析视图名。
实现自己的观点很容易做到;你创建一个类,让它实现ViewResolver接口(参见清单 8-1 ,并提供必要的实现。清单 8-6 显示我们的com.apress.prospringmvc.bookstore.web.view.SimpleConfigurableViewResolver。
package com.apress.prospringmvc.bookstore.web.view;
// Other imports omitted*
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
public class SimpleConfigurableViewResolver implements ViewResolver {
private Map<String, ? extends View> views = new HashMap<>();
@Override
public View resolveViewName(String viewName, Locale locale) {
return this.views.get(viewName);
}
public void setViews(Map<String, ? extends View> views) {
this.views = views;
}
}
Listing 8-6SimpleConfigurableViewResolver
我们将在下一节中使用这个实现来添加 PDF 和 Excel 的视图。
查看技术
Spring MVC 支持许多不同的技术,如果没有支持,您可以通过实现org.springframework.web.servlet.View或扩展所提供的视图类来添加它。本节讨论几种视图技术,并展示 Spring MVC 如何支持它们。对一些人来说,有广泛的支持;对其他人来说,很少。图 8-3 显示了视图类的层次结构,在这里你可以看到一些支持的视图技术。对于某些技术,我们需要指定一个特定的ViewResolver来工作;其他的与配置的视图解析器一起工作。
图 8-3
视图层次结构
本节的下一部分将简要介绍一些受支持的视图技术。它展示了支持类以及如何设置 Spring 来使用指定的技术。它没有深入介绍所有不同的受支持视图技术;这里提到的大部分技术都有其他的书籍。
TilesViewResolver在本节的大多数清单中有 2 阶,以确保它在正确的时刻执行,特别是当使用ContentNegotiatingViewResolver时,它应该在TilesViewResolver之前执行(这是默认的)。
JavaServer 页面
到目前为止,我们的应用一直使用 JavaServer Pages。Spring 对它有很好的支持,包括它自己的标签库(见章节 5 和 6 )。Spring 有支持和集成类,一般来说,它是与org.springframework.web.servlet.view.InternalResourceViewResolver一起使用的工具,以启用 JSTL 支持并与 Sun 的默认格式和函数库集成。
瓷砖
Apache Tiles 该项目现已退休,Spring 框架团队非常喜欢百里叶 3 和小胡子、 4 但 Tiles 在很长一段时间内都是 Spring 的最爱,仍然值得关注。这些页面组件可以在不同的页面布局中重用和配置。最初它被设计成一个 JSP 组合框架;但是,它也可以构成基于 FreeMarker 的视图。
配置图块
要开始使用 Tiles,我们必须为它配置和引导引擎。接下来,我们需要配置视图解析器来返回基于 tiles 的视图,最后,我们需要指定页面组成并添加不同的模板(Tiles)。
我们需要将org.springframework.web.servlet.view.tiles3.TilesConfigurer添加到我们的配置中。接下来,我们需要特殊的 org . spring framework . web . servlet . view . tiles 3 . tilesviewrolver。清单 8-7 显示了图块的最基本配置。
package com.apress.prospringmvc.bookstore.web.config;
*// Other imports omitted*
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
@Configuration
public class ViewConfiguration {
@Bean
public TilesConfigurer tilesConfigurer() {
return new TilesConfigurer();
}
@Bean
public TilesViewResolver tilesViewResolver() {
TilesViewResolver tilesViewResolver = new TilesViewResolver();
tilesViewResolver.setOrder(2);
return tilesViewResolver;
}
}
Listing 8-7ViewConfiguration for Tiles
默认情况下,TilesConfigurer从WEB-INF目录加载一个名为tiles.xml的文件;该文件包含页面定义。在我们查看定义文件之前,让我们看看表 8-4 中的配置器的属性。
表 8-4
TilesConfigurer属性
财产
|
目的
|
| --- | --- |
| checkRefresh | 我们应该检查图块定义的变化吗?默认为false;将其设置为 true 会影响性能,但在开发过程中会很有用。 |
| completeAutoload | 当设置为 true(默认为false)时,图块的初始化完全由图块本身完成。它使这个配置器类的其他属性变得无用。 |
| definitions | 包含定义的文件列表。默认是指/WEB-INF/tiles.xml。 |
| definitionsFactoryClass | 设置用于创建图块定义的org.apache.tiles.definition.DefinitionsFactory实现。默认情况下使用org.apache.tiles.definition.UrlDefinitionsFactory类。 |
| preparerFactoryClass | 设置要使用的org.apache.tiles.preparer.PreparerFactory实现。默认情况下使用org.apache.tiles.preparer.BasicPreparerFactory类。 |
| tilesInitializer | 设置自定义初始值设定项来初始化图块。当设置自定义实现时,初始化器应该完全初始化 Tiles,因为设置这个属性会使这个类上的其他属性变得无用。 |
| useMutableTilesContainer | 我们应该使用可变瓷砖容器吗?默认为false。 |
| validateDefinitions | 指定我们是否应该验证定义 XML 文件。默认为true。 |
TilesViewResolver 没有要设置的附加属性;它与 UrlBasedViewResolver 具有相同的属性集。它是一个方便的子类,可以自动配置要返回的正确视图类型。对于图块,我们需要创建org.springframework.web.servlet.view.tiles3.TilesView的实例。
配置和创建模板
平铺需要一个或多个文件来定义我们的页面;这些被称为定义文件。TilesConfigurer 加载的默认文件是/WEB-INF/tiles.xml(参见清单 8-8 )。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<!-- definition 1 -->
<definition name="template" template="/WEB-INF/templates/template.jsp">
<put-attribute name="header" value="/WEB-INF/templates/header.jsp"/>
<put-attribute name="footer" value="/WEB-INF/templates/footer.jsp"/>
</definition>
<!-- definition 2 -->
<definition name="*" extends="template">
<put-attribute name="title" value="{1}.title" />
<put-attribute name="body" value="/WEB-INF/views/{1}.jsp" />
</definition>
<!-- definition 3 -->
<definition name="*/*" extends="template">
<put-attribute name="title" value="{1}.{2}/title" />
<put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
</definition>
</tiles-definitions>
Listing 8-8Tiles Definitions
我们创造了三个定义。
-
名为 template 的定义是总体布局配置。
-
其他定义扩展了这种常规布局(并且可以覆盖预定义的属性)。通过在定义名称中使用通配符(
*)来声明多个定义。{1}占位符指的是星星的值。 -
更多的定义扩展了这种常规布局,但是位于更深层次的目录中。位置层级由
/表示。占位符{1}表示第一颗星的值,{2}表示第二颗星的值。 5
为了让 Spring 选择正确的定义,我们的定义名称必须与视图匹配(或者像我们在示例中那样使用*通配符)。我们的模板页面(template.jsp)由三个瓦片(header、footer和body)组成,我们需要一个包含消息键的属性标题,这样我们就可以使用我们的消息源(参见章节 5 国际化的讨论)来解析实际的标题。清单 8-9 显示的是template.jsp,为总图。
<!DOCTYPE HTML>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ tagib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %>
<html>
<head>
<meta charset="utf-8">
<c:set var="titleKey">
<tiles:getAsString name="title" />
</c:set>
<title>
Bookstore | <spring:message code="${titleKey}"
text="Your Home in Books"/>
</title>
<link rel="stylesheet" type="text/css"
href="<c:url value="/resources/css/style.css"/>" >
</head>
<body>
<div id="wrap">
<tiles:insertAttribute name="header"/>
<div class="center_content">
<div class="left_content">
<h1>
<spring:message code="${titleKey}"
text="${titleKey}"/>
</h1>
<tiles:insertAttribute name="body" />
</div><!--end of left content-->
<div class="right_content">
<div class="right_box">
<div class="title">
<span class="title_icon">
<img
src="<c:url value="/resourcimg/bullet4.gif"/>"
alt="" title="" />
</span>
<spring:message code="main.title.randombooks"/>
</div>
<c:forEach items="${randomBooks}" var="book">
<div class="new_prod_box">
<c:url value="/book/${book.id}" var="bookUrl" />
<a href="${bookUrl}">${book.title}</a>
<div class="new_prod_img">
<c:url
value="/book/${book.isbn}/image" var="bookImage"/>
<a href="${bookUrl}">
<img src="${bookImage}" alt="${book.title}"
title="${book.title}" class="thumb"
border="0" width="100px"/>
</a>
</div>
</div>
</c:forEach>
</div><!--end of right box-->
</div><!--end of right content-->
<div class="clear"></div>
</div><!--end of center content-->
<tiles:insertAttribute name="footer" />
</div>
</body>
</html>
Listing 8-9template.jsp content
突出显示的代码根据来自我们的tiles.xml的title属性的内容设置了一个变量。这样,我们可以在 tiles 配置上指定一个键,并使用 Spring 消息标记来检索国际化值。清单 8-10 显示了我们的index.jsp,它被用作欢迎页面的主体。
<p>Welcome to the Book Store</p>
Listing 8-10index.jsp Used as Content
图 8-4 显示了结果页面。
图 8-4
结果欢迎页面
FreeMarker 和百里香叶
FreeMarker 6 和百里香叶都是用 Java 编写的模板框架。您可以使用它们来创建 HTML 页面的模板。它们是基于文本的模板引擎,两者都广泛用于各种模板解决方案的应用中。
Spring 使用的另一个 HTML 模板框架叫做 Velocity,但是它不再被支持了。Velocity 包在 Spring 4.3 中被弃用,并在 5.0.1 7 中被完全移除,以支持 FreeMarker。速度是相当古老的;最新版本发布于 2010 年。
FreeMarker 和 Thymeleaf 模板不像 JSP 那样编译成 Java 代码。它们在运行时由它们的模板引擎解释,这很像我们前面讨论的 XSLT 处理。您可能认为这种解释(而不是编译)会导致应用的性能下降,但这通常不是真的。两个引擎都有大量的解释模板缓存,这使得它们很快。
与 JSP 相比,使用模板方法的另一个优点是,在后一种情况下,您可能会倾向于将 Java 代码放在 JSP 中。将 Java 代码放在页面中,尽管是可能的,但不是您应该采用的方法。这通常会导致页面难以维护、调试和修改。
当使用 FreeMarker 和 Thymeleaf 模板时,需要额外的配置来设置正确的模板引擎和视图解析器。首先,我们需要配置我们选择的模板引擎。然后,我们需要为模板引擎配置视图解析。
配置模板引擎
Spring 框架广泛支持 FreeMarker 和 Thymeleaf,并且有一些助手类可以使引擎的配置更加容易。FreeMarker 有org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer(见表 8-5 )。
表 8-5
FreeMarkerConfigurer属性
财产
|
目的
|
| --- | --- |
| configLocation | 包含 FreeMarker 引擎设置的配置文件的位置。 |
| defaultEncoding | 设置 FreeMarker 配置文件的编码。默认情况下使用平台编码。 |
| freemarkerSettings | 直接设置模板引擎的属性。它可以覆盖配置文件中的属性,或者完全配置模板引擎。 |
| freemarkerVariables | 设置已知 FreeMarker 对象的贴图。这些对象作为变量传递给 FreeMarker 配置。 |
| postTemplateLoaders | 指定 freemarker . cache . template loader 来加载模板。它们是在默认模板加载器之后注册的。 |
| preferFileSystemAccess | 我们应该选择文件系统访问来加载 FreeMarker 模板吗?默认值为 true 如果您的模板不在文件系统上,例如,在 jar 文件的类路径上,则将此设置为 false。 |
| preTemplateLoaders | 指定 freemarker . cache . template loader 来加载模板。它们在默认模板加载器之前注册。 |
| templateLoaderPathtemplateLoaderPaths | 设置 FreeMarker 模板的路径。templateLoaderPaths的值可以是逗号分隔的路径列表。它可以混合不同的资源路径(参见第二章中的“资源加载”)。 |
该表中最重要的属性是设置加载模板的位置的属性:templateLoaderPath。最佳实践是让 web 客户端无法访问它们,这可以通过将它们放在WEB-INF目录中来实现。
还有
org.springframework.beans.factory.FactoryBean来配置 FreeMarker 模板引擎,引导引擎使用非 web 模板,如电子邮件。
百里香发动机通过org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver和org.thymeleaf.spring5.SpringTemplateEngine两种类型与 Spring 框架轻松集成(见表 8-6 )。需要一个SpringResourceTemplateResolver bean 来支持百里香模板资源。
表 8-6
百里香叶的特性SpringResourceTemplateResolver
财产
|
目的
|
| --- | --- |
| applicationContext | 这个属性需要被设置为 Spring ApplicationContext实例,这样它就可以访问模板资源。 |
| prefix | 添加到所有模板名称的前缀,用于将模板名称转换为资源名称。 |
| suffix | 添加到所有模板名称的后缀,用于将模板名称转换为资源名称。 |
| forceSuffix | 模板上应该强制使用后缀吗?如果设置为true,无论模板名称的扩展名如何,都将应用配置的后缀。默认为false。 |
| templateMode | 应用于百里香解析器解析的模板的模板模式。默认为 HTML。 |
| forceTemplateMode | 是否应该在模板资源上强制使用模板模式?如果设置为true,则解析不在模板资源名称上,而是在配置的suffix上。默认是false。 |
| characterEncoding | 读取资源的字符编码。 |
| cacheable | 百里香解析器解析的模板应该缓存吗?默认值是true,但是在开发过程中,我们建议您将该属性设置为false。 |
| order | 此视图解析程序在链中的调用顺序。数字越大,在链中的顺序越低。默认值为 1。 |
上表中最重要的属性是定义模板资源位置的属性(suffix和prefix),以及必须访问它们的applicationContext。最佳实践是让 web 客户端无法访问它们,这可以通过将它们放在WEB-INF目录中来实现。
还有另一个特定于 Spring 的实现,由实现
ServletContextAware的org.thymeleaf.templateresolver.ServletContextTemplateResolver类提供,并且依赖于 servlet 上下文。这个实现使用 Servlet 资源解析机制来解析模板,而SpringResourceTemplateResolver使用 Spring 的资源解析机制来解析模板。这些类大多是可互换的,但是推荐使用SpringResourceTemplateResolver,因为它可以自动与 Spring 的资源解析基础设施集成。
除了设置不同的引擎,我们还需要配置一个视图解析器来解析正确的视图实现。春运用org.springframework.web.servlet.view.freemarker.FreemarkerViewResolver。百里香框架为同样的目的提供了org.thymeleaf.spring5.view.ThymeleafViewResolver。不要求使用这些专门的视图解析器;配置广泛的InternalResourceViewResolver也可以。然而,使用这些专门的视图解析器使我们的生活变得更容易。清单 8-11 显示了一个 FreeMarker 配置示例。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
@Configuration
public class ViewConfiguration {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer freeMarkerConfigurer;
freeMarkerConfigurer = new FreeMarkerConfigurer();
freeMarkerConfigurer.setTemplateLoaderPath("WEB-INF/freemarker");
return freeMarkerConfigurer;
}
@Bean
public ViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
viewResolver.setSuffix(".ftl");
return viewResolver;
}
}
Listing 8-11FreeMarker Configuration
当控制器现在返回index作为视图名称时,对于 FreeMarker 模板,它变成了WEB-INF/freemarker/index.ftl。视图名称前有templateLoaderPath。视图解析器还允许设置额外的前缀(从AbstractTemplateViewResolver继承而来)。表 8-7 描述了视图解析器的不同属性。
表 8-7
FreeMarker 视图解析器的其他属性
|财产
|
目的
|
| --- | --- |
| allowRequestOverride | 当我们合并模型时,请求属性应该覆盖模型属性吗?当设置为 true 时,当以相同的名称存储时,请求属性可以覆盖模型属性。默认值为 false,这将在遇到同名属性时导致异常。 |
| allowSessionOverride | 当我们合并模型时,会话属性应该覆盖模型属性吗?设置为 true 时,会话属性可以覆盖以相同名称存储的模型属性。默认值为 false,这将在遇到同名属性时导致异常。 |
| exposeRequestAttributes | 所有的请求属性都应该放在模型中吗?默认值为 false。 |
| exposeSessionAttributes | 是否应该将所有会话属性都放入模型中?默认值为 false。 |
| exposeSpringMacroHelpers | 是否应该暴露宏(见表 8-8 )以便它们可用于渲染?默认值为 true。 |
清单 8-12 显示了百里香叶配置样本。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
@Configuration
public class ViewConfiguration implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
var resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
resolver.setPrefix("/WEB-INF/thymeleaf/");
resolver.setSuffix(".html");
//HTML is the default value, added here for clarity
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
@Bean
@Description("Thymeleaf Template Engine")
public SpringTemplateEngine templateEngine() {
var templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
@Bean
@Description("Thymeleaf View Resolver")
public ThymeleafViewResolver viewResolver() {
var viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
}
Listing 8-12Thymeleaf Configuration
当控制器现在返回index作为视图名称时,对于百里香模板,它变成了WEB-INF/AbstractTemplateViewResolver/index.html。将prefix值添加到视图名称之前,并将suffix值添加到视图名称之后。
模板语言
既然我们已经配置了环境,我们还需要编写一个显示页面的模板。FreeMarker 和百里香有点相似。列表 8-13 和 8-14 分别显示了 FreeMarker 和百里香的图书搜索页面。
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{template/layout :: head('Search books')}"></head>
<body>
<div id="header" th:replace="~{template/layout :: header}" ></div>
<h1 id="pagetitle" th:text="#{book.searchcriteria}">SEARCH TITLE</h1>
<form action="#" th:action="@{/book/search}" th:object="${bookSearchCriteria}" method="GET" id="bookSearchForm">
<fieldset>
<legend th:text="#{book.searchcriteria}">SEARCH CRITERIA</legend>
<table>
<tr>
<td><label for="title" th:text="#{book.title}">TITLE</label></td>
<td><input type="text" th:field="*{title}"/></td>
</tr>
<tr>
<td><label for="category" th:text="#{book.category}">CATEGORY</label></td>
<td>
<select th:field="*{category}">
<option th:each="c : ${categories}" th:value="${c.id}" th:text="${c.name}" th:selected="${i==1}">
</option>
</select>
</td>
</tr>
</table>
</fieldset>
<button id="search" th:text="#{button.search}">SEARCH</button>
</form>
<!-- Javascript functions ommitted -->
<table id="bookSearchResults" th:if="${bookList ne null and not #lists.isEmpty(bookList)}">
<thead>
<tr>
<th th:text="#{book.title}">TITLE</th>
<th th:text="#{book.description}">DESCRIPTION</th>
<th th:text="#{book.price}">PRICE</th>
<th></th>
</tr>
</thead>
<tbody>
<th:block th:each="book : ${bookList}">
<tr>
<td><a th:href="@{/book/detail/} + ${book.id}" th:text="${book.title}">TITLE</a></td>
<td th:text="${book.description}">DESC</td>
<td th:text="${book.price}">PRICE</td>
<td><a th:href="@{/cart/add/} + ${book.id}" th:text="${book.addtocart}">CART</a></td>
</tr>
</th:block>
</tbody>
</table>
</body>
</html>
Listing 8-14books/search.html Thymeleaf Template
<#ftl>
<#import "/spring.ftl" as spring />
<!DOCTYPE HTML>
<html>
<head>
<title>Booksearch</title>
</head>
<body>
<h1><@spring.message code="book.title" /></h1>
<p>
<form method="POST">
<fieldset>
<legend><@spring.message code="book.searchcriteria" /></legend>
<table>
<tr>
<td><@spring.message code="book.title" /></td>
<td><@spring.formInput"searchCriteria.title" /></td>
</tr>
<tr>
<td><@spring.message code="book.category" /></td>
<td><@spring.formSingleSelect
"searchCriteria.category", categories, "" /></td>
</tr>
</table>
</fieldset>
<button id="search"><@spring.message code="book.search" /></button>
</form>
<!-- Javascript functions ommitted -->
<#if bookList?has_content>
<table>
<tr>
<th><@spring.message code="book.title"/></th>
<th><@spring.message
code="book.description"/></th>
<th><@spring.message code="book.price" /></th>
</tr>
<#list bookList as book>
<tr>
<td>${book.title}</td>
<td>${book.description}</td>
<td>${book.price}</td>
<td><a
href="<@spring.url "/cart/add/${book.id}"/>">
<@spring.message code="book.addtocart"/></a></td>
</tr>
</#list>
</table>
</#if>
</p>
</body>
</html>
Listing 8-13books/search.ftl FreeMarker Template.
FreeMarker 模板类似于 Apache 图块模板。FreeMarker(参见清单 8-13 )模板也有可用的标签库(在绑定到 Spring 的清单中)。这两个库为 JSP 提供了与 Spring Form 标记库相同的支持。
百里香叶不同于 FreeMarker。百里叶是一个现代的服务器端 Java 模板引擎,适用于 web 和独立环境。当 Spring 开始远离 Apache Tiles 时,它转向了百里香,因为它的创建者为 Spring 设计了这个模板框架。
Thymeleaf 支持多种模板:HTML、XML、JavaScript、CSS,甚至纯文本,但是最容易设计和使用的是 HTML 模板。百里香模板对于开发流程来说是优雅而自然的,因为它们是用 HTML 编写的,所以可以在设计阶段使用浏览器进行测试。百里香的最大优点是它可以很容易地与 Spring 控制器、本地化和验证集成。
前一个模板包含几个百里香标签,这些标签很自然地适合 HTML 内容。它们以th:为前缀,由百里香模板引擎解释生成相应的 HTML 页面。
表 8-8 提供了不同 FreeMarker 标签的概述。百里香等效结构只是丰富的 HTML 标签,因此没有必要进行比较。
表 8-8
Tage 可用于 FreeMarker 和百里香等效的 HTML 构造
|巨
|
FreeMarker
| | --- | --- | | 消息(根据 code 参数从资源包中输出一个字符串) | | | messageText (根据 code 参数从资源包中输出一个字符串,返回默认参数的值) | | | url (用应用的上下文根作为相对 url 的前缀) | | | formInput (用于收集用户输入的标准输入字段) | | | **formHiddenInput *** (用于提交非用户输入的隐藏输入字段,例如 CSRF 令牌) | | | formPasswordInput *(收集密码的标准输入字段) | | | formTextarea (用于收集长的自由格式文本输入的大文本字段) | | | formSingleSelect (允许选择单个所需值的下拉框) | | | formMultiSelect (允许用户选择 0 个或多个值的选项列表框) | | | formRadioButtons (一组单选按钮,允许从可用选项中进行单项选择) | | | 表单复选框(一组允许选择 0 个或多个值的复选框) | | | 表单复选框(单个复选框) | | | 显示错误 | |
列出的任何宏的参数都有一致的含义。
-
path :要绑定的字段名称(即
searchCriteria.title)。 -
选项:包含可从输入栏中选择的所有可用值的映射。映射的键表示从表单回发并绑定到命令对象的值。属于键的值被用作向用户显示的标签。通常,这种图由控制器作为参考数据提供。根据所需的行为,可以使用任何 Map 实现。
-
分隔符:当多个选项作为离散元素(单选按钮或复选框)可用时,字符序列在列表中分隔每个选项(例如,
<br/>)。 -
属性:包含在 HTML 标签本身中的任意标签或文本的附加字符串。该字符串由宏直接回显。例如,在 textarea 字段中,您可以提供属性作为
rows="5" cols="60",或者您可以传递样式信息,如style="border:1px solid silver"。 -
classOrStyle :对于 showErrors 宏,包装每个错误的 span 标签使用的 CSS 类的名称。如果没有提供任何信息(或者值为空),错误将被包含在
<b></b>标签中。
表中标记(*)的两个宏用于 FreeMarker 然而,它们不是必需的,因为您可以使用普通的formInput宏指定 hidden 或 password 作为fieldType参数的值。
使用 FreeMarker,您可以指定使用哪个库。在 FreeMarker 中,我们需要使用 import 指令来指定库(参见清单 8-13 )。
百里香不使用任何需要在模板中引用的特殊标签库。百里香模板引擎寻找th:结构并动态解析它们。最重要的百里香叶构建体用于以下目的。
-
th:fragment 声明了一个 HTML 元素,它是布局的一部分,可以被子页面继承或覆盖。片段可以接收一个参数。比如
<head th:fragment="head(title)"/>。 -
th:replace 声明了一个 HTML 元素,它替换了一个从布局继承的元素。如果片段被参数化,则需要一个参数。比如
<head th:replace="~{template/layout :: head('Search')}"/>。 -
th:text (对于带有文本值的 HTML 元素)告诉百里香叶引擎用一个动态获得的值替换这个结构的值。编写 HTML 模板时,HTML 元素的默认值通常用大写字母书写。这有助于在浏览器中打开模板,因为它描绘了正确的视图。当描述默认文本而不是动态解析的文本时,它还有助于发现引擎配置问题。
th:text结构中的值要么是模型属性<title th:text="${title}"> TITLE </title>的值,要么是国际化文本<title th:text="#{book.title}"> TITLE </title>的值。 -
th:href 为
<a/>和<link />HTML 元素设置带有上下文 URL 的href属性。元素<link rel="stylesheet" type="text/css" th:href="@{/resources/css/style.css}" >的href属性由一个 URL 填充,该 URL 指向应用上下文中的style.css文件。 -
th:if 决定页面上是否应该显示 HTML 元素或文本。例如,我们可以使用类似于
<li th:if="${session.account ne null}"><a th:href="@{/logout}" th:text="#{nav.logout}">LOGOUT</a></li>的结构,以用户会话中存在一个account实例作为注销选项的条件。
百里叶使用 Spring 表达式语言进行表达式求值,并且有这个库的扩展用于 Spring Security 支持。这种与 Spring 框架的强大集成清楚地表明了为什么这个模板框架是 Spring web 应用的完美之选。
便携文档格式
Spring 可以集成 iText 8 或者 OpenPDF 9 来支持渲染 PDF 视图。Spring 团队推荐 OpenPDF,因为它得到了积极的维护,并修复了不受信任的 PDF 内容的一个重要漏洞。
为了能够呈现 PDF 视图,我们需要编写自己的视图实现,为此,我们需要扩展org.springframework.web.servlet.view.document.AbstractPdfView。当我们扩展这个类时,我们必须实现buildPdfDocument方法。
我们创建了一个 PDF 文件,在我们的帐户页面上概述了我们的一个订单。清单 8-15 显示了视图实现。
package com.apress.prospringmvc.bookstore.web.view;
// Other imports omitted
import org.springframework.web.servlet.view.document.AbstractPdfView;
import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Table;
import com.lowagie.text.pdf.PdfWriter;
public class OrderPdfView extends AbstractPdfView {
@Override
protected void buildPdfDocument(Map<String, Object> model,
Document document,
PdfWriter writer,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
Order order = (Order) model.get("order");
document.addTitle("Order :" + order.getId());
document.add(new Paragraph("Order date: " + order.getOrderDate()));
document.add(new Paragraph("Delivery date: " + order.getDeliveryDate()));
Table orderDetails = new Table(4);
orderDetails.addCell("Title");
orderDetails.addCell("Price");
orderDetails.addCell("#");
orderDetails.addCell("Total");
for (OrderDetail detail : order.getOrderDetails()) {
orderDetails.addCell(detail.getBook().getTitle());
orderDetails.addCell(detail.getBook().getPrice().toString());
orderDetails.addCell(String.valueOf(detail.getQuantity()));
orderDetails.addCell(detail.getPrice().toString());
}
document.add(orderDetails);
}
}
Listing 8-15View Implementation to Create a PDF
接下来,我们来补充一下org.springframework.web.servlet.view。ContentNegotiatingViewResolver对我们的视图进行配置。我们这样做是为了让我们的订单页面呈现为 HTML 或 PDF 格式,我们不想改变com.apress.prospringmvc.bookstore.web.controller.OrderController,因为它已经在做我们想要做的事情了——将选中的订单添加到模型中。清单 8-16 显示了变更后的com.apress.prospringmvc.bookstore.web.config.ViewConfiguration。这也是我们开始使用自定义视图解析器的地方。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.servlet.view.document.AbstractPdfView;
import com.apress.prospringmvc.bookstore.web.view.OrderPdfView;
import com.apress.prospringmvc.bookstore.web.view.SimpleConfigurableViewResolver;
@Configuration
public class ViewConfiguration {
@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver viewResolver;
viewResolver = new ContentNegotiatingViewResolver();
List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>();
viewResolvers.add(pdfViewResolver());
viewResolver.setViewResolvers(viewResolvers);
return viewResolver;
}
@Bean
public ViewResolver pdfViewResolver() {
SimpleConfigurableViewResolver viewResolver;
viewResolver = new SimpleConfigurableViewResolver();
Map<String, AbstractPdfView> views;
views = new HashMap<String, AbstractPdfView>();
views.put("order", new OrderPdfView());
viewResolver.setViews(views);
return viewResolver;
}
// Other methods omitted
}
Listing 8-16ViewConfiguration with ContentNegotiatingViewResolver
更改后的配置包含了我们的视图解析器,我们用它来解析com.apress.prospringmvc.bookstore.web.view.OrderPdfView。此配置还允许我们解析 Excel 文档的订单视图(参见“Excel”一节)。
经过这些更改后,我们需要重新部署我们的应用。如果我们登录并导航到我们的帐户页面,我们现在可以单击 PDF 链接并获得 PDF 而不是 HTML 版本。图 8-5 显示了点击 PDF 链接的结果。
图 8-5
生成的 PDF
虽然这种方法非常灵活,但缺点是我们需要为我们想要的每个 PDF 编码 PDF 的构造。如果我们有一些复杂的 PDF 或需要应用某种样式,这是繁琐和难以维护的。在这种情况下,可能值得考虑像 JasperReports 这样的解决方案(参见“JasperReports”一节)。
超过
Spring 有两种呈现 Excel 文档的方式。第一种是使用 JExcel 库, 10 ,另一种是使用 Apache POI 库。 11 这两种方法都需要我们实现一个视图(和 PDF 一样);为此,我们扩展了org.springframework.web.servlet.view.document.AbstractXlsView或org.springframework.web.servlet.view.document.AbstractXlsxView。它们分别适用于 XLS 和 XLSX 格式。两种实现都隐藏了设置,并允许加载和处理 XLS 模板;我们需要添加特定于视图的渲染。我们需要为此实现 buildExcelDocument 方法。清单 8-17 显示了一个使用 Apache POI 的 Excel 文档订单的View实现示例。
package com.apress.prospringmvc.bookstore.web.view;
// Other imports omitted
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.web.servlet.view.document.AbstractXlsView;
public class OrderExcelView extends AbstractXlsView {
@Override
protected void buildExcelDocument(Map<String, Object> model,
WritableWorkbook workbook,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
response.setHeader("Content-Disposition",
"attachment; filename=\"order.xls\"");
Order order = (Order) model.get("order");
Sheet sheet = workbook.createSheet();
sheet.createRow(1).createCell(0)
.setCellValue("Order: " + order.getId());
sheet.createRow(2).createCell(0)
.setCellValue("Order Date: " + order.getOrderDate());
sheet.createRow(3).createCell(0)
.setCellValue("Delivery Date: " + order.getDeliveryDate());
sheet.createRow(4).createCell(0)
.setCellValue("Order: " + order.getId());
Row header = sheet.createRow(5);
header.createCell(0).setCellValue("Quantity");
header.createCell(1).setCellValue("Title");
header.createCell(2).setCellValue("Price");
int row = 5;
for (OrderDetail detail : order.getOrderDetails()) {
row++;
Row detailRow = sheet.createRow(row);
detailRow.createCell(0).setCellValue(detail.getQuantity());
detailRow.createCell(1)
.setCellValue(detail.getBook().getTitle());
detailRow.createCell(2).setCellValue(
detail.getPrice().doubleValue() * detail.getQuantity());
}
row++;
Row footer = sheet.createRow(row);
footer.createCell(0).setCellValue("Total");
footer.createCell(1).setCellValue(
order.getTotalOrderPrice().doubleValue());
}
}
Listing 8-17OrderExcelView
在视图旁边,我们需要添加一个视图解析器。在我们的示例应用中,我们将这个(就像 PDF 视图一样)添加到我们的ViewConfiguration类中。我们添加了自定义实现的另一个实例(参见清单 8-18 )并让ContentNegotiatingViewResolver决定做什么。
package com.apress.prospringmvc.bookstore.web.config;
//Other imports omitted
import org.springframework.web.servlet.view.document.AbstractJExcelView;
import org.springframework.web.servlet.view.document.AbstractPdfView;
import com.apress.prospringmvc.bookstore.web.view.OrderExcelView;
@Configuration
public class ViewConfiguration {
@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver viewResolver;
viewResolver = new ContentNegotiatingViewResolver();
List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>();
viewResolvers.add(pdfViewResolver());
viewResolvers.add(xlsViewResolver());
viewResolver.setViewResolvers(viewResolvers);
return viewResolver;
}
@Bean
public ViewResolver xlsViewResolver() {
SimpleConfigurableViewResolver viewResolver;
viewResolver = new SimpleConfigurableViewResolver();
Map<String, AbstractJExcelView> views;
views = new HashMap<String, AbstractJExcelView>();
views.put("order", new OrderExcelView());
viewResolver.setViews(views);
return viewResolver;
}
// Other methods omitted
}
Listing 8-18ViewConfiguration with OrderExcelView
但是,等等,难道我们的应用不会因为我们有多个解析为订单视图名称的视图实现而崩溃吗?特殊的视图解析器ContentNegotiatingViewResolver可以在这里帮助我们。它使用Accept头确定哪个解析的视图最匹配请求的内容类型。不需要改变我们的控制器,只需添加一些配置(和视图实现),我们就可以区分哪个视图被服务。
要测试,点击XLS链接,会下载一个 Excel 文档供您查看。
XML 和 JSON
Spring MVC 有另一种方式向我们的客户提供 XML 或 JSON。我们可以利用ContentNegotiatingViewResolver成为我们的优势。Spring 有两个特殊的视图实现来将对象转换成 XML 或 JSON,分别是org.springframework.web.servlet.view.xml.MarshallingView和org.springframework.web.servlet.view.json.MappingJackson2JsonView。基于 XML 的视图使用 Spring XML 支持将我们的模型编组为 XML。JSON 视图使用 Jackson 库。 12 我们可以轻松地配置视图解析器,将 XML 和/或 JSON 公开给客户。我们可以简单地为 XML 和 JSON 添加一个默认视图(我们也可以添加额外的视图解析器,就像我们对 PDF 和 Excel 文档所做的那样)。清单 8-19 是修改后的配置(参见突出显示的部分)。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.xstream.XStreamMarshaller;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.servlet.view.xml.MarshallingView;
import com.apress.prospringmvc.bookstore.web.view.OrderExcelView;
import com.apress.prospringmvc.bookstore.web.view.OrderPdfView;
@Configuration
public class ViewConfiguration {
@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver viewResolver;
viewResolver = new ContentNegotiatingViewResolver();
List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>();
viewResolvers.add(pdfViewResolver());
viewResolvers.add(xlsViewResolver());
viewResolver.setViewResolvers(viewResolvers);
List<View> defaultViews = new ArrayList<View>();
defaultViews.add(jsonOrderView());
defaultViews.add(xmlOrderView());
viewResolver.setDefaultViews(defaultViews);
return viewResolver;
}
@Bean
public MappingJackson2JsonView jsonOrderView() {
MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
jsonView.setModelKey("order");
return jsonView;
}
@Bean
public MarshallingView xmlOrderView() {
MarshallingView xmlView = new MarshallingView(marshaller());
xmlView.setModelKey("order");
return xmlView;
}
@Bean
public Marshaller marshaller() {
return new XStreamMarshaller();
}
// Other methods omitted, see previous listings
}
Listing 8-19ViewConfiguration for XML and JSON
为了让 XML 工作,我们还需要配置一个org.springframework.oxm.Marshaller实现。我们在这里选择使用 XStream 13 库是因为使用起来快捷方便。要使用另一种解决方案,只需配置适当的封送拆收器。关于编组和 XML 的更多信息可以在 Spring 参考指南中找到。将modelKey属性设置为要封送的对象。如果未指定,将在模型映射中搜索受支持的值类型。
在 ORM 实现中使用这种类型的视图时(就像在我们的例子中),由于集合正在初始化,您可能会遇到延迟加载或加载一半数据库的情况。
如果我们现在将浏览器中的 URL 改为。json 或者。xml,我们得到订单的 JSON 或 XML 表示(见图 8-6 是 JSON 示例)。现在,我们有五种不同的方式来查看订单(HTML、PDF、Excel、JSON 和 XML ),无需触摸控制器,只需更改配置即可。
图 8-6
JSON 表示我们的order
摘要
本章讲述了 Spring MVC 的视图部分。我们通过介绍几种通用的 ViewResolver 实现来研究视图解析。我们还介绍了 Spring MVC 支持的几种视图技术,并解释了如何配置 Spring 来使用它们。我们从 JSP 开始,我们简要介绍了 JSF 以及如何将 Spring Integration 到 JSF 应用中。接下来,您看到了几种模板解决方案;具体来说,瓷砖,速度和自由标记。
在基于 web 的视图之后,我们研究了不同的视图技术,比如如何在不改变控制器的情况下创建 PDF 和 Excel,只是简单地添加了ContentNegotiatingViewResolver和一个适当的视图实现。
前一章介绍了 JSON,这一章介绍了另一种将模型公开为 JSON 或 XML 的方法。最后,我们看了呈现 PDF 和 Excel 视图。
从这一章学到的一件重要的事情是控制器逻辑和视图逻辑的分离(通过我们的顺序的不同表示来证明)。这显示了应用关注点分离的力量以及它给人的灵活性。
您可能永远不会在一个应用中使用所有的技术。您可能只使用两到三种不同的技术(对于我们的页面,可能会创建一个 PDF 或 Excel 文件)。但是,能够灵活地更改或简单地向我们的应用添加一个新的视图层也不错。
Footnotes 1https://www.iana.org/assignments/media-types/media-types.xhtml
2
https://attic.apache.org/projects/tiles.html
3
4
5
https://tiles.apache.org/framework/tutorial/advanced/wildcard.html
6
https://freemarker.apache.org/
7
https://github.com/spring-projects/spring-framework/issues/18368
8
9
https://github.com/LibrePDF/OpenPDF
10
http://jexcelapi.sourceforge.net
11
12
https://github.com/FasterXML/jackson
13
九、Spring WebFlux 简介
在前面的章节中,典型的 Java web 应用是在 Apache Tomcat 服务器的一个实例上构建和运行的,该实例在应用的外部或嵌入在应用中。不管是哪种情况,Spring DispatcherServlet负责将传入的 HTTP 请求定向到应用中声明的所有处理程序。但是,像我们到目前为止开发的应用能在真实的生产环境中使用吗?DispatcherServlet可以同时处理多少 HTTP 请求?这个数字还能增加吗?它们会在可接受的时间框架内得到处理吗?在与世界共享 web 应用之前,需要回答这些问题以及许多其他问题。
生产 web 应用需要处理大量的用户和大量的数据,并且在面对更多的信息进入、系统中的错误或者仅仅是系统中的速度变慢时,能够保持弹性。想想 Twitter、脸书或 YouTube,以及一天中任何时候有多少内容被上传或下载。当打开你的脸书页面时,你希望它能有响应,即使有数百万其他用户登录并和你做同样的事情:阅读消息,或发布消息,视频,图片,或玩游戏。这相当于同时处理数量惊人的请求。这些请求可能需要来自数据库或文件的数据,或者来自其他服务的数据,这引入了阻塞 I/O 操作的可能性。
如果这些应用是使用 Spring 开发的,DispatcherServlet是所有请求的入口点。DispatcherServlet在它能处理的请求数量上没有发言权。servlet 容器定义了这一点;在我们的例子中,是 Apache Tomcat 服务器。
接下来的几章将重点介绍如何使用 Spring WebFlux 来构建在 Netty、Undertow 和 Servlet 3.1+容器等非阻塞服务器上运行的反应式 web 应用。为了理解为什么反应式应用最适合处理大量的用户和数据,有必要解释一下自从互联网出现以来 HTTP 请求是如何被处理的。
HTTP 请求处理
HTTP 是超文本传输协议的首字母缩写,使用超文本链接加载网页。一个典型的 HTTP 流包括一台客户机向服务器发出一个请求,然后服务器发送一个响应消息。在 HTTP (1.0)的初始版本中,每个请求/响应对都需要一个连接。您可以看到为什么这是低效的,因为建立新的连接包括 TCP 握手过程,以将通信方相互介绍。?? 1
在 HTTP 1.1 中,引入了持久 HTTP 连接。 2 这意味着一个连接保持活动状态,并被多个 HTTP 请求重用,从而减少双方之间的通信延迟。
这对 Apache Tomcat 这样的 servlet 容器意味着什么?Apache Tomcat 是构建和维护基于 Java 软件平台的动态网站和应用的流行选择。Java Servlet API 使 web 服务器能够使用 HTTP 协议处理动态的基于 Java 的 web 内容。旧版本的 Tomcat 有一个阻塞的 HTTP 连接器,遵循的是每个连接一个线程的模型。这意味着它为正在处理的每个 HTTP 连接分配一个 Java 线程。因此,可以同时连接到应用的用户数量受到应用服务器支持的线程数量的限制。当建立 HTTP 连接时不创建线程,在 HTTP 连接关闭后销毁线程,因为这样效率很低。相反,服务器管理一个为 HTTP 连接提供线程的线程池。当一个 HTTP 连接建立后,一个线程从池中被分配给它。线程完成接收请求和提供响应的工作,当 HTTP 连接关闭时,线程被回收到池中,并准备好分配给另一个请求。
这种方法的问题是,只要 HTTP 连接是打开的,线程在不使用时就处于空闲状态。如果一个或多个用户在请求之间花费时间,或者忘记关闭连接,最终,服务器会耗尽线程并停止接受新的连接。一种解决方案是增加池中线程的数量;但是,线程池的大小受到特征(内存、CPU 等)的限制。)安装服务器的虚拟机/计算机。显而易见的解决方案是获得更强大的服务器。在软件开发中,这被称为垂直扩展,它在一定程度上起作用,受到现有硬件及其成本的限制。明智的解决方案是将应用安装在多台服务器上。如今,通过将您的应用部署到像 AWS 3 或 GCP 4 这样的云平台,并建立一个包括自动负载平衡器的云配置,可以在必要时跨越虚拟机,就可以轻松做到这一点。这种方法被称为水平缩放并且很有效,但是它也会变得非常昂贵,尤其是当你的应用变得流行的时候。
Tomcat 的新版本(在 Java 4 之后)和其他流行的 web 服务器都使用每个请求一个线程的模型,这意味着一个持久的 HTTP 连接不需要一直分配一个线程给它。这在 Java 4 之前是不可能的,因为在 JDK 中没有非阻塞 IO API。只有在处理请求时,才能将线程分配给 HTTP 连接。如果连接是空闲的,线程可以被回收,连接被放在一个集中的 NIO 5 select set 中检测新的请求,而不消耗单独的线程。这意味着处理相同数量的用户所需的线程数量更少,而且由于线程使用的资源少得多,扩展应用所需的财务投资也更少。
有几件事需要考虑。对于同时连接的大量用户,资源消耗仍然很高。对于短期请求,它具有与每连接线程模型相同的行为和性能。当在处理每个请求的过程中有长时间的暂停时,线程仍然保持空闲。唯一的优点是每请求线程模式比每连接线程模式的伸缩性稍好。
并发处理 HTTP 请求的应用还必须设计为共享资源,对于不能共享的资源,必须实现同步访问,这可能会导致阻塞。我们如何在 Java 应用中避免阻塞?通过使用异步处理。Servlet API 3.0 引入了异步 Servlet 处理支持,因此慢速任务(例如,等待一些资源变得可用)可以在一个单独的线程中处理,而不会阻塞服务器管理的线程池,并且当完成时,通知容器分配一个新的容器管理的线程,以将结果发送回客户端。
这些是针对典型 web 问题的典型解决方案,但是它们仍然需要在将响应发送给客户端之前完整地构建响应。以搜索引擎 web 应用为例。您将如何实现解决方案来响应搜索查询?考虑到互联网上可用内容的数量,让您的客户端等待直到您扫描完所有索引内容将会花费很长时间,并且响应的大小如此之大,以至于需要很长时间来传输。向你的客户发送完整的回复是不可行的。对于这种情况,流式方法更合适。您扫描一些索引资源,然后向客户端发送一个部分响应,然后扫描更多的资源,发送另一个资源,以此类推,直到没有发现其他内容。另一个可能出现的问题是,如果您发送部分结果,但您将它们发送到 fast,而客户端无法处理它们,您就有阻塞客户端的风险。因此,您的解决方案需要提供一个允许客户端调节流量的数据流。
如果软件的例子对你来说太令人费解,想象一下下面的场景。你有一个叫吉姆的朋友。你还有一桶不同颜色的球。吉姆告诉你把所有的红球都给他。你有两种方法做这件事。
-
你把所有的红球都捡起来,放到另一个桶里,然后把桶递给吉姆。这是典型的请求-完成响应模型。这是一个异步模型,如果选择红球花费的时间太长,Jim 会在你进行分类时做其他事情,当你完成时,你会通知他一桶红球准备好了。它是异步的,因为 Jim 没有被你分类球所阻挡;他做其他事情,直到他们准备好。
-
你从你的桶里一个接一个地拿出红色的球,扔向吉姆。这是你的数据流,或者说是球流。如果你找到它们并扔出去的速度比吉姆接住它们的速度快,你就有障碍。吉姆告诉你慢下来;他在控制球的流动。
这将软件转化为反应式应用。
构建反应式应用
在处理大量数据时,反应式应用是解决方案。反应式应用是以弹性、响应性和可伸缩性为优先考虑的应用。反应宣言 6 描述了反应应用的特点。Reactive Streams API 规范 7 提供了应用组件应该实现的最小接口集,因此应用可以被认为是反应式的。因此,Reactive Streams API 是一个互操作性规范,它确保了反应组件的无缝集成,并保持操作的非阻塞和异步。
描述反应式应用有四个关键术语。
-
响应迅速:以快速一致的响应时间为特点。
-
弹性:在故障期间保持响应并能够恢复。
-
弹性:在高负荷时保持反应灵敏。
-
消息驱动:通信是异步的,应用反压力是为了防止消息的生产者压倒消费者。
反应式应用应该更加灵活、松散耦合和可伸缩,但同时更容易开发、更易于改变和更能容忍失败。构建反应式应用需要遵循反应式编程范式的原则。
反应式编程简介
反应式编程是用异步数据流编程。Reactive Streams 是一个为非阻塞反压异步流处理提供标准的倡议。它们对于解决需要跨线程边界复杂协调的问题非常有用。操作符允许您将数据收集到所需的线程上,并确保线程安全操作,在大多数情况下,不需要过度使用synchronized和 volatile constructs。
在版本 8 中引入 Streams API 之后,Java 向反应式编程迈进了一步,但是反应式流直到版本 9 才可用。在需要高效处理大量数据的世界里,反应式编程变得流行起来。当 Oracle 致力于在 JDK 中实现反应流和模块时,像 RxJava、 8 Akka、 9 和 Project Reactor10这样的项目似乎为 Java 世界中缺少的反应流 API 实现提供了替代方案。
无法等待延迟六个月发布的 JDK 9,Pivotal 开源团队, 11 同一个创建 Spring 的团队,使用 Project Reactor 构建 Spring WebFlux 12 ,他们自己的反应库。Spring WebFlux 是一个框架,旨在构建反应式的、非阻塞的、基于服务器的 web 应用,这些应用需要少量的线程来解决大量的请求。
然而,在全面深入 Spring WebFlux 之前,您需要了解一下 Java 中使用反应流编程的一些皮毛。
用流编程
从命令式编程转换到反应式编程的第一步是将您的思维从变量转换到流。
假设你需要计算两个数的和。在命令式编程中,您声明这两个变量,然后执行添加它们的语句。程序一个接一个地执行语句。如果这两个变量在计算总和后被修改,保存总和的变量不会被修改。
当使用 streams 时,要添加的两个值作为一个流提供给你。您声明要对发出的值执行的操作——在本例中是加法操作。流发出的任何额外值都会影响结果。
流是按时间顺序排列的一系列事件。这些事件可以是下列事件之一:
-
发射值是要消耗的某种类型的值。
-
错误是意外的无效值,其处理方式与值不同。
-
一个完成信号是一个通知,表示不再发送更多的值。
大多数情况下,后两个可以省略,开发人员只编写函数来处理发出的值。应该处理由流发出的值或在值发出时执行某些操作的函数必须订阅该流。然后他们倾听溪流的声音。他们正在观察 13 流并等待消耗发出的值。
使用术语函数而不是方法可能看起来很奇怪,但是当涉及到反应式编程时,你很少能缺一不可。函数式编程是基于纯函数的。纯函数是不改变输入的函数,总是返回一个值,返回值完全基于输入(它们没有副作用)。因为它们的动作是原子性的,所以可以很容易地组合函数。Lambda 表达式也适用于这种类型的代码编写。因此,功能反应式编程是一种编程范例,它通过将纯功能应用于反应式流来提供解决方案。 14
总之,回到溪流。流有一个源,一个提供要发出的值的实体。您也可以将流视为从源到目的地的移动过程中的数据。这个源可以是任何东西:变量、用户输入、属性、数据结构(比如集合),甚至是另一个流。流发出值,直到源耗尽或发生错误。
订阅流的函数使用值并将结果作为新流的一部分返回。这些类型的功能有时被称为变压器、处理器或运算符。它们不修改初始流,因为流是不可变的。结束一个链的功能被称为终端操作员或最终变压器。处理多个流发射值并计算单个值的函数称为归约器,将流发射值累加到集合中的函数称为收集器。
纯函数链有时被称为管道。
在图 9-1 中,你可以看到一个糟糕的绘图,它用一桶彩球和一个偶然出现的代表错误信号的苹果描绘了流处理。
图 9-1
使用一桶球的流处理表示
这种表示并不完全准确,因为将球涂成红色应该会创建一个新的红色球,但它足够接近流如何工作以及应该如何使用的想法。
如果有帮助的话,想象一下使用流编写代码类似于设计一台 Rube Goldberg 机器。 十五
在引入 streams 之前,所有开发人员都必须管理数据集,即集合。让我们做一个简单的任务:给定一组不同颜色和不同大小的球,选择所有直径大于 3 的蓝色球,将它们涂成红色,并计算它们直径的总和。在 Java 8 之前,代码看起来类似于清单 9-1 中的片段。
List<Ball> bucket = List.of(
new Ball("BLUE",9),
new Ball("RED", 4),
// other instances omitted
);
Integer sum = 0;
for(Ball ball : bucket) {
if (ball.getColor().equals("BLUE") && ball.getDiameter() >= 3) {
ball.setColor("RED");
sum += ball.getDiameter();
}
}
System.out.println("Diameter sum is " + sum);
Listing 9-1Filtering Balls and Adding Their Diameters Before Java 8
这是命令式代码的描述。它使用一系列要执行的语句来改变球列表的状态。它描述了程序应该如何完成状态改变。
从 Java 8 开始,使用 streams 可以编写清单 9-2 中描述的相同代码。
import java.util.function.Function;
import java.util.function.Predicate;
// other imports and code omitted
Predicate<Ball> predicate = ball -> ball.getColor().equals("BLUE")
&& ball.getDiameter() >= 3;
Function<Ball, Ball> redBall =
ball -> new Ball("RED", ball.getDiameter());
Function<Ball, Integer> quantifier = Ball::getDiameter;
int sum = bucket.stream()
.filter(predicate)
.map(redBall)
.map(quantifier)
.reduce(0, Integer::sum);
System.out.println("Diameter sum is " + sum);
Listing 9-2Filtering Balls and Adding Their Diameters Starting with Java 8
清单 9-2 中的管道由以下方法调用组成。
stream()方法返回一个使用初始集合作为源的java.util.stream.Stream<Ball>实例。
*** filter(Predicate<T>) 方法使用作为参数提供的谓词来过滤流,并返回包含与谓词匹配的元素的流。谓词由测试球的组合布尔条件组成。
* `map(Function<T, R>)` **方法**是一个转换函数,它接受流的元素,应用作为参数提供的函数,并将结果作为流返回。
* `reduce(T, BinaryOperator<T>)` **方法**是一个累加器函数。它有两个参数:一个初始值和一个`java.util.function.BinaryOperator<T>`实例,声明在两个相同类型的操作数之间执行的操作,产生相同类型的结果。在前面的清单中,使用的函数是声明为方法引用的整数的典型求和函数。(查 Java 方法引用;如果你以前没用过,它们很酷。)**
**清单 9-2 中的代码以三个需要解释的语句开始。
-
predicate实例是Predicate<T>功能接口的内联实现。该接口公开了一个名为test(..)的抽象方法,该方法必须用谓词实现,以便根据提供的参数进行评估。 -
redBall实例是Function<T, R>功能接口的内联实现。该接口公开了一个名为apply(..)的抽象方法,必须用代码实现该方法才能应用于所提供的参数。在本例中,我们正在创建一个新的Ball实例。 -
quantifier实例是对来自Ball实例的名为getDiameter()的方法的引用。在 Java 中,它们被称为方法引用,因为这听起来很酷。
引入这三个实例是为了具体化应该如何修改流。对于声明式编程方法来说,它们是必需的。 16 通过在管道外声明它们,结果是一个代码片段,声明需要实现什么。如何成为一个参数,改变那些引用所指向的不会影响什么。您可以将代码管道想象成一条装配线,操作员就是工作站。改变工作站内部的东西(谓词或函数)不应该影响管道设置。
Java 8 中引入的流 API 提供了许多用于流处理的实用方法,而反应式流 API 甚至进一步扩展了该集合。根据您试图解决的问题,lambda 表达式和流的组合可能会产生可读性更好、更优雅的解决方案。
既然您已经知道了如何使用流编写代码,那么您已经准备好了锦上添花:反应式流。
反应流
反应式流为 Java 中的反应式编程提供了一个通用的 API。它由四个简单的接口组成,这四个接口为具有非阻塞背压的异步流处理提供了标准。如果您想编写一个可以与其他反应式组件集成的组件,您需要实现其中的一个。
在抽象层次上,组件以及它们之间的关系,如反应流规范中所描述的,看起来如图 9-2 所示。
图 9-2
反应流规范抽象表示
现在,详细的解释。
-
一个发布者是一个潜在的无限的数据生产者。在 Java 中,数据生产者必须实现
org.reactivestreams.Publisher<T>。 -
一个订阅者向发布者注册来消费数据。在 Java 中,数据消费者必须实现
org.reactivestreams.Subscriber<T>。 -
在订阅时,会创建一个订阅对象来表示发布者和订阅者之间的一对一关系。该对象用于向发布者请求数据,也用于取消对数据的需求。在 Java 中,订阅类必须实现
org.reactivestreams.Subscription。 -
发布者根据订阅者的要求发出值。
-
一个处理器是一个特殊的组件,具有与发布者和订阅者相同的属性。在 Java 中,数据处理器必须实现
org.reactivestreams.Processor<T,R>。处理器可以被链接以形成流处理流水线。 -
处理器从链中它前面的发布者/处理器消费数据,并发出数据供链中它后面的处理器/订户消费。
-
如果生产者/处理器不能足够快地消费数据,则订户/处理器在发送数据时施加背压以减慢生产者/处理器的速度。
这是反应流如何工作的基本思想。在基本的情况下,有一个发布者、一个订阅者和一个它们所响应的事件流。在更复杂的情况下,会涉及到处理器。
您可以在 IDE 或 GitHub 上查看代码。 17
JVM 的大多数反应式实现都是并行开发的。不同的开发团队为他们的 Reactive Streams 接口的实现选择了不同的名称。这就是为什么最初引入 streams 时,每个组件都有不止一个名称。
当 JDK 采用反应流规范时,决定将所有接口从org.reactivestreams包复制到java.util.concurrent.Flow类中。大多数库现在也支持适配器与 JDK 集成。表 9-1 显示了最常用的 Java 反应库中反应流实现的名称。
表 9-1
反应流实现名称
|反应流 API
|
RxJava
|
项目反应器
|
阿卡
|
JDK*
| | --- | --- | --- | --- | --- | | 出版者 | 可观察的,单一 | 流,猴子 | 来源 | 流动。出版商* | | 订户 | 观察者 | 核心订阅服务器 | 水槽 | 流动。订户* | | 处理器 | 科目 | 通量处理器、单处理器 | 流动 | 流动。处理器* | | 签署 | - | - | - | - |
- JDK 反应流规范组件在表 9-1 中用*标记,因为它们不扩展反应流 API,而是复制流类中的所有组件。
用反应式流编写的代码看起来与用非反应式流编写的代码非常相似,但是幕后发生的事情是不同的。反应流是异步的,但是您不必编写处理它的逻辑。您需要声明当某个值在流上发出时必须发生什么。当流异步发出一个项目时,您正在编写的代码被调用,独立于主程序流。如果涉及到多个处理器,每个处理器都在自己的线程上执行。
**由于您的代码是异步运行的,所以您必须小心使用作为参数提供给转换器方法的函数。确保它们是纯函数,它们应该只通过它们的参数和返回值与程序交互,并且它们永远不应该修改需要同步的对象。
现在让我们看看 Project Reactor 是如何实现 Reactive Streams 规范的,以及它的类是如何编写 Reactive 代码的。
使用 Project Reactor
Project Reactor 是反应式编程的第一批库之一。它为反应式应用提供了一个无阻塞的稳定基础,并提供了高效的需求管理。它提供了一些类,使得使用反应流设计代码变得非常实用。它适用于 Java 8,但为所有 JDK9+版本提供了适配器类。Project Reactor 适合编写微服务应用,并提供了比 JDK 更多的类,旨在使反应式应用的开发更加实用。
Project Reactor 提供了两个主要的 publisher 实现。
-
reactor.core.publisher.Mono<T>是表示零个或一个元素的反应流发布器。 -
reactor.core.publisher.Flux<T>是一个反应式流发布器,表示从零到无穷大元素的异步序列。
Mono<T>和Flux<T>类似于java.util.concurrent.Future<V>。它们表示异步计算的结果。它们之间的区别在于,当您试图用get()方法获得结果时,Future<V>会阻塞当前线程,直到计算完成。Mono<T>和Flux<T>都提供了一系列block*()方法,用于检索不阻塞当前线程的异步计算的值。
在到达之前,我们先了解一下Flux<T>和Mono<T>。
图 9-3 描述了Flux<T>和Mono<T>的类层次结构,包括来自反应流规范包的根父类。
图 9-3
项目反应器堆芯组件的等级体系
CorePublisher<T>接口声明了一个要实现的方法,它自己版本的subscribe(..)方法需要一个CoreSubscriber<T>类型的参数。
CoreSubscriber<T>接口声明了一个名为currentContext()的默认方法,用于访问包含与下游或终端操作者共享的信息的反应上下文。它还声明了一个抽象方法onSubscribe(Subscription),实现者需要为其提供一个实现,以便在发出值之前初始化流状态。
Flux<T>和Mono<T>都继承了Publisher<T>,这意味着如果实现了Subscriber<T>,它们可以与任何类型的订户链接。当使用 project reactor 编写代码时,扩展BaseSubscriber<T>抽象类是很实用的。这个类对从Subscriber<T>继承的所有方法都有最小的实现,但也为它们声明了钩子方法(拦截器方法),当发出上一节提到的三个信号之一:值、错误或完成时,您可以实现这些方法来定制订阅者行为。它还包含一个用于向发布者发送订阅信号的方法的钩子,以及一个用于添加在任何终止事件之后执行的行为的 final 钩子:error、complete 或 cancel。因为所有的方法都有一个最小化的实现(一个空的主体),这允许你只重写你感兴趣的方法,并且在开发过程中有一个方法最重要:hookOnNext(T)。
Mono<T>是一种特殊类型的反应流,它只能返回 0 或 1 的单值。知道流发出的值的数量会导致编写更简单的代码来使用它们。此外,不是所有的异步进程都返回值,所以可以使用一个Mono<Void>来表示这个进程的完成。由于Mono<T>对其值的限制如此之低,与Flux<T>相比,它只提供了一小组操作符。
在继续之前,先看看清单 9-3 中的代码。它包含清单 9-2 中代码的反应流版本。
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
// other imports and code omitted
Subscriber<Integer> subscriber = new BaseSubscriber<Integer>() {
@Override
protected void hookOnNext(Integer sum) {
System.out.println("Diameter sum is " + sum);
}
};
Flux.fromIterable(bucket) // Flux<Ball>
.filter(predicate)
.map(redBall)
.map(quantifier) // Flux<Integer>
.reduce(0, Integer::sum) // Mono<Integer>
.subscribe(subscriber);
Listing 9-3Filtering Balls and Adding Their Diameters Using Reactive Streams
添加了一些注释,以便在返回的流上的对象类型发生变化时更加清楚。实现中的所有方法要么返回Flux<T>要么返回Mono<T>,这就是为什么它们可以很好地链接在一起。链中的最后一个组件是订阅者,它使用由reduce(..)累加器函数返回的流中的元素。因为这个累加器返回一个Mono<T>,如果需要的话,更多的函数可以被链接。
对于这个例子,订户对象是通过扩展BaseSubscriber<T>类型并将其实例化来创建的。hookOnNext(..)方法的实现在控制台中打印出发出的值。整个流水线执行是异步的,发出值就打印出来,不会阻塞主线程。
Flux<T>或Mono<T>类提供了subscribe(..)方法的丰富版本,允许开发者定义java.util.function.Consumer<T>函数来处理错误和完成信号。假设在订阅之前操作符可能出错,这允许正确处理错误,并在流程成功完成后执行额外的操作。
因此,使用这种方法,清单 9-3 中的代码变成了清单 9-4 中的代码。
Flux.fromIterable(bucket)
.filter(predicate)
.map(redBall)
.map(quantifier)
.reduce(0, Integer::sum)
.subscribe(
sum -> System.out.println("Diameter sum is " + sum),
error -> System.err.println("Something went wrong " + error),
() -> System.out.println("Pipeline executed successfully.")
);
Listing 9-4Declaring
Consumers for a Different Kind of Signal
在幕后,创建了一个订户实例,并由适当的钩子方法调用作为参数提供的消费者。
也许这段代码看起来比前面的实现更复杂,但是代码仍然是可读的,并且是异步和非阻塞的。对于简单的用例场景,使用反应性组件是多余的。在反应式应用中,非阻塞进程的效率和可靠性超过了拥有一长串不可读的函数和代码的成本,反应式流显示了它们的真正威力。即使你认为长长的反应函数链看起来不切实际,相信我,只用 Java 并发 API 编写的相同代码更是如此。
要了解为什么反应式编程对于中等负载和中等复杂性(不像网飞或脸书 1F642)处理的应用效率低下,让我们修改前面的实现,以便每个函数打印线程 ID。通过在管道中添加对delayElements(Duration.ofSeconds(1))的调用来模拟处理值的延迟。
在清单 9-5 中,描述了修改后的redBall函数,但是对于管道中的所有其他函数,修改是相似的。管道代码不会改变,pure 函数返回的结果不受 print 语句的影响。
// other imports and code omitted
Function<Ball, Ball> redBall = ball -> {
System.out.println("[RedBall]Executing thread: "
+ Thread.currentThread().getId());
return new Ball("RED", ball.getDiameter());
};
Flux.fromIterable(bucket)
.delayElements(Duration.ofSeconds(1))
.filter(predicate)
.map(redBall)
.map(quantifier)
.reduce(0, Integer::sum)
.subscribe(
sum -> System.out.println("Diameter sum is " + sum),
error -> System.err.println("Something went wrong " + error),
() -> System.out.println("Pipeline executed successfully.")
);
Listing 9-5Pure Function Modified To Print the ID of Its Executing Thread
当我们现在执行管道时,控制台中的输出应该看起来非常接近清单 9-6 中描述的输出。
[Predicate]Executing thread: 17
[GetDiameter]Executing thread: 17
[Predicate]Executing thread: 18
[Predicate]Executing thread: 19
[Predicate]Executing thread: 20
[GetDiameter]Executing thread: 20
[RedBall]Executing thread: 20
[GetDiameter]Executing thread: 20
[GetDiameter]Executing thread: 20
[Predicate]Executing thread: 21
[Predicate]Executing thread: 22
[Predicate]Executing thread: 23
[GetDiameter]Executing thread: 23
[Predicate]Executing thread: 24
...
[Subscriber]Executing thread: 24
Listing 9-6Reactive Pipeline Output
不同的线程 id 意味着每个纯函数都由自己的线程执行。正在使用的线程数量等于处理器拥有的内核数量,并且负载分布均匀:每个内核一个线程。对于一个简单的任务,创建如此多的线程并协调它们是不值得的。
Flux<T>和Mono<T>功能强大,实用。在编写 Spring 反应式应用时,您很少需要使用其他任何东西。它们都提供了丰富的操作符列表,这些操作符可以创建、组合和控制反应流 18 ,并允许实际的流水线设计。
要记住的一点是,在编写反应式应用时,应用的每个组件都必须是反应式的,否则应用就不是真正的反应式应用,非反应式组件可能会成为瓶颈并破坏整个流程。
例如:具有典型层的三层应用:表示层、服务层、数据库层只有在这三层都是反应性的情况下才是反应性的。一个反应式 Spring WebFlux 应用必须有反应式视图、反应式控制器、反应式服务、反应式存储库和反应式数据库。并且调用应用的客户端也必须是被动的。一个应用可以使用由另一个应用被动公开的数据,在这种情况下成为一个客户端。为了使通信顺利进行,两个应用都必须是反应式的。除了视图和数据库之外,如果其余的都是用 Java 编写的,那么 API 中每个方法的输入和输出都必须是Flux<T>或Mono<T>(或者前面提到的任何其他反应流的实现)实例,这样就可以组合它们,而不需要编写额外的代码来将它们封装在Flux<T>或Mono<T>实例中。
事不宜迟,让我们看看如何从使用 Spring MVC 编写应用转移到使用 Spring WebFlux 做同样的事情。
Spring WebFlux 简介
Spring Web MVC 是围绕DispatcherServlet设计的,它是将 HTTP 请求映射到处理程序的网关,并设置了主题配置、国际化、文件上传和视图解析。Spring MVC 是为 Servlet API 和 Servlet 容器构建的。这意味着它主要使用阻塞 I/O 和每个 HTTP 请求一个线程。支持请求的异步处理是可能的,但是需要更大的线程池,这反过来需要更多的资源。而且,很难规模化。
Spring WebFlux 是在 Spring 5 中添加的一个反应式堆栈 web 框架,它是 Spring 对阻塞 I/O 架构这一新兴问题的回应。它可以在 Servlet 3.1+容器上运行,但可以适应其他本地服务器 API。首选的服务器是 Netty 19 ,它在异步、非阻塞领域已经得到很好的应用。Spring WebFlux 构建时考虑了函数式反应式编程,并允许以声明式风格编写代码。
这两个框架有一些共同点,甚至可以一起使用。图 9-4 是来自 Spring 官方参考文档的图表,展示了 Spring MVC 和 Spring WebFlux 的共同点以及它们如何相互支持。
图 9-4
Spring MVC 和 Spring web 流量图
从 Spring 5 开始,spring-web模块增加了底层基础设施和 HTTP 抽象来构建反应式 web 应用。所有公共 API 都被修改,以支持将Publisher<T>和Subscriber<T>作为参数和返回类型。该模块是spring-webflux模块的依赖项,是 Spring 反应式应用最重要的依赖项。那么,我们如何编写一个 Spring Web Flux 应用呢?很简单,我们从配置入手。
Spring WebFlux 配置:反应式控制器
前面提到过,反应式应用可以部署在 Servlet 3.1+容器上,比如 Tomcat、Jetty 或 Undertow。这里的技巧是不要使用DispatcherServlet,它是 HTTP 请求处理程序/控制器的中央调度程序。再厉害,也还是个阻断成分。Tomcat 和 Jetty 的核心是非阻塞的,所以关键是使用它们来处理 HTTP 请求,而不需要 servlet facade。
这就是新的和改进的spring-web组件通过引入org.springframework.http.server.reactive.HttpHandler来提供帮助的地方。这个接口代表了反应式 HTTP 请求处理的最低层次的契约,Spring 基于它为每个支持的服务器提供了服务器适配器。表 9-2 列出了 Spring WebFlux 支持的服务器以及适配器类的名称,这些适配器类代表了每个服务器的非阻塞 I/O 到反应流桥的核心。
表 9-2
支持的服务器
|服务器名称
|
Spring 适配器
|
使用的桥
|
| --- | --- | --- |
| 妮蒂 | ReactorHttpHandlerAdapter | 使用 Reactor Netty 库的 Netty API |
| 逆流 | UndertowHttpHandlerAdapter | spring-web逆流到激流桥 |
| 雄猫 | TomcatHttpHandlerAdapter | spring-web: Servlet 3.1 到反应流桥的无阻塞 I/O |
| 码头 | JettyHttpHandlerAdapter | spring-web: Servlet 3.1 到反应流桥的无阻塞 I/O |
HttpHandler界面非常基础。其内容如清单 9-7 所示。
package org.springframework.http.server.reactive;
import reactor.core.publisher.Mono;
// other comments omitted
public interface HttpHandler {
/**
* Handle the given request and write to the response.
* @param request current request
* @param response current response
* @return indicates completion of request handling
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}
Listing 9-7HttpHandler Interface
在此基础上,Spring 提供了 WebHandler 接口,这是一个稍微高级一点的契约,描述了所有具有过滤器链样式处理和异常处理的通用服务器 API。
与经典的 Spring Web MVC 应用相比,这对 Spring 配置意味着什么?这意味着你需要的不是org.springframework.web.servlet.DispatcherServlet,而是org.springframework.web.reactive.DispatcherHandler。DispatcherHandler,作为dispatcher servlet,被设计为前端控制器。它是核心的WebHandler实现,并为可配置组件执行的请求处理提供算法。它委托给特殊的 beans 来处理请求和呈现适当的响应,并且它们的实现如预期的那样是非阻塞的。类似于 Spring MVC 生态系统,有一个HandlerMapping bean 将请求映射到一个处理程序,一个HandlerAdapter bean 调用一个处理程序,一个org.springframework.web.server.WebExceptionHandler bean 处理异常,一个HandlerResultHandler bean 从处理程序获取结果并完成响应,所有这些都声明在org.springframework.web.reactive包中。
按照 Spring 的典型方式,大多数情况下,DispatcherHandler的配置不需要直接描述它的代码。要配置一个在 Servlet 3.1+容器中运行的 Spring WebFlux 应用,您需要执行以下操作。
-
声明一个 Spring WebFlux 配置类,并用
@Configuration和@EnableWebFlux.对其进行注释 -
扩展
org.springframework.web.server.adapter.AbstractReactiveWebInitializer类,实现getConfigClasses()方法,并在其中注入您的 Spring WebFlux 配置类。
清单 9-8 描述了AppConfiguration,一个 Spring WebFlux 应用的定制配置类。
package com.apress.prospringmvc.bookstore;
import org.springframework.web.reactive.config.EnableWebFlux;
// other imports omitted
@EnableWebFlux
@Configuration
public class AppConfiguration {
}
Listing 9-8AppConfiguration Class
@EnableWebFlux注释是org.springframework.web.reactive.config包的一部分,它支持使用带注释的控制器和功能端点。当 Spring IoC 找到这个注释时,它从org.springframework.web.reactive.config.WebFluxConfigurationSupport导入所有 Spring WebFlux 配置。如果您想以任何方式定制导入的配置,您所要做的就是让带注释的类实现org.springframework.web.reactive.config.WebFluxConfigurer。该接口包含用于配置静态访问资源处理程序、格式化程序、验证程序、消息源、视图解析器等的方法。
清单 9-9 描述了WebAppInitializer,,它扩展了AbstractReactiveWebInitializer以提供与 Servlet 3.1+容器的集成。
package com.apress.prospringmvc.bookstore;
import org.springframework.web.server.adapter.AbstractReactiveWebInitializer;
public class WebAppInitializer extends AbstractReactiveWebInitializer {
@Override
protected Class<?>[] getConfigClasses() {
return new Class<?>[]{AppConfiguration.class};
}
}
Listing 9-9WebAppInitializer Class
AbstractReactiveWebInitializer实现了WebApplicationInitializer,并且是在 Servlet 容器上安装 Spring Web Reactive 应用所必需的。
为了帮助你从 Spring MVC 过渡到 Spring WebFlux,请看表 9-3 。下表显示了 Spring MVC 和 Spring WebFlux 应用之间的配置组件的对应关系。
表 9-3
Spring MVC 和 WebFlux 比较
|框架
|
spring webflux
|
| --- | --- |
| @EnableWebMvc | @EnableWebFlux |
| WebMvcConfigurer | WebFluxConfigurer |
| WebMvcConfigurationSupport | WebFluxConfigurationSupport |
| WebApplicationInitializer(界面) | AbstractReactiveWebInitializer(类) |
| DispatcherServlet | DispatcherHandler |
一旦编写了两个配置类,下一步就是编写一个反应式控制器。由于我们使用的是 Spring WebFlux,我们知道这可以通过确保方法返回Flux<T>或Mono<T >来实现。在编写 web 应用时,我经常做的一件事就是编写一个IndexController,打印出“它工作了!”清单 9-10 描述了一个简单的IndexController返回一个发出单个值的Mono<T>实例。
package com.apress.prospringmvc.bookstore;
import reactor.core.publisher.Mono;
// other imports omitted
@RestController
public class IndexController {
@ResponseStatus(HttpStatus.OK)
@GetMapping(path="/", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Mono<String> index(){
return Mono.just("It works!");
}
}
Listing 9-10Reactive IndexController Implementation
使用@RestControllers是因为反应式应用专注于流数据,但是也可以使用@Controllers,正如你在下一章中看到的。
produces属性的值是text/event-stream,它代表返回内容的类型。此内容类型描述来自源的事件流。如果您将这个应用部署到 Apache Tomcat 服务器上,并试图打开http://localhost:8080/页面,一些浏览器会敦促您将输出保存到一个文件中,因为浏览器无法呈现响应。响应是服务器发送的事件(SSE),这是一种服务器推送技术,使客户端能够通过 HTTP 连接从服务器接收更新流,HTTP 连接是 HTML5 的一部分。 20 在经典轮询模型中,客户端和服务器之间的通信需要通过 HTTP 连接进行一系列的请求/响应,因为客户端必须重复轮询数据。服务器发送的事件允许服务器在数据可用时向客户端推送数据,而无需客户端请求。因此最适合反应式 web 应用。
除了这个头,我们如何知道我们的应用是反应性的呢?这很简单——我们不是返回一个Mono<T>,而是返回一个Flux<T>,并降低发射元素的速率。
通过使用一个名为zip的操作合并两个流,可以降低值发出的速率。有三种组合操作可应用于流,如图 9-5 所示。
图 9-5
流组合操作
使用图 9-5 中描述的操作符之一,可以将两个或多个Flux<T>实例合并成一个Flux<T>。下面的列表描述了 de 运算符和结果流。
-
concat连接两个流。流的顺序很重要,因为第二个流仅在第一个流发出onComplete信号后被订阅。对于连接流,可以使用Flux.concat(..)实用程序方法。还有一个名为concatWith(Flux<T>)的方法,可以在一个Flux<T>实例上调用,将它与作为参数接收的流连接起来。清单 9-11 展示了一个流连接的例子,使用两个非常方便的流作为源:一个发出X值,另一个发出Y值。 -
merge合并两条溪流。流的顺序并不重要,因为两个流是同时订阅的。结果流随机地从任何一个源流中发出值。对于合并流,可以使用Flux.merge(..)实用程序方法。还有一个名为mergeWith(Flux<T>)的方法,可以在一个Flux<T>实例上调用它,将它与作为参数接收的流合并。清单 9-12 展示了一个流合并的例子,使用两个非常方便的流作为源:一个发出X值,另一个发出Y值。
Flux<String> y = Flux.just("Y", "Y");
Flux<String> x = Flux.just("X", "X");
Flux.concat(y,x).subscribe(str -> System.out.print(str + " "));
// or
y.concatWith(x).subscribe(str -> System.out.print(str + " "));
// expect: Y Y X X
Listing 9-11Stream Concatenation Examples
zip是一个发出值的流,这些值是通过将每个流发出的值包装在一起而创建的。此操作的结果应用于两到八个流。当一个流发出onComplete信号时,来自其他流的所有不能合并的值都被丢弃。压缩流时,可以使用Flux.zip(..)实用程序方法。这个方法有不止一个版本,它们可以压缩 2 到 8 个流。结果值属于reactor.util.function.Tuple*类型,其中*替换被组合的值的数量。还有一个名为zipWith(Flux<T>)的方法,可以在一个Flux<T>实例上调用,用作为参数接收的流压缩它。
Flux<String> y = Flux.just("Y", "Y");
Flux<String> x = Flux.just("X", "X");
Flux.merge(y,x).subscribe(str -> System.out.print(str + " "));
// or
y.mergeWith(x).subscribe(str -> System.out.print(str + " "));
// expect multiple X and Y elements being written in any order
Listing 9-12Stream Merging Examples
清单 9-13 展示了一个流压缩的例子,使用三个非常方便的流作为源:一个发出X值,一个发出Y值,一个发出Z值。
Flux<String> y = Flux.just("Y", "Y");
Flux<String> x = Flux.just("X", "X");
Flux<String> z = Flux.just("Z", "Z");
Flux.zip(y,x).subscribe(t -> System.out.print(str + " "));
// or
y.zipWith(x).subscribe(str -> System.out.print(str + " "));
// expect multiple Tuple2 instances: [Y,X]
Flux.zip(y,x,z).subscribe(t -> System.out.print(str + " "));
// expect multiple Tuple3 instances: [Y,X,Z]
Listing 9-13Zipping Merging Examples
zipWith(..)操作通过将一个流与一个反应性间隔流相结合来降低该流的发射速率。可通过调用Flux.interval(Duration)创建反应性间隔流。该方法将一个Duration实例作为参数,并创建一个Flux<Long>,它发出从 0 开始的long值,并在全局计时器上以指定的时间间隔递增。在初始延迟等于作为参数提供的持续时间之后,发出第一个元素。
如果我们压缩一个带有一秒钟Duration的反应式间隔流的反应式数据流,这会导致一个每秒钟发出一个Tuple2实例的流。然后,我们将map应用于元组流,以分离出我们感兴趣的值。
让我们把事情变得有趣一些,修改IndexController并为/debug路径添加一个处理程序方法,该方法以每秒一个的速度返回包含所有 bean 名称及其类型的流。每个发出的值都是类型Pair<S,T>,包含 bean 名称及其类型。属性produces的值将该方法返回的类型声明为MediaType.APPLICATION_STREAM_JSON_VALUE,它是一个带有application/stream+json值的常量。这意味着这个流发出的每个值都被转换成 JSON。
清单 9-14 中描述了提议的实现。
package com.apress.prospringmvc.bookstore;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
import org.springframework.data.util.Pair;
// other imports omitted
@RestController
public class IndexController implements ApplicationContextAware {
// other code omitted
@ResponseStatus(HttpStatus.OK)
@GetMapping(path="/debug", produces =
MediaType.APPLICATION_STREAM_JSON_VALUE)
public Flux<Pair<String,String>> debug() {
List<Pair<String,String>> info = new ArrayList<>();
Arrays.stream(ctx.getBeanDefinitionNames())
.forEach(beanName -> info.add(Pair.of(beanName,
ctx.getBean(beanName).getClass().getName())));
return Flux.fromIterable(info)
.zipWith(Flux.interval(Duration.ofSeconds(1))).map(Tuple2::getT1);
}
}
Listing 9-14Reactive IndexController Implementation
Returning a Flux<T>
当在浏览器中访问时,如果浏览器可以解析它,您会看到这些值以一秒钟的延迟按顺序显示。如果您的浏览器不能这样做,几秒钟后(取决于数据集有多大),系统会提示您将响应保存为文件。如果您有基于 UNIX 的操作系统,您可以使用curl命令打开那个http://localhost:8080/debug。
curl -H -v "application/stream+json" http://localhost:8080/debug
这个命令打印转换成 JSON 的值。但是,这在哪里配置呢?这不是显式完成的,尽管如果需要,您可以声明自己的转换器。使用Encoder<T>和Decoder<T>bean 完成Flux<T>和Mono<T>到字节的转换,反之亦然。这两个接口是org.springframework.core.codec包的一部分,基本实现是spring-core和spring-web模块的一部分。
在 Spring WebFlux 应用中,org.springframework.http.codec.HttpMessageWriter<T> bean 被默认配置为使用现有的编码器实现来编码类型为<T>的对象流,并将它们写入数据缓冲区流以传输响应内容。默认情况下,org.springframework.http.codec.HttpMessageReader<T> bean 被配置为使用现有的解码器实现来将包含请求数据的数据缓冲区流解码为类型为<T>的对象流。
配置 Spring WebFlux 应用类似于配置 Spring MVC 应用。唯一改变的是处理程序方法的返回类型,以及在用@RequestMapping和变体注释的方法中作为参数可用的选项。
-
支持将反应类型作为参数,但是您不应该将反应类型用于不需要非阻塞 I/O 的参数(例如,对于保存书籍的 POST 请求处理程序方法,将参数类型声明为
Mono<Book>是没有意义的)。) -
org.springframework.web.server.ServerWebExchange可以用作参数来提供对 HTTP 请求、响应和其他服务器端属性的访问。在清单 9-15 中,IndexController.debug(..)方法已经被修改为接收ServerWebExchange作为参数。对请求进行分析,以检查请求的user-agent报头,如果请求是使用curl命令发出的,则在响应中添加一个 cookie。请求和响应对象都是通过ServerWebExchange参数来访问的。 -
如果一个具有
Mono<Void>返回类型的方法也有一个ServerHttpResponse,或者一个ServerWebExchange参数,或者一个@ResponseStatus注释,那么它就被认为已经完全处理了响应。 -
返回类型支持
Flux<ServerSentEvent>、Observable<ServerSentEvent>或其他反应类型。当只需要编写简单的文本时,可以省略ServerSentEvent包装类型。在这种情况下,produces属性必须设置为text/event-stream值(就像第一个版本的IndexController一样)。
package com.apress.prospringmvc.bookstore;
import org.springframework.web.server.ServerWebExchange;
// other imports omitted
@RestController
public class IndexController implements ApplicationContextAware {
// other code omitted
@ResponseStatus(HttpStatus.OK)
@GetMapping(path="/debug", produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Flux<Pair<String,String>> debug(ServerWebExchange exchange) {
if(Objects.requireNonNull(exchange.getRequest()
.getHeaders().get("user-agent"))
.stream().anyMatch(v-> v.startsWith("curl"))){
logger.debug("Development request with id: {}", exchange.getRequest().getId());
ResponseCookie devCookie = ResponseCookie
.from("Invoking.Environment.Cookie", "dev")
.maxAge(Duration.ofMinutes(5)).build();
exchange.getResponse().addCookie(devCookie);
}
List<Pair<String,String>> info = new ArrayList<>();
Arrays.stream(ctx.getBeanDefinitionNames()).forEach(beanName ->
info.add(Pair.of(beanName, ctx.getBean(beanName).getClass().getName()))
);
return Flux.fromIterable(info).zipWith(Flux.interval(Duration.ofSeconds(1))).map(Tuple2::getT1);
}
}
Listing 9-15Reactive IndexController Using the ServerWebExchange Argument
Spring Boot 网络流量应用
当使用 Spring Boot 构建 Spring WebFlux 应用时,事情变得更加简单。使用spring-boot-starter-webflux作为依赖项和 Spring Boot 依赖注入确保应用已经默认配置了所有必要的基础设施 beans。
剩下要做的就是编写反应式控制器和它们所需的其他定制 bean,比如存储库或服务 bean。不过,有一件重要的事情需要提一下。不支持创建可部署的 war 文件。 21
因为 Spring WebFlux 并不严格依赖于 Servlet API,并且应用默认部署在嵌入式 Reactor Netty 服务器上,所以 WebFlux 应用不支持 war 部署。
这是什么意思?这意味着我们不能使用 Spring Boot 构建一个可部署的 war,我们不能在 Tomcat 服务器上部署它,这实际上并不重要。既然反应式应用最适合写微服务应用,那么无论如何,有一个嵌入式服务器,把你的应用打包成可执行文件jar更合适。
Spring WebFlux 配置:功能端点
Spring WebFlux 提供了一个功能模型,作为将请求映射到处理程序的@Controller注释类的替代方法。配置可以很好地组合,并且具有不变性的优势。
在这个模型中,请求由一个HandlerFunction<T>处理。这是一个简单的函数接口,声明了一个方法。清单 9-16 显示了这个接口的代码。
// other comments omitted
package org.springframework.web.reactive.function.server;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
Listing 9-16HandlerFunction<T> Code
handle(..)方法接受一个ServerRequest并返回一个Mono<ServerResponse>。它相当于一个用@RequestMapping标注的@Controller方法。
ServerRequest将请求体公开为一个Flux<T>或Mono<T>实例。它还提供对请求参数、路径变量、HTTP 方法和头的访问。作为一个整体,ServerResponse接受任何Publisher<T>实现。ServerRequest和ServerResponse都是不可变的。通过调用各种静态方法(例如ok()、badRequest()等)来构建响应。)在公开一个BodyBuilder的ServerResponse类上。这个实例提供了多种方法来定制响应:设置 HTTP 响应状态代码、添加标头和提供主体。
处理函数通常被分组到特定于被处理对象类型的组件中。例如,处理特定于Book对象的请求的处理函数应该被分组到一个名为BookHandler的组件中。
清单 9-17 描述了BookHandler类。声明了这种类型的 bean,它是 Spring WebFlux 应用配置的一部分,它的方法被用作管理Book实例的请求的处理函数。
package com.apress.prospringmvc.bookstore;
// other imports omitted
@Component
public class BookHandler {
private final BookService bookService;
public HandlerFunction<ServerResponse> list;
public HandlerFunction<ServerResponse> delete;
public BookHandler(BookService bookService) {
this.bookService = bookService;
/* 1 */
list = serverRequest -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(bookService.findAll(), Book.class);
/* 2 */
delete = serverRequest -> ServerResponse.noContent()
.build(bookService.delete(serverRequest.pathVariable("id")));
}
/* 3 */
public Mono<ServerResponse> findByIsbn(ServerRequest serverRequest) {
Mono<Book> bookMono = bookService.findByIsbn(serverRequest.pathVariable("isbn"));
return bookMono
.flatMap(book -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(book))
.switchIfEmpty(ServerResponse.notFound().build());
}
/* 4 */
public Mono<ServerResponse> save(ServerRequest serverRequest) {
Mono<Book> bookMono = serverRequest.bodyToMono(Book.class).doOnNext(bookService::save);
return bookMono
.flatMap(book -> ServerResponse.created(URI.create("/books/" + book.getId())).contentType(MediaType.APPLICATION_JSON).bodyValue(book))
.switchIfEmpty(ServerResponse.status(HttpStatus .INTERNAL_SERVER_ERROR).build());
}
}
Listing 9-17The BookHandler Class
在清单 9-17 中,每个处理函数都标有一个数字。下面的列表讨论了每个处理函数,项目符号编号与函数编号相匹配。
-
list是一个简单的处理函数,它返回被动BookService.findAll()方法返回的所有Book实例。它被声明为类型为HandlerFunction的字段,并且是BookHandler类的成员。它不能在声明的同一行初始化,因为它依赖于bookService。要初始化该域,必须首先初始化bookService域。因为它是在构造函数中初始化的,所以list字段的初始化也是构造函数的一部分。最初的ServerResponse.ok()将 HTTP 响应状态设置为 200 (OK ),并返回对内部BodyBuilder的引用,该引用允许链接其他方法来描述请求。该链必须以返回一个Mono<ServerResponse>的body*(..)方法之一结束。 -
delete是一个简单的处理函数,删除一个 ID 与 path 变量匹配的Book实例。通过调用serverRequest.pathVariable("id")提取路径变量。“id”参数表示路径变量的名称。bookService.delete()方法返回Mono<Void>,因此Mono<ServerResponse>发出一个响应,其主体为空,状态码 204(无内容)由ServerResponse.noContent()设置。 -
findByIsbn是一个处理函数,它返回一个由ISBN路径变量标识的Book实例。通过调用返回一个Mono<Book>的bookService.findByIsbn(..)来检索实例。如果这个流发出一个值,这意味着找到了一本书与 path 变量匹配,并且创建了一个响应,其状态代码为 200,主体由 JSON 的Book实例表示。为了访问由流无阻塞发出的Book实例,使用了flatMap(..)函数。如果流没有发出一个值,这意味着没有找到具有预期的ISBN的书,因此通过调用switchIfEmpty(ServerResponse.notFound().build())创建一个状态为 404(未找到)的空响应。 -
save是一个处理函数,存储请求体中包含的一个新的Book实例。由于通过调用serverRequest.bodyToMono(Book.class)请求体被读取为Mono<Book>,所以当发出值时doOnNext(bookService::save)方法被链接以调用bookService.save(book)。该方法返回一个Mono<Book>。当一个成功的save被执行时,这个流发出一个值,当创建的资源可以被访问时,响应被填充一个指向 URL 的Location头。为了访问由流无阻塞发出的Book实例,使用了flatMap(..)函数。这是通过调用ServerResponse.created()来完成的,它将响应状态设置为 201(已创建),并声明一个 URI 作为参数。作为参数提供的值成为位置头的值。如果流没有发出值,这意味着保存操作失败。这里提供的实现返回一个空的响应体,响应状态为 500(内部服务器错误)。
现在我们有了处理函数,它们是如何映射到请求的呢?嗯,这是一个路由器 bean 的工作。
org.springframework.web.reactive.function.server.RouterFunction<T>是一个简单的函数接口,描述了一个将传入请求路由到HandlerFunction<T>实例的函数。RouterFunction<T>乘ServerRequest返回Mono<HandlerFunction<T>>。如果没有找到处理函数,它返回一个空的Mono<Void>。RouterFunction<T>与@Controller类中的@RequestMapping注释有相似的用途。
org.springframework.web.reactive.function.server.RouterFunctions是一个实用程序抽象类,它提供了构建简单和嵌套路由函数的静态方法,甚至可以将RouterFunction<T>转换为HttpHandler,这使得应用可以在 Servlet 3.1+容器中运行。
在进一步讨论路由器功能之前,我们先来看一个例子。清单 9-18 描述了BookRouterConfiguration配置类。它声明了一个类型为RouterFunction<ServerResponse>,的 bean,它是一个路由器函数,将传入的请求路由到前面介绍的BookHandler bean 中声明的处理程序函数。
package com.apress.prospringmvc.bookstore;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
// other imports omitted
@Configuration
public class BookRouterConfiguration {
private final Logger logger = LoggerFactory.getLogger(BookRouter.class);
@Bean
RouterFunction<ServerResponse> routerFunction(BookHandler bookHandler) {
return route(GET("/books"), bookHandler.list) /* 1 */
.andRoute(GET("/books/{isbn}"), bookHandler::findByIsbn) /* 2 */
.andRoute(POST("/books"), bookHandler::save) /* 3 */
.andRoute(DELETE("/books/{id}"), bookHandler.delete) /* 4 */
.filter((request, next) -> { /* 5 */
logger.info("Before handler invocation: " + request.path());
return next.handle(request);
});
}
}
Listing 9-18The BookRouterConfiguration Configuration Class
在清单 9-18 中,每个处理函数都标有一个数字。下面的列表讨论了每一行的内容,项目符号编号与函数编号相匹配。
-
route(GET("/books"), bookHandler.list)基于org.springframework.web.reactive.function.server.RequestPredicate和HandlerFunction<T>创建一个RouterFunction<T>。RequestPredicate实例代表一个函数,它根据一组属性评估ServerRequest,比如请求方法和 URL 模板。GET("/books")是来自org.springframework.web.reactive.function.server.RequestPredicates抽象实用程序类的一个静态方法,它创建一个将 GET 请求与/booksURL 相匹配的request predicate。 -
.andRoute(GET("/books/{isbn}"), bookHandler::findByIsbn)将谓词和处理函数作为参数,并创建一个路由器函数,该函数被添加到调用该方法的路由器函数中。它返回表示两者组合的路由器函数。 -
.andRoute(POST("/books"), bookHandler::save)是来自org.springframework.web.reactive.function.server.RequestPredicates实用程序类的一个静态方法,它创建一个将 POST 请求匹配到/booksURL 的request predicate。 -
.andRoute(DELETE("/books/{id}"), bookHandler.delete)是来自org.springframework.web.reactive.function.server.RequestPredicates实用程序类的一个静态方法,它创建一个将删除请求匹配到/books/{id}URL 的request predicate,其中ID是路径变量的名称。 -
.filter((request, next)→{..})是在RouterFunction中声明的一个方法,可以实现该方法来根据某些条件向过滤处理函数添加一个HandlerFilterFunction<T,S>或者添加日志记录代码,如清单 9-18 中的例子所示。HandlerFilterFunction<T,S>的功能相当于@ControllerAdvice或javax.servlet.Filter。
在 Spring WebFlux 应用中,如果您想要获取包含处理程序函数定义的 bean,必须在声明 bean 的包中启用组件扫描。
路由器功能 beans 可以在任何@Configuration注释类中声明,并通过对声明配置类的包启用组件扫描来获得。
Spring WebFlux 应用适合流数据,这就是为什么大多数应用不解析对视图的请求,而是返回数据流。首选的方法是使用 React、 22 TypeScript、 23 等技术编写一个与后端分离的 web 应用。这在升级资源和扩展应用时带来了很大的灵活性。客户端和服务器之间的首选通信方式是通过 WebSocket 协议, 24 ,它允许在 web 浏览器和服务器之间建立双向通信。
WebSocket 是反应式应用的完美协议,因为双方可以随时开始发送数据,这意味着客户端可以施加背压。但是,在接下来的章节中会有更多的介绍。
摘要
这一章是对反应式编程的一个小介绍,它只是触及了 Spring WebFlux 的表面,所以你会更熟悉接下来两章中的代码。
这里有一些你应该记住的事情。
-
流可以被视为正在移动的数据。
-
反应流是支持背压的非阻塞流。消费者调节生产者发射元素的速率。
-
纯函数是不改变输入的函数,总是返回一个值,并且返回值完全基于输入(它们没有副作用)。
-
函数式反应式编程是用反应式流和纯函数编程。
-
反应式组件的关键特征应该是可组合性和可读性。
-
使用反应式编程并根据这种范式编写代码,并不总是会产生如反应式宣言所描述的反应式系统。
-
反应式应用的所有组件都必须是反应式的;否则有堵塞的风险。
-
反应式代码是完全声明性的,在订阅者连接到发布者之前什么都不会发生。
-
Spring WebFlux 应用可以部署在 Servlet 3.1 容器上。
-
Spring WebFlux 是 Spring MVC 的反应式替代品。
-
Spring WebFlux 应用通过带注释的方法和功能端点支持请求映射。
-
Spring WebFlux 提供了一种函数式方法来将传入的请求映射到处理函数。
-
函数式反应式编程最适合实现微服务应用。Spring Boot WebFlux 是微服务应用的完美构建模块。
https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start
2
https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1
3
4
5
http://tutorials.jenkov.com/java-nio/nio-vs-io.html
6
https://www.reactivemanifesto.org/
7
https://www.reactive-streams.org/
8
https://github.com/ReactiveX/RxJava
9
https://doc.akka.io/docs/akka/current/stream/reactive-streams-interop.html
10
11
https://tanzu.vmware.com/open-source
12
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html
13
https://en.wikipedia.org/wiki/Observer_pattern
14
https://blog.danlew.net/2017/07/27/an-introduction-to-functional-reactive-programming/
15
https://en.wikipedia.org/wiki/Rube_Goldberg_machine
16
https://ui.dev/imperative-vs-declarative-programming/
17
18
https://projectreactor.io/docs/core/release/api/index.html
19
20
https://html.spec.whatwg.org/multipage/server-sent-events.html
21
22
23
https://www.typescriptlang.org/
24
https://tools.ietf.org/html/rfc6455
****