{
val cityMap = linkedMapOf(
"LDN" to "London",
"PRS" to "Paris",
"NYC" to "New York"
)
return hashMapOf("cityMap" to cityMap)
}
```
该代码现在产生输出,其中无线电值是相关的代码,但用户仍然可以看到更方便用户的城市名称,如下所示:
```
Town:
London
Paris
New York
```
###### HTML 转义
前面描述的表格宏的默认使用会导致 HTML 元素与 HTML4.01 兼容,并且使用在`web.xml`文件中定义的 HTML 转义的默认值,正如 Spring 的 BIND 支持所使用的那样。要使元素与 XHTML 兼容或覆盖默认的 HTML 转义值,你可以在模板中指定两个变量(或者在模型中,它们对模板是可见的)。在模板中指定它们的优点是,它们可以在以后的模板处理中更改为不同的值,从而为表单中的不同字段提供不同的行为。
要为标记切换到 XHTML 遵从性,请为名为`true`的模型或上下文变量指定一个值`xhtmlCompliant`,如下例所示:
```
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
```
在处理此指令之后,由 Spring 宏生成的任何元素现在都与 XHTML 兼容。
以类似的方式,你可以指定每个字段的 HTML 转义,如下例所示:
```
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
```
#### 1.10.3.Groovy 标记
[Groovy 标记模板引擎](http://groovy-lang.org/templating.html#_the_markuptemplateengine)主要用于生成类似 XML 的标记(XML、XHTML、HTML5 和其他标记),但你可以使用它生成任何基于文本的内容。 Spring 框架具有用于使用带有 Groovy 标记的 Spring MVC 的内置集成。
| |Groovy 标记模板引擎需要 Groovy2.3.1+。|
|---|---------------------------------------------------------|
##### Configuration
下面的示例展示了如何配置 Groovy 标记模板引擎:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
```
下面的示例展示了如何在 XML 中配置相同的内容:
```
```
##### 例子
与传统的模板引擎不同,Groovy Markup 依赖于使用 Builder 语法的 DSL。下面的示例展示了 HTML 页面的示例模板:
```
yieldUnescaped ''
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page')
}
body {
p('This is an example of HTML contents')
}
}
```
#### 1.10.4.脚本视图
[WebFlux](web-reactive.html#webflux-view-script)
Spring 框架具有用于使用 Spring MVC 和任何模板库的内置集成,这些模板库可以在[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)Java 脚本引擎之上运行。我们在不同的脚本引擎上测试了以下模板库:
|脚本库| Scripting Engine |
|----------------------------------------------------------------------------------|-----------------------------------------------------|
|[Handlebars](https://handlebarsjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[Mustache](https://mustache.github.io/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[React](https://facebook.github.io/react/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[EJS](https://www.embeddedjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[ERB](https://www.stuartellis.name/articles/erb/)| [JRuby](https://www.jruby.org) |
|[字符串模板](https://docs.python.org/2/library/string.html#template-strings)| [Jython](https://www.jython.org/) |
|[Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating)| [Kotlin](https://kotlinlang.org/) |
| |集成任何其他脚本引擎的基本规则是,它必须实现`ScriptEngine`和`Invocable`接口。|
|---|------------------------------------------------------------------------------------------------------------------------------|
##### 所需经费
[WebFlux](web-reactive.html#webflux-view-script-dependencies)
你需要在 Classpath 上有脚本引擎,其细节因脚本引擎而异:
* [Nashorn](https://openjdk.java.net/projects/nashorn/)JavaScript 引擎由 Java8+ 提供。强烈推荐使用最新的可用更新版本。
* [JRuby](https://www.jruby.org)应该作为 Ruby 支持的依赖项添加。
* [Jython](https://www.jython.org)应该作为 Python 支持的依赖项添加。
* 对于 Kotlin 脚本支持,应该添加`org.jetbrains.kotlin:kotlin-script-util`依赖项和一个`META-INF/services/javax.script.ScriptEngineFactory`文件,该文件包含`org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`行。有关更多详细信息,请参见[这个例子](https://github.com/sdeleuze/kotlin-script-templating)。
你需要有脚本模板库。JavaScript 的一种方法是通过[WebJars](https://www.webjars.org/)。
##### 脚本模板
[WebFlux](web-reactive.html#webflux-script-integrate)
你可以声明一个`ScriptTemplateConfigurer` Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn JavaScript 引擎:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
```
下面的示例用 XML 展示了相同的排列方式:
```
```
对于 Java 和 XML 配置,控制器看起来没有什么不同,如下例所示:
Java
```
@Controller
public class SampleController {
@GetMapping("/sample")
public String test(Model model) {
model.addAttribute("title", "Sample title");
model.addAttribute("body", "Sample body");
return "template";
}
}
```
Kotlin
```
@Controller
class SampleController {
@GetMapping("/sample")
fun test(model: Model): String {
model["title"] = "Sample title"
model["body"] = "Sample body"
return "template"
}
}
```
下面的示例展示了小胡子模板:
```
{{title}}
{{body}}
```
使用以下参数调用呈现函数:
* `String template`:模板内容
* `Map model`:视图模型
* `RenderingContext renderingContext`:[`RenderingContext`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/script/renderingcontext.html),它提供了对应用程序上下文、区域设置、模板装入器和 URL(自 5.0 起)的访问权限
`Mustache.render()`与此签名原生兼容,因此你可以直接调用它。
如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,[Handlerbars](https://handlebarsjs.com)在使用模板之前需要对其进行编译,并且需要[polyfill](https://en.wikipedia.org/wiki/Polyfill)来模拟服务器端脚本引擎中不可用的一些浏览器功能。
下面的示例展示了如何做到这一点:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
```
| |当使用非线程安全的
脚本引擎时,需要将`sharedEngine`属性设置为`false`,该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或
React。在那种情况下,由于[this bug](https://bugs.openjdk.java.net/browse/JDK-8076099),Java SE8Update60 是必需的,但是一般情况下
推荐在任何情况下使用最近发布的 Java SE 补丁。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
`polyfill.js`只定义了处理栏正常运行所需的`window`对象,如下所示:
```
var window = {};
```
这个基本的`render.js`实现在使用模板之前对其进行编译。生产就绪的实现还应该存储任何重用的缓存模板或预编译模板。你可以在脚本端这样做(并处理你需要的任何定制——例如,管理模板引擎配置)。下面的示例展示了如何做到这一点:
```
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
```
查看 Spring Framework Unit 测试,[Java](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script)和[resources](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script),以获得更多配置示例。
#### 1.10.5.JSP 和 JSTL
Spring 框架具有用于使用 Spring MVC 与 JSP 和 JSTL 的内置集成。
##### 视图解析器
在使用 JSP 进行开发时,通常声明`InternalResourceViewResolver` Bean。
`InternalResourceViewResolver`可用于向任何 Servlet 资源进行调度,但特别是用于 JSP。作为一种最佳实践,我们强烈建议将你的 JSP 文件放置在`'WEB-INF'`目录下的目录中,这样客户端就不能直接访问它。
```
```
##### JSP 与 JSTL
当使用 JSP 标准标记库时,你必须使用一个特殊的视图类`JstlView`,因为 JSTL 需要在诸如 i18n 特性之类的功能工作之前进行一些准备。
##### Spring 的 JSP 标记库
Spring 提供请求参数到命令对象的数据绑定,如前面几章所描述的。 Spring 为了促进结合那些数据绑定特性的 JSP 页面的开发,提供了一些使事情变得更容易的标记。 Spring 所有标记都具有 HTML 转义功能,以启用或禁用字符的转义。
`spring.tld`标记库描述符包含在`spring-webmvc.jar`中。有关单个标记的全面引用,请浏览[API 参考](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/package-summary.html#package.description)或查看标记库描述。
##### Spring 的表单标记库
在版本 2.0 中, Spring 提供了一组全面的数据绑定感知标记,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都提供了对其对应的 HTML 标记对应物的属性集的支持,使标记变得熟悉且使用起来直观。标记生成的 HTML 兼容 HTML4.01/XHTML1.0。
与其他表单/输入标记库不同, Spring 的表单标记库与 Spring Web MVC 集成在一起,使标记能够访问你的控制器处理的命令对象和引用数据。正如我们在下面的示例中所示,表单标记使 JSP 更易于开发、读取和维护。
我们通过表单标记,并查看每个标记如何使用的示例。我们已经包含了生成的 HTML 片段,其中某些标记需要进一步的注释。
###### Configuration
表单标记库捆绑在`spring-webmvc.jar`中。库描述符称为`spring-form.tld`。
要使用这个库中的标记,请在 JSP 页面的顶部添加以下指令:
```
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
```
其中`form`是你要为这个库中的标记使用的标记名称前缀。
###### 表单标签
此标记呈现 HTML“Form”元素,并将绑定路径公开给内部标记以进行绑定。它将命令对象放在`PageContext`中,以便可以通过内部标记访问命令对象。这个库中的所有其他标记都是`form`标记的嵌套标记。
假设我们有一个名为`User`的域对象。它是一个 JavaBean,具有`firstName`和`lastName`等属性。我们可以使用它作为表单控制器的表单支持对象,它返回`form.jsp`。下面的示例显示了`form.jsp`可能是什么样子的:
```
```
由页面控制器从放置在`PageContext`中的命令对象检索`firstName`和`lastName`值。继续阅读,以查看更多关于内部标记如何与`form`标记一起使用的复杂示例。
下面的清单显示了生成的 HTML,它看起来像一个标准表单:
```
```
前面的 JSP 假设表单支持对象的变量名为`command`。如果你已将表单备份对象以另一个名称(肯定是最佳实践)放入模型中,则可以将表单绑定到已命名的变量,如下例所示:
```
```
###### `input`标签
默认情况下,此标记呈现带有绑定值和`type='text'`元素的 HTML`input`。有关此标记的示例,请参见[表单标签](#mvc-view-jsp-formtaglib-formtag)。你也可以使用 HTML5 特定的类型,例如`email`、`tel`、`date`和其他类型。
###### `checkbox`标签
此标记呈现 HTML`input`标记,其`type`设置为`checkbox`。
假设我们的`User`具有首选项,例如订阅时事通讯和列出兴趣爱好。下面的示例显示了`Preferences`类:
Java
```
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
```
Kotlin
```
class Preferences(
var receiveNewsletter: Boolean,
var interests: StringArray,
var favouriteWord: String
)
```
相应的`form.jsp`可能如下所示:
```
Subscribe to newsletter?: |
<%-- Approach 1: Property is of type java.lang.Boolean --%>
|
Interests: |
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
Quidditch:
Herbology:
Defence Against the Dark Arts:
|
Favourite Word: |
<%-- Approach 3: Property is of type java.lang.Object --%>
Magic:
|
```
对于`checkbox`标记,有三种方法可以满足你的所有复选框需求。
* 方法一:当绑定值类型为`java.lang.Boolean`时,如果绑定值为`checked`,则将`input(checkbox)`标记为`checked`。`value`属性对应于`setValue(Object)`值属性的解析值。
* 方法二:当绑定值类型为`array`或`java.util.Collection`时,如果配置的`checked`值存在于绑定值`Collection`中,则将`input(checkbox)`标记为`checked`。
* 方法三:对于任何其他绑定值类型,如果配置的`setValue(Object)`等于绑定值,则将`checked`标记为`checked`。
请注意,无论采用哪种方法,都会生成相同的 HTML 结构。以下 HTML 片段定义了一些复选框:
```
Interests: |
Quidditch:
Herbology:
Defence Against the Dark Arts:
|
```
你可能不希望在每个复选框之后看到额外的隐藏字段。当 HTML 页面中的复选框未被选中时,其值不会在表单提交后作为 HTTP 请求参数的一部分发送到服务器,因此我们需要在 HTML 中解决此问题,以使 Spring 表单数据绑定工作。`checkbox`标记遵循现有的 Spring 约定,即为每个复选框包含一个以下划线(`_`)为前缀的隐藏参数。通过这样做,你可以有效地告诉 Spring“复选框在表单中是可见的,并且我希望表单数据绑定到的对象能够反映复选框的状态,无论发生什么情况。”
###### `checkboxes`标签
此标记将呈现多个 HTML`input`标记,并将`type`设置为`checkbox`。
本节以前面`checkbox`标记部分的示例为基础。有时,你不希望在 JSP 页面中列出所有可能的爱好。你更愿意在运行时提供一个可用选项的列表,并将其传递给标记。这就是`checkboxes`标记的目的。可以传入`Array`、`List`或`Map`属性中包含可用选项的`items`。通常,绑定属性是一个集合,以便它可以保存用户选择的多个值。下面的示例展示了使用此标记的 JSP:
```
Interests: |
<%-- Property is of an array or of type java.util.Collection --%>
|
```
这个示例假设`interestList`是一个`List`,作为包含要从中选择的值的字符串的模型属性可用。如果使用`Map`,则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用`itemValue`提供该值的属性名称,并通过使用`itemLabel`提供标签。
###### `radiobutton`标签
此标记呈现 HTML`input`元素,其`type`设置为`radio`。
典型的使用模式涉及绑定到相同属性但具有不同值的多个标记实例,如下例所示:
```
Sex: |
Male:
Female:
|
```
###### `radiobuttons`标签
此标记呈现多个 HTML`input`元素,并将`type`设置为`radio`。
与[`checkboxes`标记](#mvc-view-jsp-formtaglib-checkboxestag)一样,你可能希望将可用选项作为运行时变量传递。对于这种用法,你可以使用`radiobuttons`标记。你传入一个`Array`、一个`List`或一个`Map`,它包含`items`属性中的可用选项。如果使用`Map`,则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用`itemValue`提供该值的属性名称,并通过使用`itemLabel`提供标签,如下例所示:
```
Sex: |
|
```
###### `password`标签
这个标记呈现一个 HTML`input`标记,其类型设置为`password`,并具有绑定值。
```
Password: |
|
```
请注意,默认情况下,不会显示密码值。如果确实希望显示密码值,可以将`showPassword`属性的值设置为`true`,如下例所示:
```
Password: |
|
```
###### `select`标签
此标记呈现 HTML“select”元素。它支持与所选选项的数据绑定,以及使用嵌套的`option`和`options`标记。
假设`User`有一个技能列表。相应的 HTML 可以如下所示:
```
Skills: |
|
```
如果`User’s`技能是草药学的,那么“技能”行的 HTML 源可以如下所示:
```
Skills: |
|
```
###### `option`标签
此标记呈现 HTML`option`元素。它基于绑定值设置`selected`。下面的 HTML 显示了它的典型输出:
```
House: |
|
```
如果`User’s`house 位于 Gryffindor,则“house”行的 HTML 源代码如下:
```
House: |
|
```
|**1**|注意添加了一个`selected`属性。|
|-----|--------------------------------------------|
###### tag`options`
此标记呈现 HTML`option`元素的列表。它基于绑定值设置`selected`属性。下面的 HTML 显示了它的典型输出:
```
Country: |
|
```
如果`User`存在于 UK 中,则“country”行的 HTML 源代码如下:
```
Country: |
|
```
|**1**|注意添加了一个`selected`属性。|
|-----|--------------------------------------------|
正如前面的示例所示,将`option`标记与`options`标记合并使用,会生成相同的标准 HTML,但允许你在 JSP 中显式地指定一个值,该值仅用于显示(在它所属的位置),例如示例中的默认字符串:“--请选择”。
`items`属性通常填充有条目对象的集合或数组。`itemValue`和`itemLabel`引用那些条目对象的 Bean 属性(如果指定的话)。否则,项目对象本身就会变成字符串。或者,你可以指定项目的`Map`,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果`itemValue`或`itemLabel`(或两者兼而有之)恰好也被指定,则 Item Value 属性将应用于 map 键,而 Item Label 属性将应用于 map 值。
###### tag`textarea`
此标记呈现 HTML`textarea`元素。下面的 HTML 显示了它的典型输出:
```
Notes: |
|
|
```
###### `hidden`标签
此标记将呈现一个 HTML`input`标记,该标记的`type`设置为`hidden`,并具有绑定值。要提交未绑定的隐藏值,请使用 HTML`input`标记,并将`type`设置为`hidden`。下面的 HTML 显示了它的典型输出:
```
```
如果我们选择将`house`值作为隐藏的值提交,则 HTML 将如下所示:
```
```
###### `errors`标签
此标记在 HTML`span`元素中呈现字段错误。它提供对在控制器中创建的错误或由与控制器关联的任何验证器创建的错误的访问。
假设我们希望在提交表单后显示`firstName`和`lastName`字段的所有错误消息。对于`User`类的实例,我们有一个名为`UserValidator`的验证器,如下例所示:
Java
```
public class UserValidator implements Validator {
public boolean supports(Class candidate) {
return User.class.isAssignableFrom(candidate);
}
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
}
}
```
Kotlin
```
class UserValidator : Validator {
override fun supports(candidate: Class<*>): Boolean {
return User::class.java.isAssignableFrom(candidate)
}
override fun validate(obj: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")
}
}
```
`form.jsp`可以如下:
```
```
如果我们提交一个在`firstName`和`lastName`字段中具有空值的表单,则 HTML 将如下所示:
```
```
如果我们想要显示给定页面的整个错误列表,该怎么办?下一个示例显示`errors`标记还支持一些基本的通配符功能。
* `path="*"`:显示所有错误。
* `path="lastName"`:显示与`lastName`字段关联的所有错误。
* 如果省略`path`,则只显示对象错误。
下面的示例在页面顶部显示错误列表,然后在字段旁边显示特定于字段的错误:
```
```
HTML 将如下所示:
```
```
`spring-form.tld`标记库描述符包含在`spring-webmvc.jar`中。有关单个标记的全面参考,请浏览[API 参考](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/form/package-summary.html#package.description)或查看标记库描述。
###### HTTP 方法转换
REST 的一个关键原则是使用“统一接口”。这意味着所有资源(URL)都可以通过使用相同的四种 HTTP 方法进行操作:GET、PUT、POST 和 DELETE。对于每种方法,HTTP 规范都定义了确切的语义。例如,get 应该始终是一个安全的操作,这意味着它没有副作用,而 put 或 delete 应该是幂等的,这意味着你可以一遍又一遍地重复这些操作,但最终结果应该是相同的。虽然 HTTP 定义了这四种方法,但 HTML 只支持两种:GET 和 POST。幸运的是,有两种可能的解决方法:你可以使用 JavaScript 来执行 PUT 或 DELETE,或者你可以使用“Real”方法作为附加参数(以 HTML 形式的隐藏输入字段建模)来执行 POST。 Spring 的`HiddenHttpMethodFilter`使用了后一种技巧。 Servlet 该过滤器是一个普通的过滤器,因此,它可以与任何 Web 框架(而不仅仅是 Spring MVC)组合使用。将此筛选器添加到你的 web.xml 中,带有隐藏`method`参数的 POST 将被转换为相应的 HTTP 方法请求。
为了支持 HTTP 方法转换,更新了 Spring MVC 表单标记以支持设置 HTTP 方法。例如,以下片段来自宠物诊所样本:
```
```
前面的示例执行 HTTP POST,在请求参数后面隐藏“real”delete 方法。下面的示例显示了在 web.xml 中定义的`HiddenHttpMethodFilter`来选择它:
```
httpMethodFilter
org.springframework.web.filter.HiddenHttpMethodFilter
httpMethodFilter
petclinic
```
下面的示例显示了相应的`@Controller`方法:
Java
```
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
this.clinic.deletePet(petId);
return "redirect:/owners/" + ownerId;
}
```
Kotlin
```
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
clinic.deletePet(petId)
return "redirect:/owners/$ownerId"
}
```
###### HTML5 标签
Spring 表单标记库允许输入动态属性,这意味着可以输入任何 HTML5 特定的属性。
表单`input`标记支持输入`text`以外的类型属性。这旨在允许呈现新的 HTML5 特定的输入类型,例如`email`,`date`,`range`,以及其他类型。请注意,不需要输入`type='text'`,因为`text`是默认类型。
#### 1.10.6.瓦片
你可以在使用 Spring 的 Web 应用程序中集成图块——就像任何其他视图技术一样。这一节以一种宽泛的方式描述了如何做到这一点。
| |本节重点介绍 Spring 对`org.springframework.web.servlet.view.tiles3`包中的磁贴版本 3 的支持。|
|---|-------------------------------------------------------------------------------------------------------------------------|
##### 依赖关系
为了能够使用磁贴,你必须在项目中添加对磁贴版本 3.0.1 或更高版本和[它的传递依赖关系](https://tiles.apache.org/framework/dependency-management.html)的依赖关系。
##### 配置
为了能够使用磁贴,你必须使用包含定义的文件来配置它(有关定义和其他磁贴概念的基本信息,请参见[https://tiles.apache.org](https://tiles.apache.org))。在 Spring 中,这是通过使用`TilesConfigurer`来完成的。下面的`ApplicationContext`配置示例展示了如何这样做:
```
/WEB-INF/defs/general.xml
/WEB-INF/defs/widgets.xml
/WEB-INF/defs/administrator.xml
/WEB-INF/defs/customer.xml
/WEB-INF/defs/templates.xml
```
前面的示例定义了五个包含定义的文件。这些文件都位于`WEB-INF/defs`目录中。在初始化`WebApplicationContext`时,文件被加载,定义工厂被初始化。完成此操作后,定义文件中包含的磁贴可以用作 Spring Web 应用程序中的视图。为了能够使用这些视图,与 Spring 中的任何其他视图技术一样,你必须有一个`ViewResolver`:通常是一个方便的`TilesViewResolver`。
你可以通过添加下划线和区域设置来指定特定于区域设置的磁贴定义,如下例所示:
```
/WEB-INF/defs/tiles.xml
/WEB-INF/defs/tiles_fr_FR.xml
```
在前面的配置中,`tiles_fr_FR.xml`用于使用`fr_FR`语言环境的请求,默认情况下使用`tiles.xml`。
| |由于下划线是用来指示区域设置的,因此我们建议不要在文件名中使用
,否则将其用于瓷砖定义。|
|---|------------------------------------------------------------------------------------------------------------------------------------|
###### `UrlBasedViewResolver`
`UrlBasedViewResolver`为它必须解析的每个视图实例化给定的`viewClass`。下面的 Bean 定义了`UrlBasedViewResolver`:
```
```
###### `SimpleSpringPreparerFactory`和`SpringBeanPreparerFactory`
作为一种高级特性, Spring 还支持两种特殊的贴片`PreparerFactory`实现方式。有关如何在磁贴定义文件中使用`ViewPreparer`引用的详细信息,请参见磁贴文档。
你可以根据指定的准备程序类,将`SimpleSpringPreparerFactory`指定为 AutoWire`ViewPreparer`实例,应用 Spring 的容器回调以及应用已配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围注释配置已被激活,`ViewPreparer`类中的注释将被自动检测并应用。请注意,这需要在磁贴定义文件中的准备程序类,就像默认的`PreparerFactory`所做的那样。
你可以指定`SpringBeanPreparerFactory`来对指定的编制者名称(而不是类)进行操作,从而从 DispatcherServlet 的应用程序上下文中获得相应的 Spring Bean。在这种情况下,完整的 Bean 创建过程处于 Spring 应用程序上下文的控制中,允许使用显式依赖注入配置、作用域 bean 等。请注意,你需要为每个准备者名称定义一个 Spring Bean 定义(如你的磁贴定义中所使用的)。下面的示例展示了如何在`TilesConfigurer` Bean 上定义`SpringBeanPreparerFactory`属性:
```
/WEB-INF/defs/general.xml
/WEB-INF/defs/widgets.xml
/WEB-INF/defs/administrator.xml
/WEB-INF/defs/customer.xml
/WEB-INF/defs/templates.xml
```
#### 1.10.7.RSS 和 Atom
`AbstractAtomFeedView`和`AbstractRssFeedView`都继承自`AbstractFeedView`基类,并分别用于提供 Atom 和 RSS 提要视图。它们基于[ROME](https://rometools.github.io/rome/)项目,位于包`org.springframework.web.servlet.view.feed`中。
`AbstractAtomFeedView`要求你实现`buildFeedEntries()`方法,并可选地覆盖`buildFeedMetadata()`方法(默认实现为空)。下面的示例展示了如何做到这一点:
爪哇
```
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List buildFeedEntries(Map model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
```
Kotlin
```
class SampleContentAtomView : AbstractAtomFeedView() {
override fun buildFeedMetadata(model: Map,
feed: Feed, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedEntries(model: Map,
request: HttpServletRequest, response: HttpServletResponse): List {
// implementation omitted
}
}
```
类似的要求也适用于实现`AbstractRssFeedView`,如下例所示:
爪哇
```
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List- buildFeedItems(Map model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
```
Kotlin
```
class SampleContentRssView : AbstractRssFeedView() {
override fun buildFeedMetadata(model: Map,
feed: Channel, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedItems(model: Map,
request: HttpServletRequest, response: HttpServletResponse): List
- {
// implementation omitted
}
}
```
如果你需要访问区域设置,`buildFeedItems()`和`buildFeedEntries()`方法将传入 HTTP 请求。HTTP 响应仅在设置 Cookie 或其他 HTTP 头时传入。方法返回后,提要将自动写入响应对象。
有关创建 Atom 视图的示例,请参见 ALEFArendsen Spring 团队博客[entry](https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support)。
#### 1.10.8.PDF 和 Excel
Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。本节描述如何使用这些特性。
##### 文档视图介绍
HTML 页面并不总是用户查看模型输出的最佳方式, Spring 使得从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是视图,并从服务器以正确的内容类型进行流媒体传输,以(希望)使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。
为了使用 Excel 视图,你需要将 Apache POI 库添加到 Classpath 中。为了生成 PDF,你需要添加(最好是)OpenPDF 库。
| |如果可能的话,你应该使用底层文档生成库的最新版本
。特别是,我们强烈推荐 OpenPDF(例如,OpenPDF1.2.12)
而不是过时的原始 iText2.1.7,因为 OpenPDF 是积极维护的,
修复了不可信 PDF 内容的一个重要漏洞。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
##### PDF 视图
一个用于单词列表的简单 PDF 视图可以扩展`org.springframework.web.servlet.view.document.AbstractPdfView`并实现`buildPdfDocument()`方法,如下例所示:
爪哇
```
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List words = (List) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
```
Kotlin
```
class PdfWordList : AbstractPdfView() {
override fun buildPdfDocument(model: Map, doc: Document, writer: PdfWriter,
request: HttpServletRequest, response: HttpServletResponse) {
val words = model["wordList"] as List
for (word in words) {
doc.add(Paragraph(word))
}
}
}
```
控制器可以从外部视图定义(通过名称引用它)返回这样的视图,也可以从处理程序方法返回`View`实例。
##### Excel 视图
Spring Framework4.2 以来,`org.springframework.web.servlet.view.document.AbstractXlsView`被提供为 Excel 视图的基类。它是基于 Apache POI 的,具有专门的子类(`AbstractXlsxView`和`AbstractXlsxStreamingView`)来取代过时的`AbstractExcelView`类。
编程模型类似于`AbstractPdfView`,以`buildExcelDocument()`作为中心模板方法,控制器能够从外部定义(通过名称)返回这样的视图,或者作为处理程序方法的`View`实例。
#### 1.10.9.Jackson
[WebFlux](web-reactive.html#webflux-view-httpmessagewriter)
Spring 提供对 JacksonJSON 库的支持。
##### 基于 Jackson 的 JSON MVC 视图
[WebFlux](web-reactive.html#webflux-view-httpmessagewriter)
`MappingJackson2JsonView`使用 Jackson 库的`ObjectMapper`将响应内容呈现为 JSON。默认情况下,模型映射的全部内容(除了特定于框架的类)都被编码为 JSON。对于需要对映射的内容进行筛选的情况,可以指定一组特定的模型属性来使用`modelKeys`属性进行编码。你还可以使用`extractValueFromSingleKeyModel`属性,将单键模型中的值直接提取和序列化,而不是作为模型属性的映射。
你可以根据需要使用 Jackson 提供的注释来定制 JSON 映射。当你需要进一步的控制时,你可以通过`ObjectMapper`属性注入一个自定义的`ObjectMapper`,用于需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况。
##### 基于 Jackson 的 XML 视图
[WebFlux](web-reactive.html#webflux-view-httpmessagewriter)
`MappingJackson2XmlView`使用[JacksonXML 扩展的](https://github.com/FasterXML/jackson-dataformat-xml)`XmlMapper`将响应内容呈现为 XML。如果模型包含多个条目,你应该使用`modelKey` Bean 属性显式地设置要序列化的对象。如果模型包含单个条目,则自动对其进行序列化。
你可以根据需要使用 JAXB 或 Jackson 提供的注释来定制 XML 映射。当需要进一步的控制时,可以通过`ObjectMapper`属性注入自定义`XmlMapper`,用于需要为特定类型提供序列化器和反序列化器的自定义 XML。
#### 1.10.10.XML 编组
`MarshallingView`使用 XML`Marshaller`(在`org.springframework.oxm`包中定义)将响应内容呈现为 XML。可以使用`MarshallingView`实例的`modelKey` Bean 属性显式地设置要编组的对象。或者,该视图对所有模型属性进行迭代,并封送`Marshaller`支持的第一个类型。有关`org.springframework.oxm`包中的功能的更多信息,请参见[使用 O/X 映射器编组 XML](data-access.html#oxm)。
#### 1.10.11.XSLT 视图
XSLT 是一种 XML 转换语言,在 Web 应用程序中作为一种视图技术很受欢迎。如果你的应用程序自然地处理 XML,或者你的模型可以很容易地转换为 XML,那么 XSLT 作为一种视图技术是一个很好的选择。下面的部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。
Spring 这个示例是一个简单的应用程序,它在`控制器`中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及 XSLT 视图的视图名称。有关 Spring Web MVC 的`Controller`接口的详细信息,请参见[带注释的控制器](#mvc-controller)。XSLT 控制器将单词列表转换为一个简单的 XML 文档,以便进行转换。
##### 豆子
对于简单的 Spring Web 应用程序,配置是标准的:MVC 配置必须定义`XsltViewResolver` Bean 和常规的 MVC 注释配置。下面的示例展示了如何做到这一点:
爪哇
```
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
```
Kotlin
```
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun xsltViewResolver() = XsltViewResolver().apply {
setPrefix("/WEB-INF/xsl/")
setSuffix(".xslt")
}
}
```
##### Controller
我们还需要一个封装我们的字生成逻辑的控制器。
控制器逻辑封装在`@Controller`类中,处理程序方法定义如下:
爪哇
```
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
```
Kotlin
```
import org.springframework.ui.set
@Controller
class XsltController {
@RequestMapping("/")
fun home(model: Model): String {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val root = document.createElement("wordList")
val words = listOf("Hello", "Spring", "Framework")
for (word in words) {
val wordNode = document.createElement("word")
val textNode = document.createTextNode(word)
wordNode.appendChild(textNode)
root.appendChild(wordNode)
}
model["wordList"] = root
return "home"
}
}
```
到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,你也可以将 XML 文件加载为`Resource`,并使用它来代替自定义 DOM 文档。
有一些软件包可以自动对对象图进行“domify”,但是,在 Spring 之内,你可以完全灵活地以你选择的任何方式从你的模型中创建 DOM。这可以防止 XML 转换在模型数据的结构中起到太大的作用,这在使用工具管理 Domification 过程时是一种危险。
##### 转换
最后,`XsltViewResolver`解析“home”XSLT 模板文件,并将 DOM 文档合并到其中以生成视图。如`XsltViewResolver`配置中所示,XSLT 模板位于`war`目录中的`war`文件中,并以`WEB-INF/xsl`文件扩展名结束。
下面的示例展示了一个 XSLT 转换:
```
Hello!
My First Words
```
前面的转换呈现为以下 HTML:
```
Hello!
My First Words
```
### 1.11.MVC 配置
[WebFlux](web-reactive.html#webflux-config)
MVC 爪哇 配置和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,并提供了一个配置 API 来对其进行定制。
有关配置 API 中没有的更高级的定制,请参见[高级 爪哇 配置](#mvc-config-advanced-java)和[高级 XML 配置](#mvc-config-advanced-xml)。
你不需要理解由 MVC 爪哇 配置和 MVC 名称空间创建的底层 bean。如果你想了解更多信息,请参见[Special Bean Types](#mvc-servlet-special-bean-types)和[Web MVC 配置](#mvc-servlet-config)。
#### 1.11.1.启用 MVC 配置
[WebFlux](web-reactive.html#webflux-config-enable)
在 爪哇 配置中,可以使用`@EnableWebMvc`注释来启用 MVC 配置,如下例所示:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig {
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig
```
在 XML 配置中,可以使用``元素来启用 MVC 配置,如下例所示:
```
```
前面的示例注册了 Spring MVC的数量,并适应于 Classpath 上可用的依赖关系(例如,用于 JSON、XML 和其他的有效负载转换器)。
#### 1.11.2.MVC 配置 API
[WebFlux](web-reactive.html#webflux-config-customize)
在 爪哇 配置中,你可以实现`WebMvcConfigurer`接口,如下例所示:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
// Implement configuration methods...
}
```
在 XML 中,你可以检查``的属性和子元素。你可以查看[Spring MVC XML schema](https://schema.spring.io/mvc/spring-mvc.xsd),或者使用 IDE 的代码完成功能来发现哪些属性和子元素是可用的。
#### 1.11.3.类型转换
[WebFlux](web-reactive.html#webflux-config-conversion)
默认情况下,安装了各种数字和日期类型的格式化程序,并支持在字段上通过`@NumberFormat`和`@DateTimeFormat`进行定制。
要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
```
要在 XML Config 中执行相同的操作,请使用以下方法:
```
```
Spring 默认情况下,MVC 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
```
| |参见[the`FormatterRegistrar`SPI]和`FormattingConversionServiceFactoryBean`有关何时使用
FormatterRegistrar 实现的更多信息。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 1.11.4.验证
[WebFlux](web-reactive.html#webflux-config-validation)
默认情况下,如果[Bean Validation](core.html#validation-beanvalidation-overview)存在于 Classpath(例如, Hibernate 验证器)上,则`LocalValidatorFactoryBean`注册为全局[Validator](core.html#validator),用于控制器方法参数上的`@Valid`和`Validated`。
在 爪哇 配置中,你可以自定义全局`Validator`实例,如下例所示:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun getValidator(): Validator {
// ...
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
请注意,你也可以在本地注册`Validator`实现,如下例所示:
爪哇
```
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
```
Kotlin
```
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
```
| |如果需要将`LocalValidatorFactoryBean`注入到某个地方,请创建一个 Bean 并将
标记为`@Primary`,以避免与 MVC 配置中声明的值发生冲突。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 1.11.5.拦截器
在 爪哇 配置中,你可以注册拦截器以应用于传入的请求,如下例所示:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(LocaleChangeInterceptor())
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*")
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
#### 1.11.6.内容类型
[WebFlux](web-reactive.html#webflux-config-content-negotiation)
你可以配置 Spring MVC 如何从请求中确定所请求的媒体类型(例如,`Accept`报头、URL 路径扩展、查询参数和其他)。
默认情况下,只检查`Accept`标头。
如果必须使用基于 URL 的内容类型解析,请考虑在路径扩展上使用查询参数策略。有关更多详细信息,请参见[后缀匹配](#mvc-ann-requestmapping-suffix-pattern-match)和[后缀匹配和 RFD](#mvc-ann-requestmapping-rfd)。
在 爪哇 配置中,你可以自定义所请求的内容类型分辨率,如下例所示:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON)
configurer.mediaType("xml", MediaType.APPLICATION_XML)
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
json=application/json
xml=application/xml
```
#### 1.11.7.消息转换器
[WebFlux](web-reactive.html#webflux-config-message-codecs)
你可以通过覆盖[`configureMessageConverters()`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet//config/annotation/webmvccconfigurer.html#configureMessageConverters-java.util.util.list-)(以取代由 Spring MVC 创建的默认转换器),或者通过覆盖[https:///moverwomfrandframework=“2677”/DOCS:/[56.mframework:/jumframework:/
下面的示例使用定制的`ObjectMapper`来添加 XML 和 JacksonJSON 转换器,而不是默认的转换器:
爪哇
```
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfiguration : WebMvcConfigurer {
override fun configureMessageConverters(converters: MutableList>) {
val builder = Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(ParameterNamesModule())
converters.add(MappingJackson2HttpMessageConverter(builder.build()))
converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))
```
在前面的示例中,[`Jackson2ObjectMapperBuilder`](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2objectmapperbuilder.html)用于为`MappingJackson2HttpMessageConverter`和`MappingJackson2XmlHttpMessageConverter`创建一个通用配置,并启用了缩进、自定义的日期格式,以及注册[<`jackson-module-parameter-names`](https://gitHub.com/gitHub-module-module-names-names),这为访问参数添加了参数
这个构建器自定义 Jackson 的默认属性如下:
* [`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/deserializationfeature.html#fail_on_unknown_properties)被禁用。
* [`MapperFeature.DEFAULT_VIEW_INCLUSION`](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/mapperfeature.html#default_view_inclusion)被禁用。
如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块:
* [Jackson-数据类型-Joda](https://github.com/FasterXML/jackson-datatype-joda):支持 Joda-time 类型。
* [Jackson-数据类型-JSR310](https://github.com/FasterXML/jackson-datatype-jsr310):支持 爪哇8 日期和时间 API 类型。
* [Jackson-数据类型-JDK8](https://github.com/FasterXML/jackson-datatype-jdk8):支持其他 Java8 类型,例如`Optional`。
* [`jackson-module-kotlin`](https://github.com/fasterxml/Jackson-module- Kotlin):支持 Kotlin 类和数据类。
| |在 JacksonXML 支持下启用缩进需要[`woodstox-core-asl`](https://search. Maven.org/#search%7CGAv%7c1%7cg%3a%22org.codehaus.woodstox%22%20and%20a%3a%22woodstox-core-asl%22)的依赖性,此外还需要[`jackson-dataformat-xml`](https://search. Maven.xml%7c1%7caga%7ca%3a%22%xml-dataformat-22%22%22)的依赖性。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
还有其他有趣的 Jackson 模块可供选择:
* [Jackson-数据类型-货币](https://github.com/zalando/jackson-datatype-money):支持`javax.money`类型(非官方模块)。
* [jackson-datatype-hibernate](https://github.com/FasterXML/jackson-datatype-hibernate):支持 Hibernate 特定的类型和属性(包括惰性加载方面)。
下面的示例展示了如何用 XML 实现相同的配置:
```
```
#### 1.11.8.视图控制器
这是一个用于定义`ParameterizableViewController`的快捷方式,该快捷方式在调用时立即转发到视图。如果在视图生成响应之前没有要运行的 Java 控制器逻辑,则可以在静态情况下使用它。
下面的 Java 配置示例将对`/`的请求转发到一个名为`home`的视图:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/").setViewName("home")
}
}
```
下面的示例通过使用``元素,实现了与前面示例相同的功能,但是使用了 XML:
```
```
如果`@RequestMapping`方法被映射到任何 HTTP 方法的 URL,则不能使用视图控制器来处理相同的 URL。这是因为 URL 与带注释的控制器的匹配被认为是对端点所有权的足够强的指示,因此可以将 405(方法 \_ 不允许)、415(不支持 \_media\_type)或类似的响应发送到客户机,以帮助进行调试。出于这个原因,建议避免在带注释的控制器和视图控制器之间分割 URL 处理。
#### 1.11.9.视图解析器
[WebFlux](web-reactive.html#webflux-config-view-resolvers)
MVC 配置简化了视图解析程序的注册。
下面的 Java 配置示例通过使用 JSP 和 Jackson 作为 JSON 呈现的默认`View`配置内容协商视图解析:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.jsp()
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
但是,请注意,自由标记、磁贴、Groovy 标记和脚本模板也需要配置底层视图技术。
MVC 命名空间提供了专用的元素。下面的示例与 Freemarker 一起工作:
```
```
在 Java 配置中,你可以添加相应的`Configurer` Bean,如下例所示:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.freeMarker().cache(false);
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/freemarker");
return configurer;
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.freeMarker().cache(false)
}
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/freemarker")
}
}
```
#### 1.11.10.静态资源
[WebFlux](web-reactive.html#webflux-config-static-resources)
此选项提供了一种方便的方式来从[`Resource`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/resource.html)-based 位置的列表中提供静态资源。
在下一个示例中,给定一个以`/resources`开头的请求,相对路径用于在 Web 应用程序根目录下或在`/static`下的 Classpath 上查找和服务相对于`/public`的静态资源。这些资源将在一年后到期,以确保最大程度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求。`Last-Modified`信息是从`Resource#lastModified`推导出来的,因此`"Last-Modified"`头支持 HTTP 条件请求。
下面的清单展示了如何使用 Java 配置来实现这一点:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
另见[对静态资源的 HTTP 缓存支持](#mvc-caching-static-resources)。
资源处理程序还支持[`ResourceResolver`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/resource/resourceresolver.html)实现和[`ResourceTransformer`](https://DOCS. Spring.io/ Spring.io/ Spring-framework/DOCS/5.3.16/javoc-api/org/org/web/ Servlet/resource/resourcefork/resourcer.html)实现,你可以使用这些实现来创建一个优化的工具链,以便使用
你可以使用`VersionResourceResolver`来实现基于从内容、固定应用程序版本或其他版本计算的 MD5 散列的版本管理的资源 URL。`ContentVersionStrategy`(md5hash)是一个很好的选择——除了一些明显的例外,例如与模块加载程序一起使用的 JavaScript 资源。
下面的示例展示了如何在 Java 配置中使用`VersionResourceResolver`:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
然后,你可以使用`ResourceUrlProvider`重写 URL,并应用整个解析器和变压器链——例如,用于插入版本。MVC 配置提供了`ResourceUrlProvider` Bean,这样就可以将其注入到其他配置中。你还可以使用`ResourceUrlEncodingFilter`对 ThymeLeaf、JSP、Freemarker 和其他具有依赖于`HttpServletResponse#encodeURL`的 URL 标记的文件进行透明的重写。
请注意,当同时使用`EncodedResourceResolver`(例如,用于服务 gzipped 或 brotli 编码的资源)和`VersionResourceResolver`时,必须按此顺序注册它们。这确保了基于内容的版本总是基于未编码的文件进行可靠的计算。
[WebJars](https://www.webjars.org/documentation)也通过`WebJarsResourceResolver`支持,这是在 Classpath 上存在`org.webjars:webjars-locator-core`库时自动注册的。解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从`/jquery/jquery.min.js`到`/jquery/1.2.0/jquery.min.js`。
| |基于`ResourceHandlerRegistry`的 Java 配置为细粒度控制提供了进一步的选项
,例如,上次修改行为和优化的资源解析。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 1.11.11.默认值 Servlet
Spring MVC 允许将`DispatcherServlet`映射到`/`(从而覆盖容器的默认值 Servlet 的映射),同时仍然允许由容器的默认值处理静态资源请求 Servlet。它使用`DefaultServletHttpRequestHandler`的 URL 映射配置`/**`,并且相对于其他 URL 映射的优先级最低。
此处理程序将所有请求转发到缺省 Servlet。因此,它必须以所有其他 URL`HandlerMappings`的顺序保持在最后。如果使用``,就是这种情况。或者,如果你设置了自己定制的`HandlerMapping`实例,请确保将其`order`属性设置为一个低于`DefaultServletHttpRequestHandler`的值,即`Integer.MAX_VALUE`。
下面的示例展示了如何通过使用默认设置来启用该功能:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable()
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
要重写`/` Servlet 映射的注意事项是,默认 Servlet 的`RequestDispatcher`必须按名称而不是按路径检索。`DefaultServletHttpRequestHandler`尝试在启动时自动检测容器的默认 Servlet,使用大多数主要 Servlet 容器(包括 Tomcat、 Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)的已知名称列表。如果默认值 Servlet 已被定制配置为不同的名称,或者在默认值 Servlet 名称未知的情况下正在使用不同的 Servlet 容器,那么你必须显式地提供默认值 Servlet 的名称,如下例所示:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable("myCustomDefaultServlet")
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
#### 1.11.12.路径匹配
[WebFlux](web-reactive.html#webflux-config-path-matching)
你可以自定义与路径匹配和 URL 处理相关的选项。有关单个选项的详细信息,请参见[`PathMatchConfigurer`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/config/annotation/pathmatchconfigrer.html)Javadoc。
下面的示例展示了如何在 Java 配置中定制路径匹配:
Java
```
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setPatternParser(new PathPatternParser())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
// ...
}
}
```
Kotlin
```
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setPatternParser(patternParser)
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
}
fun patternParser(): PathPatternParser {
//...
}
}
```
下面的示例展示了如何用 XML 实现相同的配置:
```
```
#### 1.11.13.高级 Java 配置
[WebFlux](web-reactive.html#webflux-config-advanced-java)
`@EnableWebMvc`imports`DelegatingWebMvcConfiguration`,其中:
* 为 Spring MVC 应用程序提供默认的 Spring 配置
* 检测并委托`WebMvcConfigurer`实现来定制该配置。
对于高级模式,可以删除`@EnableWebMvc`并直接从`DelegatingWebMvcConfiguration`进行扩展,而不是实现`WebMvcConfigurer`,如下例所示:
Java
```
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
```
Kotlin
```
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {
// ...
}
```
你可以将现有的方法保留在`WebConfig`中,但是你现在也可以重写 Bean 来自基类的声明,并且你仍然可以在 Classpath 上拥有任何数量的其他`WebMvcConfigurer`实现。
#### 1.11.14.高级 XML 配置
MVC 命名空间没有高级模式。如果你需要在 Bean 上自定义一个你无法以其他方式更改的属性,那么你可以使用 Spring `BeanPostProcessor`生命周期钩子`ApplicationContext`,如下例所示:
Java
```
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
```
Kotlin
```
@Component
class MyPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
// ...
}
}
```
请注意,你需要将`MyPostProcessor`声明为 Bean,可以在 XML 中显式地声明,也可以通过``声明来检测它。
### 1.12.http/2
[WebFlux](web-reactive.html#webflux-http2)
Servlet 需要 4 个容器来支持 HTTP/2,并且 Spring Framework5 与 Servlet API4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见[HTTP/2Wiki 页面](https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support)。
Servlet API 确实公开了一个与 HTTP/2 相关的构造。你可以使用`javax.servlet.http.PushBuilder`来主动地将资源推送到客户端,并且它被支持为[方法参数](#mvc-ann-arguments)到`@RequestMapping`的方法。
## 2. REST 客户
本节描述客户端访问 REST 端点的选项。
### 2.1.`RestTemplate`
`RestTemplate`是执行 HTTP 请求的同步客户端。 Spring 它是最初的 REST 客户机,在底层 HTTP 客户库上公开了一个简单的模板方法 API。
| |截至 5.0,`RestTemplate`处于维护模式,只有少量的
更改请求和 bug 被接受。请考虑使用[WebClient](web-reactive.html#webflux-client),它提供了一个更现代的 API,并且
支持同步、异步和流场景。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
详见[REST 端点](integration.html#rest-client-access)。
### 2.2.`WebClient`
`WebClient`是执行 HTTP 请求的非阻塞、反应式客户端。它是在 5.0 中引入的,并提供了`RestTemplate`的现代替代方案,有效地支持同步和异步以及流场景。
与`RestTemplate`相反,`WebClient`支持以下内容:
* 非阻塞 I/O。
* 反应性气流反压。
* 高并发性与较少的硬件资源.
* 功能风格的、流畅的 API,充分利用了 Java8Lambdas。
* 同步和异步交互。
* 从服务器往上流或往下流。
有关更多详细信息,请参见[WebClient](web-reactive.html#webflux-client)。
## 3. 测试
[Same in Spring WebFlux](web-reactive.html#webflux-test)
本节总结了用于 Spring MVC 应用程序的`spring-test`中可用的选项。
* Servlet API 模拟:用于单元测试控制器、过滤器和其他 Web 组件的 Servlet API 合同的模拟实现。有关更多详细信息,请参见[Servlet API](testing.html#mock-objects-servlet)模拟对象。
* TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法对加载的配置进行有效缓存,以及支持用`MockServletContext`加载`WebApplicationContext`。有关更多详细信息,请参见[TestContext 框架](testing.html#testcontext-framework)。
* Spring MVC 测试:一种框架,也称为`MockMvc`,用于通过`DispatcherServlet`测试带注释的控制器(即,支持注释),用 Spring MVC 基础设施完成,但没有 HTTP 服务器。有关更多详细信息,请参见[Spring MVC Test](testing.html#spring-mvc-test-framework)。
* 客户端 REST:`spring-test`提供了一个`MockRestServiceServer`,你可以将其用作模拟服务器,用于测试内部使用`RestTemplate`的客户端代码。有关更多详细信息,请参见[客户机 REST 测试](testing.html#spring-mvc-test-client)。
* `WebTestClient`:用于测试 WebFlux 应用程序,但也可以用于通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个非阻塞的、反应性的客户机,非常适合于测试异步和流媒体场景。
## 4. WebSockets
[WebFlux](web-reactive.html#webflux-websocket)
参考文档的这一部分涵盖了对 Servlet 堆栈、 WebSocket 消息传递的支持,这些消息传递包括原始 WebSocket 交互、 WebSocket 通过 Sockjs 的模拟,以及通过 STOMP 作为 WebSocket 上的子协议的发布-订阅消息传递。
### 4.1. WebSocket 介绍
WebSocket 协议[RFC 6455](https://tools.ietf.org/html/rfc6455)提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。
WebSocket 交互以一个 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互:
```
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
```
|**1**|`Upgrade`标头。|
|-----|-------------------------------|
|**2**|使用`Upgrade`连接。|
具有 WebSocket 支持的服务器将返回类似于以下内容的输出,而不是通常的 200 状态代码:
```
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
```
|**1**|协议转换|
|-----|---------------|
在成功握手之后,HTTP 升级请求中的 TCP 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。
关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,HTML5 的 WebSocket 章,或 Web 上的许多介绍和教程中的任何一个。
注意,如果 WebSocket 服务器运行在 Web 服务器(例如 Nginx)的后面,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,则检查与 WebSocket 支持相关的云提供商的指令。
#### 4.1.1.HTTP 与 WebSocket
WebSocket 即使被设计为与 HTTP 兼容并且以 HTTP 请求开始,重要的是要理解这两个协议导致非常不同的体系结构和应用程序编程模型。
在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。
相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息都在相同的 TCP 连接上流动。这指向了一种完全不同的异步、事件驱动的消息传递体系结构。
WebSocket 也是一种低级传输协议,其与 HTTP 不同,不对消息的内容规定任何语义。这意味着,除非客户机和服务器在消息语义上达成一致,否则就没有路由或处理消息的方法。
WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议(例如,STOMP),通过头上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。
#### 4.1.2.何时使用 WebSockets
WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。
例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。 WebSocket 是低延迟、高频率和大容量的组合,这是使用 WebSocket 的最佳情况。
还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递`Upgrade`头,或者是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公共的应用程序使用 WebSocket 是一个更直接的决定。
### 4.2. WebSocket API
[WebFlux](web-reactive.html#webflux-websocket-server)
Spring 框架提供了一个 WebSocket API,你可以使用该 API 来编写处理 WebSocket 消息的客户端和服务器端应用程序。
#### 4.2.1.`WebSocketHandler`
[WebFlux](web-reactive.html#webflux-websocket-server-handler)
创建 WebSocket 服务器就像实现`WebSocketHandler`一样简单,或者更有可能的是,扩展`TextWebSocketHandler`或`BinaryWebSocketHandler`。下面的示例使用`TextWebSocketHandler`:
```
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
```
有专门的 WebSocket Java 配置和 XML 命名空间支持,用于将前面的 WebSocket 处理程序映射到特定的 URL,如下例所示:
```
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
前面的示例是用于 Spring MVC 应用程序中的,并且应该包括在[](#MVC- Servlet)的配置中。然而, Spring 的 WebSocket 支持并不依赖于 Spring MVC。在[`WebSocketHttpRequestHandler`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/server/support/websockettprequesthandler.html)的帮助下,将`WebSocketHandler`集成到其他 HTTP 服务环境中是相对简单的。
当直接 VS 间接地使用`WebSocketHandler`API 时,例如通过[STOMP](#websocket-stomp)消息传递时,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话(JSR-356)不允许并发。一个选项是将`WebSocketSession`与[`ConcurrentWebSocketSessionDecorator`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/handler/concurrentwebsocketsessiondecorator.html)包装在一起。
#### 4.2.2. WebSocket 握手
[WebFlux](web-reactive.html#webflux-websocket-server-handshake)
定制初始 HTTP WebSocket 握手请求的最简单方法是通过`HandshakeInterceptor`,该方法公开了握手之前和之后的方法。你可以使用这样的拦截器来阻止握手或使`WebSocketSession`的任何属性可用。下面的示例使用内置的拦截器将 HTTP 会话属性传递给 WebSocket 会话:
```
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
一个更高级的选项是扩展执行 WebSocket 握手步骤的`DefaultHandshakeHandler`,包括验证客户机原点、协商子协议和其他细节。如果应用程序需要配置自定义`RequestUpgradeStrategy`以适应 WebSocket 服务器引擎和尚未支持的版本,则可能还需要使用此选项(有关此主题的更多信息,请参见[Deployment](#websocket-server-deployment))。Java 配置和 XML 命名空间都使配置自定义`HandshakeHandler`成为可能。
| |Spring 提供了一个`WebSocketHandlerDecorator`基类,你可以使用它来使用附加行为来装饰
a`WebSocketHandler`。当使用 WebSocket Java 配置
或 XML 命名空间时,默认提供并添加日志记录和异常处理
实现。`ExceptionWebSocketHandlerDecorator`捕获由任何`WebSocketHandler`方法产生的所有未捕获的
异常,并关闭 WebSocket
具有状态`1011`的会话,这表示服务器错误。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.2.3.部署
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中`DispatcherServlet`同时服务于 HTTP WebSocket 握手和其他 HTTP 请求。通过调用`WebSocketHttpRequestHandler`,也很容易集成到其他 HTTP 处理场景中。这很方便,也很容易理解。但是,对于 JSR-356 运行时,需要进行特殊的考虑。
Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的 Servlet 容器 Classpath 扫描( Servlet 3 特征)。另一种是在 Servlet 容器初始化时使用的注册 API。这两种机制都不可能对所有 HTTP 处理(包括 WebSocket 握手和所有其他 HTTP 请求)使用单一的“前置控制器”,例如 Spring MVC 的。
这是 JSR-356 的一个重大限制,即 Spring 的 WebSocket 支持具有特定于服务器的`RequestUpgradeStrategy`实现的地址,即使在 JSR-356 运行时也是如此。此类策略目前存在于 Tomcat、 Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及 Wildfly)。
| |在 Java WebSocket API 中克服前面的限制的请求已被
创建,并且可以在[eclipse-ee4j/websocket-api#211](https://github.com/eclipse-ee4j/websocket-api/issues/211)处被遵循。
Tomcat, Undertow,和 WebSphere 提供了它们自己的 API 替代方案,使其能够做到这一点,并且与 Jetty 一起也是可能的。我们希望
更多的服务器也能做到这一点。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
第二个考虑因素是,支持 JSR-356 的 Servlet 容器预计将执行`ServletContainerInitializer`扫描,这可能会大大减慢应用程序的启动速度——在某些情况下。如果在升级到具有 JSR-356 支持的 Servlet 容器版本后观察到重大影响,则应该可以通过使用``中的``元素选择性地启用或禁用 Web 片段(和 SCI 扫描),如下例所示:
```
```
然后,你可以根据名称选择性地启用 Web 片段,例如 Spring 自己的`SpringServletContainerInitializer`,它为 Servlet 3Java 初始化 API 提供了支持。下面的示例展示了如何做到这一点:
```
spring_web
```
#### 4.2.4.服务器配置
[WebFlux](web-reactive.html#webflux-websocket-server-config)
WebSocket 每个底层引擎都公开了控制运行时特性的配置属性,例如消息缓冲区大小、空闲超时等。
对于 Tomcat、Wildfly 和 GlassFish,可以在 WebSocket Java 配置中添加`ServletServerContainerFactoryBean`,如下例所示:
```
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
| |对于客户端 WebSocket 配置,你应该使用`WebSocketContainerFactoryBean`或`ContainerProvider.getWebSocketContainer()`(Java 配置)。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
对于 Jetty,你需要提供一个预先配置的 Jetty `WebSocketServerFactory`,并通过 WebSocket Java 配置将其插入 Spring 的`DefaultHandshakeHandler`。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
#### 4.2.5.允许的来源
[WebFlux](web-reactive.html#webflux-websocket-server-cors)
在 Spring Framework4.1.5 中, WebSocket 和 Sockjs 的默认行为是仅接受同源请求。也可以允许所有或指定的源列表。这种检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户机修改`Origin`标头值(有关更多详细信息,请参见[RFC6454:Web Origin 概念](https://tools.ietf.org/html/rfc6454))。
这三种可能的行为是:
* 只允许同源请求(默认):在这种模式下,当启用 Sockjs 时,IFRAME HTTP 响应头设置为,并且禁用 JSONP 传输,因为它不允许检查请求的源。因此,当启用此模式时,IE6 和 IE7 将不受支持。
* 允许指定的源列表:每个允许的源列表必须以`http://`或`https://`开头。在这种模式下,当启用 Sockjs 时,iframe 传输将被禁用。因此,当启用此模式时,IE6 到 IE9 将不受支持。
* 允许所有原点:要启用此模式,你应该提供`*`作为允许的原点值。在这种模式下,所有的传输都是可用的。
你可以配置 WebSocket 和 Sockjs 允许的起源,如下例所示:
```
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
### 4.3.Sockjs 后援
在公共互联网上,不受你控制的限制性代理可能会阻止 WebSocket 交互,要么是因为它们未被配置为传递`Upgrade`头,要么是因为它们关闭了似乎处于空闲状态的长期连接。
这个问题的解决方案是 WebSocket 仿真——即,尝试先使用 WebSocket,然后再使用基于 HTTP 的技术,该技术模拟 WebSocket 交互并公开相同的应用程序级 API。
在 Servlet 栈上, Spring 框架为 Sockjs 协议提供了服务器(以及客户端)支持。
#### 4.3.1.概述
SockJS 的目标是让应用程序使用 WebSocket API,但在运行时在必要时退回到非 WebSocket 替代方案,而不需要更改应用程序代码。
Sockjs 包括:
* 将[Sockjs 协议](https://github.com/sockjs/sockjs-protocol)定义为可执行文件[叙述测试](https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html)的形式。
* [Sockjs JavaScript 客户端](https://github.com/sockjs/sockjs-client/)——用于浏览器的客户库。
* Sockjs 服务器实现,包括在 Spring 框架`spring-websocket`模块中的一个。
* `spring-websocket`模块中的 Sockjs Java 客户端(自版本 4.1 起)。
Sockjs 是为在浏览器中使用而设计的。它使用各种技术来支持各种浏览器版本。有关 Sockjs 传输类型和浏览器的完整列表,请参见[Sockjs 客户端](https://github.com/sockjs/sockjs-client/)页面。传输可分为三大类: WebSocket、HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参见[这篇博文](https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/)。
Sockjs 客户机通过发送`GET /info`开始从服务器获取基本信息。在那之后,它必须决定使用什么交通工具。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流媒体选项。如果不是,则使用 HTTP(长)轮询。
所有传输请求都具有以下 URL 结构:
```
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
```
地点:
* `{server-id}`对于群集中的路由请求很有用,但不用于其他方式。
* `{session-id}`关联属于 Sockjs 会话的 HTTP 请求。
* `{transport}`表示传输类型(例如,`websocket`,`xhr-streaming`等)。
WebSocket 传输只需要一个 HTTP 请求就可以完成 WebSocket 握手。此后的所有消息都在该套接字上交换。
HTTP 传输需要更多的请求。例如,Ajax/XHR 流依赖于对服务器到客户端消息的一个长时间运行的请求,以及对客户端到服务器消息的额外 HTTP POST 请求。长轮询是类似的,只是它在每个服务器到客户端发送后结束当前请求。
Sockjs 添加了最小的消息框架。例如,服务器最初发送字母`o`(“打开”框架),消息被发送为`a["message1","message2"]`(JSON 编码的数组),如果 25 秒内没有消息流(默认情况下),则发送字母`h`(“心跳”框架),并将字母`c`(“关闭”框架)关闭会话。
要了解更多信息,请在浏览器中运行一个示例,并观察 HTTP 请求。Sockjs 客户机允许固定传输列表,因此可以一次查看每个传输。Sockjs 客户机还提供了一个调试标志,可以在浏览器控制台中启用有用的消息。在服务器端,你可以启用`TRACE`的`org.springframework.web.socket`日志记录。有关更多详细信息,请参见 Sockjs 协议[旁白测试](https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html)。
#### 4.3.2.启用 Sockjs
你可以通过 Java 配置启用 Sockjs,如下例所示:
```
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
前面的示例是用于 Spring MVC 应用程序中的,并且应该包括在[](#MVC- Servlet)的配置中。然而, Spring 的 WebSocket 和 Sockjs 支持并不依赖于 Spring MVC。在[`SockJsHttpRequestHandler`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/sockkjs/support/sockjshtprequesthandler.html)的帮助下,集成到其他 HTTP 服务环境是相对简单的。
在浏览器方面,应用程序可以使用[`sockjs-client`](https://github.com/sockjs/sockjs-client/)(版本 1.0.x)。它模拟 W3C WebSocket API,并与服务器通信以根据其运行的浏览器选择最佳传输选项。请参阅[Sockjs-客户端](https://github.com/sockjs/sockjs-client/)页面和浏览器支持的传输类型列表。客户机还提供了几个配置选项——例如,指定要包含哪些传输。
#### 4.3.3.IE8 和 IE9
Internet Explorer8 和 9 仍在使用中。它们是拥有袜子的一个关键原因。本节介绍了在这些浏览器中运行的重要注意事项。
Sockjs 客户机通过使用微软的[`XDomainRequest`](https://blogs.msdn.com/b/ieinnals/archive/2010/05/13/xdomainRequest-Restrictions-Limitions-and-Workarounds.ASPX),在 IE8 和 9 中支持 Ajax/XHR 流媒体。它可以跨域工作,但不支持发送 cookie。对于 Java 应用程序来说,Cookie 通常是必不可少的。然而,由于 Sockjs 客户机可以用于许多服务器类型(而不仅仅是 Java 类型),因此它需要知道 Cookie 是否重要。如果是这样的话,Sockjs 客户机更喜欢 Ajax/XHR。否则,它依赖于一种基于 iframe 的技术。
来自 Sockjs 客户机的第一个`/info`请求是对可能影响客户机选择传输方式的信息的请求。这些细节之一是服务器应用程序是否依赖 Cookie(例如,出于身份验证目的或使用粘性会话进行集群)。 Spring 的 Sockjs 支持包括一个名为`sessionCookieNeeded`的属性。默认情况下,它是启用的,因为大多数 Java 应用程序依赖于`JSESSIONID`cookie。如果你的应用程序不需要它,你可以关闭此选项,然后 Sockjs 客户端应该在 IE8 和 IE9 中选择`xdr-streaming`。
如果确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应头`X-Frame-Options`设置为`DENY`、`SAMEORIGIN`或`ALLOW-FROM `,指示浏览器阻止在给定页面上使用 iframes。这是用来防止[点击劫持](https://www.owasp.org/index.php/Clickjacking)。
| |Spring Security3.2+ 为在每个
响应上设置`X-Frame-Options`提供了支持。默认情况下, Spring Security Java 配置将其设置为`DENY`。
在 3.2 中, Spring Security XML 命名空间默认情况下不设置该标头
,但可以配置为这样做。在将来,它可能会默认设置它。
有关如何配置[默认安全标头](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers)标题`X-Frame-Options`的设置的详细信息,请参见 Spring 安全文档的[默认安全标头](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers)。你还可以查看[SEC-2501](https://jira.spring.io/browse/SEC-2501)以获取更多背景信息。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
如果你的应用程序添加了`X-Frame-Options`响应报头(应该如此!)并依赖于基于 iframe 的传输,则需要将报头值设置为`SAMEORIGIN`或`ALLOW-FROM `。 Spring Sockjs 支持还需要知道 Sockjs 客户机的位置,因为它是从 iframe 加载的。默认情况下,iframe 被设置为从 CDN 位置下载 Sockjs 客户端。将此选项配置为使用来自与应用程序相同来源的 URL 是一个好主意。
下面的示例展示了如何在 Java 配置中实现这一点:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
```
XML 名称空间通过``元素提供了类似的选项。
| |在最初的开发过程中,启用 Sockjs 客户机`devel`模式,该模式可以防止
浏览器缓存原本会缓存
的 Sockjs 请求(如 iframe)。有关如何启用它的详细信息,请参见[Sockjs 客户端](https://github.com/sockjs/sockjs-client/)页面。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.3.4.心跳
Sockjs 协议要求服务器发送心跳消息,以防止代理得出连接已挂起的结论。 Spring Sockjs 配置具有一个名为`heartbeatTime`的属性,你可以使用该属性来定制频率。默认情况下,假设在该连接上没有发送其他消息,则会在 25 秒后发送心跳。对于公共互联网应用程序,这个 25 秒的值与下面的[IETF 推荐](https://tools.ietf.org/html/rfc6202)一致。
| |在使用 STOMP over WebSocket 和 Sockjs 时,如果 STOMP 客户机和服务器协商
要交换的心跳,则禁用 Sockjs 心跳。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------|
Spring Sockjs 支持还允许你配置`TaskScheduler`来调度心跳任务。任务计划程序由线程池支持,并根据可用处理器的数量进行默认设置。你应该考虑根据你的特定需求自定义设置。
#### 4.3.5.客户端断开连接
HTTP 流和 HTTP 长轮询 Sockjs 传输要求连接的打开时间比通常更长。有关这些技术的概述,请参见[这篇博文](https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/)。
在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该异步支持允许退出 Servlet 容器线程,处理请求,并继续写入来自另一个线程的响应。
一个具体的问题是, Servlet API 不为已经消失的客户机提供通知。见[eclipse-ee4j/servlet-api#44](https://github.com/eclipse-ee4j/servlet-api/issues/44)。然而, Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下为每 25 秒),这意味着通常会在该时间段内(如果发送消息的频率更高,则会更早)检测到客户端断开连接。
| |结果,由于客户端已断开连接,可能会发生网络 I/O 故障,而
会用不必要的堆栈跟踪来填充日志。 Spring 尽最大努力通过使用专用日志类别来标识
表示客户端断开连接(特定于每个服务器)和日志
的这样的网络故障,`DISCONNECTED_CLIENT_LOG_CATEGORY`(在`AbstractSockJsSession`中定义)。如果需要查看堆栈跟踪,可以将
日志类别设置为跟踪。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.3.6.Sockjs 和 CORS
如果允许跨源请求(参见[允许的来源](#websocket-server-allowed-origins)),那么 Sockjs 协议在 XHR 流和轮询传输中使用 CORS 提供跨域支持。因此,CORS 头是自动添加的,除非检测到响应中存在 CORS 头。因此,如果应用程序已经被配置为提供 CORS 支持(例如,通过 Servlet 过滤器),则 Spring 的`SockJsService`跳过了这一部分。
还可以通过在 Spring 的 SockjsService 中设置`suppressCors`属性来禁用这些 CORS 头的添加。
Sockjs 期望以下标题和值:
* `Access-Control-Allow-Origin`:从`Origin`请求头的值初始化。
* `Access-Control-Allow-Credentials`:总是设置为`true`。
* `Access-Control-Request-Headers`:从等效请求头的值初始化。
* `Access-Control-Allow-Methods`:传输支持的 HTTP 方法(参见`TransportType`枚举)。
* `Access-Control-Max-Age`:设置为 31536000(1 年)。
有关确切的实现,请参见`addCorsHeaders`中的`AbstractSockJsService`和源代码中的`TransportType`枚举。
或者,如果 CORS 配置允许,可以考虑排除具有 Sockjs 端点前缀的 URL,从而让 Spring 的`SockJsService`处理它。
#### 4.3.7.`SockJsClient`
Spring 提供了一种 Sockjs Java 客户端,以在不使用浏览器的情况下连接到远程 Sockjs 端点。当需要在公共网络上的两个服务器之间进行双向通信时(即,在网络代理可以排除使用 WebSocket 协议的情况下),这可能是特别有用的。对于测试目的(例如,模拟大量并发用户),Sockjs Java 客户机也非常有用。
Sockjs Java 客户端支持`websocket`、`xhr-streaming`和`xhr-polling`传输。剩下的那些只有在浏览器中使用才有意义。
你可以将`WebSocketTransport`配置为:
* `StandardWebSocketClient`在 JSR-356 运行时中。
* `JettyWebSocketClient`通过使用 Jetty 9+ 本机 WebSocket API。
* Spring 的`WebSocketClient`的任意实现。
根据定义,`XhrTransport`同时支持`xhr-streaming`和`xhr-polling`,因为从客户机的角度来看,除了用于连接到服务器的 URL 之外,没有其他区别。目前有两种实现方式:
* `RestTemplateXhrTransport`将 Spring 的`RestTemplate`用于 HTTP 请求。
* `JettyXhrTransport`将 Jetty 的`HttpClient`用于 HTTP 请求。
下面的示例展示了如何创建 Sockjs 客户机并连接到 Sockjs 端点:
```
List transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
```
| |Sockjs 使用 JSON 格式化的数组来处理消息。默认情况下,使用 Jackson2 并且需要
才能在 Classpath 上。或者,你可以配置`SockJsMessageCodec`的自定义实现,并在`SockJsClient`上配置它。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
要使用`SockJsClient`来模拟大量并发用户,你需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。下面的示例展示了如何使用 Jetty 来实现这一点:
```
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
```
下面的示例显示了你还应该考虑定制的服务器端 Sockjs 相关属性(详细信息请参见 Javadoc):
```
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
```
|**1**|将`streamBytesLimit`属性设置为 512KB(默认值为 128KB—`128 * 1024`)。|
|-----|-----------------------------------------------------------------------------------------------------|
|**2**|将`httpMessageCacheSize`属性设置为 1,000(默认值为`100`)。|
|**3**|将`disconnectDelay`属性设置为 30 个属性秒(默认值为 5 秒—`5 * 1000`)。|
### 4.4.跺脚
WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,用于客户端和服务器协商在 WebSocket 之上使用的子协议(即更高级别的消息传递协议)来定义各自可以发送什么样的消息、格式是什么、每个消息的内容等等。子协议的使用是可选的,但无论哪种方式,客户机和服务器都需要在定义消息内容的某些协议上达成一致。
#### 4.4.1.概述
[STOMP](https://stomp.github.io/stomp-specification-1.2.html#Abstract)(简单的文本消息传递协议)最初是为脚本语言(例如 Ruby、Python 和 Perl)创建的,用于连接到 Enterprise 消息代理。它旨在解决常用消息传递模式的最小子集。Stomp 可以在任何可靠的双向流网络协议上使用,例如 TCP 和 WebSocket。尽管 STOMP 是一种面向文本的协议,但消息负载可以是文本的,也可以是二进制的。
STOMP 是一种基于帧的协议,其帧是以 HTTP 为模型的。下面的清单显示了 Stomp 框架的结构:
```
COMMAND
header1:value1
header2:value2
Body^@
```
客户端可以使用`SEND`或`SUBSCRIBE`命令发送或订阅消息,以及一个`destination`头,该头描述消息的内容以及应该由谁接收。这启用了一个简单的发布-订阅机制,你可以使用该机制通过代理向其他连接的客户机发送消息,或者向服务器发送消息,以请求执行某些工作。
当你使用 Spring 的 STOMP 支持时, Spring WebSocket 应用程序充当客户的 STOMP 代理。消息被路由到`@Controller`消息处理方法或简单的内存代理,该代理跟踪订阅并向订阅的用户广播消息。还可以配置 Spring 来使用专用的 Stomp 代理(例如 RabbitMQ、ActiveMQ 和其他代理)来实际广播消息。在这种情况下, Spring 维护到代理的 TCP 连接,将消息中继到代理,并将消息从代理向下传递到已连接的 WebSocket 客户端。因此, Spring Web 应用程序可以依赖统一的基于 HTTP 的安全性、公共验证和熟悉的编程模型来进行消息处理。
下面的示例显示了订阅接收股票报价的客户机,服务器可能会定期发送该报价(例如,通过调度任务通过`SimpMessagingTemplate`向经纪人发送消息):
```
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
```
下面的示例显示了一个发送交易请求的客户机,服务器可以通过`@MessageMapping`方法处理该请求:
```
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
```
执行后,服务器可以向客户端广播交易确认消息和详细信息。
在 Stomp 规范中,目的地的含义是故意不透明的。它可以是任何字符串,完全由 Stomp 服务器来定义它们所支持的目标的语义和语法。然而,很常见的情况是,目标是类似路径的字符串,其中`/topic/..`表示发布-订阅(一对多),而`/queue/`表示点对点(一对一)消息交换。
Stomp 服务器可以使用`MESSAGE`命令向所有订阅者广播消息。下面的示例显示了一个服务器,该服务器将股票报价发送到一个已订阅的客户端:
```
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
```
服务器不能发送未经请求的消息。来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的`subscription-id`头必须与客户端订阅的`id`头匹配。
前面的概述旨在提供对 STOMP 协议的最基本的理解。我们建议对[规格](https://stomp.github.io/stomp-specification-1.2.html)协议进行全面审查。
#### 4.4.2.福利
与使用原始 WebSockets 相比,使用 STOMP 作为子协议使得 Spring 框架和 Spring 安全性提供了更丰富的编程模型。关于 HTTP 相对于原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,也可以提出同样的观点。以下是一系列好处:
* 无需发明定制的消息传递协议和消息格式。
* 在 Spring 框架中,包括[Java 客户端](#websocket-stomp-client)在内的 STOMP 客户机是可用的。
* 你可以(可选地)使用消息代理(例如 RabbitMQ、ActiveMQ 和其他代理)来管理订阅和广播消息。
* 应用程序逻辑可以在任意数量的`@Controller`实例中进行组织,并且可以基于 stomp 目标头将消息路由到它们,而不是针对给定连接使用单个`WebSocketHandler`处理原始消息。
* Spring 可以使用安全性来保护基于 STOMP 目的地和消息类型的消息。
#### 4.4.3.启用 Stomp
WebSocket 支持在`spring-messaging`和`spring-websocket`模块中可用。一旦有了这些依赖关系,就可以使用[Sockjs 后援](#websocket-fallback)在 WebSocket 上公开 Stomp 端点,如下例所示:
```
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS(); (1)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app"); (2)
config.enableSimpleBroker("/topic", "/queue"); (3)
}
}
```
|**1**|`/portfolio`是 WebSocket(或 Sockjs)
客户端为 WebSocket 握手需要连接到的端点的 HTTP URL。|
|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|目标头以`/app`开头的 stomp 消息被路由到`@MessageMapping`类中的`@Controller`方法。|
|**3**|使用内置的消息代理进行订阅和广播,并将目标头以`/topic `或`/queue`开头的消息路由到代理。|
下面的示例展示了与前面示例类似的 XML 配置:
```
```
| |对于内置的简单代理,`/topic`和`/queue`前缀没有任何特殊的
含义。它们仅仅是区分发布订阅和点对点
消息传递(即多个订阅者和一个消费者)的一种约定。当你使用外部代理时,
检查代理的 stomp 页面,以了解它支持什么样的 stomp 目的地和
前缀。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
要从浏览器连接,对于 Sockjs,你可以使用[`sockjs-client`](https://github.com/sockjs/sockjs-client)。对于 Stomp,许多应用程序使用[jmesnil/stomp-websocket](https://github.com/jmesnil/stomp-websocket)库(也称为 stomp.js),它是功能完备的,已经在生产中使用了多年,但不再维护。目前,[JSteunou/WebStomp-客户端](https://github.com/JSteunou/webstomp-client)是该库最活跃的维护和不断发展的后继库。下面的示例代码是基于它的:
```
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
```
或者,如果你通过 WebSocket(不使用 Sockjs)进行连接,则可以使用以下代码:
```
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
```
注意,在前面的示例中`stompClient`不需要指定`login`和`passcode`头。即使这样做了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参见[连接到代理](#websocket-stomp-handle-broker-relay-configure)和[认证](#websocket-stomp-authentication)。
有关更多示例代码,请参见:
* [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/)——入门指南。
* [股票投资组合](https://github.com/rstoyanchev/spring-websocket-portfolio)—一个示例应用程序。
#### 4.4.4. WebSocket 服务器
要配置底层 WebSocket 服务器,应用[服务器配置](#websocket-server-runtime-configuration)中的信息。然而,对于 Jetty,你需要通过`StompEndpointRegistry`设置`HandshakeHandler`和`WebSocketPolicy`:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
```
#### 4.4.5.消息流
一旦公开了一个 STOMP 端点, Spring 应用程序就成为连接客户端的 STOMP 代理。本节描述服务器端的消息流。
`spring-messaging`模块包含对起源于[Spring Integration](https://spring.io/spring-integration)的消息传递应用程序的基本支持,该支持后来被提取并合并到 Spring 框架中,以便在许多[Spring projects](https://spring.io/projects)和应用程序场景中更广泛地使用。下面的列表简要描述了一些可用的消息传递抽象:
* [Message](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/Message.html):消息的简单表示,包括消息头和有效负载。
* [MessageHandler](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageHandler.html):用于处理消息的契约。
* [MessageChannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageChannel.html):用于发送消息的契约,该消息允许在生产者和消费者之间进行松散耦合。
* [下标 bablechannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/SubscribableChannel.html):`MessageChannel`与`MessageHandler`订阅者。
* [执行者下标 bablechannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/support/ExecutorSubscribableChannel.html):`SubscribableChannel`使用`Executor`传递消息。
Java 配置(即`@EnableWebSocketMessageBroker`)和 XML 名称空间配置(即``)都使用前面的组件来组装消息工作流。下图显示了启用简单的内置消息代理时使用的组件:
![消息流简单代理](images/message-flow-simple-broker.png)
前面的图表显示了三个消息通道:
* `clientInboundChannel`:用于传递从 WebSocket 客户端接收的消息。
* `clientOutboundChannel`:用于向 WebSocket 客户端发送服务器消息。
* `brokerChannel`:用于从服务器端应用程序代码中向 Message Broker 发送消息。
下一个关系图显示了当外部代理(例如 RabbitMQ)被配置为管理订阅和广播消息时所使用的组件:
![消息流代理中继](images/message-flow-broker-relay.png)
前面两个图之间的主要区别是使用“代理中继”通过 TCP 将消息传递到外部的 Stomp 代理,并将消息从代理传递到订阅的客户机。
当接收到来自 WebSocket 连接的消息时,将它们解码为 Stomp 帧,转换为 Spring `Message`表示,并将其发送到`clientInboundChannel`以进行进一步的处理。例如,目标标头以`/app`开头的 stomp 消息可以路由到带注释的控制器中的`@MessageMapping`方法,而`/topic`和`/queue`消息可以直接路由到消息代理。
处理来自客户端的 stomp 消息的带注释的`@Controller`可以通过`brokerChannel`向消息代理发送消息,并且代理通过`clientOutboundChannel`将消息广播给匹配的订阅者。相同的控制器也可以对 HTTP 请求做出相同的响应,因此客户端可以执行 HTTP POST,然后使用`@PostMapping`方法向消息代理发送消息,以将消息广播到订阅的客户端。
我们可以通过一个简单的例子来追踪这个流程。考虑以下设置服务器的示例:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}
```
前面的示例支持以下流程:
1. 客户机连接到`[http://localhost:8080/portfolio](http://localhost:8080/portfolio)`,并且,一旦建立了 WebSocket 连接,Stomp 帧就开始在其上流动。
2. 客户机发送一个订阅帧,其目的标头为`/topic/greeting`。一旦接收并解码,消息将被发送到`clientInboundChannel`,然后路由到消息代理,该代理存储客户端订阅。
3. 客户端将发送帧发送到`/app/greeting`。`/app`前缀有助于将其路由到带注释的控制器。在去掉`/app`前缀之后,剩余的`/greeting`部分目标被映射到`@MessageMapping`中的`@MessageMapping`方法。
4. 从`GreetingController`返回的值被转换为 Spring `Message`,其有效负载基于返回值和默认的目的标头`/topic/greeting`(派生自输入目的标头,由`/app`替换为`/topic`)。生成的消息被发送到`brokerChannel`,并由消息代理处理。
5. 消息代理找到所有匹配的订阅者,并通过`clientOutboundChannel`向每个订阅者发送消息帧,从这里消息被编码为 Stomp 帧并在 WebSocket 连接上发送。
下一节将提供更多有关带注释方法的详细信息,包括所支持的参数和返回值的类型。
#### 4.4.6.带注释的控制器
应用程序可以使用带注释的`@Controller`类来处理来自客户端的消息。这样的类可以声明`@MessageMapping`、`@SubscribeMapping`和`@ExceptionHandler`方法,如以下主题中所述:
* [`@MessageMapping`](# WebSocket-stomp-message-mapping)
* [`@SubscribeMapping`](# WebSocket-stomp-subscribe-mapping)
* [`@MessageExceptionHandler`](# WebSocket-stomp-exception-handler)
##### `@MessageMapping`
你可以使用`@MessageMapping`对基于目的地路由消息的方法进行注释。它在方法级和类型级都受到支持。在类型级别,`@MessageMapping`用于表示控制器中所有方法之间的共享映射。
默认情况下,映射值是 Ant 样式的路径模式(例如`/thing*`,`/thing/**`),包括对模板变量的支持(例如,`/thing/{id}`)。这些值可以通过`@DestinationVariable`方法参数进行引用。应用程序还可以切换到用于映射的以点分隔的目标约定,如[作为分隔器的点](#websocket-stomp-destination-separator)中所解释的那样。
###### 支持的方法参数
下表描述了方法参数:
| Method argument |说明|
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Message` |以获取完整的消息。|
| `MessageHeaders` |用于访问`Message`中的标题。|
|`MessageHeaderAccessor`, `SimpMessageHeaderAccessor`, and `StompHeaderAccessor`|用于通过类型化访问器方法访问标头。|
| `@Payload` |为了访问消息的有效负载,通过配置的`MessageConverter`进行转换(例如,从 JSON),
不需要存在此注释,因为默认情况下是这样,假设没有
其他参数匹配。
你可以用`@javax.validation.Valid`或 Spring 的`@Validated`、
对有效负载参数进行注释,以使有效负载参数被自动验证。|
| `@Header` |用于访问特定的标头值——如果有必要,还可以使用`org.springframework.core.convert.converter.Converter`进行类型转换。|
| `@Headers` |用于访问消息中的所有头。此参数必须可分配给`java.util.Map`。|
| `@DestinationVariable` |用于访问从消息目标提取的模板变量。
值根据需要转换为声明的方法参数类型。|
| `java.security.Principal` |反映在 WebSocket HTTP 握手时登录的用户。|
###### 返回值
默认情况下,来自`@MessageMapping`方法的返回值通过匹配的`MessageConverter`序列化到有效负载,并作为`Message`发送到`brokerChannel`,从那里向订阅者广播。出站消息的目的地与入站消息的目的地相同,但前缀为`/topic`。
你可以使用`@SendTo`和`@SendToUser`注释来定制输出消息的目标。`@SendTo`用于自定义目标目的地或指定多个目的地。`@SendToUser`用于将输出消息引导到仅与输入消息相关联的用户。见[用户目的地](#websocket-stomp-user-destination)。
你可以在同一个方法上同时使用`@SendTo`和`@SendToUser`,并且这两种方法在类级别上都是受支持的,在这种情况下,它们充当类中方法的默认值。但是,请记住,任何方法级别的`@SendTo`或`@SendToUser`注释都会覆盖类级别的任何此类注释。
消息可以异步处理,并且`@MessageMapping`方法可以返回`ListenableFuture`、`CompletableFuture`或`CompletionStage`。
请注意,`@SendTo`和`@SendToUser`仅仅是一种方便,相当于使用`SimpMessagingTemplate`来发送消息。如果有必要,对于更高级的场景,`@MessageMapping`方法可以直接使用`SimpMessagingTemplate`。可以这样做,而不是返回一个值,或者可能是另外返回一个值。见[发送消息](#websocket-stomp-handle-send)。
##### `@SubscribeMapping`
`@SubscribeMapping`类似于`@MessageMapping`,但仅将映射范围缩小到订阅消息。它支持与[方法参数](#websocket-stomp-message-mapping)相同的`@MessageMapping`。但是对于返回值,默认情况下,消息是直接发送到客户机的(通过`clientOutboundChannel`,响应订阅),而不是发送到代理的(通过`brokerChannel`,作为对匹配订阅的广播)。添加`@SendTo`或`@SendToUser`将重写此行为并将其发送给代理。
这个什么时候有用?假设代理被映射到`/topic`和`/queue`,而应用程序控制器被映射到`/app`。在此设置中,代理存储所有用于重复广播的`/topic`和`/queue`的订阅,并且不需要应用程序参与其中。客户机还可以订阅某些`/app`目标,并且控制器可以响应该订阅返回一个值,而不涉及代理,而无需存储或再次使用订阅(实际上是一次性的请求-回复交换)。这样做的一个用例是在启动时用初始数据填充 UI。
这什么时候没用?不要尝试将代理和控制器映射到相同的目标前缀,除非出于某种原因希望两者独立处理消息(包括订阅)。入站消息是并行处理的。不能保证代理或控制器是否首先处理给定的消息。如果目标是在订阅被存储并准备好广播时得到通知,那么如果服务器支持该订阅,客户端应该要求提供收据(Simple Broker 不支持)。例如,使用 Java[STOMP 客户端](#websocket-stomp-client),你可以执行以下操作来添加收据:
```
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});
```
在`brokerChannel`上的服务器端选项是[要注册](#websocket-stomp-interceptors)和`ExecutorChannelInterceptor`,并实现`afterMessageHandled`方法,该方法在处理完消息(包括订阅)后调用。
##### `@MessageExceptionHandler`
应用程序可以使用`@MessageExceptionHandler`方法来处理来自`@MessageMapping`方法的异常。如果希望访问异常实例,可以在注释本身中声明异常,也可以通过方法参数声明异常。下面的示例通过方法参数声明异常:
```
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
```
`@MessageExceptionHandler`方法支持灵活的方法签名,并支持与[`@MessageMapping`](# WebSocket-stomp-message-mapping)方法相同的方法参数类型和返回值。
通常,`@MessageExceptionHandler`方法应用于声明它们的`@Controller`类(或类层次结构)中。如果你希望这样的方法更多地全局应用(跨控制器),那么可以在标记为`@ControllerAdvice`的类中声明它们。这类似于 Spring MVC 中可用的[类似的支持](#mvc-ann-controller-advice)。
#### 4.4.7.发送消息
如果你想要从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向`brokerChannel`发送消息。这样做的最简单的方法是注入`SimpMessagingTemplate`并使用它发送消息。通常,你将按类型注入它,如下例所示:
```
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
```
但是,如果存在另一个相同类型的 Bean,你也可以通过它的名称(`brokerMessagingTemplate`)来限定它。
#### 4.4.8.简单经纪人
内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。代理支持类似路径的目标,包括对 Ant 风格的目标模式的订阅。
| |应用程序也可以使用点分隔(而不是斜杠分隔)的目的地。
参见[作为分隔器的点](#websocket-stomp-destination-separator)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------|
如果配置了任务调度程序,那么简单代理支持[跺脚心跳](https://stomp.github.io/stomp-specification-1.2.html#Heart-beating)。要配置计划程序,你可以声明自己的`TaskScheduler` Bean,并通过`MessageBrokerRegistry`对其进行设置。或者,你可以使用在内置 WebSocket 配置中自动声明的配置,但是,你需要`@Lazy`来避免在内置 WebSocket 配置和`WebSocketMessageBrokerConfigurer`之间的循环。例如:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private TaskScheduler messageBrokerTaskScheduler;
@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
// ...
}
}
```
#### 4.4.9.外部经纪人
Simple Broker 非常适合入门,但只支持一组 STOMP 命令(它不支持 ACK、Receipts 和其他一些特性),依赖于一个简单的消息发送循环,并且不适合集群。作为替代方案,你可以升级应用程序以使用功能齐全的消息代理。
查看选择的消息代理的 STOMP 文档(例如[RabbitMQ](https://www.rabbitmq.com/stomp.html),[ActiveMQ](https://activemq.apache.org/stomp.html)等),安装代理,并在启用了 STOMP 支持的情况下运行它。然后,你可以在 Spring 配置中启用 Stomp 代理中继(而不是简单的代理)。
下面的示例配置启用了功能齐全的代理:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
在前面的配置中,Stomp 代理中继是一个 Spring[`MessageHandler`](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/springframework/messing/messagehandler.html),它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的 TCP 连接,将所有消息转发给它,然后将从代理收到的所有消息通过其 WebSocket 会话转发给客户机。从本质上讲,它充当了双向转发消息的“中继”。
| |为 TCP 连接管理将`io.projectreactor.netty:reactor-netty`和`io.netty:netty-all`依赖项添加到项目中。|
|---|-------------------------------------------------------------------------------------------------------------------------------|
此外,应用程序组件(例如 HTTP 请求处理方法、业务服务和其他)也可以向代理中继发送消息,如[发送消息](#websocket-stomp-handle-send)中所述,以将消息广播到已订阅的客户端 WebSocket。
实际上,代理中继支持健壮和可伸缩的消息广播。
#### 4.4.10.连接到代理
Stomp 代理中继维护与代理的单个“系统”TCP 连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。可以为此连接配置 STOMP 凭据(即 STOMP 框架`login`和`passcode`标头)。这在 XML 名称空间和 Java 配置中都公开为`systemLogin`和`systemPasscode`属性,其默认值为`guest`和`guest`。
Stomp 代理中继还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接。你可以配置用于代表客户机创建的所有 TCP 连接的 STOMP 凭据。这在 XML 名称空间和 Java 配置中都公开为`clientLogin`和`clientPasscode`属性,其默认值为`guest`和`guest`。
| |Stomp 代理中继总是在它代表客户转发给代理的每个`CONNECT`框架上设置`login`和`passcode`头。因此, WebSocket 客户机
不需要设置那些头。他们被忽视了。正如[认证](#websocket-stomp-authentication)部分所解释的那样, WebSocket 客户端应该依赖 HTTP 身份验证来保护
WebSocket 端点并建立客户端标识。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Stomp 代理中继还通过“System”TCP 连接向消息代理发送和接收来自消息代理的心跳。你可以配置发送和接收心跳的间隔(默认情况下各为 10 秒)。如果失去了与代理的连接,代理中继将继续尝试每 5 秒重新连接一次,直到成功。
任何 Spring Bean 都可以实现`ApplicationListener`,以在与代理的“系统”连接丢失并重新建立时接收通知。例如,当没有活动的“系统”连接时,广播股票报价的股票报价服务可以停止尝试发送消息。
默认情况下,STOMP 代理中继总是连接到相同的主机和端口,如果连接丢失,则根据需要重新连接。如果你希望提供多个地址,那么在每次尝试连接时,你可以配置一个地址供应商,而不是一个固定的主机和端口。下面的示例展示了如何做到这一点:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
private ReactorNettyTcpClient createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}
```
你还可以使用`virtualHost`属性配置 Stomp 代理中继。此属性的值被设置为每个`CONNECT`帧的`host`头,并且可以是有用的(例如,在云环境中,其中建立 TCP 连接的实际主机与提供基于云的 Stomp 服务的主机不同)。
#### 4.4.11.作为分隔器的点
当消息路由到`@MessageMapping`方法时,它们将与`AntPathMatcher`进行匹配。默认情况下,模式应该使用斜杠(`/`)作为分隔符。这是 Web 应用程序中的一种很好的约定,类似于 HTTP URL。但是,如果你更习惯于消息传递约定,则可以切换到使用 dot(`.`)作为分隔符。
下面的示例展示了如何在 Java 配置中实现这一点:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
在此之后,控制器可以在`@MessageMapping`方法中使用一个点(`.`)作为分隔符,如下例所示:
```
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
```
客户机现在可以向`/app/red.blue.green123`发送消息。
在前面的示例中,我们没有更改“代理中继”的前缀,因为这些前缀完全依赖于外部消息代理。请参阅你使用的代理的 STOMP 文档页,以了解它对目标头支持哪些约定。
另一方面,“简单代理”确实依赖于配置的`PathMatcher`,因此,如果你切换分隔符,该更改也适用于代理以及代理从消息到订阅模式的目标匹配方式。
#### 4.4.12.认证
在 WebSocket 消息传递会话中的每一次重击都是从一个 HTTP 请求开始的。这可以是一个升级到 WebSockets 的请求(即 WebSocket 次握手),或者在 Sockjs 回退的情况下,是一系列 Sockjs HTTP 传输请求。
许多 Web 应用程序已经具有适当的身份验证和授权,以保护 HTTP 请求。通常,通过使用诸如登录页面、HTTP 基本身份验证或另一种方式的某种机制,通过 Spring 安全性对用户进行身份验证。经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一基于 Cookie 的会话中的后续请求相关联。
因此,对于 WebSocket 握手或对于 Sockjs HTTP 传输请求,通常已经存在通过`HttpServletRequest#getUserPrincipal()`可访问的经过身份验证的用户。 Spring 自动地将该用户与为他们创建的 WebSocket 或 Sockjs 会话相关联,随后,与通过该会话通过用户头传输的所有 Stomp 消息相关联。
简而言之,一个典型的 Web 应用程序只需要做它在安全性方面已经做过的事情。通过基于 Cookie 的 HTTP 会话(该会话随后与为该用户创建的 WebSocket 或 Sockjs 会话相关联)维护安全上下文,在 HTTP 请求级别上对用户进行身份验证,并在流经该应用程序的每个`Message`上标记一个用户标头。
在`CONNECT`框架上,Stomp 协议确实有`login`和`passcode`头。它们最初是为 TCP 上的 Stomp 而设计的,现在也需要这样做。然而,对于 STOMP over WebSocket,默认情况下, Spring 忽略了 STOMP 协议级别上的身份验证头,并假定用户已经在 HTTP 传输级别上进行了身份验证。期望 WebSocket 或 Sockjs 会话包含经过身份验证的用户。
#### 4.4.13.令牌认证
[Spring Security OAuth](https://github.com/spring-projects/spring-security-oauth)提供了对基于令牌的安全性的支持,包括 JSON Web 令牌。你可以将其用作 Web 应用程序中的身份验证机制,包括对 WebSocket 交互的 stomp,如上一节所述(即,通过基于 cookie 的会话来维护身份)。
同时,基于 Cookie 的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中,或者在通常使用头进行身份验证的移动应用程序中)。
[WebSocket protocol, RFC 6455](https://tools.ietf.org/html/rfc6455#section-10.5)“并没有规定服务器可以在 WebSocket 握手过程中对客户端进行身份验证的任何特定方式。”然而,在实践中,浏览器客户机只能使用标准的身份验证头(即基本的 HTTP 身份验证)或 Cookie,并且不能(例如)提供自定义的头。同样,Sockjs JavaScript 客户机也不提供一种发送带有 Sockjs 传输请求的 HTTP 头的方法。见[Sockjs-客户端第 196 期](https://github.com/sockjs/sockjs-client/issues/196)。相反,它确实允许发送查询参数,你可以使用这些参数来发送令牌,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。
| |上述限制适用于基于浏览器的客户机,并且不适用于
Spring 基于 Java 的 Stomp 客户机,该客户机确实支持发送带有
WebSocket 和 Sockjs 请求的头。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
因此,希望避免使用 Cookie 的应用程序可能没有任何好的 HTTP 协议级别的身份验证替代方案。与其使用 Cookie,他们可能更喜欢在 Stomp 消息传递协议级别使用头进行身份验证。这样做需要两个简单的步骤:
1. 使用 STOMP 客户机在连接时传递身份验证头。
2. 用`ChannelInterceptor`处理身份验证头。
下一个示例使用服务器端配置来注册自定义身份验证拦截器。请注意,拦截器只需要验证和设置 Connect`Message`上的用户头。 Spring 记录并保存经过身份验证的用户,并将其与相同会话上的后续 Stomp 消息关联。下面的示例展示了如何注册自定义身份验证拦截器:
```
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message> preSend(Message> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}
```
另外,请注意,当你对消息使用 Spring Security 的授权时,目前,你需要确保身份验证`ChannelInterceptor`配置是在 Spring Security 的授权之前进行的。最好的方法是在其自己的`WebSocketMessageBrokerConfigurer`实现中声明自定义拦截器,该实现被标记为`@Order(Ordered.HIGHEST_PRECEDENCE + 99)`。
#### 4.4.14.授权
Spring 安全性提供了[WebSocket sub-protocol authorization](https://docs.spring.io/spring-security/reference/servlet/integrations/websocket.html#websocket-authorization),其使用`ChannelInterceptor`基于其中的用户头来授权消息。另外, Spring 会话提供了[WebSocket integration](https://docs.spring.io/spring-session/reference/web-socket.html),以确保在 WebSocket 会话仍然处于活动状态时用户的 HTTP 会话不会过期。
#### 4.4.15.用户目的地
应用程序可以发送针对特定用户的消息, Spring 的 STOMP 支持为此目的识别带有`/user/`前缀的目的地。例如,客户端可能订阅`/user/queue/position-updates`目的地。`UserDestinationMessageHandler`处理此目的地并将其转换为用户会话所独有的目的地(例如`/queue/position-updates-user123`)。这提供了订阅一个通用命名的目的地的便利,同时,确保不与订阅相同目的地的其他用户发生冲突,以便每个用户都可以接收唯一的股票位置更新。
| |在使用用户目标时,配置代理和
应用程序目标前缀是很重要的,如[启用 Stomp](#websocket-stomp-enable)中所示,否则
代理将处理“/user”前缀消息,这些消息只应由`UserDestinationMessageHandler`处理。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
在发送端,消息可以被发送到诸如`/user/{username}/queue/position-updates`的目的地,这反过来又通过`UserDestinationMessageHandler`被翻译成一个或多个目的地,一个针对每个会话与用户相关联。这使得应用程序中的任何组件都可以发送针对特定用户的消息,而不必知道他们的名字和通用目的地以外的任何信息。这也通过注释和消息传递模板得到了支持。
一种消息处理方法可以通过`@SendToUser`注释(也支持在类级上共享一个公共目的地)向与正在处理的消息相关联的用户发送消息,如下例所示:
```
@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}
```
如果用户有一个以上的会话,默认情况下,目标用户是订阅给定目标的所有会话。然而,有时可能需要只针对发送要处理的消息的会话。可以通过将`broadcast`属性设置为 false 来实现此目的,如下例所示:
```
@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}
```
| |虽然用户目的地通常意味着经过身份验证的用户,但并不是严格要求的。
与经过身份验证的用户不关联的 WebSocket 会话
可以订阅用户目的地。在这种情况下,`@SendToUser`注释
的行为与`broadcast=false`完全相同(即仅针对发送正在处理的消息的
会话)。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
你可以通过注入由 Java 配置或 XML 命名空间创建的`SimpMessagingTemplate`,从任何应用程序组件向用户目的地发送消息。(如果使用`@Qualifier`进行限定,则 Bean 名称为`brokerMessagingTemplate`。)下面的示例展示了如何这样做:
```
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
```
| |当你使用带有外部消息代理的用户目的地时,你应该检查代理
关于如何管理非活动队列的文档,这样,当用户会话
结束时,所有唯一的用户队列都将被删除。例如,当你使用诸如`/exchange/amq.direct/position-updates`之类的目标时,RabbitMQ 会创建自动删除
队列。
因此,在这种情况下,客户端可以订阅`/user/exchange/amq.direct/position-updates`。
类似地,ActiveMQ 也有[配置选项](https://activemq.apache.org/delete-inactive-destinations.html)用于清除不活动的目标。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
在多应用服务器场景中,由于用户连接到不同的服务器,用户目的地可能仍未解决。在这种情况下,你可以配置一个目标来广播未解决的消息,以便其他服务器有机会尝试。这可以通过 Java 配置中`MessageBrokerRegistry`的`userDestinationBroadcast`属性和 XML 中`message-broker`元素的`user-destination-broadcast`属性来完成。
#### 4.4.16.消息顺序
来自代理的消息被发布到`clientOutboundChannel`,从那里它们被写到 WebSocket 会话。由于通道由`ThreadPoolExecutor`支持,消息在不同的线程中进行处理,客户端接收的结果序列可能与发布的确切顺序不匹配。
如果这是一个问题,请启用`setPreservePublishOrder`标志,如下例所示:
```
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
设置该标志后,同一客户机会话中的消息将一次发布到`clientOutboundChannel`,以保证发布的顺序。请注意,这会带来很小的性能开销,因此你应该仅在需要时才启用它。
#### 4.4.17.事件
发布了几个`ApplicationContext`事件,并且可以通过实现 Spring 的`ApplicationListener`接口来接收这些事件:
* `BrokerAvailabilityEvent`:表示代理何时变得可用或不可用。虽然“简单”代理在启动时立即可用,并且在应用程序运行时仍然可用,但 STOMP“代理中继”可能会失去与功能齐全的代理的连接(例如,如果代理被重新启动)。代理中继具有重新连接逻辑,并在代理恢复时重新建立与代理的“系统”连接。因此,每当状态从连接变为断开时,此事件就会发布,反之亦然。使用`SimpMessagingTemplate`的组件应该订阅此事件,并避免在代理不可用时发送消息。在任何情况下,他们都应该准备好在发送消息时处理`MessageDeliveryException`。
* `SessionConnectEvent`:当接收到新的 Stomp 连接时发布,以表示新客户端会话的开始。该事件包含表示连接的消息,包括会话 ID、用户信息(如果有的话)以及客户端发送的任何自定义标头。这对于跟踪客户端会话非常有用。订阅此事件的组件可以用`SimpMessageHeaderAccessor`或`StompMessageHeaderAccessor`包装所包含的消息。
* `SessionConnectedEvent`:在`SessionConnectEvent`之后不久发布,此时代理已经发送了一个 Stomp Connected 帧来响应该连接。在这一点上,Stomp 会话可以被认为是完全成立的。
* `SessionSubscribeEvent`:在接收到新的 Stomp 订阅时发布。
* `SessionUnsubscribeEvent`:当收到新的 stomp 退订时发布。
* `SessionDisconnectEvent`:在 stomp 会话结束时发布。断开连接可以是已经从客户端发送的,也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,此事件在每个会话中发布不止一次。对于多个断开事件,组件应该是幂等的。
| |当你使用功能齐全的代理时,如果代理暂时不可用,则 STOMP“代理中继”会自动重新连接
“系统”连接。但是,
客户端连接不会自动重新连接。假设启用了心跳,客户机
通常会注意到代理在 10 秒内没有响应。客户端需要
实现自己的重新连接逻辑。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.4.18.拦截
[Events](#websocket-stomp-appplication-context-events)为 stomp 连接的生命周期提供通知,但不是为每个客户机消息提供通知。应用程序还可以注册一个`ChannelInterceptor`来拦截任何消息和处理链的任何部分。下面的示例展示了如何截获来自客户端的入站消息:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}
```
自定义`ChannelInterceptor`可以使用`StompHeaderAccessor`或`SimpMessageHeaderAccessor`来访问有关消息的信息,如下例所示:
```
public class MyChannelInterceptor implements ChannelInterceptor {
@Override
public Message> preSend(Message> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getStompCommand();
// ...
return message;
}
}
```
应用程序还可以实现`ExecutorChannelInterceptor`,这是`ChannelInterceptor`的子接口,在处理消息的线程中具有回调。对于发送到通道的每条消息,都会调用一次`ChannelInterceptor`,而`ExecutorChannelInterceptor`在每个订阅了来自通道的消息的`MessageHandler`的线程中提供钩子。
注意,与前面描述的`SessionDisconnectEvent`一样,断开连接消息可以是来自客户端的,或者也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开事件,组件应该是幂等的。
#### 4.4.19.STOMP 客户端
Spring 提供了在 WebSocket 客户端上的 stomp 和在 TCP 客户端上的 stomp。
首先,你可以创建和配置`WebSocketStompClient`,如下例所示:
```
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
```
在前面的示例中,你可以将`StandardWebSocketClient`替换为`SockJsClient`,因为这也是`WebSocketClient`的实现。`SockJsClient`可以使用 WebSocket 或基于 HTTP 的传输作为后备。有关更多详细信息,请参见[`SockJsClient`](# WebSocket-fallback-sockjs-client)。
接下来,你可以建立一个连接,并为 STOMP 会话提供一个处理程序,如下例所示:
```
String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);
```
当会话准备好使用时,将通知处理程序,如下例所示:
```
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
// ...
}
}
```
一旦建立了会话,就可以发送任何有效负载,并使用配置的`MessageConverter`进行序列化,如下例所示:
```
session.send("/topic/something", "payload");
```
你也可以订阅目的地。`subscribe`方法需要一个订阅消息的处理程序,并返回一个`Subscription`句柄,你可以使用它来取消订阅。对于每个接收到的消息,处理程序可以指定目标`Object`类型,有效负载应该反序列化到该类型,如下例所示:
```
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
```
要启用 Stomp heartbeat,可以使用`WebSocketStompClient`配置`TaskScheduler`并可选地自定义心跳间隔(10 秒用于写不活动,导致发送心跳;10 秒用于读不活动,关闭连接)。
`WebSocketStompClient`仅在不活动的情况下发送心跳,即没有发送其他消息时。当使用外部代理时,这可能会带来挑战,因为具有非代理目的地的消息表示活动,但 AREN 并未实际转发给代理。在这种情况下,你可以在初始化`TaskScheduler`时配置`TaskScheduler`,从而确保仅在发送具有非代理目的地的消息时也将心跳转发到代理。
| |当你使用`WebSocketStompClient`进行性能测试以模拟来自同一台机器的数千个
客户端时,请考虑关闭心跳,因为每个
连接都调度自己的心跳任务,而这并未针对
在同一台机器上运行的大量客户端进行优化。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
STOMP 协议还支持 Receipts,其中客户端必须添加`receipt`头,服务器在处理发送或订阅后用接收帧对其进行响应。为了支持这一点,`StompSession`提供了`setAutoReceipt(boolean)`,这会导致在随后的每个发送或订阅事件上添加`receipt`头。或者,你也可以手动将收据标题添加到`StompHeaders`中。send 和 subscribe 都返回`Receiptable`的实例,你可以使用该实例来注册接收成功和失败的回调。对于此功能,你必须为客户机配置`TaskScheduler`和收据过期前的时间(默认情况下为 15 秒)。
请注意,`StompSessionHandler`本身是`StompFrameHandler`,这使得它除了用于处理消息异常的`handleException`回调和用于处理包括`ConnectionLostException`在内的传输级别错误的`handleTransportError`回调外,还可以处理错误帧。
#### 4.4.20. WebSocket 范围
WebSocket 每个会话都有一个属性映射。映射作为头附加到入站客户端消息,并且可以从控制器方法访问,如以下示例所示:
```
@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map attrs = headerAccessor.getSessionAttributes();
// ...
}
}
```
你可以在`websocket`范围中声明一个 Spring 管理的 Bean。你可以将 WebSocket 范围的 bean 注入控制器和在`clientInboundChannel`上注册的任何通道拦截器。这些通常是单例,并且比任何单独的会话活得更长 WebSocket。因此,你需要对 WebSocket 范围的 bean 使用范围代理模式,如下例所示:
```
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@Controller
public class MyController {
private final MyBean myBean;
@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}
```
与任何自定义作用域一样, Spring 在第一次从控制器访问新的`MyBean`实例时初始化该实例,并将该实例存储在 WebSocket 会话属性中。随后将返回相同的实例,直到会话结束。 WebSocket-作用域 bean 具有调用的所有 Spring 生命周期方法,如前面的示例中所示。
#### 4.4.21.表现
谈到业绩,没有灵丹妙药。许多因素都会影响它,包括消息的大小和数量,应用程序方法是否执行需要阻塞的工作,以及外部因素(例如网络速度和其他问题)。本节的目标是提供可用配置选项的概述,以及关于如何推理缩放的一些想法。
在消息传递应用程序中,消息通过通道传递,以进行由线程池支持的异步执行。配置这样的应用程序需要对通道和消息流有很好的了解。因此,建议复习[消息流](#websocket-stomp-message-flow)。
显而易见的开始是配置线程池,这些线程池支持`clientInboundChannel`和`clientOutboundChannel`。默认情况下,这两个处理器的配置都是可用处理器数量的两倍。
如果在带注释的方法中处理消息主要是 CPU 绑定的,那么`clientInboundChannel`的线程数量应该与处理器数量保持接近。如果他们所做的工作更受 IO 约束,并且需要阻塞或等待数据库或其他外部系统,则线程池的大小可能需要增加。
| |`ThreadPoolExecutor`有三个重要的属性:核心线程池大小,
最大线程池大小,以及队列存储
没有可用线程的任务的能力。
一个常见的混淆之处是,配置核心池大小(例如,10)
和最大线程池大小(例如,20)会导致线程池中包含 10 到 20 个线程,实际上,如果将容量保持在其默认值 integer.max\_value,
,则线程池永远不会超过核心池大小而增加,由于
所有额外的任务都是排队的。
参见`ThreadPoolExecutor`的 Javadoc 来了解这些属性是如何工作的,以及
了解各种排队策略。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
在`clientOutboundChannel`方面,这完全是关于向 WebSocket 客户端发送消息。如果客户机在快速网络上,线程的数量应该与可用处理器的数量保持接近。如果它们速度较慢或带宽较低,则会花费更长的时间来消耗消息,并给线程池带来负担。因此,增加线程池的大小是必要的。
虽然`clientInboundChannel`的工作负载是可以预测的——毕竟,它是基于应用程序所做的工作——但如何配置“ClientoutboundChannel”比较困难,因为它基于应用程序无法控制的因素。因此,还有两个属性与消息的发送有关:`sendTimeLimit`和`sendBufferSizeLimit`。你可以使用这些方法来配置允许发送多长时间,以及在向客户机发送消息时可以缓冲多少数据。
一般的想法是,在任何给定的时间,只能使用单个线程发送到客户端。同时,所有附加的消息都会得到缓冲,你可以使用这些属性来决定允许发送消息需要多长时间,以及在此期间可以缓冲多少数据。有关重要的附加详细信息,请参见 XMLSchema 的 Javadoc 和文档。
下面的示例展示了一种可能的配置:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
还可以使用前面显示的 WebSocket 传输配置来配置传入的 STOMP 消息的最大允许大小。从理论上讲, WebSocket 条消息的大小几乎可以是无限的。在实践中, WebSocket 服务器施加了限制——例如,在 Tomcat 上施加 8K,在 Jetty 上施加 64K。出于这个原因,STOMP 客户机(例如 JavaScript[WebStomp-客户端](https://github.com/JSteunou/webstomp-client)和其他)在 16K 边界分割较大的 STOMP 消息,并将它们作为多个消息发送 WebSocket,这需要服务器进行缓冲和重新组装。
Spring 的 Stomp-over- WebSocket 支持做到了这一点,因此应用程序可以为 Stomp 消息配置最大大小,而与 WebSocket 服务器特定的消息大小无关。请记住, WebSocket 消息大小是自动调整的,如果需要的话,以确保它们能够至少携带 16k WebSocket 消息。
下面的示例展示了一种可能的配置:
```
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
```
下面的示例展示了与前面示例类似的 XML 配置:
```
```
关于扩展的一个要点是使用多个应用程序实例。目前,你无法使用简单的代理来实现这一点。然而,当使用全功能代理(例如 RabbitMQ)时,每个应用程序实例都连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的 WebSocket 客户端。
#### 4.4.22.监测
当使用`@EnableWebSocketMessageBroker`或``时,关键基础设施组件会自动收集统计信息和计数器,它们为应用程序的内部状态提供了重要的见解。该配置还声明了类型`WebSocketMessageBrokerStats`的 Bean,该类型在一个位置收集所有可用的信息,并且默认情况下每 30 分钟将其记录在`INFO`级别。这个 Bean 可以通过 Spring 的`MBeanExporter`导出到 JMX,以便在运行时查看(例如,通过 JDK 的`jconsole`)。以下清单概述了可用的信息:
客户端 WebSocket 会话
当前
指示当前有多少个客户端会话,计数进一步细分为 WebSocket 与 HTTP 流和 Polling Sockjs 会话的比较。
合计
指示总共建立了多少个会话。
异常关闭
连接失败
建立了会话,但在 60 秒内没有收到任何消息后关闭。这通常表示代理或网络问题。
超过发送限制
会话在超过配置的发送超时或发送缓冲区限制后关闭,这可能发生在客户端速度较慢的情况下(请参见上一节)。
传输错误
会话在传输错误之后关闭,例如未能读或写到 WebSocket 连接或 HTTP 请求或响应。
Stomp 框架
处理的连接帧、连接帧和断开帧的总数,表示在 STOMP 级别上连接了多少个客户端。请注意,当会话异常关闭或当客户端关闭而不发送断开连接帧时,断开连接计数可能会更低。
Stomp 经纪商接力
TCP 连接
指示代表客户端 WebSocket 会话向代理建立了多少 TCP 连接。这应该等于客户端 WebSocket 会话的数量 + 用于从应用程序内发送消息的 1 个额外的共享“系统”连接。
Stomp 框架
代表客户端转发给代理或从代理接收的连接、已连接和断开连接帧的总数。请注意,无论客户端 WebSocket 会话是如何关闭的,断开连接帧都会被发送到代理。因此,较低的断开帧计数表示代理正在主动关闭连接(可能是由于心跳没有及时到达,输入帧无效或其他问题)。
客户端入站通道
来自线程池的统计数据支持`clientInboundChannel`,这些统计数据提供了对传入消息处理的健康状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果存在 I/O 绑定任务(例如,缓慢的数据库查询、对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。
客户端出站通道
来自线程池的统计数据支持`clientOutboundChannel`,该线程池提供了对向客户广播消息的健康状况的深入了解。在这里排队等待的任务表明客户端太慢,无法使用消息。解决这个问题的一种方法是增加线程池大小,以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(请参见上一节)。
Sockjs 任务调度程序
来自用于发送心跳的 SockJS 任务计划程序的线程池的统计信息。请注意,当心跳在 Stomp 级别协商时,Sockjs 心跳将被禁用。
#### 4.4.23.测试
在使用 Spring 的 Stomp-over- WebSocket 支持时,有两种主要的方法来测试应用程序。第一种方法是编写服务器端测试,以验证控制器的功能及其带注释的消息处理方法。第二种方法是编写完整的端到端测试,其中涉及运行客户机和服务器。
这两种方法并不相互排斥。相反,它们在总体测试策略中都有一席之地。服务器端测试更加集中,并且更容易编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。
服务器端测试的最简单形式是编写控制器单元测试。然而,这是不够有用的,因为控制器所做的很大程度上取决于它的注释。纯粹的单元测试根本无法测试这一点。
理想情况下,被测控制器应该像在运行时那样被调用,这很像通过使用 Spring MVC 测试框架来测试处理 HTTP 请求的控制器的方法——也就是说,不运行 Servlet 容器,而是依赖 Spring 框架来调用带注释的控制器。与 Spring MVC 测试一样,这里有两种可能的选择,要么使用“基于上下文”的设置,要么使用“独立”的设置:
* 借助 Spring TestContext 框架加载实际的 Spring 配置,注入`clientInboundChannel`作为测试字段,并使用它发送要由控制器方法处理的消息。
* 手动设置调用控制器(即`SimpAnnotationMethodMessageHandler`)所需的最低 Spring 框架基础设施,并将控制器的消息直接传递给它。
这两种设置场景都在[股票投资组合的测试](https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web)示例应用程序中进行了演示。
第二种方法是创建端到端集成测试。为此,你需要以嵌入式模式运行一个 WebSocket 服务器,并将其作为一个 WebSocket 客户端连接到它,该客户端发送 WebSocket 包含 Stomp 帧的消息。[股票投资组合的测试](https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web)示例应用程序还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单的 Stomp 客户端来演示这种方法。
## 5. 其他 Web 框架
本章详细介绍了 Spring 与第三方 Web 框架的集成。
Spring 框架的核心价值主张之一是使 * 选择 * 成为可能。在一般意义上, Spring 并不强迫你使用或购买任何特定的架构、技术或方法(尽管它肯定会推荐一些而不是其他)。这种选择与开发人员及其开发团队最相关的架构、技术或方法的自由,可以说在 Web 领域最为明显, Spring 在该领域提供了自己的 Web 框架([Spring MVC](#mvc)和[Spring WebFlux](webflux.html#webflux)),同时,支持与许多流行的第三方 Web 框架的集成。
### 5.1.公共配置
在深入了解每个受支持的 Web 框架的集成细节之前,让我们先来看看不特定于任何一个 Web 框架的常见配置 Spring。(本节同样适用于 Spring 自己的 Web 框架变体。)
Spring 的轻量级应用程序模型支持的概念之一(因为没有更好的词)是分层架构。请记住,在一个“经典”的分层架构中,Web 层只是许多层中的一个。它充当服务器端应用程序的入口点之一,并将其委托给在服务层中定义的服务对象(Facades),以满足特定于业务(和表示技术无关)的用例。在 Spring 中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象存在于不同的“业务上下文”中,其中不包含 Web 或表示层对象(表示对象,例如 Spring MVC 控制器,通常配置在不同的“表示上下文”中)。本节详细介绍了如何配置包含应用程序中所有“business bean”的 Spring 容器(a`WebApplicationContext`)。
接下来讨论细节,你所需要做的就是在标准的 Java EE Servlet `web.xml`文件中声明一个[`ContextLoaderListener`(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/contextloaderlistener.html),并添加一个`contextConfigLocation`节(在同一文件中),该节定义要加载的哪组 XML 配置文件 Spring。
考虑以下``配置:
```
org.springframework.web.context.ContextLoaderListener
```
进一步考虑以下``配置:
```
contextConfigLocation
/WEB-INF/applicationContext*.xml
```
如果没有指定`contextConfigLocation`上下文参数,则`ContextLoaderListener`查找要加载的名为`/WEB-INF/applicationContext.xml`的文件。一旦加载了上下文文件, Spring 将基于 Bean 定义创建一个[`WebApplicationContext`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/webapplicationcontext.html)对象,并将其存储在 Web 应用程序的`ServletContext`中。
所有 Java Web 框架都建立在 Servlet API 之上,因此你可以使用以下代码片段来访问由`ApplicationContext`创建的“业务上下文”`ContextLoaderListener`。
下面的示例展示了如何获得`WebApplicationContext`:
```
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
```
[`WebApplicationContextUtils`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/support/webapplicationcontextutils.html)类是为了方便,所以你不需要记住`ServletContext`属性的名称。如果在`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`键下不存在对象,则其`getWebApplicationContext()`方法返回`null`。与其冒险在应用程序中获得`NullPointerExceptions`,不如使用`getRequiredWebApplicationContext()`方法。当`ApplicationContext`丢失时,此方法抛出一个异常。
一旦有了对`WebApplicationContext`的引用,就可以根据 bean 的名称或类型检索 bean。大多数开发人员按名称检索 bean,然后将它们强制转换到其实现的接口之一。
幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法。它们不仅使从 Spring 容器获得 bean 变得容易,而且还允许你在它们的控制器上使用依赖注入。每个 Web Framework 部分都有关于其特定集成策略的更多详细信息。
### 5.2.JSF
JavaServer Faces 是 JCP 的标准的基于组件的、事件驱动的 Web 用户界面框架。它是 Java EE 保护伞的正式部分,但也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。
请注意,最近的 JSF 版本与应用程序服务器中的 CDI 基础架构紧密相关,一些新的 JSF 功能仅在这样的环境中工作。 Spring 的 JSF 支持不再是积极发展的,主要是为了在更新基于 JSF 的旧应用程序时的迁移目的而存在。
Spring 的 JSF 集成中的关键元素是 JSF`ELResolver`机制。
#### 5.2.1. Spring Bean 解析器
`SpringBeanFacesELResolver`是一个兼容 JSF 的`ELResolver`实现,与 JSF 和 JSP 使用的标准统一 EL 集成。它首先委托给 Spring 的“业务上下文”`WebApplicationContext`,然后委托给底层 JSF 实现的默认解析器。
在配置方面,你可以在 JSF`faces-context.xml`文件中定义`SpringBeanFacesELResolver`,如下例所示:
```
org.springframework.web.jsf.el.SpringBeanFacesELResolver
...
```
#### 5.2.2.使用`FacesContextUtils`
当将属性映射到`faces-config.xml`中的 bean 时,自定义`ELResolver`很好地工作,但是,有时你可能需要显式地获取 Bean。[`FacesContextUtils`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/jsf/facescontextutils.html)类使这一点变得很简单。它类似于`WebApplicationContextUtils`,只是它需要一个`FacesContext`参数,而不是`ServletContext`参数。
下面的示例展示了如何使用`FacesContextUtils`:
```
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
```
### 5.3.Apache Struts2.x
由 Craig McClanahan 发明的[Struts](https://struts.apache.org)是一个由 Apache 软件基金会主持的开源项目。当时,它极大地简化了 JSP/ Servlet 编程范式,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此像 Beer 一样是免费的),并且它拥有一个庞大的社区,这让该项目得以发展并在 Java Web 开发人员中流行起来。
作为原始 Struts1.x 的后续版本,请查看 Struts2.x 和 Struts-提供的[Spring Plugin](https://struts.apache.org/release/2.3.x/docs/spring-plugin.html)用于内置 Spring 集成。
### 5.4.Apache Tapestry5.x
[Tapestry](https://tapestry.apache.org/)是一个“面向组件的框架,用于在 Java 中创建动态的、健壮的、高度可扩展的 Web 应用程序。”
Spring 虽然具有自己的[强大的 Web 层](#mvc),但是通过使用用于 Web 用户界面的 Tapestry 和用于较低层的 Spring 容器的组合来构建 Enterprise 的 Java 应用程序有许多独特的优点。
有关更多信息,请参见 Tapestry 的专用[integration module for Spring](https://tapestry.apache.org/integrating-with-spring-framework.html)。
### 5.5.更多资源
下面的链接指向关于本章中描述的各种 Web 框架的更多参考资料。
* [JSF](https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html)主页
* [Struts](https://struts.apache.org/)主页
* [Tapestry](https://tapestry.apache.org/)主页