countOfMoviesPerCatalog) {
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
}
}
```
### 1.9.8 使用 @PostConstruct 和 @PreDestroy
`CommonAnnotationBeanPostProcessor` 不仅识别 `@Resource` 注解,还识别 JSR-250 生命周期注释:`jakara.annotation.PostConstruct` 和 `jakara.annotation.PreDestroy`。在 Spring 2.5 中引入了对这些注解的支持,为初始化回调和销毁回调中描述的生命周期回调机制提供了一种替代方案。如果 `CommonAnnotationBeanPostProcessor` 在 Spring `ApplicationContext` 中注册,则在生命周期的同一点调用携带这些注解之一的方法,该方法与相应的 Spring 生命周期接口方法或显式声明的回调方法相同。在以下示例中,缓存在初始化时预填充,并在销毁时清除:
```java
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
```
有关组合各种生命周期机制的效果的详细信息,请参阅组合生命周期机制。
> 与 `@Resource` 一样,`@PostConstruct` 和 `@PreDestroy` 注解类型是 JDK 6 到 8 标准 Java 库的一部分。然而,整个 `javax.annotation` 包与 JDK 9 中的核心 Java 模块分离,最终在 JDK 11 中删除。从 Jakarta EE 9 开始,该包现在位于 `jakarta.annotation` 中。如果需要,`jakarta.annotation-api` 工件现在需要通过 Maven Central 获得,只需像任何其他库一样添加到应用程序的类路径。
## 1.10 类路径扫描和托管组件
本章中的大多数示例使用 XML 指定在 Spring 容器中生成每个 `BeanDefinition` 的配置元数据。上一节(基于注释的容器配置)演示了如何通过源级别注释提供大量配置元数据。然而,即使在这些示例中,“基本” bean 定义也在 XML 文件中明确定义,而注解仅驱动依赖注入。本节介绍通过扫描类路径隐式检测候选组件的选项。候选组件是与筛选条件匹配的类,并在容器中注册了相应的 bean 定义。这消除了使用 XML 执行 bean 注册的需要。相反,您可以使用注解(例如,`@Component`)、AspectJ 类型表达式或您自己的自定义筛选条件来选择哪些类具有向容器注册的 bean 定义。
> 从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多特性都是核心 Spring 框架的一部分。这允许您使用 Java 而不是使用传统的 XML 文件来定义 bean。查看 `@Configuration`、`@Bean`、`@Import` 和 `@DependsOn` 注解,了解如何使用这些新功能的示例。
### 1.10.1 @Component 和更多传统模式注解
`@Repository` 注解是实现存储库(也称为数据访问对象或 DAO)角色或传统模式的任何类的标记。此标记的用途之一是异常的自动转换,如“异常转换”中所述。
Spring 提供了更多的传统模式注解:`@Component`、`@Service` 和 `@Controller`。`@Component` 是任何 Spring 管理组件的泛型传统模式。`@Repository`、`@Service` 和 `@Controller` 是 `@Component` 的专门化,用于更具体的用例(分别在持久性、服务和表示层)。因此,您可以使用 `@Component` 来注解组件类,但是,通过改用 `@Repository`、`@Service` 或 `@Controller` 来注解它们,您的类更适合由工具处理或与切面关联。例如,这些传统模式注解是切入点的理想目标。`@Repository`、`@Service` 和 `@Controller` 也可以在未来的 Spring Framework 版本中携带额外的语义。因此,如果您在服务层使用 `@Component` 或 `@Service` 之间进行选择,`@Service` 显然是更好的选择。类似地,如前所述,`@Repository` 已经被支持作为持久层中自动异常转换的标记。
### 1.10.2 使用元注解和组合注解
Spring 提供的许多注解可以在您自己的代码中用作元注解。元注解是可以应用于另一个注解的注解。例如,前面提到的 `@Service` 注解是用 `@Component` 元注解的,如下例所示:
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {
// ...
}
// (1) `@Component` 使得 `@Service` 可以像 `@Component` 一样被处理
```
您还可以组合元注解来创建“组合注解”。例如,Spring MVC 中的 `@RestController` 注解由 `@Controller` 和 `@ResponseBody` 组成。
此外,组合注解可以选择性地从元注解中重新声明属性,以允许自定义。当您只想公开元注解属性的子集时,这可能特别有用。例如,Spring 的 `@SessionScope` 注解将作用域名称硬编码为 `session`,但仍然允许定制 `proxyMode`。以下列表显示 `SessionScope` 注解的定义:
```java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
```
然后,可以使用 `@SessionScope` 而不声明 `proxyMode`,如下所示:
```java
@Service
@SessionScope
public class SessionScopedService {
// ...
}
```
您还可以重写 `proxyMode` 的值,如下例所示:
```java
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
```
有关更多详细信息,请参阅 Spring Annotation Programming Model wiki页面。
### 1.10.3 自动检测类和注册 Bean 定义
Spring 可以自动检测定型类,并向 `ApplicationContext` 注册相应的 `BeanDefinition` 实例。例如,以下两个类可用于此类自动检测:
```java
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
```
```java
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
```
要自动检测这些类并注册相应的 bean,需要将 `@ComponentScan` 添加到 `@Configuration` 类中,其中 `basePackages` 属性是两个类的公共父包。(或者,可以指定逗号、分号或空格分隔的列表,其中包含每个类的父包。)
```java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
```
为了简洁起见,前面的示例可能使用了注解的 `value` 属性(即 `@ComponentScan("org.example")`)。
以下替代方案使用 XML:
```xml
```
> `` 的使用隐式地启用了 `` 的功能。在使用 `` 时,通常不需要包含 `` 元素。
> 扫描类路径包需要在类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 时,请确保您没有激活 JAR 任务的仅文件开关。此外,在某些环境中,基于安全策略,类路径目录可能不会公开——例如,JDK 1.7.0_45 和更高版本上的独立应用程序(需要在清单中设置“Trusted-Library” — 参考 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).
>
> 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常按预期工作。但是,请确保在 `module-info` 描述符中导出了组件类。如果您希望 Spring 调用类的非公共成员,请确保它们是“打开的”(即,它们在模块信息描述符中使用 `opens` 声明而不是 `exports` 声明)。
此外,当您使用组件扫描元素时,`AutowiredAnnotationBeanPostProcessor` 和 `CommonAnnotationBeamPostProcessor` 都是隐式包含的。这意味着这两个组件是自动检测并连接在一起的——所有这些都没有 XML 中提供的任何 bean 配置元数据。
> 通过包含值为 `false` 的 `annotation-config` 属性,可以禁用 `AutowiredAnnotationBeanPostProcessor` 和 `CommonAnnotationBeamPostProcessor` 的注册。
### 1.10.4 使用过滤器来自定义扫描
默认情况下,用 `@Component`、`@Repository`、`@Service`、`@Controller`、`@Configuration` 注解的类,或用 `@Component` 注解的自定义注解,是唯一检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为 `@ComponentScan` 注解的 `includeFilters` 或 `excludeFilters` 属性(或添加为 XML 配置中 `` 元素的 `` 或 `` 子元素)。每个筛选器元素都需要 `type` 和 `expression` 属性。下表介绍了过滤选项:
| 过滤器类型 | 示例表达式 | Description |
| :----------- | :--------------------------- | :----------------------------------------------------------- |
| 注解(默认) | `org.example.SomeAnnotation` | 在目标组件的类型级别上出现或间接出现的注解 |
| 可归类 | `org.example.SomeClass` | 目标组件可归于(扩展或实现)的类(或接口)。 |
| aspectj | `org.example..*Service+` | 要由目标组件匹配的 AspectJ 类型表达式。 |
| 正则表达式 | `org\.example\.Default.*` | 要与目标组件的类名匹配的正则表达式。 |
| 自定义 | `org.example.MyTypeFilter` | `org.springframework.core.type.TypeFilter` 接口的自定义实现。 |
以下示例显示了忽略所有 `@Repository` 注解并使用“存根(stub)”存储库的配置:
```java
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
// ...
}
```
以下列表显示了等效的 XML:
```xml
```
> 您还可以通过在注释上设置 `useDefaultFilters=false` 或通过将 `useDefaultFilters="false"` 作为 `` 元素的属性来禁用默认过滤器。这有效地禁用了对用 `@Component`、`@Repository`、`@Service`、`@Controller`、`@RestController` 或 `@Configuration` 注解的类或元注解的自动检测。
### 1.10.5 在组件中定义 Bean 元数据
Spring 组件还可以向容器提供 bean 定义元数据。您可以使用用于在 `@Configuration` 注解类中定义 Bean 元数据的 `@Bean` 注解来实现这一点。以下示例显示了如何执行此操作:
```java
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
```
前面的类是一个 Spring 组件,其 `doWork()` 方法中包含特定于应用程序的代码。然而,它还提供了一个 bean 定义,该定义具有引用方法 `publicInstance()` 的工厂方法。`@Bean` 注解标识工厂方法和其他 Bean 定义属性,例如通过 `@Qualifier` 注解的限定符值。可以指定的其他方法级注解包括 `@Scope`、`@Lazy` 和自定义限定符注解。
> 除了它在组件初始化中的作用外,您还可以将 `@Lazy` 注解放置在标记为 `@Autowired` 或 `@Inject` 的注入点上。在这种情况下,它会导致延迟解析代理的注入。然而,这种代理方法相当有限。对于复杂的惰性交互,特别是与可选依赖项的组合,我们建议改用 `ObjectProvider`。
如前所述,支持自动装配字段和方法,同时还支持 `@Bean` 方法的自动装配。以下示例显示了如何执行此操作:
```java
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
```
该示例将 `String` 方法参数 `country` 自动装配到另一个名为 `privateInstance` 的 bean 上 `age` 属性的值。Spring Expression Language 元素通过符号 `#{}` 定义属性的值。对于 `@Value` 注解,表达式解析器被预先配置为在解析表达式文本时查找 bean 名称。
从 Spring Framework 4.3 开始,您还可以声明 `InjectionPoint`(或其更具体的子类:`DependencyDescriptor`)类型的工厂方法参数,以访问触发当前 bean 创建的请求注入点。注意,这只适用于 bean 实例的实际创建,而不适用于现有实例的注入。因此,这个特性对于原型作用域的 bean 来说最有意义。对于其他作用域,工厂方法只会看到在给定作用域中触发创建新 bean 实例的注入点(例如,触发创建惰性单例 bean 的依赖项)。在这样的场景中,您可以使用所提供的注入点元数据,并注意语义。以下示例显示了如何使用 `InjectionPoint`:
```java
@Component
public class FactoryMethodComponent {
@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
```
常规 Spring 组件中的 `@Bean` 方法的处理方式不同于 Spring `@Configuration` 类中的对应方法。不同之处在于,`@Component` 类并没有通过 CGLIB 增强以拦截方法和字段的调用。CGLIB 代理是调用 `@Configuration` 类中 `@Bean` 方法中的方法或字段创建对协作对象的 Bean 元数据引用的方法。这样的方法不是用正常的 Java 语义调用的,而是通过容器来提供 Spring Bean 的通常生命周期管理和代理,即使在通过对 `@Bean` 方法的编程调用来引用其他 Bean 时也是如此。相反,在纯 `@Component` 类中调用 `@Bean` 方法中的方法或字段具有标准的 Java 语义,没有特殊的 CGLIB 处理或其他约束。
> 您可以将 `@Bean` 方法声明为 `static`,允许在不将其包含的配置类创建为实例的情况下调用它们。这在定义后处理器 bean(例如,类型为 `BeanFactoryPostProcessor` 或 `BeanPostProcessor`)时特别有意义,因为这样的 bean 在容器生命周期的早期就被初始化,应该避免在此时触发配置的其他部分。
>
> 由于技术限制,对静态 `@Bean` 方法的调用永远不会被容器拦截,即使在 `@Configuration` 类中也不会被拦截(如本节前面所述):CGLIB 子类化只能覆盖非静态方法。因此,对另一个 `@Bean` 方法的直接调用具有标准 Java 语义,从而直接从工厂方法本身返回一个独立实例。
>
> `@Bean` 方法的 Java 语言可见性不会对 Spring 容器中生成的 Bean 定义产生直接影响。您可以根据需要自由声明非 `@Configuration` 类的工厂方法以及任何地方的静态方法。然而,`@Configuration` 类中的常规 `@Bean` 方法需要是可重写的——也就是说,它们不能被宣布为 `private` 或 `final`。
>
> `@Bean` 在给定组件或配置类的基类上可以被发现,以及在组件或配置类实现的接口中声明的 Java 8 默认方法上也可以被发现。这在组合复杂的配置安排方面提供了很大的灵活性,从 Spring 4.2 开始,甚至可以通过 Java 8 默认方法进行多重继承。
>
> 最后,一个类可以为同一个 Bean 保存多个 `@Bean` 方法,这是根据运行时可用的依赖关系使用的多个工厂方法的排列。这与在其他配置场景中选择“贪婪”构造函数或工厂方法的算法相同:在构建时选择具有最多可满足依赖项的变量,类似于容器如何在多个 `@Autowired` 构造函数之间进行选择。
### 1.10.6 命名自动检测组件
当组件作为扫描过程的一部分被自动检测时,其 bean 名称由该扫描器已知的 `BeanNameGenerator` 策略生成。默认情况下,任何包含名称值的 Spring 传统模式注解(`@Component`、`@Repository`、`@Service` 和 `@Controller`)都会将该名称提供给相应的 bean 定义。
如果这样的注解不包含名称值或任何其他检测到的组件(例如自定义过滤器发现的组件),则默认 bean 名称生成器将返回未大写的非限定类名。例如,如果检测到以下组件类,则名称将为 `myMovieLister` 和 `movieFinderImpl`:
```java
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
```
```java
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
```
如果不想依赖默认的 bean 命名策略,可以提供自定义 bean 命名策略。首先,实现 `BeanNameGenerator` 接口,并确保包含默认的无参数构造函数。然后,在配置扫描器时提供完全限定的类名,如下例注解和 bean 定义所示。
> 如果由于多个自动检测到的组件具有相同的非限定类名(即,具有相同名称但驻留在不同包中的类)而导致命名冲突,则可能需要配置默认为生成的 bean 名称的完全限定类名的 `BeanNameGenerator`。从 Spring Framework 5.2.3 开始,位于 `org.springframework.context.annotation` 包中的 `FullyQualifiedAnnotationBeanGenerator` 可用于此类目的。
```java
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
```
```xml
```
作为一般规则,考虑在其他组件可能对其进行显式引用时使用注解指定名称。另一方面,只要容器负责装配,自动生成的名称就足够了。
### 1.10.7 为自动检测的组件提供作用域
与一般的 Spring 管理组件一样,自动检测组件的默认和最常见范围是 `singleton`。但是,有时您需要一个可以由 `@Scope` 注解指定的不同作用域。可以在注解中提供作用域的名称,如下例所示:
```java
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
```
> `@Scope` 注解仅在具体的 bean 类(对于带注解的组件)或工厂方法(对于 `@Bean` 方法)上进行内省。与 XML bean 定义相反,没有 bean 定义继承的概念,类级别的继承层次结构与元数据目的无关。
有关特定于 web 的作用域(如 Spring 上下文中的“请求”或“会话”)的详细信息,请参阅“请求、会话、应用和 WebSocket 作用域”。与这些作用域的预构建注解一样,您也可以使用 Spring 的元注解方法来编写自己的作用域注解:例如,使用 `@Scope("prototype")` 元注解的自定义注解,也可能声明自定义作用域代理模式。
> 要为作用域解析提供自定义策略而不是依赖基于注解的方法,可以实现 `ScopeMetadataResolver` 接口。确保包含默认的无参数构造函数。然后,您可以在配置扫描器时提供完全限定的类名,如下面的注解和 bean 定义示例所示:
>
> ```java
> @Configuration
> @ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
> public class AppConfig {
> // ...
> }
> ```
>
> ```xml
>
>
>
> ```
当使用某些非单例作用域时,可能需要为作用域对象生成代理。推理在“作为依赖的作用域 Bean”中描述。为此,组件扫描元素上提供了作用域代理属性。三个可能的值是:`no`、`interfaces` 和 `targetClass`。例如,以下配置生成标准 JDK 动态代理:
```java
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
// ...
}
```
```xml
```
### 1.10.8 通过注解提供限定符元数据
`@Qualifier` 注解在“使用限定符微调基于注解的自动装配”中讨论。该节中的示例演示了 `@Qualifier` 注解和自定义限定符注解的使用,以在解析自动装配候选项时提供细粒度控制。因为这些示例基于 XML bean 定义,所以通过使用 XML 中 `bean` 元素的 `qualifier` 或 `meta` 子元素,在候选 bean 定义上提供了限定符元数据。当依赖类路径扫描来自动检测组件时,可以为候选类提供带有类级别注解的限定符元数据。以下三个示例演示了该技术:
```java
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
```
```java
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
```
```java
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
```
> 与大多数基于注解的替代方案一样,请记住,注解元数据绑定到类定义本身,而 XML 的使用允许同一类型的多个 bean 在其限定符元数据中提供变化,因为该元数据是按实例而不是按类提供的。
### 1.10.9 生成候选组件的索引
虽然类路径扫描非常快,但通过在编译时创建候选的静态列表,可以提高大型应用程序的启动性能。在此模式下,作为组件扫描目标的所有模块都必须使用此机制。
> 您现有的 `@ComponentScan` 或 `` 指令必须保持不变,才能请求上下文扫描某些包中的候选项。当 `ApplicationContext` 检测到这样的索引时,它会自动使用它,而不是扫描类路径。
要生成索引,请向包含组件扫描指令目标的组件的每个模块添加一个附加依赖项。以下示例显示了如何使用 Maven 执行此操作:
```xml
org.springframework
spring-context-indexer
6.0.4
true
```
对于 Gradle 4.5 及更低版本,应在 `compileOnly` 配置中声明依赖项,如下例所示:
```groovy
dependencies {
compileOnly "org.springframework:spring-context-indexer:6.0.4"
}
```
对于 Gradle 4.6 及更高版本,应在 `annotationProcessor` 配置中声明依赖关系,如下例所示:
```groovy
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:6.0.4"
}
```
`spring-context-indexer` 工件生成包含在 jar 文件中的 `META-INF/spring.components` 文件。
> 在 IDE 中使用此模式时,必须将 `spring-context-indexer` 注册为注解处理器,以确保在更新候选组件时索引是最新的。
> 当在类路径上找到 `META-INF/spring.components` 文件时,索引将自动启用。如果索引部分可用于某些库(或用例),但无法为整个应用程序构建,则可以通过将 `spring.index.ignore` 设置为 `true`(作为 JVM 系统属性或通过 `SpringProperties` 机制),返回到常规的类路径排列(就好像根本不存在索引一样)。
## 1.11 使用 JSR 330 标准注解
从 Spring 3.0 开始,Spring 支持 JSR-330 标准注解(依赖注入)。这些注解的扫描方式与 Spring 注解相同。要使用它们,您需要在类路径中有相关的 jar。
> 如果您使用 Maven,`jakarta.inject` 工件在标准 Maven 存储库(https://repo1.maven.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/). 您可以将以下依赖项添加到文件 pom.xml中:
>
> ```xml
>
> jakarta.inject
> jakarta.inject-api
> 1
>
> ```
### 1.11.1 使用 @Inject 和 @Named 依赖注入
您可以使用 `@jakarta.inject.Inject` 代替 `@Autowired`,如下所示:
```java
import jakarta.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
// ...
}
}
```
与 `@Autowired` 一样,您可以在字段级、方法级和构造函数参数级使用 `@Inject`。此外,您可以将注入点声明为 `Provider`,允许按需访问较短范围的 bean,或者通过 `Provider.get()` 调用延迟访问其他 bean。以下示例提供了上一示例的变体:
```java
import jakarta.inject.Inject;
import jakarta.inject.Provider;
public class SimpleMovieLister {
private Provider movieFinder;
@Inject
public void setMovieFinder(Provider movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
```
如果要为应该注入的依赖项使用限定名,则应使用 `@Named` 注解,如下例所示:
```java
import jakarta.inject.Inject;
import jakarta.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
```
与 `@Autowired` 一样,`@Inject` 也可以与 `java.util.Optional` 或 `@Nullable` 一起使用。这在这里更适用,因为 `@Inject` 没有必需的属性。以下两个示例显示了如何使用 `@Inject` 和 `@Nullable`:
```java
public class SimpleMovieLister {
@Inject
public void setMovieFinder(Optional movieFinder) {
// ...
}
}
```
```java
public class SimpleMovieLister {
@Inject
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
// ...
}
}
```
### 1.11.2 @Named 和 @ManagedBean:@Component 注解的标准等价物
您可以使用 `@jakarta.inject.Name` 或 `jakarta.notation.ManagedBean` 代替 `@Component`,如下例所示:
```java
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
```
在不指定组件名称的情况下使用 `@Component` 是非常常见的。`@Named` 可以以类似的方式使用,如下例所示:
```java
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
```
使用 `@Named` 或 `@ManagedBean` 时,可以使用与使用 Spring 注解时完全相同的方式使用组件扫描,如下例所示:
```java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
```
> 与 `@Component` 不同,JSR-330 `@Named` 和 JSR-250 `@ManagedBean` 注解是不可组合的。您应该使用 Spring 的传统模式模型来构建自定义组件注解。
### 1.11.3 JSR-330 标准注解的限制
使用标准注解时,应该知道一些重要功能不可用,如下表所示:
| Spring | jakarta.inject.* | jakarta.inject 限制 / 解释 |
| :------------------ | :-------------------- | :----------------------------------------------------------- |
| @Autowired | @Inject | `@Inject` 没有 'required' 属性。作为替代,可以和 Java 8 的 `Optional` 一起使用. |
| @Component | @Named / @ManagedBean | JSR-330 并不提供可组合模型,只是确认命名组件的方式。 |
| @Scope("singleton") | @Singleton | JSR-330 默认作用域类似于 Spring 的 `prototype`。然而,为了使其与 Spring 的一般默认值保持一致,Spring 容器中声明的 JSR-330 bean 默认为 `singleton` 。为了使用 `singleton` 以外的作用域,应该使用 Spring 的 `@Scope` 注解。`jakarta.inject` 还提供了一个 `jakarta.iject.Scope` 注解:然而,这个注解仅用于创建自定义注解。 |
| @Qualifier | @Qualifier / @Named | `jakarta.inject.Qualifier` 只是用于构建自定义限定符的元注解。具体的 `String` 限定符(如 Spring 的带值的 `@Qualifier`)可以通过 `jakarta.inject.Named` 关联。 |
| @Value | - | 没有等价物 |
| @Lazy | - | 没有等价物 |
| ObjectFactory | Provider | `jakarta.inject.Provider` 是 Spring 的 `ObjectFactory` 的直接替代品,只是使用了更短的 `get()` 方法名。它还可以与 Spring 的 `@Autowired` 或非注解构造函数和 setter 方法结合使用。 |
## 1.12 基于 Java 的容器配置
本节介绍如何在 Java 代码中使用注解来配置 Spring 容器。它包括以下主题:
- 基本概念:`@Bean` 和 `@Configuration`
- 使用 `AnnotationConfigApplicationContext` 实例化 Spring 容器
- 使用 `@Bean` 注解
- 使用 `@Configuration` 注解
- 合成基于 Java 的配置
- Bean 定义配置文件
- `PropertySource` 摘要
- 使用 `@PropertySource`
- 语句中的占位符解析
### 1.12.1 基本概念:@Bean 和 @Configuration
Spring 的 Java 配置支持中的核心工件是 `@Configuration` 注解类和 `@Bean` 注解方法。
`@Bean` 注解用于指示方法实例化、配置和初始化要由 Spring IoC 容器管理的新对象。对于熟悉 Spring 的 `` XML 配置的人来说,`@Bean` 注解扮演着与 `` 元素相同的角色。您可以在任何 Spring `@Component` 中使用 `@Bean` 注解方法。然而,它们最常用于 `@Configuration` bean。
用 `@Configuration` 注解类表明它的主要目的是作为 bean 定义的源。此外,`@Configuration` 类允许通过调用同一类中的其他 `@Bean` 方法来定义 bean 之间的依赖关系。最简单的 `@Configuration` 类如下:
```java
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
```
前面的 `AppConfig` 类等效于下面的 Spring `` XML:
```xml
```
> **完整 @Configuration vs “lite” @Bean 模式?**
>
> 当 `@Bean` 方法在没有 `@Configuration` 注解的类中声明时,它们被称为以“lite”模式处理。在 `@Component` 中或者甚至在普通的旧类中声明的 Bean 方法被认为是“lite”,其中包含类的主要目的不同,`@Bean` 方法是一种好处。例如,服务组件可以通过每个适用组件类上的附加 `@Bean` 方法向容器公开管理视图。在这种情况下,`@Bean` 方法是一种通用的工厂方法机制。
>
> 与完整的 `@Configuration` 不同,lite `@Bean` 方法不能声明 Bean 间依赖关系。相反,它们对其包含组件的内部状态进行操作,也可以对它们可能声明的参数进行操作。因此,此类 `@Bean` 方法不应调用其他 `@Bean` 方法。每个这样的方法实际上只是特定 bean 引用的工厂方法,没有任何特殊的运行时语义。这里的积极副作用是,在运行时不必应用 CGLIB 子类化,因此在类设计方面没有任何限制(即,包含类可能是 `final` 等等)。
>
> 在常见的场景中,`@Bean` 方法将在 `@Configuration` 类中声明,确保始终使用“完全”模式,因此跨方法引用将被重定向到容器的生命周期管理。这可以防止通过常规 Java 调用意外调用同一 `@Bean` 方法,这有助于减少在“lite”模式下操作时难以跟踪的细微错误。
`@Bean` 和 `@Configuration` 注解将在以下章节中深入讨论。然而,首先,我们将介绍通过使用基于 Java 的配置创建 Spring 容器的各种方法。
### 1.12.2 使用 AnnotationConfigApplicationContext 实例化 Spring 容器
以下部分记录了 Spring 3.0 中引入的 Spring 的 `AnnotationConfigApplicationContext`。这个通用的 `ApplicationContext` 实现不仅能够接受 `@Configuration` 类作为输入,还能够接受普通的 `@Component` 类和用 JSR-330 元数据注解的类。
当 `@Configuration` 类作为输入提供时,`@Configuration` 类本身被注册为 bean 定义,并且类中所有声明的 `@Bean` 方法也被注册为 bean 定义。
当提供 `@Component` 和 JSR-330 类时,它们被注册为 bean 定义,并且假设在这些类中使用了 DI 元数据,如 `@Autowired` 或 `@Inject`。
#### 简单构造
与 Spring XML 文件在实例化 `ClassPathXmlApplicationContext` 时用作输入的方式大致相同,您可以在实例化 `AnnotationConfigApplicationContext` 时使用 `@Configuration` 类作为输入。这允许完全无 XML 地使用 Spring 容器,如下例所示:
```java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
```
如前所述,`AnnotationConfigApplicationContext` 不限于仅使用 `@Configuration` 类。任何 `@Component` 或 JSR-330 注解类都可以作为构造函数的输入提供,如下例所示:
```java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
```
前面的示例假设 `MyServiceImpl`、`Dependency1` 和 `Dependency2` 使用 Spring 依赖注入注解,例如 `@Autowired`。
#### 使用 register(Class> ...) 编程式构建容器
可以使用无参数构造函数实例化 `AnnotationConfigApplicationContext`,然后使用 `register()` 方法对其进行配置。当以编程方式构建 `AnnotationConfigApplicationContext` 时,这种方法特别有用。以下示例显示了如何执行此操作:
```java
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
```
#### 使用 scan(String ...) 启动组件扫描
要启用组件扫描,可以按如下方式注解 `@Configuration` 类:
```java
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig {
// ...
}
```
> 有经验的 Spring 用户可能熟悉 Spring 的 `context:` 命名空间中的 XML 声明,如下例所示:
>
> ```xml
>
>
>
> ```
在前面的示例中,扫描 `com.acme` 包以查找任何 `@Component` 注解类,这些类在容器中注册为 Spring bean 定义。`AnnotationConfigApplicationContext` 公开 `scan(String ...)` 方法来实现相同的组件扫描功能,如下例所示:
```java
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
```
> 请记住,`@Configuration` 类是用 `@Component` 进行元注解的,因此它们是组件扫描的候选对象。在前面的示例中,假设 `AppConfig` 是在 `com.acme` 包(或其下的任何包)中声明的,它在调用 `scan()` 时被获取。在 `refresh()` 之后,它的所有 `@Bean` 方法都被处理并注册为容器中的 Bean 定义。
#### 使用 AnnotationConfigWebApplicationContext 支持 Web 应用
`AnnotationConfigApplicationContext` 的 `WebApplicationContext` 变体可与 `AnnotationConfigWebApplicationContext` 一起使用。配置 Spring `ContextLoaderListener` servlet 监听器、Spring MVC `DispatcherServlet` 等时,可以使用此实现。以下 `web.xml` 片段配置了一个典型的 Spring MVC web 应用程序(注意使用 `contextClass` context-param 和 init-param):
```xml
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.AppConfig
org.springframework.web.context.ContextLoaderListener
dispatcher
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.web.MvcConfig
dispatcher
/app/*
```
> 对于编程用例,可以使用 `GenericWebApplicationContext` 作为 `AnnotationConfigWebApplicationContext` 的替代方案。有关详细信息,请参阅 `GenericWebApplicationContext` Javadoc。
### 1.12.3 使用 @Bean 注解
`@Bean` 是一个方法级注解,是 XML `` 元素的直接模拟。注解支持 `` 提供的一些属性,例如:
- 初始化方法
- 销毁方法
- 自动装配
- `name`
您可以在 `@Configuration` 注解类或 `@Component` 注解类中使用 `@Bean` 注解。
#### 声明 Bean
要声明 bean,可以使用 @Bean 注解注解方法。使用此方法在 `ApplicationContext` 中注册 bean 定义,该定义的类型指定为方法的返回值。默认情况下,bean 名称与方法名称相同。以下示例显示了 `@Bean` 方法声明:
```java
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
```
前面的配置完全等同于以下 Spring XML:
```xml
```
这两个声明都使 `ApplicationContext` 中名为 `transferService` 的 bean 可用,并绑定到 `TransferServiceImpl` 类型的对象实例,如下图所示:
```
transferService -> com.acme.TransferServiceImpl
```
您还可以使用默认方法来定义 bean。这允许通过在默认方法上实现带有 bean 定义的接口来组合 bean 配置。
```java
public interface BaseConfig {
@Bean
default TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
public class AppConfig implements BaseConfig {
}
```
您还可以使用接口(或基类)返回类型声明 `@Bean` 方法,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
```
然而,这将高级类型预测的可见性限制为指定的接口类型(`TransferService`)。然后,只有在实例化了受影响的单例 bean 之后,容器才知道完整类型(`TransferServiceImpl`)。非惰性单例 bean 根据其声明顺序进行实例化,因此您可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试通过非声明类型进行匹配(例如 `@Autowired TransferServiceImpl`,它仅在 `transferService` bean 实例化后才解析)。
> 如果您始终通过声明的服务接口引用您的类型,那么 `@Bean` 返回类型可以安全地加入该设计决策。然而,对于实现多个接口的组件或可能由其实现类型引用的组件,更安全的做法是尽可能声明最具体的返回类型(至少与引用 bean 的注入点所要求的一样具体)。
#### Bean 依赖
`@Bean` 注解方法可以具有任意数量的参数,这些参数描述构建该 Bean 所需的依赖关系。例如,如果 `TransferService` 需要 `AccountRepository`,我们可以使用方法参数实现该依赖关系,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
```
解析机制与基于构造函数的依赖注入几乎相同。有关详细信息,请参阅相关章节。
#### 接受生命周期回调
使用 `@Bean` 注解定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的 `@PostConstruct` 和 `@PreDestroy` 注解。有关详细信息,请参阅 JSR-250 注解。
也完全支持常规的 Spring 生命周期回调。如果 bean 实现 `InitializingBean`、`DisposableBean` 或 `Lifecycle`,则容器将调用它们各自的方法。
还完全支持标准的 `*Aware` 接口集(如 BeanFactoryAware、BeanNameAware、MessageSourceAware、ApplicationContextAware 等)。
`@Bean` 注解支持指定任意初始化和销毁回调方法,就像 Spring XML 在 `bean` 元素上的 `init-method` 和 `destroy-method` 属性一样,如下例所示:
```java
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
```
> 默认情况下,使用 Java 配置定义的具有公共 `close` 或 `shutdown` 方法的 bean 将自动与销毁回调一起登记。如果您有一个公共 `close` 或 `shutdown` 方法,并且不希望在容器关闭时调用它,那么可以将 `@Bean(destroyMethod = "")` 添加到 Bean 定义中,以禁用默认`(inferred)`模式。
>
> 默认情况下,您可能希望对使用 JNDI 获取的资源执行此操作,因为它的生命周期在应用程序之外进行管理。特别是,确保始终为 `DataSource` 执行此操作,因为众所周知,Jakarta EE 应用程序服务器上存在问题。
>
> 以下示例显示如何防止 `DataSource` 的自动销毁回调:
>
> ```java
> @Bean(destroyMethod = "")
> public DataSource dataSource() throws NamingException {
> return (DataSource) jndiTemplate.lookup("MyDS");
> }
> ```
>
> 此外,对于 `@Bean` 方法,通常使用编程 JNDI 查找,通过使用 Spring 的 `JndiTemplate` 或 `JndiLocatorDelegate` 助手或直接使用 JNDI `InitialContext`,而不是 `JndiObjectFactoryBean` 变量(这将迫使您将返回类型声明为 `FactoryBean` 类型,而不是实际的目标类型,从而使其更难用于其他 `@Bean` 方法中的交叉引用调用,这些方法打算引用此处提供的资源)。
对于前面注解中示例中的 `BeanOne`,在构建过程中直接调用 `init()` 方法同样有效,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
```
> 当您直接在 Java 中工作时,您可以对对象执行任何您喜欢的操作,而不必总是依赖容器生命周期。
#### 指定 Bean 作用域
Spring 包含 `@Scope` 注解,因此您可以指定 bean 的作用域。
##### 使用 @Scope 注解
您可以指定用 `@Bean` 注解定义的 Bean 应该具有特定的范围。您可以使用“Bean 作用域”部分中指定的任何标准作用域。
默认范围是 `singleton`,但您可以使用 `@Scope` 注解覆盖它,如下例所示:
```java
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
```
##### @Scope 和 scoped-proxy
Spring 提供了一种通过作用域代理处理作用域依赖关系的便捷方式。在使用 XML 配置时创建此类代理的最简单方法是 `` 元素。使用 `@Scope` 注释在 Java 中配置 bean 可以提供与 `proxyMode` 属性等效的支持。默认值为 `ScopedProxyMode.DEFAULT`,这通常表示不应创建作用域代理,除非在组件扫描指令级别配置了不同的默认值。可以指定 `ScopedProxyMode.TARGET_CLASS`、`ScopedProxyMode.INTERFACES` 或 `ScopedProxyMode.NO`。
如果您使用 Java 将 XML 参考文档中的作用域代理示例(请参见作用域代理)移植到 `@Bean`,它类似于以下内容:
```java
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}
@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}
```
#### 自定义 Bean 命名
默认情况下,配置类使用 `@Bean` 方法的名称作为结果 Bean 的名称。但是,可以使用 `name` 属性覆盖此功能,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}
```
#### Bean 别名
正如在命名 bean 中所讨论的,有时需要为单个 bean 提供多个名称,也称为 bean 别名。`@Bean` 注解的 `name` 属性为此目的接受 String 数组。以下示例显示如何为 bean 设置多个别名:
```java
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
```
#### Bean 描述
有时,为 bean 提供更详细的文本描述是有帮助的。当出于监控目的公开 bean(可能通过 JMX)时,这尤其有用。
要向 `@Bean` 添加描述,可以使用 `@Description` 注解,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
```
### 1.12.4 使用 @Configuration 注解
`@Configuration` 是一个类级注解,指示对象是 bean 定义的源。`@Configuration` 类通过 `@Bean` 注解方法声明 Bean。对 `@Configuration` 类上的 `@Bean` 方法的调用也可用于定义 Bean 间依赖关系。请参阅“基本概念:@Bean和@Configuration”了解一般介绍。
#### 注入跨 bean 依赖
当 bean 之间存在依赖关系时,表达这种依赖关系就像让一个 bean 方法调用另一个一样简单,如下例所示:
```java
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
```
在前面的示例中,`beanOne` 通过构造函数注入接收对 `beanTwo` 的引用。
> 只有在 `@Configuration` 类中声明 `@Bean` 方法时,这种声明 bean 间依赖关系的方法才有效。不能使用普通的 `@Component` 类声明 bean 间依赖关系。
#### 查找方法注入
如前所述,查找方法注入是一种高级功能,您应该很少使用。它在单例作用域 bean 依赖于原型作用域 bean 的情况下非常有用。将 Java 用于这种类型的配置为实现这种模式提供了一种自然的方法。以下示例显示了如何使用查找方法注入:
```java
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
```
通过使用 Java 配置,您可以创建 `CommandManager` 的子类,其中抽象的 `createCommand()` 方法被重写,从而查找新的(原型)命令对象。以下示例显示了如何执行此操作:
```java
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}
@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
```
#### 关于基于 Java 的配置内部如何运作的更多信息
考虑以下示例,其中显示了两次调用 `@Bean` 注解方法:
```java
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
```
`clientDao()` 在 `clientService1()` 和 `clientService2()` 中分别调用了一次。由于此方法创建了 `ClientDaoImpl` 的新实例并将其返回,因此通常需要两个实例(每个服务一个)。这肯定是有问题的:在 Spring 中,实例化的 bean 默认具有 `singleton` 作用域。这就是神奇之处:所有 `@Configuration` 类在启动时都使用 `CGLIB` 进行子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)bean。
> 根据 bean 的作用域,行为可能有所不同。我们在这里谈论的是单例。
> 从 Spring 3.2 开始,不再需要将 CGLIB 添加到类路径中,因为 CGLIB 类已在 `org.springframework.CGLIB` 下重新打包,并直接包含在 Spring 核心 JAR中。
> 由于 CGLIB 在启动时动态添加特性,因此存在一些限制。特别是,配置类不能是 final 的。但是,从 4.3 开始,配置类上允许使用任何构造函数,包括使用 `@Autowired` 或单个非默认构造函数声明进行默认注入。
>
> 如果您希望避免任何 CGLIB 强加的限制,请考虑在非 `@Configuration` 类(例如,在普通的 `@Component` 类上)。`@Bean` 方法之间的跨方法调用不会被拦截,因此您必须在构造函数或方法级别专门依赖依赖注入。
### 1.12.5 组合基于 Java 配置
Spring 基于 Java 的配置特性允许您编写注解,这可以降低配置的复杂性。
#### 使用 @Import 注解
正如 `` 元素在 Spring XML 文件中用于帮助模块化配置一样,`@Import` 注解允许从另一个配置类加载 `@Bean` 定义,如下例所示:
```java
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
```
现在,实例化上下文时不需要同时指定 `ConfigA.class` 和 `ConfigB.class`,只需要显式提供 `ConfigB`,如下例所示:
```java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
```
这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住大量的 `@Configuration` 类。
> 从 Spring Framework 4.2 开始,`@Import` 还支持对常规组件类的引用,类似于 `AnnotationConfigApplicationContext.register` 方法。如果您想避免组件扫描,这是非常有用的,通过使用一些配置类作为入口点来显式定义所有组件。
##### 在导入 @Bean 定义中注入依赖
前面的示例有效,但过于简单。在大多数实际场景中,bean 在配置类之间相互依赖。当使用 XML 时,这不是一个问题,因为不涉及编译器,您可以声明 `ref="someBean"` 并信任 Spring 在容器初始化期间解决它。当使用 `@Configuration` 类时,Java 编译器对配置模型施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,`@Bean` 方法可以有任意数量的参数来描述 Bean 依赖关系。考虑以下具有多个 `@Configuration` 类的更真实的场景,每个类都取决于其他类中声明的 bean:
```java
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
```
还有另一种方法可以达到同样的结果。请记住,`@Configuration` 类最终只是容器中的另一个 bean:这意味着它们可以利用 `@Autowired` 和 `@Value` 注入以及与任何其他 bean 相同的其他特性。
> 确保以这种方式注入的依赖关系仅是最简单的类型。`@Configuration` 类在上下文初始化期间很早就被处理,强制以这种方式注入依赖项可能会导致意外的早期初始化。如前所述,尽可能采用基于参数的注入。
>
> 此外,通过 `@Bean` 对 `BeanPostProcessor` 和 `BeanFactoryPostProcessor` 的定义要特别小心。这些方法通常应声明为 `static @Bean` 方法,而不会触发其包含的配置类的实例化。否则,`@Autowired` 和 `@Value` 可能无法在配置类本身上工作,因为可以将其创建为早于 `AutowiredAnnotationBeanPostProcessor` 的 bean 实例。
以下示例显示了如何将一个 bean 自动装配到另一个 bean:
```java
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
```
> `@Configuration` 类中的构造函数注入仅从 Spring Framework 4.3 开始受支持。还要注意,如果目标 bean 只定义了一个构造函数,则不需要指定 `@Autowired`。
在前面的场景中,使用 `@Autowired` 可以很好地工作,并提供所需的模块化,但确定自动装配 bean 定义的确切声明位置仍然有些模糊。例如,作为一名查看 `ServiceConfig` 的开发人员,您如何确切地知道 `@Autowired AccountRepository` bean 的声明位置?它在代码中并不明确,这可能很好。请记住,Spring Tools for Eclipse 提供了一种工具,可以渲染显示所有内容如何装配的图形,这可能就是您所需要的。此外,Java IDE 可以轻松找到 `AccountRepository` 类的所有声明和用法,并快速显示返回该类型的 `@Bean` 方法的位置。
如果这种模糊性是不可接受的,并且您希望在 IDE 中从一个 `@Configuration` 类直接导航到另一个,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:
```java
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
```
在前面的情况中,`AccountRepository` 的定义是完全明确的。但是,`ServiceConfig` 现在与 `RepositoryConfig` 紧密耦合。这就是权衡。通过使用基于接口或基于抽象类的 `@Configuration` 类,可以稍微减轻这种紧密耦合。考虑以下示例:
```java
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
```
现在,`ServiceConfig` 与具体的 `DefaultRepositoryConfig` 松散耦合,内置的 IDE 工具仍然很有用:您可以轻松获得 `RepositoryConfig` 实现的类型层次结构。通过这种方式,导航 `@Configuration` 类及其依赖项与导航基于接口的代码的通常过程没有什么不同。
> 如果您想影响某些 bean 的启动创建顺序,请考虑将其中一些 bean 声明为 `@Lazy`(用于在首次访问时创建,而不是在启动时创建)或 `@DependsOn` 某些其他 bean(确保在当前 bean 之前创建特定的其他 bean,而不是后者的直接依赖关系)。
#### 有条件的引入 @Configuration 类或 @Bean 方法
根据某些任意的系统状态,有条件地启用或禁用完整的 `@Configuration` 类或甚至单个 `@Bean` 方法通常很有用。一个常见的例子是,只有在 Spring 环境中启用了特定 profile 时,才使用 `@Profile` 注解来激活 Bean(有关详细信息,请参阅 “Bean 定义 Profiles”)。
`@Profile` 注解实际上是通过使用一个更加灵活的注解 `@Conditional` 实现的。`@Conditional` 注解指示在注册 `@Bean` 之前应咨询的特定 `org.springframework.context.annotation.Condition` 实现。
`Condition` 接口的实现提供了 `matches(...)` 返回 `true` 或 `false` 的方法。例如,以下列表显示了 `@Profile` 使用的实际 `Condition` 实现:
```java
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
```
参考 `@Conditional` javadoc 来获取细节
#### 组合 Java 和 XML 配置
Spring 的 `@Configuration` 类支持并不旨在 100% 完全替代 Spring XML。一些设施(如 Spring XML 命名空间)仍然是配置容器的理想方式。在 XML 方便或必要的情况下,您可以选择:要么使用 `ClassPathXmlApplicationContext` 以“以 XML 为中心”的方式实例化容器,要么使用 `AnnotationConfigApplicationContext` 和 `@ImportResource` 注解以“以 Java 为中心”方式实例化容器以根据需要导入 XML。
##### 以 XML 为中心使用 @Configuration 类
最好从 XML 中引导 Spring 容器,并以特殊方式包含 `@Configuration` 类。例如,在使用 Spring XML 的大型现有代码库中,根据需要创建 `@Configuration` 类并从现有的 XML 文件中包含这些类更容易。在本节后面,我们将介绍在这种“以 XML 为中心”的情况下使用 `@Configuration` 类的选项。
**将 `@Configuration` 类声明为纯 Spring `` 元素**
请记住,`@Configuration` 类最终是容器中的 bean 定义。在本系列示例中,我们创建了一个名为 `AppConfig` 的 `@Configuration` 类,并将其作为 `` 定义包含在 `system-test-config.xml` 中。由于 `` 已打开,容器识别 `@Configuration` 注解并正确处理 `AppConfig` 中声明的 `@Bean` 方法。
以下示例显示了 Java 中的一个普通配置类:
```java
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
```
以下示例显示了示例 `system-test-config.xml` 文件的一部分:
```xml
```
以下示例显示了可能的 `jdbc.properties` 文件:
```properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
```
```java
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
```
> 在 `system-test-config.xml` 文件中,`AppConfig` `` 没有声明 id 元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 bean 引用过它,而且它不太可能通过名称从容器中显式获取。类似地,`DataSource` bean 只按类型自动装配,因此不严格要求显式 bean `id`。
**使用 `` 获取 `@Configuration` 类**
因为 `@Configuration` 是用 `@Component` 元注解的,`@Configuration` 注解的类自动成为组件扫描的候选类。使用与前面示例中描述的相同场景,我们可以重新定义 `system-test-config.xml` 以利用组件扫描。注意,在这种情况下,我们不需要显式声明 ``,因为 `` 支持相同的功能。
以下示例显示了修改后的 `system-test-config.xml` 文件:
```xml
```
##### 以 @Configuration 类为中心通过 @ImportResource 使用 XML
在 `@Configuration` 类是配置容器的主要机制的应用程序中,仍然可能需要至少使用一些 XML。在这些场景中,您可以使用 `@ImportResource`,只定义所需的 XML。这样做实现了“以 Java 为中心”的容器配置方法,并将 XML 保持在最低限度。以下示例(包括配置类、定义 bean 的 XML 文件、财产文件和 `main` 类)显示了如何使用 `@ImportResource` 注解实现“以 Java 为中心”的配置,该配置根据需要使用 XML:
```java
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
```
```xml
properties-config.xml
```
```properties
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
```
```java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
```
## 1.13 环境抽象
`Environment` 接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:profiles 和 properties。
Profile 是一个命名的、逻辑的 bean 定义组,只有当给定的 profile 处于活动状态时才能向容器注册。bean 可以分配给一个 profile,无论是用 XML 定义的还是用注解定义的。`Environment` 对象相对于配置文件的作用是确定哪些配置文件(如果有)当前处于活动状态,以及默认情况下哪些配置文件应处于活动状态。
Properties 在几乎所有应用程序中都扮演着重要角色,它可能来自多种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数、点对点(ad-hoc) `Properties` 对象、`Map` 对象等等。与属性相关的 `Environment` 对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。
### 1.13.1 Bean 定义 Profile
Bean 定义 profile 在核心容器中提供了一种机制,允许在不同环境中注册不同的 Bean。“环境”这个词对不同的用户来说可能意味着不同的东西,这个特性可以帮助处理许多用例,包括:
- 在开发中使用内存中的数据源,而不是在 QA 或生产中从 JNDI 中查找相同的数据源。
- 仅在将应用程序部署到性能环境中时注册监视基础结构。
- 为客户 A 和客户 B 部署注册 bean 的定制实现。
考虑需要 `DataSource` 的实际应用程序中的第一个用例。在测试环境中,配置可能类似于以下内容:
```java
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
```
现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录中。我们的 `dataSource` bean 现在看起来如下所示:
```java
@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
```
问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring 用户设计了许多方法来实现这一点,通常依赖于系统环境变量和包含 `${placeholder}` 标记的XML `` 语句的组合,这些标记根据环境变量的值解析为正确的配置文件路径。Bean定义概要文件是一个核心容器特性,它为这个问题提供了解决方案。
如果我们概括前面特定于环境的 bean 定义示例中所示的用例,那么我们最终需要在某些上下文中注册某些 bean 定义,而不是在其他上下文中注册。您可以说,您希望在情况 A 中注册 bean 定义的特定 profile,在情况 B 中注册不同的概要文件。我们首先更新配置以反映这一需要。
#### 使用 @Profile
`@Profile` 注解允许您在一个或多个指定的配置文件处于活动状态时指示组件符合注册条件。使用前面的示例,我们可以如下重写 `dataSource` 配置:
```java
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
```
```java
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod = "") (1)
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
// (1) `@Bean(destroyMethod = "")` 取消了默认销毁方法推断
```
> 如前所述,对于 `@Bean` 方法,您通常选择使用编程 JNDI 查找,方法是使用 Spring 的 `JndiTemplate`/`JndiLocatorDelegate` 助手或前面显示的直接 JNDI `InitialContext` 用法,而不是 `JndiObjectFactoryBean` 变量,这将迫使您将返回类型声明为 `FactoryBean` 类型。
配置文件字符串可以包含简单的配置文件名称(例如,`production`)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如 `production & us-east`)。配置文件表达式中支持以下运算符:
- `!`:一个 profile 的逻辑 `NOT`
- `&`:一个 profile 的逻辑 `AND`
- `|`:一个 profile 的逻辑 `OR`
> 如果不使用括号,则不能混合使用 `&` 和 `|` 运算符。例如,`production & us-east | eu-central`不是有效的表达式。它必须表示为 `production & (us-east | eu-central)`。
您可以使用 `@Profile` 作为元注解来创建自定义组合注解。以下示例定义了一个自定义的 `@Production` 注解,您可以将其用作 `@Profile("production")` 的插入替换:
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
```
> 如果 `@Configuration` 类标记为 `@Profile`,则所有与该类关联的 `@Bean` 方法和 `@Import` 注解都将被忽略,除非一个或多个指定的配置文件处于活动状态。如果 `@Component` 或 `@Configuration` 类标记为 `@Profile({"p1", "p2"})`,则除非配置文件“p1”或“p2”已激活,否则不会注册或处理该类。如果给定的配置文件前缀为 NOT 运算符(`!`),则仅当该配置文件处于非活动状态时,才会注册带注解的元素。例如,给定 `@Profile({"p1", "!p2"})`,如果配置文件“p1”处于活动状态或配置文件“p2”未处于活动状态,则会发生注册。
`@Profile` 也可以在方法级别声明为只包含配置类的一个特定 bean(例如,对于特定 bean 的替代变体),如下例所示:
```java
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production") (2)
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
// (1) `standaloneDataSource` 方法仅在 `development` profile 可用
// (2) `jndiDataSource` 方法仅在 `production` profile 可用
```
> 在 `@Bean` 方法上使用 `@Profile` 时,可能会出现一种特殊情况:在重载具有相同 Java 方法名称的 `@Bean` 方法的情况下(类似于构造函数重载),需要在所有重载方法上一致声明 `@Profile` 条件。如果条件不一致,只有重载方法中第一个声明的条件才重要。因此,`@Profile` 不能用于选择具有特定参数签名的重载方法。同一 bean 的所有工厂方法之间的解析在创建时遵循 Spring 的构造函数解析算法。
>
> 如果要定义具有不同 profile 条件的替代 bean,请使用 `@Bean` 名称属性,使用指向相同 bean 名称的不同 Java 方法名称,如前一个示例所示。如果参数签名都相同(例如,所有变体都没有 arg 工厂方法),这是首先在有效 Java 类中表示这种排列的唯一方法(因为只能有一个具有特定名称和参数签名的方法)。
#### XML Bean 定义 Profile
XML 对应项是 `` 元素的 `profile` 属性。我们前面的示例配置可以在两个 XML 文件中重写,如下所示:
```xml
```
```xml
```
也可以避免在同一文件中拆分和嵌套 `` 元素,如下例所示:
```xml
```
`spring-ben.xsd` 被限制为只允许文件中最后一个元素。这将有助于提供灵活性,而不会导致 XML 文件混乱。
> XML 对应项不支持前面描述的配置文件表达式。但是,可以使用 `!` 操作人员也可以通过嵌套配置文件来应用逻辑“and”,如下例所示:
>
> ```xml
> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
> xmlns:jdbc="http://www.springframework.org/schema/jdbc"
> xmlns:jee="http://www.springframework.org/schema/jee"
> xsi:schemaLocation="...">
>
>
>
>
>
>
>
>
>
> ```
>
> 在前面的例子中,`dataSource` bean 在 `production` 和 `us-east` profile 都启用的情况下被公开。
#### 启用 Profile
现在我们已经更新了配置,我们仍然需要指示 Spring 哪个 profile 是活动的。如果我们现在启动示例应用程序,我们将看到抛出 `NoSuchBeanDefinitionException`,因为容器找不到名为 `dataSource` 的Spring bean。
激活 profile 可以通过多种方式完成,但最直接的方法是通过 `ApplicationContext` 使用 Environment API 以编程方式完成。以下示例显示了如何执行此操作:
```java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
```
此外,您还可以通过 `spring.profiles.active` 属性声明性地激活 profile,该属性可以通过系统环境变量、JVM 系统属性、`web.xml` 中的 servlet 上下文参数指定,甚至可以作为 JNDI 中的条目指定(请参阅 `PropertySource` 抽象)。在集成测试中,可以使用 spring-test 模块中的 `@ActiveProfiles` 注解来声明活动概要文件(请参阅环境 profile 的上下文配置)。
请注意,配置文件不是“非此即彼”的命题。您可以同时激活多个配置文件。通过编程,您可以为 `setActiveProfiles()` 方法提供多个配置文件名称,该方法接受`String…` varargs。以下示例激活多个配置文件:
```java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
```
声明性地,`spring.profiles.active` 可以接受以逗号分隔的配置文件名称列表,如下例所示:
```
-Dspring.profiles.active="profile1,profile2"
```
#### 默认 Profile
默认配置文件表示默认情况下启用的配置文件。考虑以下示例:
```java
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
```
如果没有配置文件处于活动状态,则会创建 `dataSource`。您可以将此视为为一个或多个 bean 提供默认定义的方法。如果启用了任何配置文件,则不应用默认配置文件。
您可以通过在 `Environment` 上使用 `setDefaultProfiles()` 或使用 `spring.profiles.default` 属性以声明方式更改默认配置文件的名称。
### 1.13.2 PropertySource 抽象
Spring 的 `Environment` 抽象在可配置的属性源层次结构上提供搜索操作。考虑以下列表:
```java
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
```
在前面的代码段中,我们看到了一种高级方式,询问 Spring 是否为当前环境定义了 `my-property` 属性。为了回答这个问题,`Environment` 对象对一组 `PropertySource` 对象执行搜索。`PropertySource` 是对任何键值对源的简单抽象,Spring 的 `StandardEnvironment` 配置有两个 PropertySource 对象 —— 一个表示 JVM 系统财产集(`System.getProperties()`),另一个表示系统环境变量集(`System.getenv()`)。
> 这些默认属性源适用于 `StandardEnvironment`,用于独立应用程序。`StandardServletEnvironment` 填充了其他默认属性源,包括 servlet 配置、servlet 上下文参数和 `JndiPropertySource`(如果 JNDI 可用)。
具体地说,当您使用 `StandardEnvironment` 时,如果运行时存在 `my-property` 系统属性或 `my-property` 环境变量,则对 `env.contentsProperty("my-property")` 的调用将返回 true。
> 执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 `env.getProperty("my-property")` 的过程中,`my-property` 属性恰好在两个位置都被设置,则系统属性值“获胜”并将被返回。请注意,属性值不会被合并,而是被前面的条目完全覆盖。
> 对于常见的 `StandardServletEnvironment`,完整层次结构如下,最高优先级条目位于顶部:
>
> 1. ServletConfig 参数(如果适用 —— 例如,在 `DispatcherServlet` 上下文的情况下)
> 2. ServletContext 参数(web.xml 上下文参数条目)
> 3. JNDI 环境变量(`java:comp/env/` 条目)
> 4. JVM 系统属性(`-D` 命令行参数)
> 5. JVM 系统环境(操作系统环境变量)
最重要的是,整个机制是可配置的。也许您有一个要集成到该搜索中的自定义财产源。为此,请实现并实例化自己的 `PropertySource`,并将其添加到当前环境的 `PropertySource` 集合中。以下示例显示了如何执行此操作:
```java
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
```
在前面的代码中,`MyPropertySource` 已在搜索中以最高优先级添加。如果它包含 `my-property` 属性,则会检测并返回该属性,这有利于任何其他 `PropertySource` 中的任何 `my-property`。`MutablePropertySources` API 公开了许多方法,这些方法允许对属性源集进行精确操作。
### 1.13.3 使用 @PropertySource
`@PropertySource` 注解为向 Spring 的 `Environment` 添加 `PropertySource` 提供了一种方便的声明性机制。
给定一个名为 `app.properties` 的文件,其中包含键值对 `testbean.name=myTestBean`,以下 `@Configuration` 类使用 `@PropertySource` 的方式是调用 `testbean.getName()` 返回 `myTestBean`:
```java
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
```
任何 `@PropertySource` 资源位置中存在的 `${…}` 占位符将根据已在环境中注册的一组属性源进行解析,如下例所示:
```java
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
```
假设 `my.placeholder` 存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符被解析为相应的值。如果没有,则默认使用 `default/path`。如果未指定默认值并且无法解析属性,则会引发 `IllegalArgumentException`。
> 根据 Java 8 约定,`@PropertySource` 注解是可重复的。然而,所有此类`@PropertySource` 注解都需要在同一级别声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。不建议混合使用直接注解和元注解,因为直接注解有效地覆盖了元注解。
### 1.13.4 语句中的占位符解析
过去,元素中占位符的值只能根据 JVM 系统属性或环境变量进行解析。现在已经不是这样了。因为 `Environment` 抽象是在整个容器中集成的,所以很容易通过它来路由占位符的解析。这意味着您可以以任何方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。您还可以根据需要将自己的属性源添加到组合中。
具体而言,无论 `customer` 属性在何处定义,只要它在 `Environment` 中可用,以下语句都有效:
```xml
```
## 1.14 注册一个 LoadTimeWeaver
Spring 使用 `LoadTimeWeaver` 在类加载到 Java 虚拟机(JVM)时动态转换它们。
要启用加载时编织,可以将 `@EnableLoadTimeWeaving` 添加到 `@Configuration` 类之一,如下例所示:
```java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
```
或者,对于 XML 配置,可以使用 `context:load-timeweaver` 元素:
```xml
```
一旦为 `ApplicationContext` 配置,该 `ApplicationContext` 中的任何 bean 都可以实现 `LoadTimeWeaverware`,从而接收对加载时 weaver 实例的引用。这与 Spring 的 JPA 支持相结合特别有用,因为 JPA 类转换可能需要加载时编织。有关详细信息,请参阅 `LocalContainerEntityManagerFactoryBean` javadoc。有关 AspectJ 加载时编织的更多信息,请参阅 Spring Framework 中的使用 AspectJ 进行加载时编织。
## 1.15 ApplicationContext 的额外功能
正如在章节介绍中所讨论的,`org.springframework.beans.factory` 包提供了管理和操作 bean 的基本功能,包括以编程方式。`org.springframework.context` 包添加了 `ApplicationContext` 接口,该接口扩展了 `BeanFactory` 接口,此外还扩展了其他接口,以更面向应用程序框架的方式提供附加功能。许多人以完全声明的方式使用 `ApplicationContext`,甚至没有以编程方式创建它,而是依赖于 `ContextLoader` 等支持类来自动实例化 `ApplicationContext`,作为 Jakarta EE web 应用程序正常启动过程的一部分。
为了以更面向框架的方式增强 `BeanFactory` 功能,上下文包还提供以下功能:
- 通过 `MessageSource` 接口访问 i18n 样式的消息。
- 通过 `ResourceLoader` 接口访问资源,如 URL 和文件。
- 事件发布,即通过使用 `ApplicationEventPublisher` 接口向实现 `ApplicationListener` 接口的 bean 发布。
- 加载多个(分层)上下文,通过 `HierarchicalBeanFactory` 接口将每个上下文集中在一个特定的层上,例如应用程序的 web 层。
### 1.15.1 使用 MessageSource 的国际化
`ApplicationContext` 接口扩展了一个名为 `MessageSource` 的接口,因此提供了国际化(“i18n”)功能。Spring 还提供了 `HierarchicalMessageSource` 接口,它可以分层解析消息。这些接口共同提供了 Spring 实现消息解析的基础。这些接口上定义的方法包括:
- `String getMessage(String code, Object[] args, String default, Locale loc)`:用于从 `MessageSource` 检索消息的基本方法。如果未找到指定区域设置的消息,则使用默认消息。使用标准库提供的 `MessageFormat` 功能,传入的任何参数都将成为替换值。
- `String getMessage(String code, Object[] args, Locale loc)`:基本上与前面的方法相同,但有一个区别:无法指定默认消息。如果找不到消息,将引发 `NoSuchMessageException`。
- `String getMessage(MessageSourceResolvable resolvable, Locale Locale)`:上述方法中使用的所有属性都封装在名为 `MessageSourceResovable` 的类中,您可以将其与此方法一起使用。
当加载 `ApplicationContext` 时,它会自动搜索上下文中定义的 `MessageSource` bean。bean 必须具有名称 `messageSource`。如果找到这样一个 bean,则对前面方法的所有调用都将委托给消息源。如果找不到消息源,`ApplicationContext` 将尝试查找包含同名 bean 的父级。如果是,则使用该 bean 作为 `MessageSource`。如果 `ApplicationContext` 找不到任何消息源,则实例化空 `DelegatingMessageSource`,以便能够接受对上面定义的方法的调用。
Spring 提供了三种 `MessageSource` 实现,`ResourceBundleMessageSource`、`ReloadableResourceBundleMessageSource` 和 `StaticMessageSource`。它们都实现了 `HierarchicalMessageSource`,以便进行嵌套消息传递。`StaticMessageSource` 很少使用,但提供了向源添加消息的编程方式。以下示例显示 `ResourceBundleMessageSource`:
```xml
format
exceptions
windows
```
该示例假设您在类路径中定义了三个资源包,称为 `format`、`exceptions` 和 `windows`。任何解析消息的请求都是通过 `ResourceBundle` 对象解析消息的 JDK 标准方式处理的。出于示例的目的,假设上述两个资源包文件的内容如下:
```properties
# in format.properties
message=Alligators rock!
```
```properties
# in exceptions.properties
argument.required=The {0} argument is required.
```
下一个示例显示了运行 `MessageSource` 功能的程序。请记住,所有 `ApplicationContext` 实现也是 `MessageSource` 实现,因此可以强制转换为 `MessageSource` 接口。
```java
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
System.out.println(message);
}
```
上述程序的结果如下:
```
Alligators rock!
```
总之,`MessageSource` 在一个名为 `beans.xml` 的文件中定义,该文件位于类路径的根目录中。`messageSource` bean 定义通过其 `basenames` 属性引用了许多资源包。列表中传递给 `basenames` 属性的三个文件作为文件存在于类路径的根目录中,分别称为 `format.properties`、`exceptions.properties` 和 `windows.properties`。
下一个示例显示传递给消息查找的参数。这些参数被转换为 `String` 对象,并插入到查找消息中的占位符中。
```xml
```
```java
public class Example {
private MessageSource messages;
public void setMessages(MessageSource messages) {
this.messages = messages;
}
public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.ENGLISH);
System.out.println(message);
}
}
```
调用 `execute()` 方法的结果输出如下:
```
The userDao argument is required.
```
关于国际化(“i18n”),Spring 的各种 `MessageSource` 实现遵循与标准 JDK `ResourceBundle` 相同的区域设置解析和回退规则。简言之,继续前面定义的示例 `messageSource`,如果您想根据英国(`en-GB`)语言环境解析消息,您将分别创建名为 `format_en_GB.properties`、`exceptions_en_GB.properties` 和 `windows_en_GB.properties` 的文件。
通常,区域设置解析由应用程序的周围环境管理。在以下示例中,解析(英国)消息所依据的区域设置是手动指定的:
```properties
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
```
```java
public static void main(final String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.UK);
System.out.println(message);
}
```
运行上述程序的结果如下:
```
Ebagum lad, the 'userDao' argument is required, I say, required.
```
您还可以使用 `MessageSourceAware` 接口获取对已定义的任何 `MessageSource` 的引用。在创建和配置 bean 时,在实现 `MessageSourceAware` 接口的 `ApplicationContext` 中定义的任何 bean 都会被注入应用程序上下文的 `MessageSource`。
> 因为 Spring 的 `MessageSource` 基于 Java 的 `ResourceBundle`,所以它不会合并具有相同基本名称的捆绑包,而是只使用找到的第一个捆绑包。具有相同基名称的后续消息束将被忽略。
> 作为 `ResourceBundleMessageSource` 的替代,Spring 提供了 `ReloadableResourceBundleMessage` 类。此变体支持相同的捆绑文件格式,但比基于标准 JDK 的 `ResourceBundleMessageSource` 实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅仅是从类路径)读取文件,并支持热重载捆绑包属性文件(同时有效地缓存它们)。有关详细信息,请参阅`ReloadableResourceBundleMessageSource` javadoc。
### 1.15.2 标准和自定义事件
`ApplicationContext` 中的事件处理是通过 `ApplicationEvent` 类和 `ApplicationListener` 接口提供的。如果将实现 `ApplicationListener` 接口的 bean 部署到上下文中,则每次将 `ApplicationEvent` 发布到 `ApplicationContext` 时,都会通知该 bean。本质上,这是标准的 Observer 设计模式。
> 从 Spring 4.2 开始,事件基础结构得到了显著改进,提供了基于注解的模型,以及发布任意事件(即,不一定从 `ApplicationEvent` 扩展的对象)的能力。当发布这样的对象时,我们会将其包装在事件中。
下表描述了 Spring 提供的标准事件:
| 事件 | 解释 |
| :--------------------------- | :----------------------------------------------------------- |
| `ContextRefreshedEvent` | 在初始化或刷新 `ApplicationContext` 时发布(例如,通过在 `ConfigurationApplicationContext` 接口上使用 `refresh()` 方法)。这里,“initialized” 意味着加载所有 bean,检测并激活后处理器 bean,预实例化单体,并准备好使用 `ApplicationContext` 对象。只要上下文尚未关闭,就可以多次触发刷新,前提是所选的 `ApplicationContext` 实际上支持此类“热”刷新。例如,`XmlWebApplicationContext` 支持热刷新,但 `GenericApplicationContext` 不支持。 |
| `ContextStartedEvent` | 在 `ConfigurationApplicationContext` 接口上使用 `start()` 方法启动 `ApplicationContext` 时发布。这里,“已启动”意味着所有 `lifecycle` bean 都会收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。 |
| `ContextStoppedEvent` | 在 `ConfigurationApplicationContext` 接口上使用 `stop()` 方法停止 `ApplicationContext` 时发布。这里,“stopped” 表示所有 `Lifecycle` bean都收到一个明确的停止信号。可以通过 `start()` 调用重新启动已停止的上下文。 |
| `ContextClosedEvent` | 通过在 `ConfigurationApplicationContext` 接口上使用 `close()` 方法或通过 JVM 关闭挂钩关闭 `ApplicationContext` 时发布。这里,“closed” 表示所有单例 bean 都将被销毁。一旦上下文关闭,它将到达生命的终点,无法刷新或重新启动。 |
| `RequestHandledEvent` | 一个特定于 web 的事件,告诉所有 bean HTTP 请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 `DispatcherServlet` 的 web 应用程序。 |
| `ServletRequestHandledEvent` | 添加 Servlet 特定上下文信息的 `RequestHandledEvent` 的子类。 |
您还可以创建和发布自己的自定义事件。以下示例显示了一个扩展 Spring 的 `ApplicationEvent` 基类的简单类:
```java
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
```
要发布自定义 `ApplicationEvent`,请在 `ApplicationEventPublisher` 上调用 `publishEvent()` 方法。通常,这是通过创建一个实现 `ApplicationEventPublisherAware` 的类并将其注册为 Spring bean 来完成的。以下示例显示了这样的类:
```java
public class EmailService implements ApplicationEventPublisherAware {
private List blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
```
在配置时,Spring 容器检测到 `EmailService` 实现了 `ApplicationEventPublisherAware` 并自动调用 `setApplicationEventPublisher()`。实际上,传入的参数是 Spring 容器本身。您正在通过其 `ApplicationEventPublisher` 接口与应用程序上下文交互。
要接收自定义 `ApplicationEvent`,可以创建一个实现 `ApplicationListener` 的类,并将其注册为 Spring bean。以下示例显示了这样的类:
```java
public class BlockedListNotifier implements ApplicationListener {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
```
请注意,`ApplicationListener` 是用自定义事件的类型(在前面的示例中为 `BlockedListEvent`)进行参数化的。这意味着 `onApplicationEvent()` 方法可以保持类型安全,避免任何向下转换的需要。您可以注册任意数量的事件侦听器,但请注意,默认情况下,事件侦听器同步接收事件。这意味着 `publishEvent()` 方法将阻塞,直到所有侦听器完成对事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布器的事务上下文中进行操作。如果需要另一种事件发布策略,请参阅 javadoc for Spring 的 `ApplicationEventMulticaster` 接口和 `SimpleApplicationEventMultiaster` 实现以获取配置选项。
以下示例显示了用于注册和配置上述每个类的 bean 定义:
```xml
known.spammer@example.org
known.hacker@example.org
john.doe@example.org
```
总之,当调用 `emailService` bean 的 `sendEmail()` 方法时,如果有任何电子邮件消息应该被阻止,则会发布 `BlockedListEvent` 类型的自定义事件。`blockedListNotifier` bean 注册为 `ApplicationListener` 并接收 `BlockedListEvent`,此时它可以通知适当的各方。
> Spring 的事件机制设计用于同一应用程序上下文中 Spring bean 之间的简单通信。然而,对于更复杂的企业集成需求,单独维护的 Spring integration 项目为构建基于众所周知的 Spring 编程模型的轻量级、面向模式、事件驱动架构提供了完整的支持。
#### 基于注解的事件监听器
您可以使用 `@EventListener` 注解在托管 bean 的任何方法上注册事件侦听器。`BlockedListNotifier` 可以重写如下:
```java
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
```
方法签名再次声明了它侦听的事件类型,但这次使用了一个灵活的名称,并且没有实现特定的侦听器接口。只要实际事件类型解析其实现层次结构中的泛型参数,也可以通过泛型缩小事件类型。
如果您的方法应该侦听多个事件,或者如果您想在没有参数的情况下定义它,那么也可以在注解本身上指定事件类型。以下示例显示了如何执行此操作:
```java
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
```
还可以通过使用定义 `SpEL` 表达式的注解的 `condition` 属性添加额外的运行时过滤,该表达式应与实际调用特定事件的方法相匹配。
以下示例显示了如何重写通知程序,以便仅在事件的 `content` 属性等于 `my-event` 时调用:
```java
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
```
每个 `SpEL` 表达式都根据专用上下文进行计算。下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:
| 名字 | 位置 | 描述 | 例子 |
| :------- | :--------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| 事件 | 根对象 | 真正的 `ApplicationEvent`. | `#root.event` 或 `event` |
| 参数数组 | 根对象 | 用来调用方法的参数(作为对象数组) | `#root.args` 或 `args`; `args[0]` 来访问第一个参数,等等 |
| 参数名字 | 求值上下文 | 任何方法参数的名称。如果由于某些原因,名称不可用(例如,因为编译的字节代码中没有调试信息),则使用 `#a<#arg>` 语法也可以使用单个参数,其中 `<#arg>` 代表参数索引(从 0 开始)。 | `#blEvent` 或 `#a0` (你也可以使用 `#p0` 或 `#p<#arg>` 参数符号作为别名) |
请注意,`#root.event` 允许您访问基础事件,即使您的方法签名实际上引用了已发布的任意对象。
如果您需要发布一个事件作为处理另一个事件的结果,则可以更改方法签名以返回应该发布的事件,如下例所示:
```java
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
```
> 该特性不支持异步监听器
`handleBlockedListEvent()` 方法为其处理的每个 `BlockedListEvent` 发布一个新的 `ListUpdateEvent`。如果需要发布多个事件,则可以返回集合或事件数组。
#### 异步监听器
如果希望特定侦听器异步处理事件,可以重用常规的 `@Async` 支持。以下示例显示了如何执行此操作:
```java
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
```
使用异步事件时,请注意以下限制:
- 如果异步事件侦听器抛出 `Exception`,则不会将其传播给调用者。有关详细信息,请参阅 `AsyncUnaughtExceptionHandler`。
- 异步事件侦听器方法无法通过返回值来发布后续事件。如果您需要作为处理结果发布另一个事件,请注入 `ApplicationEventPublisher` 以手动发布事件。
#### 监听器排序
如果需要在调用另一个监听器之前调用一个监听器,可以将 `@Order` 注解添加到方法声明中,如下例所示:
```java
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
```
#### 泛型事件
您还可以使用泛型来进一步定义事件的结构。考虑使用 `EntityCreatedEvent`,其中 `T` 是创建的实际实体的类型。例如,可以创建以下侦听器定义以仅接收 `Person` 的 `EntityCreatedEvent`:
```java
@EventListener
public void onPersonCreated(EntityCreatedEvent event) {
// ...
}
```
由于类型擦除,只有在激发的事件解析了事件侦听器筛选的通用参数(即 `class PersonCreatedEvent extends EntityCreatedEvent{…}`).
在某些情况下,如果所有事件都遵循相同的结构,这可能会变得非常乏味(上一个示例中的事件就是如此)。在这种情况下,您可以实现 `ResolutibleTypeProvider` 来指导框架超出运行时环境所提供的范围。以下事件显示了如何执行此操作:
```java
public class EntityCreatedEvent extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
```
> 这不仅适用于 `ApplicationEvent`,也适用于作为事件发送的任意对象。
### 1.15.3 便捷访问低级资源
为了优化应用程序上下文的使用和理解,您应该熟悉 Spring 的 `Resource` 抽象,如参考资料中所述。
应用程序上下文是 `ResourceLoader`,可用于加载 `Resource` 对象。`Resource` 本质上是 JDK `java.net.URL` 类的一个功能更丰富的版本。事实上,`Resource` 的实现在适当的情况下包装 `java.net.URL` 的实例。`Resource` 可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、使用标准 URL 可描述的任何位置以及一些其他变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
您可以将部署到应用程序上下文中的 bean 配置为实现特殊的回调接口 `ResourceLoaderAware`,以便在初始化时自动调用,并将应用程序上下文本身作为 `ResourceLoader` 传入。您还可以公开 `Resource` 类型的财产,用于访问静态资源。它们像其他财产一样被注入其中。您可以将这些 `Resource` 财产指定为简单的 `String` 路径,并依赖于在部署 bean 时从这些文本字符串到实际 `Resource` 对象的自动转换。
提供给 `ApplicationContext` 构造函数的一个或多个位置路径实际上是资源字符串,以简单的形式,根据特定的上下文实现进行适当处理。例如,`ClassPathXmlApplicationContext` 将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 加载定义,而不考虑实际的上下文类型。
### 1.15.4 应用启动跟踪
`ApplicationContext` 管理 Spring 应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可能具有同样复杂的组件图和启动阶段。
使用特定指标跟踪应用程序启动步骤有助于了解启动阶段所花费的时间,但也可以作为更好地了解整个上下文生命周期的一种方式。
`AbstractApplicationContext`(及其子类)装有 `ApplicationStartup`,它收集有关各种启动阶段的 `StartupStep` 数据:
- 应用程序上下文生命周期(基本包扫描、配置类管理)
- bean 生命周期(实例化、智能初始化、后处理)
- 应用程序事件处理
以下是 `AnnotationConfigApplicationContext` 中的插入示例:
```java
// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
```
应用程序上下文已检测到多个步骤。一旦记录下来,就可以使用特定的工具收集、显示和分析这些启动步骤。有关现有启动步骤的完整列表,您可以查看专用的附录部分。
默认的 `ApplicationStartup` 实现是一个无操作变量,开销最小。这意味着默认情况下,在应用程序启动期间不会收集任何度量。Spring Framework 附带了一个使用Java FlightRecorder:`FlightRecorderApplicationStartup` 跟踪启动步骤的实现。要使用此变体,必须在创建该变体后立即将其实例配置到 `ApplicationContext`。
如果开发人员提供自己的 `AbstractApplicationContext` 子类,或者希望收集更精确的数据,那么他们也可以使用 `ApplicationStartup` 基础设施。
> `ApplicationStartup` 仅用于应用程序启动期间和核心容器;这绝不是 Java profiler 或 Micrometer 等度量库的替代品。
要开始收集自定义 `StartupStep`,组件可以直接从应用程序上下文中获取 `ApplicationStartup` 实例,使其组件实现 `ApplicationStartupAware`,或在任何注入点上请求 `ApplicationStartup` 类型。
> 开发人员在创建自定义启动步骤时不应使用 `spring.*` 命名空间。此名称空间是为内部 Spring 使用而保留的,并且可能会更改。
### 1.15.5 Web 应用的便利 ApplicationContext 实例化
例如,可以使用 `ContextLoader` 以声明方式创建 `ApplicationContext` 实例。当然,您也可以通过使用 `ApplicationContext` 实现之一以编程方式创建 `ApplicationContext` 实例。
您可以使用 `ContextLoaderListener` 注册 `ApplicationContext`,如下例所示:
```xml
contextConfigLocation
/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml
org.springframework.web.context.ContextLoaderListener
```
侦听器检查 `contextConfigLocation` 参数。如果该参数不存在,则侦听器使用 `/WEB-INF/applicationContext.xml` 作为默认值。当参数确实存在时,侦听器使用预定义的分隔符(逗号、分号和空格)分隔字符串,并将这些值用作搜索应用程序上下文的位置。还支持 Ant 样式的路径模式。示例有 `/WEB-INF/*Context.xml`(适用于名称以 `Context.xml` 结尾且位于 `WEB-INF` 目录中的所有文件)和 `/WEB-IINF/**/*Context.xml`(适用于 `WEB-INF` 任何子目录中的此类文件)。
### 1.15.6 作为 Jarkarta EE RAR 文件部署 Spring ApplicationContext
可以将 Spring `ApplicationContext` 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Jakarta EE RAR 部署单元中。这相当于引导一个独立的 `ApplicationContext`(仅在 Jakarta EE 环境中托管)能够访问 Jakarta 的 EE 服务器设施。RAR 部署是部署无头 WAR 文件场景的更自然的替代方案 —— 实际上,这是一个没有任何 HTTP 入口点的 WAR 文件,仅用于在 Jakarta EE 环境中引导 Spring `ApplicationContext`。
RAR 部署非常适合不需要 HTTP 入口点,而只包含消息端点和调度作业的应用程序上下文。这种上下文中的 bean 可以使用应用程序服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC `DataSource` 实例和 JMS `ConnectionFactory` 实例,还可以向平台的 JMX 服务器注册 — 所有这些都通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施实现。应用程序组件还可以通过 Spring 的 `TaskExecutor` 抽象与应用程序服务器的 JCA `WorkManager` 交互。
有关 RAR 部署中涉及的配置详细信息,请参阅 `SpringContextResourceAdapter` 类的 javadoc。
对于 Spring ApplicationContext 作为 Jakarta EE RAR 文件的简单部署:
1. 将所有应用程序类打包到一个 RAR 文件中(这是一个具有不同文件扩展名的标准 JAR 文件)。
2. 将所有必需的库 JAR 添加到 RAR 归档的根目录中。
3. 添加 `META-INF/ra.xml` 部署描述符(如 `SpringContextResourceAdapter` 的 javadoc 中所示)和相应的 Spring XML bean 定义文件(通常为 `META-INF/applicationContext.xml`)。
4. 将生成的 RAR 文件放到应用程序服务器的部署目录中。
> 这种 RAR 部署单元通常是独立的。它们不向外部世界公开组件,甚至不向同一应用程序的其他模块公开组件。与基于 RAR 的 `ApplicationContext` 的交互通常通过与其他模块共享的 JMS 目标发生。例如,基于 RAR 的 `ApplicationContext` 还可以调度一些作业或对文件系统中的新文件做出反应(等等)。如果它需要允许从外部进行同步访问,它可以(例如)导出 RMI 端点,该端点可以由同一机器上的其他应用程序模块使用。
## 1.16 BeanFactory API
`BeanFactory` API 为 Spring 的 IoC 功能提供了基础。它的特定契约主要用于与 Spring 的其他部分和相关的第三方框架集成,其 `DefaultListableBeanFactory` 实现是更高级别 `GenericApplicationContext` 容器中的关键委托。
`BeanFactory` 和相关接口(如 `BeanFactoryAware`、`InitializingBean`、`DisposableBean`)是其他框架组件的重要集成点。通过不需要任何注解甚至反射,它们允许容器及其组件之间进行非常有效的交互。应用程序级 bean 可以使用相同的回调接口,但通常更喜欢通过注解或编程配置进行声明性依赖注入。
请注意,核心 `BeanFactory` API 级别及其 `DefaultListableBeanFactory` 实现没有对要使用的配置格式或任何组件注解进行假设。所有这些风格都是通过扩展(如 `XmlBeanDefinitionReader` 和 `AutowiredAnnotationBeanPostProcessor`)引入的,并作为核心元数据表示在共享 `BeanDefinition` 对象上运行。这就是 Spring 容器如此灵活和可扩展的本质。
### 1.16.1 BeanFactory 或 ApplicationContext
本节解释 `BeanFactory` 和 `ApplicationContext` 容器级别之间的差异以及对引导的影响。
除非有充分的理由不使用 `ApplicationContext`,否则应该使用 `ApplicationContext`。`GenericApplicationContext` 及其子类 `AnnotationConfigApplicationContext` 是自定义引导的常见实现。这些是 Spring 核心容器的主要入口点,用于所有常见目的:加载配置文件、触发类路径扫描、以编程方式注册 bean 定义和注解类,以及(从 5.0 开始)注册函数 bean 定义。
由于 `ApplicationContext` 包含 `BeanFactory` 的所有功能,因此通常建议使用它而不是普通的 `BeanFactory`,除非需要对 bean 处理进行完全控制。在 `ApplicationContext`(例如 `GenericApplicationContext` 实现)中,按约定(即,按 bean 名称或 bean 类型)检测几种 bean —— 特别是后处理器),而普通的 `DefaultListableBeanFactory` 对任何特殊 bean 都是不可知的。
对于许多扩展的容器特性,如注释处理和 AOP 代理,`BeanPostProcessor` 扩展点是必不可少的。如果只使用普通的 `DefaultListableBeanFactory`,则默认情况下不会检测到并激活此类后处理器。这种情况可能令人困惑,因为您的 bean 配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置来完全引导容器。
下表列出了 `BeanFactory` 和 `ApplicationContext` 接口和实现提供的功能。
| 特性 | `BeanFactory` | `ApplicationContext` |
| :----------------------------------- | :------------ | :------------------- |
| Bean 初始化/装配 | 是 | 是 |
| 整合的生命周期管理 | 否 | 是 |
| 自动 `BeanPostProcessor` 注册 | 否 | 是 |
| 自动 `BeanFactoryPostProcessor` 注册 | 否 | 是 |
| 便利的 `MessageSource` 访问(国际化) | 否 | 是 |
| 内建的 `ApplicationEvent` 发布机制 | 否 | 是 |
要向 `DefaultListableBeanFactory` 显式注册 bean 后处理器,需要以编程方式调用 `addBeanPostProcessor`,如下例所示:
```java
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// now start using the factory
```
要将 `BeanFactoryPostProcessor` 应用于纯 `DefaultListableBeanFactory`,需要调用其 `postProcessBeanFactory` 方法,如下例所示:
```java
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
```
在这两种情况下,显式注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种 `ApplicationContext` 变体比纯 `DefaultListableBeanFactory` 更受欢迎,尤其是在典型的企业设置中,依赖 `BeanFactoryPostProcessor` 和 `BeanPostProcessor` 实例实现扩展容器功能时。
> `AnnotationConfigApplicationContext` 注册了所有常见的注解后处理器,并可能通过配置注解(如 `@EnableTransactionManagement`)在封面下引入其他处理器。在 Spring 基于注解的配置模型的抽象级别,bean 后处理器的概念仅仅是一个内部容器细节。
# 2. 资源
本章介绍 Spring 如何处理资源以及如何在 Spring 中使用资源。它包括以下主题:
- 介绍
- `Resource` 接口
- 内置 `Resource` 实现
- `ResourceLoader` 接口
- `ResourcePatternResolver` 接口
- `ResourceLoaderAware` 接口
- 作为依赖项的资源
- 应用程序上下文和资源路径
## 2.1 介绍
不幸的是,Java 的标准 `java.net.URL` 类和各种 URL 前缀的标准处理程序对于所有低级资源的访问都不够。例如,没有标准化的 `URL` 实现可用于访问需要从类路径或相对于 `ServletContext` 获得的资源。虽然可以为专门的 `URL` 前缀注册新的处理程序(类似于现有的前缀处理程序,例如 `http:`),但这通常非常复杂,并且 `URL` 接口仍然缺少一些理想的功能,例如检查所指向资源是否存在的方法。
## 2.2 Resource 接口
Spring 位于 `org.springframework.core.io.package` 中的 `Resource` 接口旨在成为一个更强大的接口,用于抽象对低级资源的访问。下面的列表提供了 `Resource` 接口的概述。有关详细信息,请参阅 `Resource` javadoc。
```java
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
```
如 `Resource` 接口的定义所示,它扩展了 `InputStreamSource` 接口。以下列表显示 `InputStreamSource` 接口的定义:
```java
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
```
`Resource` 接口中一些最重要的方法是:
- `getInputStream()`:定位并打开资源,返回一个 `InputStream` 以读取资源。预期每次调用都会返回一个新的 `InputStream`。调用者负责关闭流。
- `exists()`:返回一个 `boolean`,指示该资源是否以物理形式存在。
- `isOpen()`:返回一个 `boolean`,指示此资源是否表示具有开放流的句柄。如果为 `true`,则不能多次读取 `InputStream`,必须仅读取一次,然后关闭以避免资源泄漏。对于所有常见的资源实现,返回 `false`,`InputStreamResource` 除外。
- `getDescription()`:返回此资源的描述,用于处理资源时的错误输出。这通常是完全限定的文件名或资源的实际 URL。
其他方法允许您获得表示资源的实际 `URL` 或 `File` 对象(如果底层实现兼容并支持该功能)。
`Resource` 接口的一些实现还为支持写入的资源实现了扩展的 `WritableResource` 接口。
当需要资源时,Spring 本身广泛使用 `Resource` 抽象,作为许多方法签名中的参数类型。某些 Spring API 中的其他方法(例如各种 `ApplicationContext` 实现的构造函数)采用一个 `Spring`,该字符串以未修饰或简单的形式用于创建适合该上下文实现的 `Resource`,或者通过 `String` 路径上的特殊前缀,让调用者指定必须创建和使用特定的 `Resource` 实现。
虽然 Spring 经常使用 `Resource` 接口,但实际上在您自己的代码中单独用作通用工具类非常方便,即使您的代码不知道或不关心 Spring 的任何其他部分,也可以访问资源。虽然这将您的代码耦合到 Spring,但它实际上只将它耦合到这一小组实用程序类,这是 URL 的一个更强大的替代品,可以被视为等同于用于此目的的任何其他库。
> `Resource` 抽象并不取代功能。它尽可能将其包裹起来。例如,`UrlResource` 包装 URL 并使用包装的 `URL` 来完成其工作。
## 2.3 内建 Resource 实现
Spring包括几个内置的 `Resource` 实现:
- [`UrlResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-urlresource)
- [`ClassPathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-classpathresource)
- [`FileSystemResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-filesystemresource)
- [`PathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-pathresource)
- [`ServletContextResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-servletcontextresource)
- [`InputStreamResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-inputstreamresource)
- [`ByteArrayResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-bytearrayresource)
有关 Spring 中可用的 `Resource` 实现的完整列表,请参阅 `Resource` javadoc 的“所有已知实现类”部分。
### 2.3.1 UrlResource
`UrlResource` 包装 `java.net.URL`,可用于访问通常可通过 URL 访问的任何对象,例如文件、HTTPS 目标、FTP 目标等。所有 URL 都有一个标准化的 `String` 表示,因此使用适当的标准化前缀来指示一种 URL 类型与另一种 URL。这包括 `file:` 用于访问文件系统路径,`https:` 用于通过 https 协议访问资源,`ftp:` 用于通过 ftp 访问资源,以及其他。
`UrlResource` 是由 Java 代码通过显式使用 `UrlResource` 构造函数创建的,但通常在调用使用 `String` 参数表示路径的 API 方法时隐式创建。对于后一种情况,JavaBeans `PropertyEditor` 最终决定要创建哪种类型的 `Resource`。如果路径字符串包含一个众所周知的(到属性编辑器,即)前缀(例如 `classpath:`),它将为该前缀创建一个适当的专用 `Resource`。但是,如果它不识别前缀,则假定该字符串是标准 URL 字符串,并创建 `UrlResource`。
### 2.3.2 ClassPathResource
此类表示应从类路径中获取的资源。它使用线程上下文类加载器、给定类加载器或给定类来加载资源。
如果类路径资源驻留在文件系统中,但对于驻留在 jar 中且尚未(通过 servlet 引擎或其他环境)扩展到文件系统的类路径资源,则此 `Resource` 实现支持将其解析为 `java.io.File`。为了解决这个问题,各种 `Resource` 实现始终支持 `java.net.URL` 的解析。
`ClassPathResource` 是由 Java 代码通过显式使用 `ClassPathResource` 构造函数创建的,但通常在调用使用 `String` 参数表示路径的 API 方法时隐式创建。对于后一种情况,JavaBeans `PropertyEditor` 识别字符串路径上的特殊前缀 `classpath:`,并在这种情况下创建 `ClassPathResource`。
### 2.3.3 FileSystemResource
这是 `java.io.File` 句柄的 `Resource` 实现。它还支持 `java.nio.file.Path` 句柄,应用 Spring 的标准基于字符串的路径转换,但通过 `java.nio.file.Files` API 执行所有操作。对于纯 `java.nio.path.Path` 支持,请改用 `PathResource`。`FileSystemResource` 支持作为 `File` 和 `URL` 进行解析。
### 2.3.4 PathResource
这是 `java.nio.file.Path` 句柄的 `Resource` 实现,通过 `Path` API 执行所有操作和转换。它支持作为 `File` 和 `URL` 的解析,还实现了扩展的 `WritableResource` 接口。`PathResource` 实际上是 `FileSystemResource` 的纯 `java.nio.path.Path` 替代品,具有不同的 `createRelative` 行为。
### 2.3.5 ServletContextResource
这是 `ServletContext` 资源的 `Resource` 实现,用于解释相关 web 应用程序根目录中的相对路径。
它始终支持流访问和 URL 访问,但仅当 web 应用程序存档被扩展并且资源物理上位于文件系统上时,才允许 `java.io.File` 访问。无论它是在文件系统上扩展还是直接从 JAR 或其他地方访问(这是可以想象的),实际上都取决于 Servlet 容器。
### 2.3.6 InputStreamResource
`InputStreamResource` 是给定 `InputStream` 的 `Resource` 实现。只有在没有适用的特定资源实施时,才应使用它。特别是,在可能的情况下,首选 `ByteArrayResource` 或任何基于文件的 `Resource` 实现。
与其他 `Resource` 实现不同,这是一个已打开资源的描述符。因此,它从 `isOpen()` 返回 `true`。如果需要将资源描述符保存在某处,或者需要多次读取流,请不要使用它。
### 2.3.7 ByteArrayResource
这是给定字节数组的 `Resource` 实现。它为给定的字节数组创建 `ByteArrayInputStream`。
它对于从任何给定字节数组加载内容都非常有用,而无需使用一次性 `InputStreamResource`。
## 2.4 ResourceLoader 接口
`ResourceLoader` 接口由可以返回(即加载)`Resource` 实例的对象实现。以下列表显示 `ResourceLoader` 接口定义:
```java
public interface ResourceLoader {
Resource getResource(String location);
ClassLoader getClassLoader();
}
```
所有应用程序上下文都实现 `ResourceLoader` 接口。因此,可以使用所有应用上下文来获得资源实例。
当您在特定的应用程序上下文上调用 `getResource()`,并且指定的位置路径没有特定的前缀时,您将返回适合于该特定应用程序上下文的 Resource 类型。例如,假设对 `ClassPathXmlApplicationContext` 实例运行了以下代码片段:
```java
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
```
针对 `ClassPathXmlApplicationContext`,该代码返回 `ClassPathResource`。如果对 `FileSystemXmlApplicationContext` 实例运行相同的方法,它将返回 `FileSystemResource`。对于 `WebApplicationContext`,它将返回 `ServletContextResource`。它同样会为每个上下文返回适当的对象。
因此,您可以以适合特定应用程序上下文的方式加载资源。
另一方面,您也可以通过指定特殊的 `classpath:` 前缀来强制使用 `ClassPathResource`,无论应用程序上下文类型如何,如下例所示:
```java
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
```
类似地,您可以通过指定任何标准 `java.net.URL` 前缀来强制使用 `UrlResource`。以下示例使用 `file` 和 `https` 前缀:
```java
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
```
```java
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
```
下表总结了将 `String` 对象转换为 `Resource` 对象的策略:
| 前缀 | 例子 | 解释 |
| :--------- | :------------------------------- | :----------------------------------------------------------- |
| classpath: | `classpath:com/myapp/config.xml` | 从类路径加载 |
| file: | `file:///data/config.xml` | 作为 `URL` 从文件系统中加载。 参考 [`FileSystemResource` 警告](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-filesystemresource-caveats). |
| https: | `https://myserver/logo.png` | 作为 `URL` 加载 |
| (无) | `/data/config.xml` | 取决于底层的 `ApplicationContext`. |
## 2.5 ResourcePatternResolver 接口
`ResourcePatternResolver` 接口是 `ResourceLoader` 接口的扩展,它定义了将位置模式(例如,Ant 样式的路径模式)解析为 `Resource` 对象的策略。
```java
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
```
如上所述,该接口还为类路径中的所有匹配资源定义了一个特殊的 `classpath*:` 资源前缀。注意,在这种情况下,资源位置应该是没有占位符的路径 —— 例如,`classpath*:/config/beans.xml`。JAR 文件或类路径中的不同目录可以包含具有相同路径和相同名称的多个文件。请参阅应用上下文构造方法资源路径及其子部分中的通配符,以了解有关 `classpath*:` 资源前缀的通配符支持的更多详细信息。
可以检查传入的 `ResourceLoader`(例如,通过 `ResourceLoaderAware` 语义提供的一个)是否也实现了这个扩展接口。
`PathMatchingResourcePatternResolver` 是一个独立的实现,可在 `ApplicationContext` 外部使用,`ResourceArrayPropertyEditor` 也使用它来填充 `Resource[]` bean 属性。`PathMatchingResourcePatternResolver` 能够将指定的资源位置路径解析为一个或多个匹配的 `Resource` 对象。源路径可以是一个简单的路径,它具有到目标资源的一对一映射,或者可以包含特殊的 `classpath*:` 前缀和/或内部 Ant 样式的正则表达式(使用 Spring 的 `org.springframework.util.AntPathMatcher` 实用程序进行匹配)。后者两者都是有效的通配符。
> 任何标准 `ApplicationContext` 中的默认 `ResourceLoader` 实际上是实现 `ResourcePatternResolver` 接口的 `PathMatchingResourcePatternSolver` 的实例。`ApplicationContext` 实例本身也是如此,它也实现 `ResourcePatternResolver` 接口并委托给默认的 `PathMatchingResourcePatternSolver`。
## 2.6 ResourceLoaderAware 接口
`ResourceLoaderAware` 接口是一个特殊的回调接口,用于标识希望提供 `ResourceLoader` 引用的组件。以下列表显示了 `ResourceLoaderAware` 接口的定义:
```java
public interface ResourceLoaderAware {
void setResourceLoader(ResourceLoader resourceLoader);
}
```
当类实现 `ResourceLoaderAware` 并部署到应用程序上下文中(作为 Spring 托管 bean)时,应用程序上下文将其识别为 `ResourceLoaderAware`。然后,应用程序上下文调用 `setResourceLoader(ResourceLoader)`,将自身作为参数提供(记住,Spring 中的所有应用程序上下文都实现 `ResourceLoader` 接口)。
由于 `ApplicationContext` 是 `ResourceLoader`,bean 还可以实现 `ApplicationContextAware` 接口,并直接使用提供的应用程序上下文来加载资源。然而,一般来说,如果您只需要使用专门的 `ResourceLoader` 接口,那么最好使用它。代码将仅耦合到资源加载接口(可以被视为实用程序接口),而不耦合到整个Spring `ApplicationContext` 接口。
在应用程序组件中,您还可以依赖 `ResourceLoader` 的自动装配来实现 `ResourceLoaderAware` 接口。传统的 `constructor` 和 `byType` 自动装配模式(如“自动装配协作者”中所述)能够分别为构造函数参数或 setter 方法参数提供 `ResourceLoader`。要获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动关联功能。在这种情况下,`ResourceLoader` 会自动装配到需要 `ResourceLoader` 类型的字段、构造函数参数或方法参数中,只要该字段、构造函数或方法带有 `@Autowired` 注解。有关详细信息,请参阅“使用`@Autowired`”。
> 要为包含通配符或使用特殊 `classpath*:` 资源前缀的资源路径加载一个或多个 `Resource` 对象,请考虑将 `ResourcePatternResolver` 的实例自动装配到应用程序组件中,而不是 `ResourceLoader`。
## 2.7 作为依赖的资源
如果 bean 本身将通过某种动态过程确定并提供资源路径,那么 bean 使用 `ResourceLoader` 或 `ResourcePatternResolver` 接口加载资源可能是有意义的。例如,考虑加载某种类型的模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,那么完全消除 `ResourceLoader` 接口(或 `ResourcePatternResolver` 接口)的使用是有意义的,让 bean 公开它需要的 `Resource` 属性,并期望它们被注入其中。
然后注入这些属性很简单,因为所有应用程序上下文都注册并使用一个特殊的 JavaBeans `PropertyEditor`,它可以将 `String` 路径转换为 `Resource` 对象。例如,以下 `MyBean` 类具有 `Resource` 类型的模板属性。
```java
public class MyBean {
private Resource template;
public setTemplate(Resource template) {
this.template = template;
}
// ...
}
```
在 XML 配置文件中,可以使用该资源的简单字符串配置 `template` 属性,如下例所示:
```xml
```
请注意,资源路径没有前缀。因此,由于应用程序上下文本身将用作 `ResourceLoader`,因此资源将通过 `ClassPathResource`、`FileSystemResource` 或 `ServletContextResource` 加载,具体取决于应用程序上下文的确切类型。
如果需要强制使用特定的资源类型,可以使用前缀。以下两个示例显示了如何强制 `ClassPathResource` 和 `UrlResource`(后者用于访问文件系统中的文件):
```xml
```
```xml
```
如果 `MyBean` 类被重构以用于注解驱动的配置,则 `myTemplate.txt` 的路径可以存储在名为 `template.path` 的键下 — 例如,在 Spring 环境可用的属性文件中(请参阅环境抽象)。然后可以使用属性占位符通过 `@Value` 注解引用模板路径(请参见使用 `@Value`)。Spring 将以字符串的形式检索模板路径的值,一个特殊的 `PropertyEditor` 将把字符串转换为要注入 `MyBean` 构造函数的 `Resource` 对象。下面的示例演示了如何实现这一点。
```java
@Component
public class MyBean {
private final Resource template;
public MyBean(@Value("${template.path}") Resource template) {
this.template = template;
}
// ...
}
```
如果我们希望支持在类路径中的多个位置的同一路径下发现的多个模板 —— 例如,在类路径中的多个 jar 中 —— 我们可以使用特殊的 `classpath*:` 前缀和通配符将 `template.path` 键定义为 `classpath*:/config/templates/*.txt`。如果我们按如下方式重新定义 `MyBean` 类,Spring 会将模板路径模式转换为可以注入 `MyBean` 构造函数的 `Resource` 对象数组。
```java
@Component
public class MyBean {
private final Resource[] templates;
public MyBean(@Value("${templates.path}") Resource[] templates) {
this.templates = templates;
}
// ...
}
```
## 2.8 应用上下文和资源路径
本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式、如何使用通配符以及其他详细信息。
### 2.8.1 构造应用上下文
应用程序上下文构造函数(针对特定的应用程序上下文类型)通常将字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。
当这样的位置路径没有前缀时,从该路径构建并用于加载 bean 定义的特定 `Resource` 类型取决于并适合于特定的应用程序上下文。例如,考虑创建 `ClassPathXmlApplicationContext` 的以下示例:
```java
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
```
bean 定义是从类路径加载的,因为使用了 `ClassPathResource`。但是,请考虑以下示例,该示例创建 `FileSystemXmlApplicationContext`:
```java
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");
```
现在,bean 定义从文件系统位置加载(在本例中,相对于当前工作目录)。
注意,在位置路径上使用特殊的 `classpath` 前缀或标准 URL 前缀会覆盖为加载 bean 定义而创建的默认类型的 `Resource`。考虑以下示例:
```java
ApplicationContext ctx =
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
```
使用 `FileSystemXmlApplicationContext` 从类路径加载 bean 定义。但是,它仍然是 `FileSystemXmlApplicationContext`。如果它随后被用作 `ResourceLoader`,则任何未固定的路径仍被视为文件系统路径。
#### 构造 ClassPathXmlApplicationContext 实例 —— 快捷方式
`ClassPathXmlApplicationContext` 公开了许多构造函数以实现方便的实例化。基本思想是,您可以只提供一个字符串数组,该数组只包含XML文件本身的文件名(不包含前导路径信息),还可以提供一个 `Class`。然后 `ClassPathXmlApplicationContext` 从提供的类中派生路径信息。
考虑以下目录布局:
```
com/
example/
services.xml
repositories.xml
MessengerService.class
```
以下示例显示了如何实例化 `ClassPathXmlApplicationContext` 实例,该实例由名为 `services.xml` 和 `repositories.xml`(位于类路径上)的文件中定义的 bean 组成:
```java
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
```
有关各种构造函数的详细信息,请参阅 `ClassPathXmlApplicationContext` javadoc。
### 2.8.2 应用上下文构造方法资源路径中的通配符
应用程序上下文构造函数值中的资源路径可以是简单的路径(如前所示),每个路径都有到目标 `Resource` 的一对一映射,或者可以包含特殊的 `classpath*:` 前缀或内部 Ant 样式模式(通过使用 Spring 的 `PathMatcher` 工具类进行匹配)。后两者都是有效的通配符。
该机制的一个用途是当您需要进行组件样式的应用程序组装时。所有组件都可以将上下文定义片段发布到一个已知的位置路径,当使用前缀为 `classpath*:` 的相同路径创建最终的应用程序上下文时,所有组件片段都会自动匹配。
请注意,此通配符特定于应用程序上下文构造函数中资源路径的使用(或者当您直接使用 `PathMatcher` 工具类类层次结构时),并在构建时解析。它与 `Resource` 类型本身无关。您不能使用 `classpath*:` 前缀来构造实际的 `Resource`,因为资源一次只能指向一个资源。
#### Ant 样式模式
路径位置可以包含 Ant 样式模式,如下例所示:
```
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml
```
当路径位置包含 Ant 样式模式时,解析器将遵循更复杂的过程来尝试解析通配符。它为到最后一个非通配符段的路径生成一个 `Resource`,并从中获取一个 URL。如果该 URL 不是 `jar:` URL 或容器特定的变量(例如 WebLogic 中的 `zip:`、WebSphere 中的 `wsjar` 等),则从中获取 `java.io.File`,并通过遍历文件系统来解析通配符。对于 jar URL,解析器要么从中获取 `java.net.JarURLConnection`,要么手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。
##### 对可移植性的影响
如果指定的路径已经是 `file` URL(隐式地,因为基本 `ResourceLoader` 是文件系统 URL,或者显式地),则保证通配符以完全可移植的方式工作。
如果指定的路径是类路径位置,则解析器必须通过调用 `Classloader.getResource()` 获取最后一个非通配符路径段 URL。由于这只是路径的一个节点(而不是末尾的文件),因此实际上(在 `ClassLoader` javadoc中)没有定义在这种情况下返回的 URL 的类型。实际上,它始终是一个 `java.io.File`,表示目录(类路径资源解析到文件系统位置)或某种类型的 jar URL(类路径路径资源解析为 jar 位置)。尽管如此,这项操作仍存在可移植性问题。
如果为最后一个非通配符段获取了 jar URL,解析器必须能够从中获取 `java.net.JarURLConnection` 或手动解析 jar URL,以便能够遍历 jar 的内容并解析通配符。这在大多数环境中都有效,但在其他环境中却失败了,我们强烈建议在依赖之前,在您的特定环境中彻底测试来自 jar 的资源的通配符解析。
#### `classpath*:` 前缀
在构建基于 XML 的应用程序上下文时,位置字符串可以使用特殊的 `classpath*:` 前缀,如下例所示:
```java
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
```
这个特殊前缀指定必须获得与给定名称匹配的所有类路径资源(在内部,这基本上是通过调用 `ClassLoader.getResources(…)`)然后合并以形成最终的应用上下文定义。
> 通配符类路径依赖于底层 `ClassLoader` 的 `getResources()` 方法。由于现在大多数应用程序服务器都提供自己的 `ClassLoader` 实现,因此行为可能有所不同,尤其是在处理 jar 文件时。检查 `classpath*` 是否有效的一个简单测试是使用 `ClassLoader` 从 classpath 的 jar 中加载文件:`getClass().getClassLoader().getResources("")`。使用具有相同名称但位于两个不同位置的文件尝试此测试 — 例如,具有相同名称和路径但位于类路径上不同jar中的文件。如果返回不正确的结果,请检查应用程序服务器文档中可能影响 `ClassLoader` 行为的设置。
您还可以在位置路径的其余部分(例如,`classpath*:META-INF/*-beans.xml`)中将 `classpath*:` 前缀与 `PathMatcher` 模式组合。在这种情况下,解析策略相当简单:在最后一个非通配符路径段上使用 `ClassLoader.getResources()` 调用以获取类加载器层次结构中的所有匹配资源,前面描述的相同 `PathMatcher` 解析策略用于通配符子路径。
#### 与通配符相关的其他注解
注意,`classpath*:` 当与 Ant 样式模式结合时,只有在模式启动之前至少一个根目录才能可靠地工作,除非实际的目标文件位于文件系统中。这意味着 `classpath*:*.xml` 之类的模式可能无法从 jar 文件的根目录检索文件,而只能从扩展目录的根目录中检索文件。
Spring 检索类路径条目的能力源自 JDK 的 `ClassLoader.getResources()` 方法,该方法只返回空字符串的文件系统位置(指示要搜索的潜在根)。Spring 还评估了 jar 文件中的 `URLClassLoader` 运行时配置和 `java.class.path` 清单,但这不能保证会导致可移植行为。
> 扫描类路径包需要在类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 时,不要激活 JAR 任务的 `files-only` 开关。此外,在某些环境中,基于安全策略,类路径目录可能不会公开 —— 例如,JDK 1.7.0_45 和更高版本上的独立应用程序(这需要在清单中设置“Trusted Library”。请参阅https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).
>
> 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常按预期工作。在这里,将资源放入专用目录也是非常推荐的,避免了在 jar 文件根级别搜索时出现的上述可移植性问题。
如果要搜索的根包在多个类路径位置可用,带有 `classpath:` 的 Ant 样式模式资源不保证能够找到匹配的资源。考虑以下资源位置示例:
```
com/mycompany/package1/service-context.xml
```
现在考虑一个 Ant 风格的路径,有人可能会使用它来查找该文件:
```
classpath:com/mycompany/**/service-context.xml
```
这样的资源可能只存在于类路径中的一个位置,但是当使用前面示例中的路径来尝试解析它时,解析器会使用 `getResource("com/mycompany");` 返回的(第一个)URL。如果此基本包节点存在于多个 `ClassLoader` 位置,则所需资源可能不存在于找到的第一个位置。因此,在这种情况下,您应该更喜欢使用 `classpath*:`,使用相同的 Ant 样式模式,该模式搜索包含 `com.mycompany` 基本包的所有类路径位置:`classpath*:com/mycompany/**/service-context.xml`。
### 2.8.3 FileSystemResource 警告
未附加到 `FileSystemApplicationContext` 的 `FileSystemResource`(即,当 `FileSystemApplicationContext` 不是实际的 `ResourceLoader` 时)将按照您的预期处理绝对路径和相对路径。相对路径相对于当前工作目录,而绝对路径相对于文件系统的根。
然而,出于向后兼容性(历史)的原因,当 `FileSystemApplicationContext` 是 `ResourceLoader` 时,这会发生变化。`FileSystemApplicationContext` 强制所有附加的 `FileSystemResource` 实例将所有位置路径视为相对路径,无论它们是否以前导斜杠开头。实际上,这意味着以下示例是等效的:
```java
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
```
```java
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");
```
以下示例也是等效的(尽管它们不同是有意义的,因为一种情况是相对的,另一种情况则是绝对的):
```java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
```
```java
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
```
实际上,如果需要真正的绝对文件系统路径,则应避免将绝对路径与 `FileSystemResource` 或 `FileSystemXmlApplicationContext` 一起使用,并通过使用 `file:` URL 前缀强制使用 `UrlResource`。以下示例显示了如何执行此操作:
```java
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
```
```java
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
new FileSystemXmlApplicationContext("file:///conf/context.xml");
```
# 3. 验证,数据绑定,和类型转换
将验证视为业务逻辑有利弊,Spring 提供了一种验证和数据绑定的设计,不排除其中任何一种。具体来说,验证不应与 web 层绑定,并且应易于本地化,并且应可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个 `Validator` 协议,它在应用程序的每一层中都是基本的且非常有用的。
数据绑定对于将用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。Spring 提供了恰当命名的 `DataBinder` 来实现这一点。`Validator` 和 `DataBinder` 组成验证包,主要用于但不限于 web 层。
`BeanWrapper` 是 Spring 框架中的一个基本概念,在很多地方都有使用。然而,您可能不需要直接使用 `BeanWrapper`。然而,由于这是参考文档,我们觉得可能需要一些解释。我们在本章中解释 `BeanWrapper`,因为如果您打算使用它,那么在尝试将数据绑定到对象时很可能会这样做。
Spring 的 `DataBinder` 和较低级别的 `BeanWrapper` 都使用 `PropertyEditorSupport` 实现来解析和格式化属性值。`PropertyEditor` 和 `PropertyEditorSupport` 类型是 JavaBeans 规范的一部分,本章也将对此进行解释。Spring 的 `core.convert` 包提供了一个泛型转换工具,以及一个用于格式化 UI 字段值的高级 `format` 包。您可以将这些包用作 `PropertyEditorSupport` 实现的更简单的替代方案。本章还将讨论这些问题。
Spring 通过设置基础设施和 Spring 自己的 `Validator` 协议的适配器支持 JavaBean 验证。应用程序可以全局启用一次 Bean Validation,如 Java Bean Validation 中所述,并将其专门用于所有验证需求。在 web 层中,应用程序可以进一步为每个 `DataBinder` 注册控制器本地 Spring `Validator` 实例,如配置 `DataBinder` 中所述,这对于插入自定义验证逻辑非常有用。
## 3.1 通过使用 Spring 的 Validator 接口验证
Spring 有一个 `Validator` 接口,您可以使用它来验证对象。`Validator` 接口通过使用 `Errors` 对象工作,以便在验证时,验证器可以向 `Errors` 对象报告验证失败。
考虑以下小数据对象的示例:
```java
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
```
下一个示例通过实现 `org.springframework.validation.Validator` 接口的以下两个方法为 `Person` 类提供验证行为:
- `supports(Class)`:此 `Validator` 是否可以验证提供的 `Class` 的实例?
- `validate(Object, org.springframework.validation.Errors)`:验证给定的对象,如果出现验证错误,则将这些对象注册到给定的 `Errors` 对象。
实现 `Validator` 相当简单,特别是当您知道 Spring Framework 也提供 `ValidationUtils` 帮助类时。以下示例实现了 `Person` 实例的 `Validator`:
```java
public class PersonValidator implements Validator {
/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
```
`ValidationUtils` 类上的静态 `rejectIfEmpty(..)` 方法用于拒绝 `name` 属性(如果该属性为 `null` 或空字符串)。看看 `ValidationUtils` javadoc,看看除了前面所示的示例之外,它还提供了哪些功能。
虽然可以实现一个 `Validator` 类来验证富对象中的每个嵌套对象,但最好将每个嵌套对象类的验证逻辑封装在自己的 `Validator` 实现中。“rich”对象的一个简单示例是 `Customer`,它由两个 `String` 属性(第一个和第二个名称)和一个复杂的 `Address` 对象组成。`Address` 对象可以独立于 `Customer` 对象使用,因此实现了一个不同的 `AddressValidator`。如果您希望 `CustomerValidator` 重用 `AddressValidator` 类中包含的逻辑而不使用复制和粘贴,则可以依赖注入或在 `CustomerValidator` 中实例化 `AddressValidator`,如下例所示:
```java
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
```
验证错误报告给传递给验证器的 `Errors` 对象。在 Spring Web MVC 的情况下,您可以使用 `` 标记来检查错误消息,但也可以自己检查 `Errors` 对象。有关它提供的方法的更多信息可以在 java doc 中找到。
## 3.2 将代码解析为错误消息
我们讨论了数据绑定和验证。本节介绍输出与验证错误相对应的消息。在上一节所示的示例中,我们拒绝了 `name` 和 `age` 字段。如果我们想使用 `MessageSource` 输出错误消息,可以使用我们在拒绝字段时提供的错误代码(在本例中为“name”和“age”)。当您从 `Errors` 接口调用(直接或间接,例如,通过使用 `ValidationUtils` 类)`rejectValue` 或其他 `reject` 方法之一时,基础实现不仅会注册传入的代码,还会注册许多其他错误代码。`MessageCodesResolver` 确定 `Errors` 接口注册的错误代码。默认情况下,使用 `DefaultMessageCodesResolver`,它(例如)不仅用您给出的代码注册消息,还注册包含传递给拒绝方法的字段名的消息。因此,如果您通过使用 `rejectValue("age", "too.darn.old")` 拒绝字段,除了 `too.darn.old` 代码之外,Spring 还注册 `too.darn.old.age` 和 `too.darn.old.age.int`(第一个包含字段名,第二个包含字段类型)。这样做是为了方便开发人员查找错误消息。
有关 `MessageCodesResolver` 和默认策略的更多信息可以分别在 `MessageCodesResolver` 和 `DefaultMessageCodesResolver` 的 javadoc 中找到。
## 3.3 Bean 操纵和 BeanWrapper
# Servlet 技术栈的 Web
本部分文档介绍了对基于 Servlet API 构建并部署到 Servlet 容器的 Servlet 技术栈 web 应用程序的支持。各个章节包括 Spring MVC、View 技术、CORS 支持和 WebSocket 支持。有关反应式技术栈 web 应用程序,请参阅反应式技术栈的 Web。
# 1. Spring Web MVC
Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从一开始就包含在 Spring 框架中。正式名称“Spring Web MVC”来自其源模块(`spring-webmvc`)的名称,但更常见的是“Spring MVC”。
与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个反应式技术栈 Web 框架,其名称“Spring WebFlux”也是基于其源模块(`spring-webflux`)。本章介绍 Spring Web MVC。下一章将介绍 Spring WebFlux。
有关基线信息以及与 Servlet 容器和 Jakarta EE 版本范围的兼容性,请参阅 Spring Framework Wiki。
## 1.1 DispatcherServlet
与许多其他 web 框架一样,Spring MVC 是围绕前端控制器模式设计的,其中的中央 `Servlet`,`DispatcherServlet`,为请求处理提供了共享算法,而实际工作则由可配置的委托组件执行。该模型非常灵活,支持多种工作流。
与任何 `Servlet` 一样,`DispatcherServlet` 需要使用 Java 配置或 `web.xml` 根据 Servlet 规范声明和映射。反过来,`DispatcherServlet` 使用 Spring 配置来发现请求映射、视图解析、异常处理等所需的委托组件。
以下 Java 配置示例注册并初始化 `DispatcherServlet`,它由 Servlet 容器自动检测(请参阅 Servlet 配置):
```java
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
```
> 除了直接使用 ServletContext API 之外,还可以扩展 `AbstractAnnotationConfigDispatcherServletInitializer` 并重写特定方法(请参阅上下文层次结构下的示例)。
> 对于编程用例,可以使用 `GenericWebApplicationContext` 作为 `AnnotationConfigWebApplicationContext` 的替代方案。有关详细信息,请参阅 `GenericWebApplicationContext` Javadoc。
以下 `web.xml` 配置示例注册并初始化 `DispatcherServlet`:
```xml
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
/WEB-INF/app-context.xml
app
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
1
app
/app/*
```
> Spring Boot 遵循不同的初始化顺序。Spring Boot 使用 Spring 配置来引导自身和嵌入式 Servlet 容器,而不是与 Servlet 容器的生命周期挂钩。在 Spring 配置中检测到过滤器和 Servlet 声明,并向 Servlet 容器注册。有关更多详细信息,请参阅 Spring Boot 文档。
### 1.1.1 上下文层次结构
`DispatcherServlet` 期望 `WebApplicationContext`(普通 `ApplicationContext` 的扩展)用于其自身的配置。`WebApplicationContext` 链接到 `ServletContext` 及其关联的 `Servlet`。它还绑定到 `ServletContext`,因此如果应用程序需要访问它,它们可以对 `RequestContextUtils` 使用静态方法来查找 `WebApplicationContext`。
对于许多应用程序,拥有单个 `WebApplicationContext` 既简单又足够。还可以具有上下文层次结构,其中根 `WebApplicationContext` 跨多个 `DispatcherServlet`(或其他 `Servlet`)实例共享,每个实例都具有自己的子 `WebApplicationContext` 配置。有关上下文层次结构功能的更多信息,请参阅应用程序上下文的其他功能。
根 `WebApplicationContext` 通常包含基础架构 bean,例如需要跨多个 `Servlet` 实例共享的数据存储库和业务服务。这些 bean 是有效继承的,并且可以在 servlet 特定的子 `WebApplicationContext` 中覆盖(即,重新声明),该上下文通常包含给定 `Servlet` 本地的 bean。
以下示例配置 `WebApplicationContext` 层次结构:
```java
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return new Class>[] { RootConfig.class };
}
@Override
protected Class>[] getServletConfigClasses() {
return new Class>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
```
> 如果不需要应用程序上下文层次结构,则应用程序可以通过 `getRootConfigClasses()` 返回所有配置,并从 `getServletConfigClasses()` 返回 `null`。
以下示例显示了与 `web.xml` 等效的文件:
```xml
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
/WEB-INF/root-context.xml
app1
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
/WEB-INF/app1-context.xml
1
app1
/app1/*
```
> 如果不需要应用程序上下文层次结构,则应用程序可以仅配置“根”上下文,并将 `contextConfigLocation` Servlet 参数留空。
### 1.1.2 特殊 Bean 类型
`DispatcherServlet` 委托特殊 bean 处理请求并呈现适当的响应。我们所说的“特殊 bean”是指实现框架协议的 Spring 托管 `Object` 实例。这些通常带有内置协议,但您可以自定义它们的属性并扩展或替换它们。
下表列出了 `DispatcherServlet` 检测到的特殊 bean:
| Bean 类型 | 说明 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `HandlerMapping` | 将请求与[拦截器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-handlermapping-interceptor)列表一起映射到处理程序进行预处理和后处理。该映射基于一些标准,其细节因 `HandlerMapping` 实现而异。两个主要的 `HandlerMaping` 实现是 `RequestMappingHandlerMapping`(支持 `@RequestMapping` 注解方法)和 `SimpleUrlHandlerMapping`(维护 URI 路径模式到处理程序的显式注册)。 |
| `HandlerAdapter` | 帮助 `DispatcherServlet` 调用映射到请求的处理程序,而不管实际如何调用该处理程序。例如,调用带注解的控制器需要解析注解。`HandlerAdapter` 的主要目的是保护 `DispatcherServlet` 不受此类细节的影响。 |
| [`HandlerExceptionResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-exceptionhandlers) | 解决异常的策略,可能将异常映射到处理程序、HTML 错误视图或其他目标。参见[异常](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-exceptionhandlers)。 |
| [`ViewResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-viewresolver) | 将处理程序返回的逻辑上基于 `string` 的视图名称解析为实际的 `View`,并用其呈现到响应。参见[视图解析](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-viewresolver)和[视图技术](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-view)。 |
| [`LocaleResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver), [LocaleContextResolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-timezone) | 解析客户端正在使用的 `Locale` 以及可能的时区,以便能够提供国际化视图。参见[区域设置](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver)。 |
| [`ThemeResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-themeresolver) | 解析 web 应用程序可以使用的主题——例如提供个性化布局。参见[主题](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-themeresolver)。 |
| [`MultipartResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart) | 借助某些 multi-part 解析库解析 multipart 请求(例如,浏览器表单文件上载)的抽象。参见 [Multipart 解析器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart)。 |
| [`FlashMapManager`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-flash-attributes) | 存储和检索可用于将属性从一个请求传递到另一个请求(通常通过重定向)的“输入”和“输出” `FlashMap`。参见 [Flash 属性](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-flash-attributes)。 |
### 1.1.3 Web MVC 配置
应用程序可以声明特殊 Bean 类型中列出的处理请求所需的基础结构 Bean。`DispatcherServlet` 检查每个特殊 bean 的 `WebApplicationContext`。如果没有匹配的 bean 类型,则返回 `DispatcherServlet.properties` 中列出的默认类型。
在大多数情况下,MVC 配置是最好的起点。它用 Java 或 XML 声明所需的 bean,并提供更高级别的配置回调 API 来定制它。
> Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外方便的选项。
### 1.1.4 Servlet 配置
在 Servlet 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方案或与 `web.xml` 文件结合使用。以下示例注册 `DispatcherServlet`:
```java
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
```
`WebApplicationInitializer` 是 Spring MVC 提供的一个接口,可确保检测到您的实现并自动用于初始化任何 Servlet 3 容器。名为 `AbstractDispatcherServletInitializer` 的 `WebApplicationInitializer` 抽象基类实现通过重写方法来指定 servlet 映射和 `DispatcherServlet` 配置的位置,从而更容易注册 `DispatcherServlet`。
对于使用基于 Java 的 Spring 配置的应用程序,建议这样做,如下例所示:
```java
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return null;
}
@Override
protected Class>[] getServletConfigClasses() {
return new Class>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
```
如果使用基于 XML 的 Spring 配置,则应直接从 `AbstractDispatcherServletInitializer` 进行扩展,如下例所示:
```java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
```
`AbstractDispatcherServletInitializer` 还提供了一种方便的方法来添加 `Filter` 实例,并将它们自动映射到 `DispatcherServlet`,如下例所示:
```java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
```
每个过滤器都会根据其具体类型添加一个默认名称,并自动映射到 `DispatcherServlet`。
`AbstractDispatcherServletInitializer` 的 `isAsyncSupported` 受保护方法提供了一个地方,可以在 `DispatcherServlet` 和映射到它的所有筛选器上启用异步支持。默认情况下,此标志设置为 `true`。
最后,如果需要进一步自定义 `DispatcherServlet` 本身,可以重写 `createDispatcherServlet` 方法。
### 1.1.5 处理
DispatcherServlet处理请求如下:
- `WebApplicationContext` 在请求中作为控制器和流程中的其他元素可以使用的属性进行搜索和绑定。默认情况下,它绑定在 `DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE` 键下。
- 区域设置解析器(locale resolver)绑定到请求,以让流程中的元素解析处理请求时使用的区域设置(呈现视图、准备数据等)。如果不需要区域设置解析,则不需要区域配置解析器。
- 主题解析器绑定到请求,以让视图等元素确定要使用的主题。如果不使用主题,可以忽略它。
- 如果指定 multipart 文件解析器,将检查请求的 multiparts。如果找到 multiparts,则将请求包装在 `MultipartHttpServletRequest` 中,以供流程中的其他元素进一步处理。有关 multipart 处理的更多信息,请参阅 Multipart 解析器。
- 搜索适当的处理程序。如果找到了处理程序,则运行与处理程序(预处理器、后处理器和控制器)关联的执行链,以准备渲染模型。或者,对于带注解的控制器,可以(在 `HandlerAdapter` 中)呈现响应,而不是返回视图。
- 如果返回模型,则会渲染视图。如果没有返回模型(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不会呈现视图,因为请求可能已经完成。
`WebApplicationContext` 中声明的 `HandlerExceptionResolver` bean 用于解决请求处理过程中引发的异常。这些异常解析器允许定制逻辑以解决异常。有关详细信息,请参阅异常。
对于 HTTP 缓存支持,处理程序可以使用 `WebRequest` 的 `checkNotModified` 方法,以及注解控制器的其他选项,如“控制器的 HTTP 缓存”中所述。
通过将 Servlet 初始化参数(`init-param` 元素)添加到 `web.xml` 文件中的 Servlet 声明中,可以自定义各个 `DispatcherServlet` 实例。下表列出了支持的参数:
| 参数 | 说明 |
| :------------------------------- | :----------------------------------------------------------- |
| `contextClass` | 实现 `ConfigurationWebApplicationContext` 的类,该类将由此 Servlet 实例化和本地配置。默认情况下,使用 `XmlWebApplicationContext`。 |
| `contextConfigLocation` | 传递给上下文实例(由 `contextClass` 指定)的字符串,以指示可以在何处找到上下文。该字符串可能由多个字符串(使用逗号作为分隔符)组成,以支持多个上下文。如果多个上下文位置包含定义了两次的 bean,则最新位置优先。 |
| `namespace` | `WebApplicationContext` 的命名空间。默认为 `[servlet-name]-servlet`。 |
| `throwExceptionIfNoHandlerFound` | 当未找到请求的处理程序时,是否引发 `NoHandlerFoundException`。然后,可以使用 `HandlerExceptionResolver`(例如,使用 `@ExceptionHandler` 控制器方法)捕获异常,并将其作为其他任何方法进行处理。默认情况下,此设置为 `false`,在这种情况下,`DispatcherServlet` 将响应状态设置为404(NOT_FOUND),而不会引发异常。注意,如果[默认 servlet 处理](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-default-servlet-handler)被配置,未解决的请求总是被转发到默认 servlet,并且永远不会引发404。 |
### 1.1.6 路径匹配
Servlet API 将完整的请求路径公开为 `requestURI`,并将其进一步细分为 `contextPath`、`servletPath` 和 `pathInfo`,它们的值取决于 Servlet 的映射方式。从这些输入中,Spring MVC 需要确定用于映射处理程序的查找路径,如果适用,应排除 `contextPath` 和任何 `servletMapping` 前缀。
`servletPath` 和 `pathInfo` 被解码,这使得它们不可能直接与完整的 `requestURI` 进行比较,从而导出 lookupPath,这使得有必要对 `requestURI` 进行解码。然而,这会带来自身的问题,因为路径可能包含编码的保留字符,例如 `"/"` 或 `";"`,这些字符在解码后会改变路径的结构,这也会导致安全问题。此外,Servlet 容器可能会在不同程度上规范化 `servletPath`,这使得进一步无法对 `requestURI` 执行 `startsWith` 比较。
这就是为什么最好避免依赖基于前缀的 `servletPath` 映射类型附带的 `servletPath`。如果 `DispatcherServlet` 被映射为带有 `"/"` 的默认 Servlet,或者没有带有 `"/*"` 的前缀,并且 Servlet 容器是 4.0+,那么 Spring MVC 能够检测 Servlet 映射类型,并完全避免使用 `servletPath` 和 `pathInfo`。在 3.1 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过 MVC 配置中的路径匹配提供 `alwaysUseFullPath=true` 的 `UrlPathHelper` 来实现等效。
幸运的是,默认的 Servlet 映射 `"/"` 是一个不错的选择。然而,仍然存在一个问题,即需要对 `requestURI` 进行解码,以便能够与控制器映射进行比较。这也是不希望的,因为可能会解码改变路径结构的保留字符。如果不需要这样的字符,则可以拒绝它们(如 Spring Security HTTP 防火墙),或者可以使用 `urlDecode=false` 配置 `UrlPathHelper`,但控制器映射需要与编码路径匹配,而编码路径可能并不总是正常工作。此外,有时 `DispatcherServlet` 需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。
当使用 `PathPatternParser` 和已解析模式作为字符串路径匹配与 `AntPathMatcher` 的替代时,可以解决上述问题。`PathPatternParser` 从 5.3 版开始可在 Spring MVC 中使用,默认情况下从 6.0 版开始启用。与需要解码查找路径或编码控制器映射的 `AntPathMatcher` 不同,解析后的 `PathPattern` 与名为 `RequestPath` 的路径解析表示相匹配,一次只能匹配一个路径段。这允许单独解码和净化路径段值,而不存在改变路径结构的风险。解析的 `PathPattern` 还支持使用 `servletPath` 前缀映射,只要使用了 Servlet 路径映射并且前缀保持简单,即没有编码字符。有关模式语法的详细信息和比较,请参阅模式比较。
### 1.1.7 拦截
所有 `HandlerMapping` 实现都支持处理程序拦截器,当您希望将特定功能应用于特定请求时,这些拦截器非常有用——例如,检查委托人。拦截器必须使用三种方法实现 `org.springframework.web.servlet` 包中的 `HandlerInterceptor`,这三种方法应提供足够的灵活性来执行各种预处理和后处理:
- `preHandle(..)`:在实际处理程序运行之前
- `postHandle(..)`:运行处理程序后
- `afterCompletion(..)`:完成请求后
`preHandle(..)` 方法返回布尔值。您可以使用此方法中断或继续执行链的处理。当此方法返回 `true` 时,处理程序执行链将继续。当它返回 false 时,`DispatcherServlet` 假定拦截器本身已经处理了请求(例如,呈现了一个适当的视图),并且不会继续执行其他拦截器和执行链中的实际处理程序。
有关如何配置拦截器的示例,请参阅 MVC 配置部分中的拦截器。您还可以通过在各个 `HandlerMapping` 实现上使用 setter 来直接注册它们。
`postHandle` 方法在 `@ResponseBody` 和 `ResponseEntity` 方法中不太有用,这些方法的响应是在 `HandlerAdapter` 中和 `postHandle` 之前编写和提交的。这意味着对响应进行任何更改(如添加额外的标头)都为时已晚。对于这种情况,您可以实现 `ResponseBodyAdvice`,并将其声明为控制器切面 bean,或者直接在 `RequestMappingHandlerAdapter` 上配置它。
### 1.1.8 异常
如果在请求映射过程中发生异常或从请求处理程序(如 `@Controller`)抛出异常,`DispatcherServlet` 将委托给 `HandlerExceptionResolver` bean 链,以解决异常并提供替代处理,这通常是错误响应。
下表列出了可用的 `HandlerExceptionResolver` 实现:
| `HandlerExceptionResolver` | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `SimpleMappingExceptionResolver` | 异常类名称和错误视图名称之间的映射。用于在浏览器应用程序中呈现错误页面。 |
| [`DefaultHandlerExceptionResolver`](https://docs.spring.io/spring-framework/docs/6.0.6/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html) | 解析 Spring MVC 引发的异常并将其映射到 HTTP 状态代码。另请参阅可选的 `ResponseEntityExceptionHandler` 和[错误响应](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions)。 |
| `ResponseStatusExceptionResolver` | 使用 `@ResponseStatus` 注解解析异常,并根据注解中的值将其映射到 HTTP 状态代码。 |
| `ExceptionHandlerExceptionResolver` | 通过调用 `@Controller` 或 `@ControllerAdvice` 类中的 `@ExceptionHandler` 方法来解决异常。请参见 [@ExceptionHandler 方法](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler)。 |
#### 解析器链
通过在 Spring 配置中声明多个 `HandlerExceptionResolver` bean 并根据需要设置其 `order` 属性,可以形成异常解析器链。`order` 属性越高,异常解析程序定位得越晚。
`HandlerExceptionResolver` 的约定指定它可以返回:
- 指向错误视图的 `ModelAndView`。
- 如果在冲突解决程序中处理了异常,则返回空 `ModelAndView`。
- 如果异常仍然未解决,则为 `null`,供后续解析器尝试;如果异常仍然在末尾,则允许冒泡到 Servlet 容器。
MVC 配置自动为默认的 Spring MVC 异常、`@ResponseStatus` 注解的异常以及 `@ExceptionHandler` 方法的支持声明内置解析器。您可以自定义或替换该列表。
#### 容器错误页面
如果任何 `HandlerExceptionResolver` 都无法解析异常,因此需要传播,或者如果响应状态设置为错误状态(即 4xx、5xx),Servlet 容器可以以 HTML 格式呈现默认错误页面。要自定义容器的默认错误页,可以在 `web.xml` 中声明错误页映射。以下示例显示了如何做到这一点:
```xml
/error
```
给定前面的示例,当出现异常或响应处于错误状态时,Servlet 容器会在容器中向配置的 URL(例如 `/error`)发出 ERROR 调度。然后由 `DispatcherServlet` 处理,可能将其映射到 `@Controller`,该 `@Controller` 可以实现为返回带有模型的错误视图名称或呈现 JSON 响应,如下例所示:
```java
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map handle(HttpServletRequest request) {
Map map = new HashMap();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
```
> Servlet API 不提供在 Java 中创建错误页映射的方法。但是,您可以同时使用`WebApplicationInitializer` 和最小的 `web.xml`。
### 1.1.9 视图解析
Spring MVC 定义了 `ViewResolver` 和 `View` 接口,这些接口允许您在浏览器中渲染模型,而无需将您绑定到特定的视图技术。`ViewResolver` 提供了视图名称和实际视图之间的映射。视图解决了在移交给特定视图技术之前准备数据的问题。
下表提供了 `ViewResolver` 层次结构的详细信息:
| ViewResolver | 描述 |
| :------------------------------- | :----------------------------------------------------------- |
| `AbstractCachingViewResolver` | `AbstractCachingViewResolver` 的子类缓存它们解析的视图实例。缓存提高了某些视图技术的性能。您可以通过将 `cache` 属性设置为 `false` 来关闭缓存。此外,如果必须在运行时刷新某个视图(例如,修改 FreeMarker 模板时),则可以使用 `removeFromCache(String viewName,Locale loc)` 方法。 |
| `UrlBasedViewResolver` | `ViewResolver` 接口的简单实现,无需显式映射定义即可将逻辑视图名称直接解析为 URL。如果逻辑名称以直接的方式与视图资源的名称匹配,而不需要任意映射,则这是合适的。 |
| `InternalResourceViewResolver` | 方便的 `UrlBasedViewResolver` 子类,支持 `InternalResourceView`(实际上是 Servlet 和 JSP)和 `JstlView` 等子类。您可以使用 `setViewClass(..)` 为该解析器生成的所有视图指定视图类。请参见 [`UrlBasedViewResolver`](https://docs.spring.io/spring-framework/docs/6.0.6/javadoc-api/org/springframework/web/reactive/result/view/UrlBasedViewResolver.html) javadoc 获取详细信息。 |
| `FreeMarkerViewResolver` | 方便的 `UrlBasedViewResolver` 子类,支持 `FreeMarkerView` 及其自定义子类。 |
| `ContentNegotiatingViewResolver` | 实现 `ViewResolver` 接口,该接口根据请求文件名或 `Accept` 标头解析视图。参见[内容协商](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multiple-representations)。 |
| `BeanNameViewResolver` | 实现 `ViewResolver` 接口,该接口将视图名称解释为当前应用程序上下文中的 bean 名称。这是一个非常灵活的变体,允许基于不同的视图名称混合和匹配不同的视图类型。每个这样的 `View` 都可以定义为 bean,例如在 XML 或配置类中。 |
#### 处理
您可以通过声明多个解析器 bean,并在必要时设置 `order` 属性以指定排序,来链接视图解析器。请记住,`order` 属性越高,视图解析器在链中的位置就越晚。
`ViewResolver` 的约定指定它可以返回 null 以指示找不到视图。然而,对于 JSP 和 `InternalResourceViewResolver`,确定 JSP 是否存在的唯一方法是通过 `RequestDispatcher` 执行分派。因此,必须始终将 `InternalResourceViewResolver` 配置为视图解析器整体顺序中的最后一个。
配置视图解析与将 `ViewResolver` bean 添加到 Spring 配置一样简单。MVC 配置为视图解析器和添加无逻辑视图控制器提供了一个专用的配置 API,这对于无控制器逻辑的 HTML 模板渲染非常有用。
#### 重定向
视图名称中的特殊 `redirect:` 前缀允许您执行重定向。`UrlBasedViewResolver`(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向 URL。
净效果与控制器返回 `RedirectView` 相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(如 `redirect://myapp/some/resources`)相对于当前 Servlet 上下文进行重定向,而名称如 `redirect:https://myhost.com/some/arbitrary/path` 重定向到绝对 URL。
注意,如果使用 `@ResponseStatus` 注解控制器方法,则注解值优先于 `RedirectView` 设置的响应状态。
#### 转发
还可以为最终由 `UrlBasedViewResolver` 和子类解析的视图名称使用特殊的 `forward:` 前缀。这将创建一个 `InternalResourceView`,该视图执行 `RequestDispatcher.forward()`。因此,此前缀对于 `InternalResourceViewResolver` 和 `InternalResourceView`(对于 JSP)不太有用,但如果您使用另一种视图技术,但仍希望强制由 Servlet/JSP 引擎处理资源的转发,则会有所帮助。请注意,您也可以链接多个视图解析器。
#### 内容协商
`ContentNegotiatingViewResolver` 不解析视图本身,而是委托给其他视图解析器,并选择与客户端请求的表示类似的视图。可以从 `Accept` 标头或查询参数(例如,`"/path?format=pdf"`)确定表示形式。
`ContentNegotiatingViewResolver` 通过将请求媒体类型与其每个 `ViewResolver` 关联的视图支持的媒体类型(也称为内容类型)进行比较,选择适当的 `View` 来处理请求。列表中第一个具有兼容内容类型的视图将表示形式返回给客户端。如果 `ViewResolver` 链无法提供兼容的视图,则会查阅通过 `DefaultViews` 属性指定的视图列表。后一个选项适用于可以呈现当前资源的适当表示的单例 `View`,而不管逻辑视图名称如何。`Accept` 标头可以包含通配符(例如 `text/*`),在这种情况下,内容类型为 `text/xml` 的视图是兼容的匹配项。
有关配置详细信息,请参阅 MVC 配置下的视图解析器。
### 1.1.10 区域
Spring 架构的大多数部分都支持国际化,就像 Spring web MVC 框架一样。`DispatcherServlet` 允许您使用客户端的区域(locale)设置自动解析消息。这是通过 `LocaleResolver` 对象完成的。
当请求传入时,`DispatcherServlet` 会查找区域设置解析器,如果找到了,它会尝试使用它来设置区域设置。通过使用 `RequestContext.getLocale()` 方法,您可以始终检索由区域设置解析器解析的区域设置。
除了自动区域设置解析之外,还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的更多信息,请参阅拦截器),以在特定情况下(例如,基于请求中的参数)更改区域设置。
区域设置解析器和拦截器在 `org.springframework.web.servlet.i18n` 包中定义,并以正常方式在应用程序上下文中配置。Spring 中包含以下语言环境解析器选择。
- [时区](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-timezone)
- [头解析器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-acceptheader)
- [Cookie 解析器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-cookie)
- [会话解析器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-session)
- [区域拦截器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-interceptor)
#### 时区
除了获取客户端的区域设置外,了解其时区通常也是有用的。`LocaleContextResolver` 接口提供了 `LocaleResolver` 的扩展,使解析器能够提供更丰富的 `LocaleContext`,其中可能包含时区信息。
如果可用,可以使用 `RequestContext.getTimeZone()` 方法获取用户的 `TimeZone`。在 Spring 的 `ConversionService` 中注册的任何 Date/Time `Converter` 和 `Formatter` 对象都会自动使用时区信息。
#### 头解析器
此区域设置解析器检查客户端(例如,web 浏览器)发送的请求中的 `accept-language` 标头。通常,此标头字段包含客户端操作系统的区域设置。请注意,此解析器不支持时区信息。
#### Cookie 解析器
此区域设置解析器检查客户端上可能存在的 `Cookie`,以查看是否指定了 `Locale` 或 `TimeZone`。如果是,则使用指定的详细信息。通过使用此区域设置解析器的财产,您可以指定 cookie 的名称以及最长期限。以下示例定义了 `CookieLocaleResolver`:
```xml
```
下表介绍了 CookieLocaleResolver 的属性:
| 属性 | 默认值 | 描述 |
| :------------- | :------------------- | :----------------------------------------------------------- |
| `cookieName` | 类名 + LOCALE | cookie 的名字 |
| `cookieMaxAge` | Servlet 容器的默认值 | cookie 在客户端上保留的最长时间。如果指定了 `-1`,则不会持久化 cookie。只有在客户端关闭浏览器之前,它才可用。 |
| `cookiePath` | / | 将 cookie 的可见性限制在网站的特定部分。当指定 `cookiePath` 时,cookie 仅对该路径及其下的路径可见。 |
#### 会话解析器
`SessionLocaleResolver` 允许您从可能与用户请求关联的会话中检索 `Locale` 和 `TimeZone`。与 `CookieLocaleResolver` 不同,此策略将本地选择的区域设置存储在 Servlet 容器的 `HttpSession` 中。因此,这些设置对于每个会话都是临时的,因此在每个会话结束时都会丢失。
注意,与外部会话管理机制(如 Spring Session 项目)没有直接关系。此 `SessionLocaleResolver` 根据当前 `HttpServletRequest` 评估并修改相应的 `HttpSession` 属性。
#### 区域拦截器
您可以通过将 `LocaleChangeInterceptor` 添加到 `HandlerMapping` 定义之一来启用区域设置的更改。它检测请求中的一个参数,并相应地更改区域设置,在调度程序的应用程序上下文中调用 `LocaleResolver` 上的 `setLocale` 方法。下一个示例显示,对包含名为 `siteLanguage` 的参数的所有 `*.view` 资源的调用现在会更改区域设置。例如,对 URL 的请求,`https://www.sf.net/home.view?siteLanguage=nl`,将站点语言更改为荷兰语。以下示例显示了如何截取区域设置:
```xml
/**/*.view=someController
```
### 1.1.11 主题
您可以应用 Spring Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是影响应用程序视觉样式的静态资源的集合,通常是样式表和图像。
> 从 6.0 开始,主题的支持已经被弃用,转而使用 CSS,并且在服务器端没有任何特殊支持。
#### 定义主题
要在 web 应用程序中使用主题,必须设置 `org.springframework.ui.context.ThemeSource` 接口的实现。`WebApplicationContext` 接口扩展了 `ThemeSource`,但将其职责委托给专用实现。默认情况下,委托是 `org.springframework.ui.context.support.ResourceBundleThemeSource` 实现,它从类路径的根目录加载属性文件。要使用自定义 `ThemeSource` 实现或配置 `ResourceBundleThemeSource` 的基本名称前缀,可以使用保留名称 `ThemeSource` 在应用程序上下文中注册 bean。web 应用程序上下文自动检测具有该名称的 bean 并使用它。
使用 `ResourceBundleThemeSource` 时,主题是在简单的属性文件中定义的。属性文件列出了构成主题的资源,如下例所示:
```properties
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
```
属性的键是引用视图代码中主题元素的名称。对于 JSP,通常使用 `spring:theme` 自定义标记来完成此操作,该标记与 `spring:message` 标记非常相似。以下 JSP 片段使用上一个示例中定义的主题自定义外观:
```jsp
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
...
```
默认情况下,`ResourceBundleThemeSource` 使用空的基名称前缀。因此,属性文件从类路径的根目录加载。因此,您可以将 `cool.properties` 主题定义放在类路径根目录中(例如,在 `/WEB-INF/classes` 中)。`ResourceBundleThemeSource` 使用标准的 Java 资源包加载机制,允许主题的完全国际化。例如,我们可以有一个 `/WEB-INF/classes/cool_nl.properties`,它引用一个特殊的背景图像,上面有荷兰语文本。
#### 解析主题
定义主题后,如前一节所述,您可以决定使用哪个主题。`DispatcherServlet` 查找名为 `themeResolver` 的 bean,以找出要使用的 `themeResolver` 实现。主题解析器的工作方式与 `LocaleResolver` 大致相同。它检测用于特定请求的主题,也可以更改请求的主题。下表描述了 Spring 提供的主题解析器:
| 类 | 描述 |
| :--------------------- | :----------------------------------------------------------- |
| `FixedThemeResolver` | 选择使用 `defaultThemeName` 属性设置的固定主题。 |
| `SessionThemeResolver` | 主题在用户的 HTTP 会话中维护。它只需要为每个会话设置一次,但不会在会话之间持久化。 |
| `CookieThemeResolver` | 所选主题存储在客户端的 cookie 中。 |
Spring 还提供了一个 `ThemeChangeInterceptor`,它允许使用一个简单的请求参数对每个请求进行主题更改。
### 1.1.12 Multipart 解析器
来自 `org.springframework.web.multipart` 包的 `MultipartResolver` 是一种用于解析包括文件上载在内的多部分请求的策略。有一个基于容器的 `StandardServletMultipartResolver` 实现用于 Servlet 多部分请求解析。请注意,基于 Apache Commons FileUpload 的过时 `CommonsMultipartResolver` 在 Spring Framework 6.0 及其新的 Servlet 5.0+ 基线中不再可用。
要启用 multipart 处理,您需要在 `DispatcherServlet` Spring 配置中声明一个名为 `multipartResolver` 的 `MultipartResolver` bean。`DispatcherServlet` 检测到它并将其应用于传入请求。当接收到内容类型为 `multipart/form-data` 的 POST 时,解析器解析内容,将当前 `HttpServletRequest` 包装为 `MultipartHttpServletRequest`,以提供对解析文件的访问,并将部分作为请求参数公开。
#### Servlet Multipart 解析
需要通过 Servlet 容器配置启用 Servlet multipart 解析。为此:
- 在 Java 中,在 Servlet 注册上设置 `MultipartConfigElement`。
- 在 `web.xml` 中,在 servlet 声明中添加 `` 部分。
以下示例显示如何在 Servlet 注册上设置 `MultipartConfigElement`:
```java
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
```
一旦 Servlet multipart 配置到位,就可以添加名称为 `multipartResolver` 的 `StandardServletMultipartResolver` 类型的 bean。
> 此解析器变体按原样使用 Servlet 容器的 multipart 解析器,可能会使应用程序暴露于容器实现的差异。默认情况下,它将尝试使用任何 HTTP 方法解析任何 `multipart/` 内容类型,但这可能不是所有 Servlet 容器都支持的。有关详细信息和配置选项,请参阅 `StandardServletMultipartResolver` javadoc。
### 1.1.13 日志
Spring MVC 中的 DEBUG 级日志被设计为紧凑、最小和人性化。它关注的是一次又一次有用的高价值信息,而不是只有在调试特定问题时才有用的其他信息。
TRACE 级日志通常遵循与 DEBUG 相同的原则(例如,也不应该是消防水带),但可以用于调试任何问题。此外,一些日志消息可能在 TRACE 和 DEBUG 中显示不同级别的详细信息。
良好的日志记录来自使用日志的经验。如果您发现任何不符合规定目标的情况,请告知我们。
#### 敏感数据
DEBUG 和 TRACE 日志记录可能记录敏感信息。这就是为什么默认情况下会屏蔽请求参数和标头,并且必须通过 `DispatcherServlet` 上的 `enableLoggingRequestDetails` 属性显式启用它们的完整日志记录。
下面的示例显示了如何使用 Java 配置来执行此操作:
```java
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
```
## 1.2 过滤器
`spring-web` 模块提供了一些有用的过滤器:
- [表格数据](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-http-put)
- [转发头](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-forwarded-headers)
- [浅 ETag](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-shallow-etag)
- [CORS](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-cors)
### 1.2.1 表格数据
浏览器只能通过HTTP GET或HTTP POST提交表单数据,但非浏览器客户端也可以使用HTTP PUT、PATCH和DELETE。Servlet API 要求 `ServletRequest.getParameter*()` 方法仅支持 HTTP POST 的表单字段访问。
`spring-web` 模块提供 `FormContentFilter`,以拦截内容类型为 `application/x-www-form-urlencoded` 的 HTTP PUT、PATCH 和 DELETE 请求,从请求主体读取表单数据,并包装 `ServletRequest`,使表单数据通过 `ServletRequest.getParameter*()` 系列方法可用。
### 1.2.2 转发头
当请求通过代理(如负载平衡器)时,主机、端口和方案可能会发生变化,这使得从客户端角度创建指向正确主机、端口或方案的链接成为一项挑战。
RFC 7239 定义了 `Forwarded` HTTP 头,代理可以使用它来提供有关原始请求的信息。还有其他非标准标头,包括 `X-Forwarded-Host`、`X-Forwardd-Port`、`X-Forward-Proto`、`X-Forced-Ssl` 和 `X-Forwarded-Prefix`。
`ForwardedHeaderFilter` 是一个 Servlet 筛选器,用于修改请求,以便 a)根据`Forwarded` 标头更改主机、端口和方案,b)删除这些标头以消除进一步的影响。过滤器依赖于包装请求,因此必须在其他过滤器(如 `RequestContextFilter`)之前对其进行排序,这些过滤器应处理修改后的请求,而不是原始请求。
由于应用程序无法知道标头是由代理按预期添加的,还是由恶意客户端添加的,因此转发的标头存在安全问题。这就是为什么应该将信任边界处的代理配置为删除来自外部的不受信任的 `Forwarded` 标头。您还可以将 `ForwardedHeaderFilter` 配置为 `removeOnly=true`,在这种情况下,它会删除但不使用标头。
为了支持异步请求和错误分派,此筛选器应与 `DispatcherType.ASYNC` 以及 `DispatcherType.ERROR` 进行映射。如果使用 Spring Framework 的 `AbstractAnnotationConfigDispatcherServletInitializer`(请参阅Servlet Config),则会自动为所有分派类型注册所有筛选器。但是,如果通过 `web.xml` 或通过 `FilterRegistrationBean` 在 Spring Boot 中注册过滤器,请确保除了 `DispatcherType.REQUEST` 之外,还包括 `DispatcherType.ASYNC` 和 `Dispatcherype.ERROR`。
### 1.2.3 浅 ETag
`ShallowEtagHeaderFilter` 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希值来创建一个“浅” ETag。下次客户端发送时,它会执行同样的操作,但它还会将计算值与 `If-None-Match` 请求头进行比较,如果两者相等,则返回 304(NOT_MODIFIED)。
此策略节省网络带宽,但不节省 CPU,因为必须为每个请求计算完整响应。前面描述的控制器级别的其他策略可以避免计算。请参阅 HTTP 缓存。
此筛选器具有 `writeWeakETag` 参数,该参数将筛选器配置为写入弱 ETag,如下所示:`W/"02a2d595e6ed9a0b24f027f2b63b134d6"`(如 RFC 7232 第 2.3 节所定义)。
为了支持异步请求,此筛选器必须与 `DispatcherType.ASYNC` 映射,以便筛选器可以延迟并成功生成 ETag,直到最后一次异步分派结束。如果使用 Spring Framework 的 `AbstractAnnotationConfigDispatcherServletInitializer`(请参阅 Servlet 配置),则会自动为所有分派类型注册所有筛选器。但是,如果通过 `web.xml` 或通过 `FilterRegistrationBean` 在 Spring Boot 中注册过滤器,请确保包含 `DispatcherType.ASYNC`。
### 1.2.4 CORS
Spring MVC 通过控制器上的注释为 CORS 配置提供细粒度支持。然而,当与 Spring Security 一起使用时,我们建议依赖内置的 `CorsFilter`,该过滤器必须在 Spring Security 的过滤器链之前订购。
有关更多详细信息,请参阅 CORS 和 CORS 过滤器部分。
## 1.3 被注解的控制器
Spring MVC 提供了一个基于注解的编程模型,其中 `@Controller` 和 `@RestController` 组件使用注解来表示请求映射、请求输入、异常处理等。带注解的控制器具有灵活的方法签名,不必扩展基类或实现特定接口。以下示例显示了由注释定义的控制器:
```java
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
```
在前面的示例中,该方法接受一个 `Model` 并返回一个作为 `String` 的视图名称,但还有许多其他选项,将在本章稍后解释。
> [spring.io](https://spring.io/guides) 上的指南和教程使用本节中描述的基于注解的编程模型。
### 1.3.1 声明
您可以通过在 Servlet 的 `WebApplicationContext` 中使用标准的 Spring bean 定义来定义控制器 bean。`@Controller` 原型允许自动检测,与 Spring 在类路径中检测 `@Component` 类并自动注册 bean 定义的一般支持保持一致。它还充当注解类的原型,指示其作为 web 组件的角色。
要启用此类 `@Controller` bean 的自动检测,可以将组件扫描添加到 Java 配置中,如下例所示:
```java
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
```
以下示例显示了与前面示例等效的 XML 配置:
```xml
```
`@RestController` 是一个复合注解,它本身用 `@Controller` 和 `@ResponseBody` 进行元注解,以指示其每个方法都继承类型级别 `@ResponsBody` 注解的控制器,因此,与视图解析和 HTML 模板渲染相比,它直接写入响应体。
#### AOP 代理
在某些情况下,您可能需要在运行时用 AOP 代理来修饰控制器。例如,如果您选择在控制器上直接使用 `@Transactional` 注解。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。这是直接在控制器上进行此类注解的自动情况。
如果控制器实现了一个接口,并且需要 AOP 代理,那么您可能需要显式地配置基于类的代理。例如,使用 `@EnableTransactionManagement` 可以更改为 `@EnableTransactionManagement(proxyTargetClass=true)`,使用 `` 可以更改为 ``。
> 请记住,从 6.0 开始,通过接口代理,Spring MVC 不再仅基于接口上的类型级别 `@RequestMapping` 注解来检测控制器。请启用基于类的代理,否则接口还必须具有 `@Controller` 注解。
### 1.3.2 请求映射
您可以使用 `@RequestMapping` 注解将请求映射到控制器方法。它具有各种属性,可通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。您可以在类级别使用它来表示共享映射,或者在方法级别使用它缩小到特定的端点映射。
`@RequestMapping` 还有特定于 HTTP 方法的快捷方式变体:
- `@GetMapping`
- `@PostMapping`
- `@PutMapping`
- `@DeleteMapping`
- `@PatchMapping`
提供快捷方式是自定义注解,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 `@RequestMapping`,默认情况下,`@RequestMapping` 与所有 HTTP 方法匹配。在类级别仍然需要 `@RequestMapping` 来表示共享映射。
以下示例具有类型和方法级别映射:
```java
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
```
#### URI 模式
`@RequestMapping` 方法可以使用 URL 模式映射。有两种选择:
- `PathPattern` —— 与 URL 路径匹配的预解析模式也预解析为 `PathContainer`。此解决方案专为 web 使用而设计,可有效处理编码和路径参数,并有效匹配。
- `AntPathMatcher` —— 根据字符串路径匹配字符串模式。这是 Spring 配置中用于选择类路径、文件系统和其他位置上的资源的原始解决方案。它的效率较低,字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战。
`PathPattern` 是 web 应用程序的推荐解决方案,也是 Spring WebFlux 中的唯一选择。它从 5.3 版开始在 Spring MVC 中启用,从 6.0 版开始默认情况下启用。有关路径匹配选项的自定义,请参阅 MVC 配置。
`PathPattern` 支持与 `AntPathMatcher` 相同的模式语法。此外,它还支持捕获模式,例如 `{*spring}`,用于匹配路径末端的 0 个或多个路径段。`PathPattern` 还限制使用 `**` 来匹配多个路径段,以便只允许在模式末尾使用。这消除了为给定请求选择最佳匹配模式时的许多不确定性。有关完整模式语法,请参阅 PathPattern 和 AntPathMatcher。
一些示例模式:
- `"/resources/ima?e.png"` - 匹配路径段中的一个字符
- `"/resources/*.png"` - 匹配路径段中的零个或多个字符
- `"/resources/**"` - 匹配多个路径段
- `"/projects/{project}/versions"` - 匹配路径段并将其捕获为变量
- `"/projects/{project:[a-z]+}/versions"` - 使用正则表达式匹配并捕获变量
可以使用 `@PathVariable` 访问捕获的 URI 变量。例如:
```java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
```
您可以在类和方法级别声明 URI 变量,如下例所示:
```java
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
```
URI 变量会自动转换为适当的类型,或引发 `TypeMismatchException`。默认情况下支持简单类型(`int`、`long`、`Date` 等),您可以注册对任何其他数据类型的支持。请参阅类型转换和 `DataBinder`。
您可以显式地命名 URI 变量(例如,`@PathVariable("customId")`),但如果名称相同且代码使用 `-parameters` 编译器标志编译,则可以省略该细节。
语法 `{varName:regex}` 使用语法为 `{varName:regex}` 的正则表达式声明 URI 变量。例如,给定 URL `"/spring-web-3.0.5.jar"`,以下方法提取名称、版本和文件扩展名:
```java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
```
URI 路径模式还可以嵌入 `${…}` 通过对本地、系统、环境和其他属性源使用 `PropertySourcesPlaceholderConfigurator` 在启动时解析的占位符。例如,您可以使用它根据某些外部配置参数化基本 URL。
#### 模式对比
当多个模式与 URL 匹配时,必须选择最佳匹配。这取决于是否启用了解析的 `PathPattern` 的使用,可以使用以下方法之一完成:
- `PathPattern.SPECIFICITY_COMPARATOR`
- `AntPathMatcher.getPatternComparator(String path)`
这两种方法都有助于对模式进行排序,并将更具体的模式放在顶部。如果模式的 URI 变量数较少(计为 1)、单通配符(计为 1)和双通配符(计为 2),则模式就不那么具体。如果得分相等,则选择较长的模式。给定相同的分数和长度,将选择 URI 变量多于通配符的模式。
默认映射模式(`/**`)从评分中排除,并始终排序在最后。此外,前缀模式(如 `/public/**`)被认为比其他没有双通配符的模式更不特定。
有关详细信息,请按照上面的链接访问模式比较器。
#### 后缀匹配
从 5.3 开始,默认情况下 Spring MVC 不再执行 `.*` 后缀模式匹配,其中映射到 `/person` 的控制器也隐式映射到 `/person.*`。因此,不再使用路径扩展来解释请求的响应内容类型 —— 例如,`/person.pdf`、`/person.xml` 等。
当浏览器用来发送难以一致解释的 `Accept` 标头时,以这种方式使用文件扩展名是必要的。目前,这不再是必要的,使用 `Accept` 标头应该是首选。
随着时间的推移,文件扩展名的使用在许多方面都被证明是有问题的。当与 URI 变量、路径参数和 URI 编码的使用重叠时,可能会导致歧义。关于基于 URL 的授权和安全性的推理(有关更多详细信息,请参阅下一节)也变得更加困难。
要在 5.3 之前的版本中完全禁用路径扩展,请设置以下内容:
- `useSuffixPatternMatch(false)`,请参见 PathMatchConfigurer
- `favorPathExtension(false)`,请参阅 ContentNegotiationConfigurer
通过 `"Accept"` 标头以外的方式请求内容类型仍然很有用,例如在浏览器中键入 URL 时。路径扩展的安全替代方案是使用查询参数策略。如果必须使用文件扩展名,请考虑通过 ContentNegotiationConfigurer 的 `mediaTypes` 属性将其限制为显式注册的扩展名列表。
#### 后缀匹配和 RFD
反射文件下载(RFD)攻击与 XSS 相似,因为它依赖于响应中反映的请求输入(例如,查询参数和 URI 变量)。然而,RFD 攻击不是将 JavaScript 插入 HTML,而是依靠浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。
在 Spring MVC 中,`@ResponseBody` 和 `ResponseEntity` 方法存在风险,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展请求这些内容类型。禁用后缀模式匹配和使用路径扩展进行内容协商可以降低风险,但不足以防止 RFD 攻击。
为了防止 RFD 攻击,在呈现响应体之前,Spring MVC 添加了 `Content-Disposition:inline;filename=f.txt` 标头,以建议一个固定且安全的下载文件。只有当 URL 路径包含既不允许安全也不明确注册内容协商的文件扩展名时,才能执行此操作。然而,当 URL 直接输入到浏览器中时,它可能会产生副作用。
默认情况下,许多公共路径扩展都是安全的。具有自定义 `HttpMessageConverter` 实现的应用程序可以显式注册用于内容协商的文件扩展名,以避免为这些扩展名添加 `Content-Disposition` 头。请参见内容类型。
有关 RFD 的其他建议,请参见 CVE-2015-5211。
#### 可消费的媒介类型
您可以根据请求的内容类型缩小请求映射,如下例所示:
```java
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
// (1) 使用 `consumes` 属性按内容类型缩小映射。
```
`consumers` 属性还支持否定表达式 —— 例如 `!text/plain` 是指除 `text/plain` 之外的任何内容类型。
您可以在类级别声明共享 `consumes` 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别使用 `consumes` 属性重写而不是扩展类级别声明。
> `MediaType` 为常用的媒体类型提供常量,如 `APPLICATION_JSON_VALUE` 和 `APPLICATIION_XML_VALUE`。
#### 可生成的媒介类型
您可以根据 `Accept` 请求标头和控制器方法生成的内容类型列表缩小请求映射,如下例所示:
```java
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
// (1) 使用 products 属性按内容类型缩小映射。
```
媒体类型可以指定字符集。支持否定表达式 —— 例如 `!text/plain` 是指除 “text/plain” 以外的任何内容类型。
您可以在类级别声明共享的 `produces` 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别 `produces` 属性重写而不是扩展类级别声明。
> `MediaType` 为常用的媒体类型提供常量,如 `APPLICATION_JSON_VALUE` 和 `APPLICATIION_XML_VALUE`。
#### 参数,标头
您可以根据请求参数条件缩小请求映射。您可以测试是否存在请求参数(`myParam`)、是否缺少请求参数(`!myParam`)或是否存在特定值(`myParam=myValue`)。以下示例显示了如何测试特定值:
```java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
// (1) 测试 `myParam` 是否等于 `myValue`。
```
也可以对请求头条件使用相同的方法,如下例所示:
```java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
// (1) 测试 `myHeader` 是否等于 `myValue`。
```
> 您可以将 `Content-Type` 和 `Accept` 与 headers 条件匹配,但最好使用 consumes 和 products。
#### HTTP HEAD, OPTIONS
`@GetMapping`(和 `@RequestMapping(method=HttpMethod.GET)`)对请求映射透明地支持 HTTP HEAD。控制器方法无需更改。`jakarta.servlet.http.HttpServlet` 中应用的响应包装器确保 `Content-Length` 标头设置为写入的字节数(而不实际写入响应)。
`@GetMapping`(和 `@RequestMapping(method=HttpMethod.GET)`)隐式映射到并支持 HTTP HEAD。处理 HTTP HEAD 请求时,就像处理 HTTP GET 一样,只是不写入正文,而是计算字节数并设置 `Content-Length` 标头。
默认情况下,HTTP OPTIONS 是通过将 `Allow` 响应头设置为所有 `@RequestMapping` 方法中列出的具有匹配 URL 模式的 HTTP 方法列表来处理的。
对于没有 HTTP 方法声明的 `@RequestMapping`,`Allow` 标头设置为 `GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS`。控制器方法应始终声明支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变量:`@GetMapping`、`@PostMapping` 和其他)。
您可以显式地将 `@RequestMapping` 方法映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这不是必需的。
#### 自定义注解
Spring MVC 支持使用组合注解进行请求映射。这些注释本身是用 `@RequestMapping` 进行元注解的,组成这些注解的目的是重新声明 `@RequestMapping` 属性的子集(或全部),以实现更窄、更具体的用途。
`@GetMapping`、`@PostMapping`、`@PutMapping`、`@DeleteMapping` 和 `@PatchMapping` 是组合注解的示例。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 `@RequestMapping`,默认情况下,`@RequestMapping` 与所有 HTTP 方法匹配。如果需要组合注解的示例,请查看这些注解是如何声明的。
Spring MVC 还支持带有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要子类化 `RequestMappingHandlerMapping` 并重写 `getCustomMethodCondition` 方法,在这里您可以检查自定义属性并返回自己的 `RequestCondition`。
#### 显式注册
您可以以编程方式注册处理程序方法,这些方法可以用于动态注册或高级情况,例如不同 URL 下的同一处理程序的不同实例。以下示例注册处理程序方法:
```java
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
// (1) 为控制器注入目标处理程序和处理程序映射。
// (2) 准备请求映射元数据。
// (3) 获取处理程序方法。
// (4) 添加注册。
```
### 1.3.3 处理器方法
`@RequestMapping` 处理程序方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。
#### 方法参数
下表描述了支持的控制器方法参数。任何参数都不支持反应类型。
JDK 8 的 `java.util.Optional` 作为方法参数与具有 `required` 属性(例如 `@RequestParam`、`@RequestHeader` 等)的注解结合使用,并且等效于 `required=false`。
| 控制器方法参数 | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `WebRequest`, `NativeWebRequest` | 对请求参数、请求和会话属性的通用访问,无需直接使用 Servlet API。 |
| `jakarta.servlet.ServletRequest`, `jakarta.servlet.ServletResponse` | 选择任何特定的请求或响应类型 —— 例如,`ServletRequest`、`HttpServletRequest` 或 Spring 的 `MultipartRequest `、`MultipartHttpServletRequest `。 |
| `jakarta.servlet.http.HttpSession` | 强制会话的存在。因此,这样的参数永远不是 `null`。请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将 `RequestMappingHandlerAdapter` 实例的 `synchronizedOnSession` 标志设置为 `true`。 |
| `jakarta.servlet.http.PushBuilder` | 用于编程 HTTP/2 资源推送的 Servlet 4.0 推送生成器 API。注意,根据 Servlet 规范,如果客户端不支持 HTTP/2 功能,则注入的 `PushBuilder` 实例可以为空。 |
| `java.security.Principal` | 当前已验证的用户 — 如果已知,则可能是特定的 `Principal` 实现类。请注意,如果对该参数进行了注解,以便允许自定义解析器在通过 `HttpServletRequest#getUserPrincipal` 返回默认解析之前解析该参数,则不会立即解析该参数。例如,Spring Security `Authentication` 实现了 `Principal`,并将通过 `HttpServletRequest#getUserPrincipal` 进行注入,除非它也用 `@AuthenticationPrincipal` 进行注解,在这种情况下,它由自定义 Spring Security 解析器通过 `Authentication#getPrincipal` 进行解析。 |
| `HttpMethod` | 请求的 HTTP 方法。 |
| `java.util.Locale` | 当前请求区域设置,由可用的最具体的 `LocaleResolver`(实际上是配置的 `LocaleVesolver` 或 `LocaleContextResolver`)确定。 |
| `java.util.TimeZone` + `java.time.ZoneId` | 与当前请求关联的时区,由 `LocaleContextResolver` 确定。 |
| `java.io.InputStream`, `java.io.Reader` | 用于访问 Servlet API 公开的原始请求体。 |
| `java.io.OutputStream`, `java.io.Writer` | 用于访问 Servlet API 公开的原始响应体。 |
| `@PathVariable` | 用于访问 URI 模板变量。参见 [URI 模式](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-uri-templates)。 |
| `@MatrixVariable` | 用于访问 URI 路径段中的名称值对。参见[矩阵变量](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-matrix-variables)。 |
| `@RequestParam` | 用于访问 Servlet 请求参数,包括多部分文件。参数值被转换为声明的方法参数类型。请参见 [`@RequestParam`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestparam) 以及 [Multipart](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart-forms)。请注意,对于简单参数值,`@RequestParam` 的使用是可选的。请参阅本表末尾的“任何其他参数”。 |
| `@RequestHeader` | 用于访问请求标头。标头值被转换为声明的方法参数类型。请参见 [`@RequestHeader`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestheader)。 |
| `@CookieValue` | 访问 cookie。Cookie 值被转换为声明的方法参数类型。请参见 [`@CookieValue`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-cookievalue)。 |
| `@RequestBody` | 用于访问 HTTP 请求主体。使用 `HttpMessageConverter` 实现将正文内容转换为声明的方法参数类型。请参见 [`@RequestBody `](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestbody)。 |
| `HttpEntity` | 用于访问请求标头和正文。使用 `HttpMessageConverter` 转换正文。参见[HttpEntity](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-httpentity)。 |
| `@RequestPart` | 要访问 `multipart/form-data` 请求中的部分,请使用 `HttpMessageConverter` 转换该部分的主体。参见 [Multipart](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart-forms)。 |
| `java.util.Map`, `org.springframework.ui.Model`, `org.springframework.ui.ModelMap` | 用于访问 HTML 控制器中使用的模型,并作为视图渲染的一部分公开给模板。 |
| `RedirectAttributes` | 指定重定向时要使用的属性(即,要附加到查询字符串),以及在重定向后请求之前临时存储的 flash 属性。请参阅[重定向属性](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-redirecting-passing-data)和 [Flash 属性](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-flash-attributes)。 |
| `@ModelAttribute` | 用于访问应用了数据绑定和验证的模型中的现有属性(如果不存在则实例化)。请参见 [`@ModelAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-modelattrib-method-args) 以及 [Model](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-modelattrib-methods) 和 [`DataBinder`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder)。请注意,`@ModelAttribute` 的使用是可选的(例如,设置其属性)。请参阅本表末尾的“任何其他参数”。 |
| `Errors`, `BindingResult` | 用于访问来自命令对象(即 `@ModelAttribute` 参数)验证和数据绑定的错误,或来自 `@RequestBody` 或 `@RequestPart` 参数验证的错误。必须在经过验证的方法参数之后立即声明 `Errors` 或 `BindingResult` 参数。 |
| `SessionStatus` + 类级别的 `@SessionAttributes` | 用于标记表单处理完成,这将触发清理通过类级别 `@SessionAttributes` 注解声明的会话属性。请参见 [`@SessionAttributes`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-sessionattributes) 获取更多详细信息。 |
| `UriComponentsBuilder` | 用于准备与当前请求的主机、端口、方案、上下文路径和 servlet 映射的文本部分相关的 URL。参见 [URI 链接](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-uri-building)。 |
| `@SessionAttribute` | 对于任何会话属性的访问,与作为类级别 `@SessionAttributes` 声明的结果存储在会话中的模型属性不同。请参见 [`@SessionAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-sessionattribute) 了解更多详情。 |
| `@RequestAttribute` | 用于访问请求属性。请参见 [`@RequestAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestattrib) 获取更多详细信息。 |
| 任何其他参数 | 如果方法参数与此表中的任何早期值不匹配,并且它是简单类型(由 [BeanTils#isSimpleProperty](https://docs.spring.io/spring-framework/docs/6.0.6/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-) 确定),它被解析为 `@RequestParam`。否则,它将被解析为 `@ModelAttribute`。 |
#### 返回值
下表描述了支持的控制器方法返回值。所有返回值都支持反应类型。
| 控制器方法返回值 | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `@ResponseBody` | 返回值通过 `HttpMessageConverter` 实现转换并写入响应。参见 [`@ResponseBody `](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-responsebody)。 |
| `HttpEntity`, `ResponseEntity` | 指定完整响应(包括 HTTP 头和正文)的返回值将通过 `HttpMessageConverter` 实现转换并写入响应。参见 [ResponseEntity](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-responseentity)。 |
| `HttpHeaders` | 用于返回带有标头而没有正文的响应。 |
| `ErrorResponse` | 要呈现 RFC 7807 错误响应并在正文中提供详细信息,请参阅[错误响应](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions) |
| `ProblemDetail` | 要呈现 RFC 7807 错误响应并在正文中提供详细信息,请参阅[错误响应](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions) |
| `String` | 要使用 `ViewResolver` 实现解析并与隐式模型一起使用的视图名称 —— 通过命令对象和 `@ModelAttribute` 方法确定。处理程序方法还可以通过声明 `Model` 参数以编程方式丰富模型(请参见[显式注册](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-registration))。 |
| `View` | 用于与隐式模型一起呈现的 `View` 实例 —— 通过命令对象和 `@ModelAttribute` 方法确定。处理程序方法还可以通过声明 `Model` 参数以编程方式丰富模型(请参见[显式注册](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-registration))。 |
| `java.util.Map`, `org.springframework.ui.Model` | 要添加到隐式模型的属性,视图名称通过 `RequestToViewNameTranslator` 隐式确定。 |
| `@ModelAttribute` | 要添加到模型中的属性,视图名称通过 `RequestToViewNameTranslator` 隐式确定。请注意,`@ModelAttribute` 是可选的。请参阅本表末尾的“任何其他返回值”。 |
| `ModelAndView` 对象 | 要使用的视图和模型属性,以及响应状态(可选)。 |
| `void` | 如果具有 `void` 返回类型(或 `null` 返回值)的方法还具有 `ServletResponse`、`OutputStream` 参数或 `@ResponseStatus` 注解,则认为该方法已完全处理了响应。如果控制器进行了肯定的 `ETag` 或 `lastModified` 时间戳检查(参见[控制器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-caching-etag-lastmodified)以获取详细信息)。如果以上任何一项都不为真,则 `void` 返回类型也可以指示 REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。 |
| `DeferredResult` | 从任何线程异步生成前面的任何返回值 —— 例如作为某个事件或回调的结果。参见[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和 [`DeferredResult`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult)。 |
| `Callable` | 在 Spring MVC 托管线程中异步生成上述任何返回值。参见[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和 [`Callable`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-callable)。 |
| `ListenableFuture`, `java.util.concurrent.CompletionStage`, `java.util.concurrent.CompletableFuture` | 作为 `DeferredResult` 的替代选项,为方便起见(例如,当基础服务返回其中一个时)。 |
| `ResponseBodyEmitter`, `SseEmitter` | 使用 `HttpMessageConverter` 实现异步发出对象流以写入响应。也支持作为 `ResponseEntity` 的主体。参见[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和 [HTTP 流](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-http-streaming)。 |
| `StreamingResponseBody` | 异步写入响应 `OutputStream`。也支持作为 `ResponseEntity` 的主体。参见[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和 [HTTP 流](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-http-streaming)。 |
| 通过 `ReactiveAdapterRegistry` 注册的 Reactor 和其他反应式类型 | 单个值类型(例如 `Mono`)相当于返回 `DeferredResult`。根据请求的媒体类型,多值类型(例如 `Flux`)可被视为流,例如 "text/event-stream", "application/json+stream",或以其他方式收集到列表中并呈现为单个值。参见[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和[反应式类型](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-reactive-types)。 |
| 其他返回值 | 如果返回值以任何其他方式未解析,则将其视为模型属性,除非它是由 [BeanTils#isSimpleProperty](https://docs.spring.io/spring-framework/docs/6.0.6/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-) 确定的简单类型,在这种情况下,它仍然无法解析。 |
#### 类型转换
如果参数声明为非字符串,则表示基于字符串的请求输入的某些带注解的控制器方法参数(例如 `@RequestParam`、`@RequestHeader`、`@PathVariable`、`@MatrixVariable` 和 `@CookieValue`)可能需要类型转换。
对于这种情况,根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(`int`、`long`、`Date` 等)。您可以通过 `WebDataBinder`(请参阅 `DataBinder`)或通过向 `FormattingConversionService` 注册 `Formatters` 来自定义类型转换。请参见 Spring 字段格式化。
类型转换中的一个实际问题是处理空字符串源值。如果由于类型转换而变为空,则此类值将被视为缺失。`Long`、`UUID` 和其他目标类型可能是这种情况。如果要允许注入 `null`,请在参数注释上使用 `required` 的标志,或将参数声明为 `@Nullable`。
> 从 5.3 开始,即使在类型转换之后,也将强制使用非空参数。如果处理程序方法也打算接受空值,请将参数声明为 `@Nullable`,或在相应的`@RequestParam` 等注释中将其标记为 `required=false`。这是最佳实践,也是 5.3 升级中遇到的回归问题的建议解决方案。
>
> 或者,在需要 `@PathVariable` 的情况下,您可以专门处理例如生成的 `MissingPathVariableException`。转换后的空值将被视为空的原始值,因此相应的 `Missing…Exception` 变体将被抛出。
#### 矩阵变量
RFC 3986 讨论了路径段中的名称值对。在 Spring MVC 中,我们根据 Tim Berners Lee 的“旧帖子”将这些变量称为“矩阵变量”,但它们也可以称为 URI 路径参数。
矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,`/cars;color=red,green;year=2012`)。也可以通过重复的变量名称指定多个值(例如,`color=red;color=green;color=blue`)。
如果 URL 预期包含矩阵变量,则控制器方法的请求映射必须使用 URI 变量来掩盖(mask)该变量内容,并确保可以独立于矩阵变量顺序和存在性成功匹配请求。以下示例使用矩阵变量:
```java
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
```
考虑到所有路径段都可能包含矩阵变量,有时可能需要消除矩阵变量应位于哪个路径变量中的歧义。以下示例显示了如何做到这一点:
```java
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
```
可以将矩阵变量定义为可选变量,并指定默认值,如下例所示:
```java
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
```
要获取所有矩阵变量,可以使用 `MultiValueMap`,如下例所示:
```java
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
```
请注意,您需要启用矩阵变量的使用。在 MVC Java 配置中,需要通过路径匹配将 `UrlPathHelper` 设置为 `removeSemicolonContent=false`。在 MVC XML 命名空间中,可以设置 ``。
#### @RequestParam
您可以使用 `@RequestParam` 注解将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
以下示例显示了如何执行此操作:
```java
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
// (1) 使用 `@RequestParam` 来绑定 `petId`
```
默认情况下,使用此注解的方法参数是必需的,但可以通过将 `@RequestParam` 注解的 `required` 标志设置为 `false` 或使用 `java.util.Optional` 包装器声明参数来指定方法参数是可选的。
如果目标方法参数类型不是 `String`,则自动应用类型转换。请参见类型转换。
将参数类型声明为数组或列表允许解析同一参数名称的多个参数值。
如果 `@RequestParam` 注解声明为 `Map` 或 `MultiValueMap`,而没有在注解中指定参数名称,则会使用每个给定参数名称的请求参数值填充映射。
注意,`@RequestParam` 的使用是可选的(例如,设置其属性)。默认情况下,任何简单值类型(由 BeanUtils#isSimpleProperty 确定)且未由任何其他参数解析器解析的参数都将被视为使用 `@RequestParam` 进行了注解。
#### @RequestHeader
可以使用 `@RequestHeader` 注解将请求头绑定到控制器中的方法参数。
考虑以下带有标题的请求:
```
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
```
以下示例获取 `Accept-Encoding` 和 `Keep-Alive` 标头的值:
```java
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
// (1) 获得 `Accept-Encoding` 标头的值
// (2) 获得 `Keep-Alive` 标头的值
```
如果目标方法参数类型不是 `String`,则自动应用类型转换。请参见类型转换。
当 `@RequestHeader` 注解用于 `Map`、`MultiValueMap` 或 `HttpHeaders` 参数时,映射将填充所有标头值。
> 内置支持可用于将逗号分隔的字符串转换为字符串数组或字符串集合或类型转换系统已知的其他类型。例如,用 `@RequestHeader("Accept")` 注解的方法参数可以是 `String` 类型,也可以是 `String[]` 或 `List` 类型。
#### @CookieValue
您可以使用 `@CookieValue` 注解将 HTTP cookie 的值绑定到控制器中的方法参数。
考虑使用以下 cookie 的请求:
```
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
```
以下示例显示如何获取 cookie 值:
```java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
// (1) 获取 `JSESSIONID` cookie 的值
```
如果目标方法参数类型不是 `String`,则自动应用类型转换。请参见类型转换。
#### @ModelAttribute
您可以在方法参数上使用 `@ModelAttribute` 注解来访问模型中的属性,或者在不存在的情况下将其实例化。model 属性还覆盖了名称与字段名称匹配的 HTTPServlet 请求参数的值。这被称为数据绑定,它使您不必处理解析和转换单个查询参数和表单字段。以下示例显示了如何执行此操作:
```java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { (1)
// method logic...
}
// (1) 绑定 `Pet` 实例
```
上述 `Pet` 实例的来源如下:
- 从可能已通过 @ModelAttribute 方法添加的模型中检索。
- 如果 model 属性列在类级别的 `@SessionAttributes` 注解中,则从 HTTP 会话中检索。
- 通过 `Converter` 获得,其中模型属性名称与请求值(如路径变量或请求参数)的名称匹配(请参见下一个示例)。
- 使用其默认构造函数实例化。
- 通过具有与 Servlet 请求参数匹配的参数的“主构造函数”实例化。参数名称通过 JavaBeans `@ConstructorProperties` 或字节码中运行时保留的参数名称确定。
使用 @ModelAttribute 方法来提供它或依赖框架来创建模型属性的另一种选择是使用 `Converter` 来提供实例。当模型属性名称与请求值(如路径变量或请求参数)的名称匹配,并且存在从字符串到模型属性类型的转换器时,将应用此选项。在以下示例中,模型属性名称是与 URI 路径变量 `account` 匹配的 `account`,并且有一个注册的 `Converter`,它可以从数据存储中加载 `Account`:
```java
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) { (1)
// ...
}
// (1) 使用特定属性名字绑定一个 `Account` 实例
```
获得模型属性实例后,应用数据绑定。`WebDataBinder` 类将 Servlet 请求参数名称(查询参数和表单字段)与目标 `Object` 上的字段名称相匹配。必要时,在应用类型转换后填充匹配字段。有关数据绑定(和验证)的详细信息,请参阅验证。有关自定义数据绑定的详细信息,请参阅 `DataBinder`。
数据绑定可能会导致错误。默认情况下,引发 `BindException`。但是,要检查控制器方法中的此类错误,可以在 `@ModelAttribute` 旁边立即添加 `BindingResult` 参数,如下例所示:
```java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
// (1) 在 `@ModelAttribute` 之后添加 BindingResult
```
在某些情况下,您可能希望在没有数据绑定的情况下访问模型属性。对于这种情况,您可以将 `Model` 注入控制器并直接访问它,或者设置 `@ModelAttribute(binding=false)`,如下例所示:
```java
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) { (1)
// ...
}
// (1) 设置 `@ModelAttribute(binding=false)`
```
通过添加 `jakarta.validation.Valid` 注解或 Spring 的 `@Validated` 注解(Bean 验证和 Spring 验证),可以在数据绑定后自动应用验证。以下示例显示了如何执行此操作:
```java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
// (1) 验证 `Pet` 实例
```
注意,使用 `@ModelAttribute` 是可选的(例如,设置其属性)。默认情况下,任何不是简单值类型(由 BeanTils#isSimpleProperty 确定)且未被任何其他参数解析器解析的参数都将被视为使用 `@ModelAttribute` 进行了注解。
#### @SessionAttributes
`@SessionAttributes` 用于在请求之间的 HTTPServlet 会话中存储模型属性。它是一个类型级注解,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应透明地存储在会话中,以供后续访问请求使用。
以下示例使用 `@SessionAttributes` 注解:
```java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
// (1) 使用 @SessionAttributes 注解
```
在第一个请求中,当名称为 `pet` 的模型属性被添加到模型中时,它会自动升级并保存在 HTTP Servlet 会话中。在另一个控制器方法使用 `SessionStatus` 方法参数清除存储之前,它将一直保持不变,如下例所示:
```java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); (2)
// ...
}
}
// (1) 在 Servlet 会话中存储 `Pet` 值
// (2) 从 Servlet 会话中清除 `Pet` 值
```
#### @SessionAttribute
如果您需要访问全局管理的预先存在的会话属性(即,在控制器外部) —— 例如,通过过滤器),并且可能存在也可能不存在,可以在方法参数上使用 `@SessionAttribute` 注解,如下例所示:
```java
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
// (1) 使用一个 `@SessionAttribute` 注解
```
对于需要添加或删除会话属性的用例,请考虑将 `org.springframework.web.context.request.WebRequest` 或 `jakarta.servlet.http.HttpSession` 注入控制器方法。
对于作为控制器工作流的一部分在会话中临时存储模型属性,请考虑使用 `@SessionAttributes`,如 `@SessionAttributes` 中所述。
#### @RequestAttribute
与 `@SessionAttribute` 类似,您可以使用 `@RequestAttribute` 注解访问先前创建的预先存在的请求属性(例如,通过 `Servlet` 筛选器或 `HandlerInterceptor`):
```java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
// (1) 使用 `@RequestAttribute` 注解
```
#### 重定向属性
默认情况下,所有模型属性都被视为在重定向 URL 中公开为 URI 模板变量。在剩下的属性中,作为基本类型或基本类型的集合或数组的属性将自动附加为查询参数。
如果模型实例是专门为重定向准备的,则将基本类型属性附加为查询参数可能是所需的结果。但是,在带注解的控制器中,模型可以包含为渲染目的而添加的其他属性(例如,下拉字段值)。为了避免此类属性出现在 URL 中,`@RequestMapping` 方法可以声明 `RedirectAttributes` 类型的参数,并使用它指定 `RedirectView` 可用的确切属性。如果该方法确实重定向,则使用 `RedirectAttributes` 的内容。否则,将使用模型的内容。
`RequestMappingHandlerAdapter` 提供了一个名为 `ignoreDefaultModelOnRedirect` 的标志,您可以使用它来指示如果控制器方法重定向,则不应使用默认模型的内容。相反,控制器方法应该声明 `RedirectAttributes` 类型的属性,如果不这样做,则不应将任何属性传递给`RedirectView`。MVC 命名空间和 MVC Java 配置都将此标志设置为 `false`,以保持向后兼容性。但是,对于新应用程序,我们建议将其设置为 `true`。
请注意,当前请求中的 URI 模板变量在扩展重定向 URL 时自动可用,您不需要通过 `Model` 或 `RedirectAttributes` 显式添加它们。以下示例显示了如何定义重定向:
```java
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
```
将数据传递到重定向目标的另一种方法是使用 flash 属性。与其他重定向属性不同,flash 属性保存在 HTTP 会话中(因此不会显示在 URL 中)。有关详细信息,请参见 Flash 属性。
#### Flash 属性
Flash 属性为一个请求提供了一种方式来存储要在另一个请求中使用的属性。这是重定向时最常需要的 —— 例如后重定向获取模式。Flash 属性在重定向之前(通常在会话中)临时保存,以便在重定向后对请求可用,并立即删除。
Spring MVC 有两个主要的抽象来支持 flash 属性。`FlashMap` 用于保存 flash 属性,而 `FlashMapManager` 用于存储、检索和管理 `FlashMap` 实例。
Flash 属性支持始终处于“开启”状态,不需要显式启用。但是,如果不使用,它将不会导致 HTTP 会话创建。在每个请求上,都有一个“输入” `FlashMap` 和一个“输出” `FlashMap`,其中包含从上一个请求(如果有)传递的属性,以及一个要为后续请求保存的属性。这两个 `FlashMap` 实例都可以通过 `RequestContextUtils` 中的静态方法从 Spring MVC 中的任何位置访问。
带注解的控制器通常不需要直接使用 `FlashMap`。相反,`@RequestMapping` 方法可以接受 `RedirectAttributes` 类型的参数,并使用它为重定向场景添加 flash 属性。通过 `RedirectAttributes` 添加的 Flash 属性会自动传播到“输出” `FlashMap`。类似地,在重定向之后,来自“输入” `FlashMap` 的属性会自动添加到为目标 URL 提供服务的控制器的模型中。
> **将请求与 flash 属性匹配**
>
> flash 属性的概念存在于许多其他 web 框架中,并被证明有时会遇到并发问题。这是因为,根据定义,flash 属性要存储到下一个请求。然而,“下一个”请求可能不是预期的接收方,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,闪存属性被过早删除。
>
> 为了减少出现此类问题的可能性,`RedirectView` 会自动用目标重定向 URL 的路径和查询参数“标记” `FlashMap` 实例。反过来,默认 `FlashMapManager` 在查找“输入” `FlashMap` 时将该信息与传入请求相匹配。
>
> 这并不能完全消除并发问题的可能性,但通过重定向 URL 中已有的信息,可以大大减少并发问题。因此,我们建议您将 flash 属性主要用于重定向场景。
#### Multipart
启用 `MultipartResolver` 后,将解析包含 `multipart/form-data` 数据的 POST 请求的内容,并将其作为常规请求参数进行访问。以下示例访问一个常规表单字段和一个上载的文件:
```java
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
```
将参数类型声明为 `List` 允许解析同一参数名称的多个文件。
当 `@RequestParam` 注解声明为 `Map` 或 `MultiValueMap` 时,没有在注解中指定参数名,则映射将填充每个给定参数名的 multipart 文件。
> 通过 Servlet multipart 解析,您还可以将 `jakarta.Servlet.http.Part` 声明为方法参数或集合值类型,而不是 Spring 的 `MultipartFile`。
您还可以将 multipart 内容用作到命令对象的数据绑定的一部分。例如,上一示例中的表单字段和文件可以是表单对象上的字段,如下例所示:
```java
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
if (!form.getFile().isEmpty()) {
byte[] bytes = form.getFile().getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
```
在 RESTful 服务场景中,也可以从非浏览器客户端提交 Multipart 请求。以下示例显示了一个 JSON 文件:
```
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
```
您可以使用 `@RequestParam` 作为 `String` 访问“元数据”部分,但您可能希望它从 JSON 反序列化(类似于 `@RequestBody`)。使用 HttpMessageConverter 转换后,使用 `@RequestPart` 注解访问 multipart:
```java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
```
您可以将 `@RequestPart` 与 `jakarta.validation.Valid` 结合使用,也可以使用 Spring 的 `@Validated` 注解,这两者都会导致应用标准 Bean 验证。默认情况下,验证错误会导致 `MethodArgumentNotValidException`,该异常将转换为 400(BAD_REQUEST)响应。或者,可以通过 `Errors` 或 `BindingResult` 参数在控制器内本地处理验证错误,如下例所示:
```java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}
```
#### @RequestBody
您可以使用 `@RequestBody` 注解通过 `HttpMessageConverter` 将请求体读取并反序列化为 `Object`。以下示例使用 `@RequestBody` 参数:
```java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
```
您可以使用 MVC 配置的消息转换器选项来配置或自定义消息转换。
您可以将 `@RequestBody` 与 `jakarta.validation.Valid` 或 Spring 的 `@Validated` 注解结合使用,这两者都会导致应用标准 Bean 验证。默认情况下,验证错误会导致 `MethodArgumentNotValidException`,该异常将转换为 400(BAD_REQUEST)响应。或者,可以通过 `Errors` 或 `BindingResult` 参数在控制器内本地处理验证错误,如下例所示:
```java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
```
#### HttpEntity
`HttpEntity` 或多或少与使用 `@RequestBody` 相同,但它基于一个公开请求头和主体的容器对象。下面的列表显示了一个示例:
```java
@PostMapping("/accounts")
public void handle(HttpEntity entity) {
// ...
}
```
#### @ResponseBody
可以在方法上使用 `@ResponseBody` 注解,通过 `HttpMessageConverter` 将返回序列化到响应体。下面的列表显示了一个示例:
```java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
```
`@ResponseBody` 在类级别也受支持,在这种情况下,它由所有控制器方法继承。这是 `@RestController` 的效果,它只是一个标记有 `@Controller` 和 `@ResponseBody` 的元注解。
您可以将 `@ResponseBody` 与反应类型一起使用。有关详细信息,请参阅异步请求和响应类型。
您可以使用 MVC 配置的消息转换器选项来配置或自定义消息转换。
您可以将 `@ResponseBody` 方法与 JSON 序列化视图相结合。有关详细信息,请参见 Jackson JSON。
#### ResponseEntity
`ResponseEntity` 类似于 `@ResponseBody`,但具有状态和标头。例如:
```java
@GetMapping("/something")
public ResponseEntity handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
```
Spring MVC 支持使用单值反应式类型异步生成 `ResponseEntity`,和/或为主体使用单值和多值反应式类型。这允许以下类型的异步响应:
- `ResponseEntity>` 或 `ResponseElement>` 使响应状态和标头立即已知,而稍后将异步提供主体。如果主体由 0..1 个值组成,则使用 `Mono`;如果主体可以生成多个值,则使用 `Flux`。
- `Mono>` 提供所有三个 —— 响应状态、标头和正文,稍后异步进行。这允许响应状态和标头根据异步请求处理的结果而变化。
#### Jackson JSON
Spring 提供了 Jackson JSON 库的支持。
##### JSON Views
Spring MVC 为 Jackson 的序列化视图提供了内置支持,它只允许呈现 `Object` 中所有字段的子集。要将其与 `@ResponseBody` 或 `ResponseEntity` 控制器方法一起使用,可以使用 Jackson 的 `@JsonView` 注解来激活序列化视图类,如下例所示:
```java
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
```
> `@JsonView` 允许视图类数组,但每个控制器方法只能指定一个。如果需要激活多个视图,可以使用复合接口。
如果您希望以编程方式执行上述操作,而不是声明 `@JsonView` 注解,请使用 `MappingJacksonValue` 包装返回值,并使用它提供序列化视图:
```java
@RestController
public class UserController {
@GetMapping("/user")
public MappingJacksonValue getUser() {
User user = new User("eric", "7!jd#h23");
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(User.WithoutPasswordView.class);
return value;
}
}
```
对于依赖视图解析的控制器,可以将序列化视图类添加到模型中,如下例所示:
```java
@Controller
public class UserController extends AbstractController {
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", new User("eric", "7!jd#h23"));
model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
return "userView";
}
}
```
### 1.3.4 Model
您可以使用 `@ModelAttribute` 注解:
- 在 `@RequestMapping` 方法中的方法参数上,从模型创建或访问 `Object`,并通过 `WebDataBinder` 将其绑定到请求。
- 作为 `@Controller` 或 `@ControllerAdvice` 类中的方法级注解,帮助在调用任何 `@RequestMapping` 方法之前初始化模型。
- 在 `@RequestMapping` 方法上,标记其返回值的是 model 属性。
本节讨论 `@ModelAttribute` 方法 —— 前一列表中的第二项。控制器可以有任意数量的 `@ModelAttribute` 方法。所有这样的方法都在同一控制器中的 `@RequestMapping` 方法之前调用。`@ModelAttribute` 方法也可以通过 `@ControllerAdvice` 在控制器之间共享。有关详细信息,请参阅控制器切面部分。
`@ModelAttribute` 方法具有灵活的方法签名。它们支持许多与 `@RequestMapping` 方法相同的参数,除了 `@ModelAttribute` 本身或与请求主体相关的任何参数。
以下示例显示了 `@ModelAttribute` 方法:
```java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
```
以下示例仅添加一个属性:
```java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
```
> 如果未显式指定名称,则根据 `Object` 类型选择默认名称,如 `Conventions` 的 javadoc 中所述。始终可以使用重载的 `addAttribute` 方法或通过 `@ModelAttribute` 上的 `name` 属性(返回值)指定显式名称。
您还可以使用 `@ModelAttribute` 作为 `@RequestMapping` 方法的方法级注解,在这种情况下,`@RequestMapping` 方法的返回值被解释为模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是一个 `String`,否则将被解释为视图名称 `@ModelAttribute` 还可以自定义模型属性名称,如下例所示:
```java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
```
### 1.3.5 DataBinder
`@Controller` 或 `@ControllerAdvice` 类可以具有初始化 `WebDataBinder` 实例的 `@InitBinder` 方法,这些方法反过来可以:
- 将请求参数(即表单或查询数据)绑定到模型对象。
- 将基于字符串的请求值(如请求参数、路径变量、标头、cookie 等)转换为控制器方法参数的目标类型。
- 渲染 HTML 表单时,将模型对象值设置为 `String` 值。
`@InitBinder` 方法可以注册控制器特定的 `java.beans.PropertyEditor` 或 Spring `Converter` 和 `Formatter` 组件。此外,您可以使用 MVC 配置在全局共享的 `FormattingConversionService` 中注册 `Converter` 和 `Formatter` 类型。
`@InitBinder` 方法支持许多与 `@RequestMapping` 方法相同的参数,但 `@ModelAttribute`(命令对象)参数除外。通常,它们使用 `WebDataBinder` 参数(用于注册)和 `void` 返回值声明。下面的列表显示了一个示例:
```java
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
// (1) 定义一个 `@InitBinder` 方法
```
或者,当您通过共享 `FormattingConversionService` 使用基于 `Formatter` 的设置时,可以重复使用相同的方法并注册控制器特定的 `Formatter` 实现,如下例所示:
```java
@Controller
public class FormController {
@InitBinder (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
// (1) 在自定义 formatter 上定义 @InitBinder 方法
```
#### Model 设计
在 web 应用程序的上下文中,数据绑定涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。
只有遵循 JavaBeans 命名约定的 `public` 属性才能用于数据绑定,例如,`firstName` 属性的 `public String getFirstName()` 和 `public void setFirstName(String)` 方法。
> 模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO(Plain Old Java Object)。
默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以将任何公共属性路径作为目标,甚至可以将一些预期不适用于给定用例的公共属性路径也作为目标。
例如,给定 HTTP 表单数据端点,恶意客户端可能会为模型对象图中存在但不是浏览器中显示的 HTML 表单的一部分的属性提供值。这可能会导致在模型对象及其任何嵌套对象上设置数据,而这些数据并不期待被更新。
推荐的方法是使用专用模型对象,该对象只公开与表单提交相关的财产。例如,在用于更改用户电子邮件地址的表单上,模型对象应该声明一组最小的属性,例如在下面的 `ChangeEmailForm` 中。
```java
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
```
如果您不能或不想为每个数据绑定用例使用专用模型对象,则必须限制数据绑定所允许的财产。理想情况下,可以通过 `WebDataBinder` 上的 `setAllowedFields()` 方法注册允许的字段模式来实现这一点。
例如,要在应用程序中注册允许的字段模式,可以在 `@Controller` 或 `@ControllerAdvice` 组件中实现 `@InitBinder` 方法,如下所示:
```java
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
```
除了注册允许的模式外,还可以通过 `DataBinder` 及其子类中的 `setDisabledFields()` 方法注册不允许的字段模式。然而,请注意,“允许列表”比“拒绝列表”更安全。因此,`setAllowedFields()` 应该比 `setDisabledFields()` 更受青睐。
注意,与允许的字段模式匹配是区分大小写的;然而,与不允许的字段模式匹配是不区分大小写的。此外,匹配不允许模式的字段将不会被接受,即使它恰好与允许列表中的模式匹配。
> 为数据绑定目的直接公开域模型时,正确配置允许和不允许的字段模式非常重要。否则,这是一个巨大的安全风险。
>
> 此外,强烈建议您不要使用域模型中的类型(如 JPA 或 Hibernate 实体)作为数据绑定场景中的模型对象。
### 1.3.6 异常
`@Controller` 和 `@ControllerAdvice` 类可以使用 `@ExceptionHandler` 方法来处理控制器方法的异常,如下例所示:
```java
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity handle(IOException ex) {
// ...
}
}
```
异常可能与正在传播的顶级异常(例如,抛出直接的 `IOException`)或包装器异常中的嵌套原因(例如,包装在 `IllegalStateException` 中的 `IOException`)匹配。从 5.3 开始,这可以在任意原因级别匹配,而以前只考虑直接原因。
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优先于原因异常匹配。更具体地说,`ExceptionDepthComparator` 用于根据抛出的异常类型的深度对异常进行排序。
或者,注解声明可以缩小异常类型以匹配,如下例所示:
```java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity handle(IOException ex) {
// ...
}
```
您甚至可以使用具有非常通用的参数签名的特定异常类型列表,如下例所示:
```java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity handle(Exception ex) {
// ...
}
```
> 根异常匹配和原因异常匹配之间的区别可能令人惊讶。
>
> 在前面显示的 `IOException` 变体中,通常以实际的 `FileSystemException` 或 `RemoteException` 实例作为参数调用该方法,因为这两个实例都是从 `IOException` 扩展的。但是,如果任何此类匹配异常在包装器异常中传播,而包装器异常本身就是 `IOException`,则传入的异常实例就是该包装器异常。
>
> 在 `handle(Exception)` 变量中,行为甚至更简单。在包装场景中,这总是使用包装器异常来调用,在这种情况下,通过 `ex.getCause()` 可以找到实际匹配的异常。传入的异常是实际的 `FileSystemException` 或 `RemoteException` 实例,仅当它们作为顶级异常抛出时。
我们通常建议您在参数签名中尽可能具体,以减少根异常类型和原因异常类型之间不匹配的可能性。考虑将多匹配方法拆分为单独的 `@ExceptionHandler` 方法,每个方法通过其签名匹配一个特定的异常类型。
在一个多 `@ControllerAdvice` 排列中,我们建议在 `@ControllerAdvice` 上声明您的主根异常映射,并按相应顺序排列优先级。虽然根异常匹配优先于原因,但这是在给定控制器或 `@ControllerAdvice` 类的方法中定义的。这意味着优先级较高的 `@ControllerAdvice` bean上的原因匹配优先于优先级较低的 `@ControllerAdvice` bean 的任何匹配(例如,根)。
最后但并非最不重要的是,`@ExceptionHandler` 方法实现可以选择放弃处理给定的异常实例,方法是以其原始形式重新考虑它。这在您只对根级匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下非常有用。重新抛出的异常会通过剩余的解析链传播,就像给定的 `@ExceptionHandler` 方法一开始就不匹配一样。
Spring MVC 中对 `@ExceptionHandler` 方法的支持基于 `DispatcherServlet` 级别的 HandlerExceptionResolver 机制。
#### 方法参数
`@ExceptionHandler` 方法支持下面参数:
| 方法参数 | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| 异常类型 | 用于访问引发的异常。 |
| `HandlerMethod` | 用于访问引发异常的控制器方法。 |
| `WebRequest`, `NativeWebRequest` | 对请求参数、请求和会话属性的通用访问,无需直接使用 Servlet API。 |
| `jakarta.servlet.ServletRequest`, `jakarta.servlet.ServletResponse` | 选择任何特定的请求或响应类型(例如,`ServletRequest `或`HttpServletRequest` 或 Spring 的`MultipartRequest `或` MultipartHttpServletRequest`)。 |
| `jakarta.servlet.http.HttpSession` | 强制会话的存在。因此,这样的参数永远不是 `null`。请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将 `RequestMappingHandlerAdapter` 实例的 `synchronizedOnSession` 标志设置为 `true`。 |
| `java.security.Principal` | 当前已验证的用户 —— 如果已知,可能是特定的 `Principal` 实现类。 |
| `HttpMethod` | 请求的 HTTP 方法。 |
| `java.util.Locale` | 当前请求区域设置,由可用的最具体的 `LocaleResolver` 确定 —— 实际上是配置的 `LocaleResolver` 或 `LocaleContextResolver`。 |
| `java.util.TimeZone`, `java.time.ZoneId` | 与当前请求关联的时区,由 `LocaleContextResolver` 确定。 |
| `java.io.OutputStream`, `java.io.Writer` | 用于访问原始响应主体,如 Servlet API 所公开的。 |
| `java.util.Map`, `org.springframework.ui.Model`, `org.springframework.ui.ModelMap` | 用于访问错误响应的模型。始终为空。 |
| `RedirectAttributes` | 指定重定向时要使用的属性——(即要附加到查询字符串)和 flash 属性,以在重定向后的请求之前临时存储。请参阅[重定向属性](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-redirecting-passing-data)和[Flash 属性](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-flash-attributes)。 |
| `@SessionAttribute` | 对于任何会话属性的访问,与作为类级别 `@SessionAttributes` 声明的结果存储在会话中的模型属性不同。请参见 [`@SessionAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-sessionattribute) 了解更多详情。 |
| `@RequestAttribute` | 用于访问请求属性。请参见 [`@RequestAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestattrib) 获取更多详细信息。 |
#### 返回值
`@ExceptionHandler` 方法支持下面的返回值:
| Return value | Description |
| :---------------------------------------------- | :----------------------------------------------------------- |
| `@ResponseBody` | 返回值通过 `HttpMessageConverter` 实例转换并写入响应。参见 [`@ResponseBody `](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-responsebody)。 |
| `HttpEntity`, `ResponseEntity` | 返回值指定通过 `HttpMessageConverter` 实例转换完整响应(包括 HTTP 标头和正文)并将其写入响应。参见 [ResponseEntity](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-responseentity)。 |
| `ErrorResponse` | 要呈现 RFC 7807 错误响应并在正文中提供详细信息,请参阅[错误响应](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions) |
| `ProblemDetail` | 要呈现 RFC 7807 错误响应并在正文中提供详细信息,请参阅[错误响应](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions) |
| `String` | 要使用 `ViewResolver` 实现解析并与隐式模型一起使用的视图名称 — 通过命令对象和 `@ModelAttribute` 方法确定。处理程序方法还可以通过声明一个 `Model` 参数(如前所述)以编程方式丰富模型。 |
| `View` | 用于与隐式模型一起呈现的 `View` 实例 —— 通过命令对象和 `@ModelAttribute` 方法确定。处理程序方法还可以通过声明 `Model` 参数(前面已描述)以编程方式丰富模型。 |
| `java.util.Map`, `org.springframework.ui.Model` | 要添加到隐式模型的属性,其视图名称通过 `RequestToViewNameTranslator` 隐式确定。 |
| `@ModelAttribute` | 要添加到模型的属性,其视图名称通过 `RequestToViewNameTranslator` 隐式确定。请注意,`@ModelAttribute` 是可选的。请参阅本表末尾的“任何其他返回值”。 |
| `ModelAndView` 对象 | 要使用的视图和模型属性,以及响应状态(可选)。 |
| `void` | 如果具有 `void` 返回类型(或 `null` 返回值)的方法还具有 `ServletResponse`、`OutputStream` 参数或 `@ResponseStatus` 注释,则认为该方法已完全处理了响应。如果控制器进行了肯定的 `ETag` 或 `lastModified` 时间戳检查(参见[控制器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-caching-etag-lastmodified)以获取详细信息)。如果以上任何一项都不为真,则 `void` 返回类型还可以指示 REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。 |
| Any other return value | 如果返回值与上述任何值都不匹配,并且不是简单类型(由 [BeanTils#isSimpleProperty](https://docs.spring.io/spring-framework/docs/6.0.6/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-) 确定),默认情况下,它被视为要添加到模型中的模型属性。如果它是一个简单类型,它将保持未解析状态。 |
### 1.3.7 控制器切面
`@ExceptionHandler`、`@InitBinder` 和 `@ModelAttribute` 方法仅适用于声明它们的 `@Controller` 类或类层次结构。相反,如果它们在 `@ControllerAdvice` 或 `@RestControllerAdvise` 类中声明,那么它们将应用于任何控制器。此外,从 5.3 开始,`@ControllerAdvice` 中的 `@ExceptionHandler` 方法可用于处理来自任何 `@Controller` 或任何其他处理程序的异常。
`@ControllerAdvice` 使用 `@Component` 进行元注解,因此可以通过组件扫描注册为 Spring bean。`@RestControllerAdvice` 是用 `@ControllerAdvise` 和 `@ResponseBody` 进行元注解的,这意味着 `@ExceptionHandler` 方法将通过响应体消息转换而不是通过 HTML 视图呈现其返回值。
启动时,`RequestMappingHandlerMapping` 和 `ExceptionHandlerExceptionResolver` 检测控制器切面 bean 并在运行时应用它们。`@ControllerAdvice` 中的全局 `@ExceptionHandler` 方法应用于 `@Controller` 中的本地方法之后。相比之下,全局 `@ModelAttribute` 和 `@InitBinder` 方法应用于本地方法之前。
`@ControllerAdvice` 注解具有一些属性,可用于缩小它们所应用的控制器和处理程序集。例如:
```java
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
```
前面示例中的选择器是在运行时评估的,如果广泛使用,可能会对性能产生负面影响。有关详细信息,请参阅 `@ControllerAdvice` javadoc。
## 1.4 函数式端点
Spring Web MVC 包括 WebMvc.fn,这是一个轻量级的函数式编程模型,其中函数用于路由和处理请求,而协约是为不变性而设计的。它是替代基于注解的编程模型的方案,但在其他情况下运行在同一 DispatcherServlet 上。
### 1.4.1 总览
在 WebMvc.fn 中,HTTP 请求由 `HandlerFunction` 处理:一个接受 `ServerRequest` 并返回 `ServerResponse` 的函数。请求和响应对象都有不可变的协约,提供对 HTTP 请求和响应的 JDK 8 友好访问。`HandlerFunction` 相当于基于注解的编程模型中 `@RequestMapping` 方法的主体。
传入请求被路由到具有 `RouterFunction` 的处理程序函数:该函数接受 `ServerRequest` 并返回可选的 `HandlerFunction`(即 `Optional`)。当路由器函数匹配时,返回处理程序函数;否则为空可选。`RouterFunction` 相当于 `@RequestMapping` 注解,但主要区别在于路由器函数不仅提供数据,还提供行为。
`RouterFunctions.route()` 提供了一个路由器生成器,可帮助创建路由器,如下例所示:
```java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction route = route() (1)
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
// (1) 通过 `route()` 创建路由
```
如果您将 `RouterFunction` 注册为 bean,例如通过在 `@Configuration` 类中公开它,servlet 将自动检测到它,如“运行服务器”中所述。
### 1.4.2 HandlerFunction
`ServerRequest` 和 `ServerResponse` 是不可变的接口,提供对 HTTP 请求和响应的 JDK 8 友好的访问,包括头、体、方法和状态代码。
#### ServerRequest
`ServerRequest` 提供对 HTTP 方法、URI、请求头和查询参数的访问,而对请求体的访问是通过 `body` 方法提供的。
以下示例将请求体提取为字符串:
```java
String string = request.body(String.class);
```
以下示例将请求体提取到 `List`,其中 `Person` 对象从序列化形式(如 JSON 或 XML)解码:
```java
List people = request.body(new ParameterizedTypeReference>() {});
```
以下示例显示了如何访问参数:
```java
MultiValueMap params = request.params();
```
#### ServerResponse
`ServerResponse` 提供对 HTTP 响应的访问,因为它是不可变的,所以可以使用构建方法来创建它。您可以使用构建器来设置响应状态、添加响应头或提供响应体。以下示例使用 JSON 内容创建 200(OK)响应:
```java
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
```
下面的示例显示了如何构建一个 201(CREATED)响应,该响应带有 `Location` 标头而没有正文:
```java
URI location = ...
ServerResponse.created(location).build();
```
您还可以使用异步结果作为正文,形式为 `CompletableFuture`、`Publisher` 或 `ReactiveAdapterRegistry` 支持的任何其他类型。例如:
```java
Mono person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
```
如果不仅正文,而且状态或标头都基于异步类型,则可以在 `ServerResponse` 上使用静态 `async` 方法,该方法接受 `CompletableFuture`、`Publisher` 或 `ReactiveAdapterRegistry` 支持的任何其他异步类型。例如:
```java
Mono asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
```
服务器发送事件可以通过 `ServerResponse` 上的静态 `sse` 方法提供。该方法提供的生成器允许您将字符串或其他对象作为 JSON 发送。例如:
```java
public RouterFunction sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
```
#### Handler 类
我们可以将处理函数写成 lambda,如下例所示:
```java
HandlerFunction helloWorld =
request -> ServerResponse.ok().body("Hello World");
```
这很方便,但在一个应用程序中,我们需要多个函数,多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数分组到一个处理程序类中是很有用的,该类的作用与基于注解的应用程序中的 `@Controller` 类似。例如,下面的类公开了一个被动的 `Person` 存储库:
```java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { (1)
List people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
// (1) `listPeople` 是一个处理程序函数,它将存储库中找到的所有 `Person` 对象作为JSON返回。
// (2) `createPerson` 是一个处理程序函数,用于存储请求主体中包含的新 `Person`。
// (3) `getPerson` 是一个处理程序函数,它返回由 `id` 路径变量标识的单个人员。我们从存储库中检索该 `Person` 并创建 JSON 响应(如果找到)。如果找不到,则返回 404 not found 响应。
```
#### 验证
函数式端点可以使用 Spring 的验证工具对请求主体应用验证。例如,给定 Person 的自定义 Spring Validator 实现:
```java
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
// (1) 创建 `Validator` 实例
// (2) 应用验证
// (3) 为 400 响应扔出一个异常
```
处理程序还可以通过创建和注入基于 `LocalValidatorFactoryBean` 的全局 `Validator` 实例来使用标准 bean 验证 API(JSR-303)。参见 Spring 验证。
### 1.4.3 RouterFunction
路由器函数用于将请求路由到相应的 `HandlerFunction`。通常,您不自己编写路由器函数,而是使用 `RouterFunctions` 实用程序类上的方法来创建路由器函数。`RouterFunctions.route()`(无参数)为创建路由器函数提供了一个流畅的生成器,而 `RouterFunctions.route(RequestPredicate,HandlerFunction)` 提供了创建路由器的直接方法。
通常,建议使用 `route()` 生成器,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现静态导入。例如,路由器函数生成器提供了 `GET(String, HandlerFunction)` 方法来创建 GET 请求的映射;以及给 POST 的 `POST(String, HandlerFunction)`。
除了基于 HTTP 方法的映射之外,路由生成器还提供了一种在映射到请求时引入附加谓词的方法。对于每个 HTTP 方法,都有一个重载变量,它将 `RequestPredicate` 作为参数,通过它可以表达额外的约束。
#### 谓词
您可以编写自己的 `RequestPredicate`,但 `RequestPredicates` 实用程序类提供了基于请求路径、HTTP 方法、内容类型等的常用实现。以下示例使用请求谓词基于 `Accept` 标头创建约束:
```java
RouterFunction route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
```
您可以使用以下方法组合多个请求谓词:
- `RequestPredicate.and(RequestPredicate)` — 必须匹配两者.
- `RequestPredicate.or(RequestPredicate)` — 任意一个可以匹配.
`RequestPredicates` 中的许多谓词都是组合的。例如,`RequestPredicates.GET(String)` 由 `RequestPredicites.method(HttpMethod) ` 和 `RequestPredicates.path(String)` 组成。上面显示的示例还使用了两个请求谓词,因为构建器在内部使用 `RequestPredictes.GET`,并将其与 `accept` 谓词组合。
#### 路由
路由器函数按顺序求值:如果第一条路由不匹配,则考虑第二条路由,依此类推。因此,在一般路由之前声明更具体的路由是有意义的。这在将路由器功能注册为 Spring bean 时也很重要,稍后将对此进行描述。请注意,这种行为与基于注解的编程模型不同,其中“最特定”的控制器方法是自动选择的。
当使用 router 函数生成器时,所有定义的路由都被组成一个 `RouterFunction`,该 `RouterFunction` 从 `build()` 返回。还有其他方法可以将多个路由器功能组合在一起:
- `add(RouterFunction)` 在 `RouterFunctions.route()` builder 上
- `RouterFunction.and(RouterFunction)`
- `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` —— 创建具有嵌套的 `RouterFunctions.route()` 的 `RouterFunction.and()` 的快捷方式。
以下示例显示了四条路由的组建:
```java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction otherRoute = ...
RouterFunction route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
// (1) 具有与 JSON 匹配的 `Accept` 标头的 `GET /person/{id}` 被路由到 `PersonHandler.getPerson`
// (2) 具有与 JSON 匹配的 `Accept` 标头的 `GET /person` 被路由到 `PersonHandler.listPeople`
// (3) 没有附加谓词的 `POST /person` 映射到 `PersonHandler.createPerson`,并且
// (4) `otherRoute` 是在其他地方创建的路由器函数,并添加到构建的路由中。
```
#### 嵌套路由
一组路由器函数通常具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配 `/person` 的路径谓词,由三个路由使用。使用注解时,可以使用映射到 `/person` 的类型级别 `@RequestMapping` 注解来删除此重复。在 WebMvc.fn 中,路径谓词可以通过路由器函数生成器上的路径方法共享。例如,上面示例的最后几行可以通过使用嵌套路由以以下方式进行改进:
```java
RouterFunction route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
// (1) 注意,路径的第二个参数是采用路由器生成器的使用者。
```
虽然基于路径的嵌套是最常见的,但您可以通过在生成器上使用 `nest` 方法来嵌套任何类型的谓词。上面仍然以共享 `Accept` 头谓词的形式包含一些重复。我们可以通过使用 `nest` 方法和 `accept` 来进一步改进:
```java
RouterFunction route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
```
### 1.4.4 运行服务器
您通常通过 MVC 配置在基于 `DispatcherHandler` 的设置中运行路由器函数,MVC 配置使用 Spring 配置来声明处理请求所需的组件。MVC Java 配置声明了以下基础架构组件以支持函数式端点:
- `RouterFunctionMapping`:检测一个或多个 `RouterFunction>` Spring 配置中的 bean,对它们进行排序,通过 `RouterFunction.andOther` 组合它们,并将请求路由到合成的 `RouterFunction`。
- `HandlerFunctionAdapter`:允许 `DispatcherHandler` 调用映射到请求的 `HandlerFunction` 的简单适配器。
前面的组件使函数式端点适合 `DispatcherServlet` 请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器)并行运行。这也是 Spring Boot Web 启动器如何启用函数式端点。
以下示例显示 WebFlux Java 配置:
```java
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction> routerFunctionA() {
// ...
}
@Bean
public RouterFunction> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
```
### 1.4.5 过滤处理器函数
您可以使用路由函数生成器上的 `before`、`after` 或 `filter` 方法来筛选处理程序函数。通过注解,您可以通过使用 `@ControllerAdvice`、`ServletFilter` 或两者实现类似的功能。过滤器将应用于由生成器生成的所有管线。这意味着嵌套路由中定义的筛选器不适用于“顶级”路由。例如,考虑以下示例:
```java
RouterFunction route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
// (1) 添加自定义请求头的 `before` 筛选器仅应用于两个 GET 路由。
// (2) 记录响应的 `after` 筛选器应用于所有路由,包括嵌套路由。
```
路由器生成器上的 `filter` 方法采用 `HandlerFilterFunction`:一个采用 `ServerRequest` 和 `HandlerFunction` 并返回 `ServerResponse` 的函数。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但如果应用了多个,它也可以是另一个过滤器。
现在,我们可以向路由添加一个简单的安全筛选器,假设我们有一个 `SecurityManager`,它可以确定是否允许特定路径。以下示例显示了如何执行此操作:
```java
SecurityManager securityManager = ...
RouterFunction route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
```
前面的示例演示了调用 `next.handle(ServerRequest)` 是可选的。我们只允许在允许访问时运行处理程序函数。
除了在路由器函数生成器上使用筛选器方法之外,还可以通过 `RouterFunction.filter(HandlerFilterFunction)` 将筛选器应用于现有的路由器函数。
> CORS 对函数式端点的支持是通过专用的 `CorsFilter` 提供的。
## 1.5 URI 链接
本节描述了 Spring 框架中使用 URI 的各种选项。
### 1.5.1 UriComponents
Spring MVC 和Spring WebFlux
`UriComponentsBuilder` 帮助使用变量从 URI 模板构建 URI,如下例所示:
```java
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
// (1) 具有 URI 模板的静态工厂方法。
// (2) 添加或替换 URI 组件。
// (3) 请求对 URI 模板和 URI 变量进行编码。
// (4) 构建 UriComponents。
// (5) 展开变量并获取 URI。
```
前面的示例可以合并为一个链,并使用 `buildAndExpand` 缩短,如下例所示:
```java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
```
您可以通过直接使用 URI(这意味着编码)来进一步缩短它,如下例所示:
```java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
```
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
```java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
```
### 1.5.2 UriBuilder
Spring MVC 和Spring WebFlux
`UriComponentsBuilder` 实现 `UriBuilder`。您可以依次使用 `UriBuilderFactory` 创建 `UriBuilder`。`UriBuilderFactory` 和 `UriBuilder` 一起提供了一种可插入的机制,可以根据共享配置(如基本 URL、编码首选项和其他详细信息)从 URI 模板构建 URI。
您可以使用 `UriBuilderFactory` 配置 `RestTemplate` 和 `WebClient`,以自定义 URI 的准备。`DefaultUriBuilderFactory` 是 `UriBuilderFactory` 的默认实现,它在内部使用 `UriComponentsBuilder` 并公开共享配置选项。
以下示例显示如何配置 `RestTemplate`:
```java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
```
下面例子配置了一个 `WebClient`:
```java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
```
此外,还可以直接使用 `DefaultUriBuilderFactory`。它类似于使用 `UriComponentsBuilder`,但它不是静态工厂方法,而是保存配置和首选项的实际实例,如下例所示:
```java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
```
### 1.5.3 URI 编码
Spring MVC 和Spring WebFlux
`UriComponentsBuilder` 在两个级别公开编码选项:
- UriComponentsBuilder#encodes():首先对 URI 模板进行预编码,然后在展开(expand)时对 URI 变量进行严格编码。
- UriComponents#encode():在 URI 变量展开后对 URI 组件进行编码。
这两个选项都用转义的八位字节替换非 ASCII 和非法字符。但是,第一个选项还将替换 URI 变量中出现的具有保留含义的字符。
> 考虑一下“;”,它在路径上是合法的,但有保留的含义。第一个选项将URI变量中的“;”替换为“%3B”,但不在 URI 模板中。相比之下,第二个选项从不替换“;”,因为它是路径中的合法字符。
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而如果 URI 变量故意包含保留字符,则第二个选项很有用。当根本不扩展 URI 变量时,第二个选项也很有用,因为这也会对任何偶然看起来像 URI 变量的内容进行编码。
以下示例使用第一个选项:
```java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
```
您可以通过直接转到 URI(这意味着编码)来缩短前面的示例,如下例所示:
```java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
```
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
```java
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
```
`WebClient` 和 `RestTemplate` 通过 `UriBuilderFactory` 策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示:
```java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
```
`DefaultUriBuilderFactory` 实现在内部使用 `UriComponentsBuilder` 来扩展和编码 URI 模板。作为一个工厂,它提供了一个单独的地方来根据以下编码模式之一配置编码方法:
- `TEMPLATE_AND_VALUES`:使用 `UriComponentsBuilder#encodes()`(对应于前面列表中的第一个选项)对 URI 模板进行预编码,并在展开时对 URI 变量进行严格编码。
- `VALUES_ONLY`:不编码 URI 模板,而是在将 URI 变量扩展到模板之前,通过 `UriUtils#encodeUriVariables` 对其应用严格编码。
- `URI_COMPONENT`:在 URI 变量展开后,使用 `UriComponents#encode()`(对应于前面列表中的第二个选项)对 URI 组件值进行编码。
- `NONE`:未应用编码。
出于历史原因和向后兼容性,`RestTemplate` 设置为 `EncodingMode.URI_COMPONENT`。 `WebClient` 依赖于 `DefaultUriBuilderFactory` 中的默认值,该值从 5.0.x 中的 `EncodingMode.URI_COMPONENT` 更改为 5.1 中的 `EncodengMode.TEMPLATE_AND_VALUES`。
### 1.5.4 相关的 Servlet 请求
您可以使用 `ServletUriComponentsBuilder` 创建与当前请求相关的 URI,如下例所示:
```java
HttpServletRequest request = ...
// Re-uses scheme, host, port, path, and query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");
```
您可以创建与上下文路径相关的 URI,如下例所示:
```java
HttpServletRequest request = ...
// Re-uses scheme, host, port, and context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
```
您可以创建与 Servlet 相关的 URI(例如 `/main/*`),如下例所示:
```java
HttpServletRequest request = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
```
> 从 5.1 开始,`ServletUriComponentsBuilder` 忽略 `Forwarded` 和 `X-Forwarded-*` 标头中的信息,这些标头指定了客户端发起的地址。考虑使用 `ForwardedHeaderFilter` 来提取和使用或丢弃此类标头。
### 1.5.5 连接到控制器
Spring MVC 提供了一种机制来准备到控制器方法的链接。例如,以下 MVC 控制器允许创建链接:
```java
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}
```
您可以通过按名称引用方法来准备链接,如下例所示:
```java
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
```
在前面的示例中,我们提供了实际的方法参数值(在本例中为 long 值:`21`),用作路径变量并插入到 URL 中。此外,我们提供值 `42` 来填充任何剩余的 URI 变量,例如从类型级请求映射继承的酒店变量。如果该方法有更多的参数,我们可以为 URL 不需要的参数提供 null。通常,只有 `@PathVariable` 和 `@RequestParam` 参数与构造 URL 相关。
还有其他方法可以使用 `MvcUriComponentsBuilder`。例如,您可以使用类似于通过代理进行模拟测试的技术,以避免按名称引用控制器方法,如下例所示(该示例假定 `MvcUriComponentsBuilder.on` 的静态导入):
```java
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
```
> 当控制器方法签名被认为可用于使用 `fromMethodCall` 创建链接时,它们的设计受到限制。除了需要正确的参数签名之外,返回类型(即为链接生成器调用生成运行时代理)也有技术限制,因此返回类型不能是 `final` 的。特别是,视图名称的常见 `String` 返回类型在这里不起作用。您应该使用 `ModelAndView` 或甚至纯 `Object`(带有 `String` 返回值)。
前面的示例在 `MvcUriComponentsBuilder` 中使用静态方法。在内部,它们依赖 `ServletUriComponentsBuilder` 从当前请求的方案、主机、端口、上下文路径和 servlet 路径准备一个基本 URL。这在大多数情况下都很有效。然而,有时,这可能是不够的。例如,您可能在请求的上下文之外(例如准备链接的批处理过程),或者可能需要插入路径前缀(例如从请求路径中删除并需要重新插入到链接中的区域设置前缀)。
对于这种情况,可以使用静态 `fromXxx` 重载方法,这些方法接受 `UriComponentsBuilder` 以使用基 URL。或者,您可以使用基 URL 创建 `MvcUriComponentsBuilder` 的实例,然后使用基于实例的 `withXxx` 方法。例如,以下列表使用 `withMethodCall`:
```java
UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
```
> 从 5.1 开始,`MvcUriComponentsBuilder` 忽略 `Forwarded` 和 `X-Forwarded-*` 标头中的信息,这些标头指定了客户端发起的地址。考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃此类标头。
### 1.5.6 视图中的连接
在 Thymelaf、FreeMarker 或 JSP 等视图中,可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注解控制器的链接。
考虑以下示例:
```java
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity getAddress(@PathVariable String country) { ... }
}
```
给定前面的控制器,您可以从 JSP 准备一个链接,如下所示:
```jsp
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
Get Address
```
前面的示例依赖于 Spring 标记库(即 META-INF/Spring.tld)中声明的 `mvcUrl` 函数,但很容易定义自己的函数或为其他模板技术准备一个类似的函数。
这是如何工作的。启动时,通过 `HandlerMethodMappingNamingStrategy` 为每个 `@RequestMapping` 分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如,`ThingController` 中的 `getThing` 方法变为“TC#getThing”)。如果存在名称冲突,可以使用 `@RequestMapping(name="..")` 来指定显式名称或实现自己的 `HandlerMethodMappingNamingStrategy`。
## 1.6 异步请求
Spring MVC 与 Servlet 异步请求处理有着广泛的集成:
- 控制器方法中的 `DeferredResult` 和 `Callable` 返回值为单个异步返回值提供了基本支持。
- 控制器可以流式传输多个值,包括 SSE 和原始数据。
- 控制器可以使用反应式客户端并返回反应式类型进行响应处理。
要了解这与 Spring WebFlux 的区别,请参阅下面的“异步Spring MVC 与 WebFlux” 部分。
### 1.6.1 DeferredResult
在 Servlet 容器中启用异步请求处理功能后,控制器方法可以使用 `DeferredResult` 包装任何受支持的控制器方法返回值,如下例所示:
```java
@GetMapping("/quotes")
@ResponseBody
public DeferredResult quotes() {
DeferredResult deferredResult = new DeferredResult();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);
```
控制器可以从不同的线程异步生成返回值 —— 例如,响应于外部事件(JMS消息)、调度任务或其他事件。
### 1.6.2 Callable
控制器可以使用 `java.util.concurrent.Callable` 包装任何支持的返回值,如下例所示:
```java
@PostMapping
public Callable processUpload(final MultipartFile file) {
return () -> "someView";
}
```
然后,可以通过配置的 `TaskExecutor` 运行给定的任务来获得返回值。
### 1.6.3 处理(Processing)
以下是 Servlet 异步请求处理的简要概述:
- 通过调用 `request.startAsync()`,可以将 `ServletRequest` 置于异步模式。这样做的主要效果是 Servlet(以及任何筛选器)可以退出,但响应保持打开状态,以便稍后完成处理。
- 对 `request.startAsync()` 的调用返回 `AsyncContext`,您可以使用它来进一步控制异步处理。例如,它提供了 `dispatch` 方法,该方法类似于 Servlet API 的转发,只是它允许应用程序在 Servlet 容器线程上恢复请求处理。
- `ServletRequest` 提供对当前 `DispatcherType` 的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度程序类型。
`DeferredResult` 处理的工作原理如下:
- 控制器返回一个 `DeferredResult`,并将其保存在内存中的某个队列或列表中,以便访问。
- Spring MVC 调用 `request.startAsync()`。
- 同时,`DispatcherServlet` 和所有配置的筛选器退出请求处理线程,但响应保持打开状态。
- 应用程序从某个线程设置 `DeferredResult`,Spring MVC 将请求分派回 Servlet 容器。
- `DispatcherServlet` 将被再次调用,处理将使用异步生成的返回值继续进行。
`Callable` 处理工作如下:
- 控制器返回一个 `Callable`。
- Spring MVC 调用 `request.startAsync()` 并将 `Callable` 提交给`TaskExecutor`,以便在单独的线程中进行处理。
- 同时,`DispatcherServlet` 和所有过滤器退出 Servlet 容器线程,但响应保持打开状态。
- 最终,`Callable` 生成一个结果,Spring MVC 将请求分派回 Servlet 容器以完成处理。
- `DispatcherServlet` 将被再次调用,处理将通过 `Callable` 异步生成的返回值继续进行。
有关进一步的背景和上下文,您还可以阅读在 Spring MVC 3.2 中引入异步请求处理支持的博客文章。
#### 异常处理
当您使用 `DeferredResult` 时,您可以选择是调用 `setResult` 还是调用带有异常的 `setErrorResult`。在这两种情况下,Spring MVC 都会将请求分派回 Servlet 容器以完成处理。然后,它被视为控制器方法返回了给定的值,或者被视为产生了给定的异常。然后,异常通过常规异常处理机制(例如,调用 `@ExceptionHandler` 方法)。
当您使用 `Callable` 时,会出现类似的处理逻辑,主要区别在于结果是从 `Callable` 返回的,或者由它引发异常。
#### 拦截
`HandlerInterceptor` 实例可以是 `AsyncHandlerInterreceptor` 类型,以接收启动异步处理的初始请求上的 `afterCurrentHandlingStarted` 回调(而不是 `postHandle` 和 `afterCompletion`)。
`HandlerInterceptor` 实现还可以注册 `CallableProcessingInterceptor` 或 `DeferredResultProcessingIntersector`,以便与异步请求的生命周期进行更深入的集成(例如,处理超时事件)。有关更多详细信息,请参阅 `AsyncHandlerInterceptor`。
`DeferredResult` 提供 `onTimeout(Runnable)` 和 `onCompletion(Runnable)` 回调。有关更多详细信息,请参阅 `DeferredResult` 的 javadoc。`Callable` 可以替代 `WebAsyncTask`,后者公开了用于超时和完成回调的其他方法。
#### 异步 Spring MVC 和 WebFlux 对比
Servlet API 最初是为通过 Filter-Servlet 链进行单次传递而构建的。异步请求处理允许应用程序退出 Filter-Servlet 链,但保留响应以供进一步处理。Spring MVC 异步支持就是围绕这个机制构建的。当控制器返回 `DeferredResult` 时,Filter-Servlet 链将退出,Servlet 容器线程将释放。稍后,当设置 `DeferredResult` 时,将进行 `ASYNC` 分发(到同一 URL),在此期间,控制器将再次映射,但不是调用它,而是使用 `DeferredResult` 值(就好像控制器返回了它一样)来恢复处理。
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的异步请求处理功能,因为它在设计上是异步的。异步处理内置于所有框架合同中,并且在请求处理的所有阶段都得到了本质上的支持。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步类型和反应类型作为控制器方法中的返回值。Spring MVC 甚至支持流式传输,包括反应式背压(reactive back pressure)。然而,对响应的单独写入仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O,并且每次写入不需要额外的线程。
另一个根本区别是,Spring MVC 不支持控制器方法参数中的异步或反应类型(例如,`@RequestBody`、`@RequestPart` 和其他),也不明确支持异步和反应类型作为模型属性。Spring WebFlux 确实支持所有这些。
最后,从配置的角度来看,异步请求处理功能必须在 Servlet 容器级别启用。
### 1.6.4 HTTP 流
您可以对单个异步返回值使用 `DeferredResult` 和 `Callable`。如果您想生成多个异步值并将这些值写入响应,该怎么办?本节介绍如何做到这一点。
#### 对象
您可以使用 `ResponseBodyEmitter` 返回值来生成一个对象流,其中每个对象都使用 `HttpMessageConverter` 进行序列化并写入响应,如下例所示:
```java
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
```
您也可以使用 `ResponseBodyEmitter` 作为 `ResponseEntity` 中的主体,从而可以自定义响应的状态和标头。
当 `emitter` 抛出 `IOException` 时(例如,如果远程客户端离开),应用程序不负责清理连接,不应调用 `emitter.complete` 或 `emitter.commpleteWithError`。相反,servlet 容器会自动启动 `AsyncListener` 错误通知,其中 Spring MVC 会调用 `completeWithError`。这个调用反过来执行对应用程序的最后一次 `ASYNC` 分发,在此期间,Spring MVC 调用配置的异常解析器并完成请求。
#### SSE
`SseEmitter`(`ResponseBodyEmitter` 的一个子类)提供了对服务器发送事件的支持,其中从服务器发送的事件根据 W3C SSE 规范进行格式化。要从控制器生成 SSE 流,请返回 `SseEmitter`,如下例所示:
```java
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
```
虽然 SSE 是流式传输到浏览器的主要选项,但请注意,Internet Explorer 不支持服务器发送的事件。考虑将 Spring 的 WebSocket 消息与针对各种浏览器的 SockJS 回退传输(包括 SSE)一起使用。
有关异常处理的注意事项,请参阅上一节。
#### 原始数据
有时,绕过消息转换并直接流式传输到响应 `OutputStream` 是很有用的(例如,对于文件下载)。您可以使用 `StreamingResponseBody` 返回值类型来执行此操作,如下例所示:
```java
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
```
您可以使用 `StreamingResponseBody` 作为 `ResponseEntity` 中的主体来自定义响应的状态和标头。
### 1.6.5 反应式类型
Spring MVC 支持在控制器中使用反应式客户端库(也可以阅读 WebFlux 部分中的反应式库)。这包括 `spring-webflux` 中的 `WebClient` 和其他一些,如 Spring Data 反应式数据存储库。在这种情况下,能够从控制器方法返回反应式类型是很方便的。
反应式返回值处理如下:
- 类似于使用 `DeferredResult`,对单值 promise 进行了调整。示例包括 `Mono`(Reactor)或 `Single`(RxJava)。
- 类似于使用 `ResponseBodyEmitter` 或 `SseEmitter`,具有流媒体类型的多值流(如 `application/x-ndjson` 或 `text/event-stream`)适用于。示例包括 `Flux`(Reactor)或 `Observable`(RxJava)。应用程序还可以返回 `Flux` 或 `Observable`。
- 具有任何其他媒体类型(如 `application/json`)的多值流都适用于,类似于使用 `DeferredResult>`。
> Spring MVC 通过 `spring-core` 的 `ReactiveAdapterRegistry` 支持 Reactor 和 RxJava,这使它能够从多个反应式库中进行调整。
对于流式传输到响应,支持反应式背压,但对响应的写入仍处于阻塞状态,并通过配置的 `TaskExecutor` 在单独的线程上运行,以避免阻塞上游源(如 `WebClient` 返回的 `Flux`)。默认情况下,`SimpleAsyncTaskExecutor` 用于阻塞写入,但在负载下不适用。如果您计划使用反应式类型进行流式传输,那么应该使用 MVC 配置来配置任务执行器。
### 1.6.6 上下文传播
通过 `java.lang.ThreadLocal` 传播上下文是很常见的。这对于同一线程上的处理是透明的,但对于跨多个线程的异步处理需要额外的工作。Micrometer 上下文传播库简化了线程之间以及 `ThreadLocal` 值、Reactor上下文、GraphQL Java 上下文等上下文机制之间的上下文传播。
如果类路径上存在 Micrometer 上下文传播,则当控制器方法返回响应式类型(如 `Flux` 或 `Mono`)时,所有有注册 `io.micrometer.ThreadLocalAccessor` 的 `ThreadLocal` 值都将作为键值对写入 Reactor `Context`,使用 `ThreadLocalAccessor` 分配的键。
对于其他异步处理场景,可以直接使用上下文传播库。例如:
```java
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();
// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
// ...
}
```
有关更多详细信息,请参阅 Micrometer 上下文传播库的文档。
### 1.6.7 断开连接
当远程客户端离开时,Servlet API 不提供任何通知。因此,在流式传输到响应时,无论是通过 SseEmitter 还是反应式类型,定期发送数据都很重要,因为如果客户端断开连接,写入就会失败。发送可以采取空(仅有注释)SSE 事件或任何其他数据的形式,对方必须将其解释为心跳并忽略这些数据。
或者,考虑使用具有内置心跳机制的 web 消息传递解决方案(如基于 WebSocket 的 STOMP 或带有 SockJS 的 WebSocket)。
### 1.6.8 配置
异步请求处理功能必须在 Servlet 容器级别启用。MVC 配置还公开了用于异步请求的几个选项。
#### Servlet 容器
Filter 和 Servlet 声明有一个 `asyncSupported` 标志,需要将其设置为 `true` 才能启用异步请求处理。此外,应该声明 Filter 映射来处理 `ASYNC` `jakarta.servlet.DispatchType`。
在 Java 配置中,当您使用 `AbstractAnnotationConfigDispatcherServlet` 初始化器初始化 Servlet 容器时,这是自动完成的。
在 `web.xml` 配置中,您可以将 `true` 添加到 `DispatcherServlet` 和 `Filter` 声明中,并将 `ASYNC` 添加到筛选器映射中。
#### Spring MVC
MVC 配置公开了以下与异步请求处理相关的选项:
- Java 配置:在 `WebMvcConfigurer` 上使用 `configureAsyncSupport` 回调。
- XML 命名空间:使用 `` 下的 `` 元素。
您可以配置以下内容:
- 异步请求的默认超时值,如果没有设置,则取决于底层 Servlet 容器。
- `AsyncTaskExecutor`,用于在使用反应式类型进行流式传输时阻止写入,并用于执行从控制器方法返回的 `Callable` 实例。如果您使用反应式类型进行流式传输或具有返回 `Callable` 的控制器方法,我们强烈建议您配置此属性,因为默认情况下,它是 `SimpleAsyncTaskExecutor`。
- `DeferredResultProcessingInterceptor` 实现和 `CallableProcessingIntersector` 实现。
请注意,您还可以设置 `DeferredResult`、`ResponseBodyEmitter` 和 `SseEmitter` 的默认超时值。对于 `Callable`,您可以使用 `WebAsyncTask` 来提供超时值。
## 1.7 CORS
Spring MVC 允许您处理 CORS(跨源资源共享)。本节介绍如何做到这一点。
### 1.7.1 介绍
出于安全原因,浏览器禁止 AJAX 调用当前来源之外的资源。例如,您可以在一个选项卡中显示您的银行帐户,在另一个选项卡上显示 evil.com。来自 evil.com 的脚本应该无法使用您的凭据向您的银行 API 发出 AJAX 请求 —— 例如,从您的帐户中取款!
跨域资源共享(CORS)是由大多数浏览器实现的 W3C 规范,它允许您指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的不太安全、功能不太强大的解决方案。
### 1.7.2 处理
CORS 规范区分飞行前请求(preflight)、简单请求和实际请求。要了解 CORS 是如何工作的,您可以阅读本文以及其他许多文章,或者查看规范以了解更多详细信息。
Spring MVC `HandlerMapping` 实现提供了对 CORS 的内置支持。在成功地将请求映射到处理程序之后,`HandlerMapping` 实现会检查给定请求和处理程序的 CORS 配置,并采取进一步的操作。直接处理前缀请求,而拦截、验证简单和实际的 CORS 请求,并设置所需的 CORS 响应标头。
为了启用跨源请求(也就是说,`Origin` 标头存在并且与请求的主机不同),您需要有一些明确声明的 CORS 配置。如果没有找到匹配的 CORS 配置,飞行前请求将被拒绝。简单和实际的 CORS 请求的响应中没有添加 CORS 头,因此浏览器会拒绝它们。
每个 `HandlerMapping` 都可以使用基于 URL 模式的 `CorsConfiguration` 映射进行单独配置。在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 `HandlerMapping` 实例。
您可以将 `HandlerMapping` 级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置相结合。例如,带注解的控制器可以使用类或方法级别的 `@CrossOrigin` 注解(其他处理程序可以实现 `CorsConfigurationSource`)。
组合全局配置和局部配置的规则通常是相加的 —— 例如,所有全局和所有本地起源。对于那些只能接受单值的属性,例如 `allowCredentials` 和 `maxAge`,本地值会覆盖全局值。有关更多详细信息,请参阅 `CorsConfiguration#combine(CorsConfigoration)`。
> 要从源代码中了解更多信息或进行高级自定义,请检查后面的代码:
>
> - `CorsConfiguration`
> - `CorsProcessor`,`DefaultCorsProcessor`
> - `AbstractHandlerMapping`
### 1.7.3 @CrossOrigin
`@CrossOrigin` 注解在带注解的控制器方法上启用跨源请求,如下例所示:
```java
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
```
默认情况下,`@CrossOrigin` 允许:
- 所有的源。
- 所有标头。
- 控制器方法映射到的所有 HTTP 方法。
`allowCredentials` 默认情况下不启用,因为它建立了一个信任级别,暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 `allowOrigins` 设置为一个或多个特定域(但不是特殊值 `"*"`),或者可以使用 `allowOriginalPatterns` 属性来匹配一组动态的源。
`maxAge` 设置为 30 分钟。
`@CrossOrigin` 在类级别也受支持,并且由所有方法继承,如下例所示:
```java
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
```
您可以在类级别和方法级别使用 `@CrossOrigin`,如下例所示:
```java
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
```
### 1.7.4 全局配置
除了细粒度的控制器方法级配置之外,您可能还想定义一些全局 CORS 配置。您可以在任何 `HandlerMapping` 上单独设置基于 URL 的 `CorsConfiguration` 映射。然而,大多数应用程序都使用 MVC Java 配置或 MVC XML 命名空间来实现这一点。
默认情况下,全局配置启用以下功能:
- 所有的源。
- 所有标头。
- GET、HEAD 和 POST 方法。
`allowCredentials` 默认情况下不启用,因为它建立了一个信任级别,暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 `allowOrigins` 设置为一个或多个特定域(但不是特殊值 `"*"`),或者可以使用 `allowOriginalPatterns` 属性来匹配一组动态的原点。
`maxAge` 设置为 30 分钟。
#### Java 配置
要在 MVC Java 配置中启用 CORS,可以使用 `CorsRegistry` 回调,如下例所示:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
```
#### XML 配置
要在XML命名空间中启用CORS,可以使用 `` 元素,如下例所示:
```xml
```
### 1.7.5 CORS 过滤器
您可以通过内置的 `CorsFilter` 应用 CORS 支持。
> 如果您尝试将 `CorsFilter` 与 Spring Security 一起使用,请记住 Spring Security 内置了对 CORS 的支持。
要配置筛选器,请将 `CorsConfigurationSource` 传递给其构造函数,如下例所示:
```java
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
```
## 1.8 错误响应
REST 服务的一个常见要求是在错误响应的主体中包含详细信息。Spring 框架支持“HTTP API 的问题详细信息”规范 RFC 7807。
以下是此支持的主要抽象:
- `ProblemDetail` — RFC 7807 问题细节的表示;一个简单的容器,用于规范中定义的标准字段和非标准字段。
- `ErrorResponse` — 公开 HTTP 错误响应详细信息的合同,包括 HTTP 状态、响应头和RFC 7807 格式的主体;这允许异常封装并公开它们如何映射到 HTTP 响应的详细信息。所有 Spring MVC 异常都实现了这一点。
- `ErrorResponseException` — 基本的 `ErrorResponse` 实现,其他人可以将其用作方便的基类。
- `ResponseEntityExceptionHandler` — 用于 @ControllerAdvice 的方便基类,该基类处理所有 Spring MVC 异常和任何 `ErrorResponseException`,并用返回体呈现错误响应。
### 1.8.1 渲染器
您可以从任何 `@ExceptionHandler` 或任何 `@RequestMapping` 方法返回 `ProblemDetail` 或 `ErrorResponse` 以呈现 RFC 7807 响应。处理方式如下:
- `ProblemDetail` 的 `status` 属性决定 HTTP 状态。
- `ProbemDetail` 的 `instance` 属性是根据当前 URL 路径设置的(如果尚未设置的话)。
- 对于内容协商,Jackson `HttpMessageConverter` 在呈现 `ProblemDetail` 时更喜欢 “application/problem+json” 而不是 “application/json”,如果找不到兼容的媒体类型,也会使用它。
要为 Spring WebFlux 异常和任何 `ErrorResponseException` 启用RFC 7807响应,请扩展 `ResponseEntityExceptionHandler` 并在 Spring 配置中将其声明为 @ControllerAdvice。该处理程序有一个 `@ExceptionHandler` 方法,用于处理任何 `ErrorResponse` 异常,其中包括所有内置的 web 异常。您可以添加更多的异常处理方法,并使用受保护的方法将任何异常映射到 `ProbemDetail`。
### 1.8.2 非标准字段
您可以通过以下两种方式之一使用非标准字段扩展 RFC 7807 响应。
第一,插入 `ProblemDetail` 的“属性” `Map`。使用 Jackson 库时,Spring Framework 会注册 `ProblemDetailJacksonMixin`,以确保此“属性” `Map` 在响应中展开并呈现为顶级 JSON 属性,同样,反序列化期间的任何未知属性都会插入此映射。
您还可以扩展 `ProblemDetail` 以添加专用的非标准属性。`ProblemDetail` 中的复制构造函数允许一个子类,使其可以很容易地从现有的 `ProblemDetail` 中创建。这可以集中完成,例如通过 `@ControllerAdvice`(如 `ResponseEntityExceptionHandler`)将异常的 `ProblemDetail` 重新创建到具有额外非标准字段的子类中。
### 1.8.3 国际化
国际化错误响应详细信息是一种常见的要求,为 Spring MVC 异常定制问题详细信息也是一种良好的实践。支持的方式如下:
- 每个 `ErrorResponse` 公开一个消息代码和参数,以通过 MessageSource 解析 “detail” 字段。实际的消息代码值是用占位符参数化的,例如要从参数中展开的 `"HTTP method {0} not supported"`。
- 每个 `ErrorResponse` 还公开一个消息代码来解析 “title” 字段。
- `ResponseEntityExceptionHandler` 使用消息代码和参数来解析 “detail” 和 “title” 字段。
默认情况下,“detail” 字段的消息代码为 “problemDetail” + 完全限定的异常类名。某些异常可能会暴露额外的消息代码,在这种情况下,会在默认消息代码中添加后缀。下表列出了 Spring MVC 异常的消息参数和代码:
| 异常 | 消息码 | 消息码参数 |
| :-------------------------------------------- | :--------------------- | :----------------------------------------------------------- |
| `AsyncRequestTimeoutException` | (默认) | |
| `ConversionNotSupportedException` | (默认) | `{0}` 属性名, `{1}` 属性值 |
| `HttpMediaTypeNotAcceptableException` | (默认) | `{0}` 支持的媒体类型的队列 |
| `HttpMediaTypeNotAcceptableException` | (默认) + ".parseError" | |
| `HttpMediaTypeNotSupportedException` | (默认) | `{0}` 不支持的媒体类型, `{1}` 支持的媒体类型 |
| `HttpMediaTypeNotSupportedException` | (默认) + ".parseError" | |
| `HttpMessageNotReadableException` | (默认) | |
| `HttpMessageNotWritableException` | (默认) | |
| `HttpRequestMethodNotSupportedException` | (默认) | `{0}` 当前 HTTP 方法, `{1}` 支持的 HTTP 方法的队列 |
| `MethodArgumentNotValidException` | (默认) | `{0}` 全局错误的队列, `{1}` 字段错误的队列。`BindingResult` 中每个错误的消息代码和参数也通过 `MessageSource` 解析。 |
| `MissingRequestHeaderException` | (默认) | `{0}` 请求头名 |
| `MissingServletRequestParameterException` | (默认) | `{0}` 请求参数名 |
| `MissingMatrixVariableException` | (默认) | `{0}` 矩阵变量名 |
| `MissingPathVariableException` | (默认) | `{0}` 路径变量名 |
| `MissingRequestCookieException` | (默认) | `{0}` cookie 名 |
| `MissingServletRequestPartException` | (默认) | `{0}` part 名 |
| `NoHandlerFoundException` | (默认) | |
| `TypeMismatchException` | (默认) | `{0}` 属性名, `{1}` 属性值 |
| `UnsatisfiedServletRequestParameterException` | (默认) | `{0}` 参数条件队列 |
默认情况下,“title” 字段的消息代码为 “problemDetail.title.” + 完全限定的异常类名。
### 1.8.4 客户端处理
客户端应用程序可以在使用 `WebClient` 时捕获 `WebClientResponseException`,或在使用 `RestTemplate` 时捕获 `RestClientResponceException`,并使用它们的 `getResponseBodyAs` 方法将错误响应主体解码为任何目标类型,如 `ProblemDetail` 或 `ProblemDetail` 的子类。
## 1.9 Web 安全
Spring Security 项目为保护 web 应用程序免受恶意攻击提供了支持。请参阅 Spring Security 参考文档,包括:
- Spring MVC 安全性
- Spring MVC 测试支持
- CSRF 保护
- 安全响应标头
HDIV 是另一个与 Spring MVC 集成的 web 安全框架。
## 1.10 HTTP 缓存
HTTP 缓存可以显著提高 web 应用程序的性能。HTTP 缓存围绕着 `Cache-Control` 响应标头,然后是条件请求标头(如 `Last-Modified` 和 `ETag`)。`Cache-Control` 建议私有(例如浏览器)和公共(例如代理)缓存如何缓存和重用响应。`ETag` 报头用于进行条件请求,如果内容没有改变,则可能导致 304(NOT_MODIFIED)而没有正文。`ETag` 可以被视为 `Last-Modified` 标头的更复杂的继承者。
本节描述了 Spring Web MVC 中可用的 HTTP 缓存相关选项。
### 1.10.1 CacheControl
`CacheControl` 支持配置与 `Cache-Control` 标头相关的设置,并在许多地方被接受为参数:
- [`WebContentInterceptor`](https://docs.spring.io/spring-framework/docs/6.0.7/javadoc-api/org/springframework/web/servlet/mvc/WebContentInterceptor.html)
- [`WebContentGenerator`](https://docs.spring.io/spring-framework/docs/6.0.7/javadoc-api/org/springframework/web/servlet/support/WebContentGenerator.html)
- 控制器
- 静态资源
虽然 RFC 7234 描述了 `Cache-Control` 响应标头的所有可能指令,但 `CacheControl` 类型采用了面向用例的方法,重点关注常见场景:
```java
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
```
`WebContentGenerator` 还接受一个更简单的 `cachePeriod` 属性(以秒为单位定义),其工作原理如下:
- `-1` 值不会生成 `Cache-Control` 响应标头。
- `0` 值通过使用 `'Cache-Control: no-store'` 指令来阻止缓存。
- `n>0` 的值通过使用 `'Cache-Control: max-age=n'` 指令将给定响应缓存 `n` 秒。
### 1.10.2 控制器
控制器可以添加对 HTTP 缓存的明确支持。我们建议这样做,因为在将资源的`lastModified` 或 `ETag` 值与条件请求头进行比较之前,需要计算该值。控制器可以将 `ETag` 标头和 `Cache-Control` 设置添加到 `ResponseEntity`,如下例所示:
```java
@GetMapping("/book/{id}")
public ResponseEntity showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
```
如果与条件请求报头的比较表明内容没有改变,则前面的示例发送具有空正文的 304(NOT_MODIFIED)响应。否则,`ETag` 和 `Cache-Control` 标头将添加到响应中。
您也可以对照控制器中的条件请求标头进行检查,如下例所示:
```java
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ... (1)
if (request.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
// (1) 特定于应用程序的计算。
// (2) 响应已设置为304(NOT_MODIFIED)—— 无需进一步处理。
// (3) 继续处理请求。
```
有三种变体用于根据 `eTag` 值、`lastModified` 值或两者来检查条件请求。对于条件 `GET` 和 `HEAD` 请求,可以将响应设置为 304(NOT_MODIFIED)。对于条件 `POST`、`PUT` 和 `DELETE`,您可以将响应设置为 412(PRECONDITION_FAILED),以防止并发修改。
### 1.10.3 静态资源
您应该为静态资源提供 `Cache-Control` 和条件响应标头,以获得最佳性能。请参阅关于配置静态资源的部分。
### 1.10.4 ETag 过滤器
您可以使用 `ShallowEtagHeaderFilter` 添加根据响应内容计算的“浅” `eTag` 值,从而节省带宽,但不节省 CPU 时间。参见浅 ETag。
## 1.11 视图技术
Spring MVC 中视图技术的使用是可插入的。您是否决定使用 Thymelaf、Groovy 标记模板、JSP 或其他技术主要取决于配置更改。本章介绍了与 Spring MVC 集成的视图技术。我们假设您已经熟悉“视图解析”。
> Spring MVC 应用程序的视图位于该应用程序的内部信任边界内。视图可以访问应用程序上下文的所有 bean。因此,不建议在模板可由外部源编辑的应用程序中使用 Spring MVC 的模板支持,因为这可能会带来安全隐患。
### 1.11.1 Thymeleaf
Thymeleaf 是一个现代的服务器端 Java 模板引擎,它强调可以通过双击在浏览器中预览的自然 HTML 模板,这对于在不需要运行服务器的情况下独立处理 UI 模板(例如,由设计师)非常有帮助。如果您想取代 JSP,Thymeleaf 提供了一组最广泛的功能,使这种转换更容易。Thymeleaf 被积极开发和维护。有关更完整的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。该配置涉及一些 bean 声明,如 `ServletContextTemplateResolver`、`SpringTemplateEngine` 和 `ThymelafViewResolver`。有关更多详细信息,请参阅 “Thymeleaf + Spring”。
### 1.11.2 FreeMarker
Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。Spring 框架具有内置集成,用于将 Spring MVC 与 FreeMarker 模板结合使用。
#### 视图配置
以下示例显示了如何将 FreeMarker 配置为视图技术:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}
```
以下示例显示了如何在 XML 中配置相同的内容:
```xml
```
或者,您也可以声明 `FreeMarkerConfigurer` bean 以完全控制所有属性,如下例所示:
```xml
```
您的模板需要存储在前面示例中显示的 `FreeMarkerConfigurer` 指定的目录中。给定前面的配置,如果您的控制器返回一个 `welcome` 的视图名称,那么解析器会查找 `/WEB-INF/freemarker/welcome.ftl` 模板。
#### FreeMarker 配置
您可以通过在 `FreeMarkerConfigurer` bean 上设置适当的 bean 属性,将FreeMarker “Settings” 和 “SharedVariables” 直接传递给 FreeMarkers `Configuration` 对象(由 Spring 管理)。`freemarkerSettings` 属性需要 `java.util.Properties` 对象,而 `freemarkerVariables` 属性需要 `java.util.Map`。以下示例显示了如何使用 `FreeMarkerConfiguration`:
```xml
```
有关应用于 `Configuration` 对象的设置和变量的详细信息,请参阅 FreeMarker 文档。
#### 表格处理
Spring 提供了一个在 JSP 中使用的标记库,其中包含一个 `` 元素。此元素主要允许表单显示表单支持对象的值,并显示 web 或业务层 `Validator` 验证失败的结果。Spring 还支持 FreeMarker 中的相同功能,并提供了额外的方便宏来生成表单输入元素。
##### 绑定宏
FreeMarker 的 `spring-webmvc.jar` 文件中维护了一组标准的宏,因此它们始终可用于适当配置的应用程序。
Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的作用域,这使得调用代码和用户模板可以看到所有宏。以下部分仅集中介绍需要从模板中直接调用的宏。如果您想直接查看宏代码,该文件名为 `spring.ftl`,位于 `org.springframework.web.servlet.view.freemarker` 包中。
##### 简单绑定
在基于 FreeMarker 模板的 HTML 表单中,作为 Spring MVC 控制器的表单视图,您可以使用类似于下一个示例的代码绑定到字段值,并以类似于 JSP 的方式显示每个输入字段的错误消息。以下示例显示了一个 `personForm` 视图:
```xml
<#import "/spring.ftl" as spring/>
...
...
```
`<@spring.bind>` 需要一个 “path” 参数,该参数由命令对象的名称(除非您在控制器配置中更改了它,否则它是 “command”)、句点和要绑定的命令对象上的字段名称组成。您也可以使用嵌套字段,如 `command.address.street`。`bind` 宏采用 `web.xml` 中 `ServletContext` 参数 `defaultHtmlEscape` 指定的默认 HTML 转义行为。
名为 `<@spring.bindEscaped>` 的宏的另一种形式采用第二个参数,该参数明确指定是否应在状态错误消息或值中使用 HTML 转义。您可以根据需要将其设置为 `true` 或 `false`。附加的表单处理宏简化了 HTML 转义的使用,您应该尽可能使用这些宏。它们将在下一节中进行解释。
##### 输入宏
FreeMarker 的附加便利宏简化了绑定和表单生成(包括验证错误显示)。从来没有必要使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 或直接调用我们之前强调的 Spring 绑定宏进行混合和匹配。
可用宏的下表显示了 FreeMarker 模板(FTL)定义和每个定义的参数列表:
| 宏 | FTL 定义 |
| :----------------------------------------------------------- | :----------------------------------------------------------- |
| `message` (基于代码参数从资源捆绑包输出字符串) | <@spring.message code/> |
| `messageText` (根据代码参数从资源束中输出一个字符串,返回到默认参数的值) | <@spring.messageText code, text/> |
| `url` (在相对 URL 前面加上应用程序的上下文根) | <@spring.url relativeUrl/> |
| `formInput` (用于收集用户输入的标准输入字段) | <@spring.formInput path, attributes, fieldType/> |
| `formHiddenInput` (用于提交非用户输入的隐藏输入字段) | <@spring.formHiddenInput path, attributes/> |
| `formPasswordInput` (用于收集密码的标准输入字段。请注意,在这种类型的字段中永远不会填充任何值。) | <@spring.formPasswordInput path, attributes/> |
| `formTextarea` (用于收集长的自由形式文本输入的大文本字段) | <@spring.formTextarea path, attributes/> |
| `formSingleSelect` (用于选择单个必需值的选项下拉框) | <@spring.formSingleSelect path, options, attributes/> |
| `formMultiSelect` (选项列表框,允许用户选择 0 个或多个值) | <@spring.formMultiSelect path, options, attributes/> |
| `formRadioButtons` (一组单选按钮,用于从可用选项中进行单个选择) | <@spring.formRadioButtons path, options separator, attributes/> |
| `formCheckboxes` (允许选择 0 个或多个值的一组复选框) | <@spring.formCheckboxes path, options, separator, attributes/> |
| `formCheckbox` (单个复选框) | <@spring.formCheckbox path, attributes/> |
| `showErrors` (简化绑定字段的验证错误显示) | <@spring.showErrors separator, classOrStyle/> |
> 在 FreeMarker 模板中,实际上不需要 `formHiddenInput` 和 `formPasswordInput`,因为您可以使用普通的 `formInput` 宏,指定隐藏或密码作为 `fieldType` 参数的值。
上述任何宏的参数具有一致的含义:
- `path`:要绑定到的字段的名称(例如,“command.name”)
- `options`:可以在输入字段中选择的所有可用值的 `Map`。映射的键表示从表单 已 POST 返回并绑定到命令对象的值。根据键存储的地图对象是显示在表单上给用户的标签,可能与表单返回的相应值不同。通常,控制器会提供这样的地图作为参考数据。您可以使用任何 `Map` 实现,具体取决于所需的行为。对于严格排序的映射,您可以使用带有合适 `Comparator` 的 `SortedMap`(如 `TreeMap`),对于应按插入顺序返回值的任意映射,可以使用 `commons-colections` 中的 `LinkedHashMap` 或 `LinkedMap`。
- `seperator`:当多个选项作为离散元素(单选按钮或复选框)可用时,用于分隔列表中每个选项的字符序列(如 `
`)。
- `attributes`: 要包含在 HTML 标记本身中的任意标记或文本的附加字符串。这个字符串是由宏直接回显的。例如,在 `textarea` 字段中,您可以提供属性(如 'rows="5" cols="60"'),也可以传递样式信息,如 'style="border:1px solid silver"'。
- `classOrStyle`:对于 `showErrors` 宏,包装每个错误的 `span` 元素使用的 CSS 类的名称。如果未提供任何信息(或值为空),则错误将封装在 `` 标记中。
以下部分概述了宏的示例。
**输入字段**
`formInput` 宏采用 `path` 参数(`command.name`)和一个附加的 `attributes` 参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一起,对路径参数执行隐式 Spring 绑定。绑定在发生新的绑定之前一直有效,因此 `showErrors` 宏不需要再次传递路径参数 — 它对上次为其创建绑定的字段进行操作。
`showErrors` 宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数 — 这次是类名或样式属性。请注意,FreeMarker 可以为属性参数指定默认值。以下示例显示了如何使用 `formInput` 和 `showErrors` 宏:
```xml
<@spring.formInput "command.name"/>
<@spring.showErrors "
"/>
```
下一个示例显示表单片段的输出,生成名称字段,并在表单提交后显示验证错误,但字段中没有值。验证通过 Spring 的验证框架进行。
生成的 HTML 类似于以下示例:
```jsp
Name:
required
```
`formTextarea` 宏的工作方式与 `formInput` 宏相同,并且接受相同的参数列表。通常,第二个参数(`attributes`)用于传递 `textarea` 的样式信息或 `rows` 和 `cols` 属性。
**选择字段**
您可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入:
- `formSingleSelect`
- `formMultiSelect`
- `formRadioButtons`
- `formCheckboxes`
四个宏中的每一个都接受一个选项 `Map`,该映射包含表单字段的值和与该值对应的标签。值和标签可以相同。
下一个例子是 FTL 中的单选按钮。表单支持对象为此字段指定了默认值 “London”,因此不需要验证。当呈现表单时,可供选择的整个城市列表将作为模型中的参考数据以 “cityMap” 的名称提供。以下列表显示了示例:
```jsp
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/>
```
上面的列表呈现了一行单选按钮,`cityMap` 中的每个值对应一个单选按钮,并使用分隔符 `""`。没有提供其他属性(宏的最后一个参数丢失)。`cityMap` 对映射中的每个键值对使用相同的 `String`。映射的键是表单实际提交的 `POST` 请求参数。贴图值是用户看到的标签。在前面的示例中,给定三个著名城市的列表和表单支持对象中的默认值,HTML 类似于以下内容:
```jsp
Town:
London
Paris
New York
```
如果您的应用程序希望通过内部代码处理城市(例如),您可以使用合适的键创建代码地图,如下例所示:
```java
protected Map referenceData(HttpServletRequest request) throws Exception {
Map cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");
Map model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
```
代码现在生成输出,其中无线电值为相关代码,但用户仍然可以看到更用户友好的城市名称,如下所示:
```jsp
Town:
London
Paris
New York
```
##### HTML 转义
前面描述的表单宏的默认使用导致 HTML 元素符合 HTML 4.01,并且使用在 `web.xml` 文件中定义的 HTML 转义的默认值,正如 Spring 的绑定支持所使用的那样。为了使元素符合 XHTML 或覆盖默认的 HTML 转义值,您可以在模板中指定两个变量(或在模型中,它们对模板可见)。在模板中指定它们的优点是,可以在稍后的模板处理中将它们更改为不同的值,从而为表单中的不同字段提供不同的行为。
要为标记切换到 XHTML 兼容,请为名为 `xhtmlCompliant` 的模型或上下文变量指定 `true` 值,如下例所示:
```jsp
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
```
处理完这个指令后,Spring 宏生成的任何元素现在都是 XHTML 兼容的。
以类似的方式,您可以为每个字段指定 HTML 转义,如下例所示:
```jsp
<#-- 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.11.3 Groovy 标记
Groovy 标记模板引擎主要用于生成类似 XML 的标记(XML、XHTML、HTML5 和其他),但您可以使用它来生成任何基于文本的内容。Spring 框架有一个内置的集成,用于将 Spring MVC 与 Groovy 标记结合使用。
> Groovy 标记模板引擎需要 Groovy 2.3.1+。
#### 配置
以下示例显示了如何配置 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;
}
}
```
以下示例显示了如何在 XML 中配置相同的内容:
```xml
```
#### 例子
与传统的模板引擎不同,Groovy Markup 依赖于使用生成器语法的 DSL。以下示例显示了 HTML 页面的示例模板:
```groovy
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.11.4 脚本视图
Spring Framework 有一个内置的集成,可以将 Spring MVC 与任何可以在 JSR-223 Java 脚本引擎上运行的模板库一起使用。我们在不同的脚本引擎上测试了以下模板库:
| 脚本库 | 脚本引擎 |
| :----------------------------------------------------------- | :---------------------------------------------------- |
| [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/) |
| [String templates](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` 接口。
#### 必需要求
您需要在类路径上有脚本引擎,其细节因脚本引擎而异:
- Nashorn JavaScript 引擎是由 Java 8+ 提供的。强烈建议使用可用的最新更新版本。
- 应该添加 JRuby 作为 Ruby 支持的依赖项。
- Jython 应该作为 Python 支持的依赖项添加。
- `org.jetbrains.kotlin:kotlin-script-util` 依赖项和一个 `META-INF/services/javax.script.ScriptEngineFactory` 文件,该文件包含一个 `org.jetbrans.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory` 行,以支持 Kotlin 脚本。有关更多详细信息,请参见此示例。
您需要有脚本模板库。JavaScript 实现这一点的一种方法是通过 WebJars。
#### 脚本模板
您可以声明 `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;
}
}
```
以下示例在 XML 中显示了相同的排列方式:
```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";
}
}
```
以下示例显示 Mustache 模板:
```html
{{title}}
{{body}}
```
使用以下参数调用渲染函数:
- `String template`:模板内容
- `Map model`:视图模型
- `RenderingContext renderingContext`:提供对应用程序上下文、区域设置、模板加载程序和 URL 的访问的 `RenderingContext`(自 5.0 起)
`Mustache.render()` 与此签名在本机上是兼容的,因此您可以直接调用它。
如果模板技术需要一些自定义,则可以提供一个实现自定义渲染函数的脚本。例如,Handlerbars 在使用模板之前需要编译模板,并且需要 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;
}
}
```
> 当使用非线程安全脚本引擎和非为并发设计的模板库时,需要将 `sharedEngine` 属性设置为 `false`,例如在 Nashorn 上运行的 Handlebars 或 React。在这种情况下,由于这个错误,需要 Java SE 8 更新 60,但通常建议在任何情况下使用最新的 Java SE 补丁版本。
`polyfill.js` 只定义 Handlebars 正确运行所需的 `windows` 对象,如下所示:
```javascript
var window = {};
```
这个基本的 `render.js` 实现在使用模板之前对其进行编译。生产就绪的实现还应该存储任何重用的缓存模板或预编译模板。您可以在脚本端执行此操作(并处理您需要的任何自定义 —— 管理模板引擎配置)。以下示例显示了如何执行此操作:
```javascript
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
```
查看 Spring Framework 单元测试、Java 和参考资料,了解更多配置示例。
### 1.11.5 JSP 和 JSTL
Spring 框架有一个内置的集成,用于将 Spring MVC 与 JSP 和 JSTL 结合使用。
#### 视图解析器
使用 JSP 进行开发时,通常会声明一个 `InternalResourceViewResolver` bean。
`InternalResourceViewResolver` 可以用于调度到任何 Servlet 资源,但特别是 JSP。作为最佳实践,我们强烈建议将 JSP 文件放在 `'WEB-INF'` 目录下的目录中,这样客户端就不能直接访问。
```xml
```
#### JSPs 对阵 JSTL
当使用 JSP 标准标记库(JSTL)时,您必须使用一个特殊的视图类 `JstlView`,因为 JSTL 在 I18N 特性等功能工作之前需要做一些准备。
#### Spring 的 JSP 标签库
Spring 提供了请求参数到命令对象的数据绑定,如前几章所述。为了方便结合这些数据绑定功能开发 JSP 页面,Spring 提供了一些标记,使事情变得更加容易。所有Spring 标记都具有 HTML 转义功能,可以启用或禁用字符转义。
`spring.tld` 标记库描述符(tld)包含在 `spring-webmvc.jar` 中。有关单个标记的全面参考,请浏览 API 参考或查看标记库描述。
#### Spring 的表格标签库
从 2.0 版开始,Spring 提供了一组全面的数据绑定感知标记,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都提供了对其对应 HTML 标记的一组属性的支持,使标记变得熟悉和直观。标记生成的 HTML 与 HTML 4.01/XHTML 1.0 兼容。
与其他表单/输入标记库不同,Spring 的表单标记库与 Spring Web MVC 集成,使标记可以访问控制器处理的命令对象和引用数据。正如我们在以下示例中所展示的,表单标记使 JSP 更易于开发、读取和维护。
我们浏览表单标签,并查看如何使用每个标签的示例。我们已经包含了生成的 HTML 片段,其中某些标记需要进一步的注释。
##### 配置
表单标记库捆绑在 `spring-webmvc.jar` 中。库描述符称为 `spring-form.tld`。
要使用此库中的标记,请将以下指令添加到 JSP 页面的顶部:
```xml
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
```
其中,`form` 是要用于此库中标记的标记名前缀。
##### 表格标签
此标记呈现 HTML “form” 元素,并向内部标记公开绑定路径以进行绑定。它将命令对象放在 `PageContext` 中,以便内部标记可以访问命令对象。此库中的所有其他标记都是表单标记的嵌套标记。
假设我们有一个名为 `User` 的域对象。它是一个具有 `firstName` 和 `lastName` 等属性的 JavaBean。我们可以将其用作表单控制器的表单支持对象,该对象返回 `form.jsp`。以下示例显示了 `form.jsp` 的样子:
```xml
```
`firstName` 和 `lastName` 值由页面控制器从放置在 `PageContext` 中的命令对象中检索。继续阅读,了解如何将内部标记与 `form` 标记一起使用的更复杂的示例。
下面的列表显示了生成的 HTML,它看起来像一个标准表单:
```xml
```
前面的 JSP 假设表单支持对象的变量名是 `command`。如果您已经将表单支持对象以另一个名称放入模型中(这绝对是一种最佳实践),则可以将表单绑定到命名变量,如下例所示:
```xml
```
##### input 标签
默认情况下,此标记呈现具有绑定值和 `type='text'` 的 HTML `input` 元素。有关此标记的示例,请参见表单标记。您还可以使用HTML5特定的类型,如 `email`、`tel`、`date` 和其他类型。
##### checkbox 标签
此标记呈现 `type` 设置为 `checkbox` 的 HTML `input` 标记。
假设我们的 `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;
}
}
```
相应的 `form.jsp` 可以类似于以下内容:
```xml
| 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` 类型时,如果绑定值为 `true`,则输入(复选框)标记为已选中。`value` 属性对应于 `setValue(Object)` value 属性的解析值。
- 方法二:当绑定值的类型为 `array` 或 `java.util.Collection` 时,如果绑定 `Collection` 中存在配置的 `setValue(Object)` 值,则 `input(checkbox)` 标记为 `checked`。
- 方法三:对于任何其他绑定值类型,如果配置的 `setValue(Object)` 等于绑定值,则 `input(checkbox)` 标记为 `checked`。
请注意,无论采用何种方法,都会生成相同的 HTML 结构。以下 HTML 代码段定义了一些复选框:
```xml
| Interests: |
Quidditch:
Herbology:
Defence Against the Dark Arts:
|
```
您可能不希望在每个复选框之后看到额外的隐藏字段。当 HTML 页面中的复选框未被选中时,表单提交后,其值不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要一个解决 HTML 中这种怪癖的方法,以便 Spring 表单数据绑定能够工作。`checkbox` 标记遵循现有的 Spring 约定,即为每个复选框包含一个以下划线(`_`)为前缀的隐藏参数。通过这样做,您实际上是在告诉 Spring “复选框在表单中是可见的,我希望表单数据绑定到的对象反映复选框的状态,无论发生什么。”
##### checkboxes 标签
此标记呈现类型设置为复选框的多个 HTML 输入标记。
本节以上一个 `checkbox` 标记部分的示例为基础。有时,您不希望在 JSP 页面中列出所有可能的爱好。您更愿意在运行时提供可用选项的列表,并将其传递给标记。这就是 `checkboxes` 标记的用途。您可以传入包含 `items` 属性中可用选项的 `Array`、`List` 或 `Map`。通常,绑定属性是一个集合,因此它可以包含用户选择的多个值。以下示例显示了一个使用此标记的 JSP:
```xml
| Interests: |
<%-- Property is of an array or of type java.util.Collection --%>
|
```
本例假设 `interestList` 是一个可用作模型属性的列表,其中包含要从中选择的值的字符串。如果使用 `Map`,则映射条目键将用作值,映射条目的值将用作要显示的标签。也可以使用自定义对象,在该对象中可以使用 `itemValue` 提供值的特性名称,使用 `itemLabel` 提供标签。
##### radiobutton 标签
此标记呈现 `type` 设置为 `radio` 的 HTML `input` 元素。
典型的使用模式涉及绑定到同一属性但具有不同值的多个标记实例,如以下示例所示:
```xml
| Sex: |
Male:
Female:
|
```
##### radiobuttons 标签
此标记呈现 `type` 设置为 `radio` 的多个 HTML `input` 元素。
与 `checkboxes` 标记一样,您可能希望将可用选项作为运行时变量传入。对于此用法,您可以使用 `radiobuttons` 标记。传入一个 `Array`、`List` 或 `Map`,其中包含 `items` 属性中的可用选项。如果使用 `Map`,则映射条目键将用作值,映射条目的值将用作要显示的标签。您也可以使用自定义对象,在该对象中,您可以通过使用 `itemValue` 提供值的特性名称,并通过使用 `itemLabel` 提供标签,如以下示例所示:
```xml
| Sex: |
|
```
##### password 标签
此标记呈现一个 HTML `input` 标记,该标记的 `type` 设置为具有绑定值的 `password`。
```xml
| Password: |
|
```
请注意,默认情况下,不会显示密码值。如果确实希望显示密码值,可以将 `showPassword` 属性的值设置为 `true`,如下例所示:
```xml
| Password: |
|
```
##### select 标签
此标记呈现 HTML “select” 元素。它支持将数据绑定到所选选项,以及使用嵌套的 `option` 和 `options` 标记。
假设 `User` 有一个技能列表。相应的 HTML 可以如下所示:
```xml
| Skills: |
|
```
如果 `User` 的技能是草药,“技能”行的 HTML 源代码可能如下:
```xml
| Skills: |
|
```
##### option 标签
此标记呈现 HTML `option` 元素。它根据绑定值设置 `selected`。以下 HTML 显示了它的典型输出:
```xml
| House: |
|
```
如果 `User` 的房子在格兰芬多,“房子”行的 HTML 源代码如下:
```xml
| House: |
|
```
(1) 注意额外的 `selected` 属性
##### options 标签
此标记呈现 HTML `option` 元素的列表。它根据绑定值设置 `selected` 的属性。以下 HTML 显示了它的典型输出:
```xml
| Country: |
|
```
如果 `User` 居住在英国,“国家”行的 HTML 源代码如下:
```xml
| Country: |
|
```
(1) 注意额外的一个 `selected` 属性
如前一个示例所示,`option` 标记和 `options` 标记的组合使用会生成相同的标准 HTML,但允许您在 JSP 中明确指定一个仅用于显示的值(它所属的位置),例如示例中的默认字符串:“--Please Select”。
`items` 属性通常由项对象(item objects)的集合或数组填充。`itemValue` 和 `itemLabel` 引用这些项对象的 bean 属性(如果指定)。否则,项对象本身将变成字符串。或者,可以指定项目 `Map`,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果碰巧也指定了 `itemValue` 或 `itemLabel`(或两者都指定),则项值属性将应用于映射键,项标签属性则应用于映射值。
##### textarea 标签
此标记呈现 HTML `textarea` 元素。以下 HTML 显示了它的典型输出:
```xml
| Notes: |
|
|
```
##### hidden 标签
此标记呈现一个 HTML 输入标记,该标记的类型设置为使用绑定值隐藏。若要提交未绑定的隐藏值,请使用类型设置为隐藏的 HTML 输入标记。以下 HTML 显示了它的典型输出:
```xml
```
如果我们选择将房屋价值作为隐藏价值提交,HTML 将如下所示:
```xml
```
##### errors 标签
此标记在 HTML `span` 元素中呈现字段错误。它提供对控制器中创建的错误或与控制器关联的任何验证器创建的错误的访问。
假设我们希望在提交表单后显示 `firstName` 和 `lastName` 字段的所有错误消息。我们有一个名为 `UserValidator` 的 `User` 类实例的验证器,如下例所示:
```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.");
}
}
```
`form.jsp` 可以如下所示:
```xml
```
如果我们提交的表单的 `firstName` 和 `lastName` 字段中有空值,HTML 将如下所示:
```xml
```
如果我们想显示给定页面的整个错误列表,该怎么办?下一个示例显示 `errors` 标记还支持一些基本的通配符功能。
- `path="*"`:显示所有错误。
- `path="lastName"`:显示与 `lastName` 字段相关的所有错误。
- 如果 `path` 被忽略,则只显示对象错误。
以下示例在页面顶部显示错误列表,然后在字段旁边显示特定于字段的错误:
```xml
```
HTML 将如下所示:
```xml
```
`spring-form.tld` 标记库描述符(TLD)包含在 `spring-webmvc.jar` 中。有关单个标记的全面参考,请浏览 API 参考或查看标记库描述。
##### 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 框架(不仅仅是 SpringMVC)结合使用。将此筛选器添加到 web.xml 中,带有隐藏方法参数的 POST 将转换为相应的 HTTP 方法请求。
为了支持 HTTP 方法转换,Spring MVC 表单标记被更新为支持设置 HTTP 方法。例如,以下片段来自宠物诊所示例:
```xml
```
前面的示例执行 HTTP POST,在请求参数后面隐藏 “real” 的 DELETE 方法。它由 web.xml 中定义的 `HiddenHttpMethodFilter` 获取,如下例所示:
```xml
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;
}
```
##### HTML 5 标签
Spring 表单标记库允许输入动态属性,这意味着您可以输入任何 HTML5 特定的属性。
表单 `input` 标记支持输入 `text` 以外的类型属性。这旨在允许呈现新的 HTML5 特定输入类型,如 `email`、`date`、`range` 和其他类型。请注意,由于 `text` 是默认类型,因此不需要输入 `type='text'`。
### 1.11.6 RSS 和 Atom
`AbstractAtomFeedView` 和 `AbstractRssFeedView` 都继承了 `AbstractFeedView` 基类,分别用于提供 Atom 和 RSSFeed 视图。它们基于 ROME 项目,位于 `org.springframework.web.servlet.view.feed` 包中。
`AbstractAtomFeedView` 要求您实现 `buildFeedEntry()` 方法,并可选地重写 `buildFeedMetadata()` 方法(默认实现为空)。以下示例显示了如何执行此操作:
```java
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
}
}
```
类似的要求适用于实现 `AbstractRssFeedView`,如下例所示:
```java
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
}
}
```
`buildFeedItems()` 和 `buildFeedEntries()` 方法传入 HTTP 请求,以防您需要访问 Locale。HTTP 响应仅用于 Cookie 或其他 HTTP 标头的设置。在方法返回后,提要会自动写入响应对象。
有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team 博客条目。
### 1.11.7 PDF 和 Excel
Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。本节介绍如何使用这些功能。
#### 文件视图的介绍
HTML 页面并不总是用户查看模型输出的最佳方式,Spring 使从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。文档是视图,并以正确的内容类型从服务器流式传输,以(希望)使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。
为了使用 Excel 视图,您需要将 Apache POI 库添加到类路径中。对于 PDF 生成,您需要(最好)添加 OpenPDF 库。
> 如果可能的话,您应该使用最新版本的底层文档生成库。特别是,我们强烈建议使用 OpenPDF(例如,OpenPDF 1.2.12),而不是过时的原始 iText 2.1.7,因为 OpenPDF 是积极维护的,并修复了不受信任的 PDF 内容的一个重要漏洞。
#### PDF 视图
单词列表的简单 PDF 视图可以扩展 `org.springframework.web.servlet.view.document.AbstractPdfView` 并实现 `buildPdfDocument()` 方法,如下例所示:
```java
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));
}
}
}
```
控制器可以从外部视图定义(按名称引用它)或作为处理程序方法的 `View` 实例返回这样的视图。
#### Excel 视图
自 Spring Framework 4.2 以来,提供了 `org.springframework.web.servlet.view.dococument.AbstractXlsView` 作为 Excel 视图的基类。它基于 Apache POI,具有专门的子类(`AbstractXlsxView` 和 `AbstractXtlsxStreamingView`),取代了过时的 `AbstractExcelView` 类。
编程模型类似于 `AbstractPdfView`,使用 `buildExcelDocument()` 作为中心模板方法,控制器能够从外部定义(按名称)或作为处理程序方法的 `View` 实例返回这样的视图。
### 1.11.8 Jackson
Spring 提供了对 Jackson JSON 库的支持。
#### 基于 Jackson 的 JSON MVC 视图
`MappingJackson2JsonView` 使用 Jackson 库的 `ObjectMapper` 将响应内容呈现为 JSON。默认情况下,模型映射的整个内容(框架特定类除外)都编码为 JSON。对于需要过滤映射内容的情况,可以使用 `modelKeys` 属性指定一组特定的模型属性进行编码。也可以使用 `extractValueFromSingleKeyModel` 属性直接提取和序列化单键模型中的值,而不是将其作为模型属性的映射。
您可以根据需要使用 Jackson 提供的注解自定义 JSON 映射。当您需要进一步控制时,可以通过 `ObjectMapper` 属性注入自定义 `ObjectMapper`,用于需要为特定类型提供自定义 JSON 序列化程序和反序列化程序的情况。
#### 基于 Jackson 的 XML 视图
`MappingJackson2XmlView` 使用 Jackson XML 扩展的 `XmlMapper` 将响应内容呈现为 XML。如果模型包含多个条目,则应使用 `modelKey` bean 属性显式设置要序列化的对象。如果模型包含单个条目,则会自动对其进行序列化。
您可以根据需要使用 JAXB 或 Jackson 提供的注解来定制 XML 映射。当需要进一步控制时,可以通过 `ObjectMapper` 属性注入自定义 `XmlMapper`,对于需要为特定类型提供序列化程序和反序列化程序的自定义 XML 的情况。
### 1.11.9 XML 封送(Marshalling)
`MarshallingView` 使用 XML `Marshaller`(在 `org.springframework.oxm` 包中定义)将响应内容呈现为 XML。您可以使用 `MarshallingView` 实例的 `modelKey` bean 属性显式设置要编组的对象。或者,视图迭代所有模型属性并封送 `Marshaller` 支持的第一个类型。有关 `org.springframework.oxm` 包中功能的更多信息,请参阅使用 O/X 映射器封送 XML。
### 1.11.10 XSLT 视图
XSLT 是 XML 的一种转换语言,作为一种视图技术在 web 应用程序中很受欢迎。如果您的应用程序自然地处理 XML,或者您的模型可以很容易地转换为 XML,那么 XSLT 作为视图技术是一个不错的选择。以下部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 进行转换。
这个例子是一个简单的 Spring 应用程序,它在 `Controller` 中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及 XSLT 视图的视图名称。有关 Spring Web MVC 的 `Controller` 接口的详细信息,请参阅带注解的控制器。XSLT 控制器将单词列表转换为一个简单的 XML 文档,以便进行转换。
#### Beans
配置是简单 Spring web 应用程序的标准配置:MVC 配置必须定义 `XsltViewResolver` bean 和常规 MVC 注解配置。以下示例显示了如何执行此操作:
```java
@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;
}
}
```
#### Controller
我们还需要一个控制器来封装我们的单词生成逻辑。
控制器逻辑封装在 `@Controller` 类中,处理程序方法定义如下:
```java
@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";
}
}
```
到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,您还可以将 XML 文件加载为 `Resource`,并使用它来代替自定义 DOM 文档。
有一些软件包可以自动“初始化”对象图,但在 Spring 中,您可以完全灵活地以任何选择的方式从模型中创建 DOM。这可以防止 XML 的转换在模型数据的结构中扮演太大的角色,而在使用工具管理 DOMiification 过程时,这是一种危险。
#### Transformation
最后,`XsltViewResolver` 解析“主” XSLT 模板文件,并将 DOM 文档合并到其中以生成我们的视图。如 `XsltViewResolver` 配置中所示,XSLT 模板位于 `WEB-INF/xsl` 目录中的 `war` 文件中,并以 `xslt` 文件扩展名结束。
以下示例显示了一个 XSLT 转换:
```xml
Hello!
My First Words
```
前面的转换呈现为以下 HTML:
```html
Hello!
My First Words
```
## 1.12 MVC 设置
MVC Java 配置和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,并提供了一个用于自定义它的配置 API。
有关配置 API 中不可用的更高级自定义,请参阅高级 Java 配置和高级 XML 配置。
您不需要理解 MVC Java 配置和 MVC 命名空间创建的底层 bean。如果您想了解更多信息,请参阅特殊 Bean 类型和 Web MVC 配置。
### 1.12.1 启用 MVC 配置
在 Java 配置中,您可以使用 `@EnableWebMvc` 注解来启用 MVC 配置,如下例所示:
```java
@Configuration
@EnableWebMvc
public class WebConfig {
}
```
在 XML 配置中,您可以使用 `` 元素来启用 mvc 配置,如下例所示:
```xml
```
前面的示例注册了许多 Spring MVC 基础结构 bean,并适应类路径上可用的依赖项(例如,JSON、XML 和其他的有效负载转换器)。
### 1.12.2 MVC 配置 API
在 Java 配置中,您可以实现 `WebMvcConfigurer` 接口,如下例所示:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
```
在 XML 中,您可以检查 `` 的属性和子元素。您可以查看 Spring MVC XML 模式,或者使用 IDE 的代码完成功能来发现哪些属性和子元素可用。
### 1.12.3 类型转换
默认情况下,安装了各种数字和日期类型的格式化程序,并支持通过 `@NumberFormat` 和 `@DateTimeFormat` 对字段进行自定义。
要在 Java 配置中注册自定义格式化程序和转换器,请使用以下命令:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
```
要在 XML 配置中执行相同操作,请使用以下操作:
```xml
```
默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求 Locale。这适用于日期表示为带有 “input” 表单字段的字符串的表单。然而,对于 “date” 和 “time” 表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以自定义如下:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
```
> 有关何时使用 FormatterRegistrar 实现的更多信息,请参阅 `FormatterRegistrar` SPI 和 `FormattingConversionServiceFactoryBean`。
### 1.12.4 校验
默认情况下,如果类路径上存在 Bean Validation(例如,Hibernate Validator),则 `LocalValidatorFactoryBean` 将注册为全局 Validator,用于控制器方法参数上的 `@Valid` 和 `Validated`。
在 Java 配置中,您可以自定义全局 `Validator` 实例,如下例所示:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
请注意,您也可以在本地注册 `Validator` 实现,如下例所示:
```java
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
```
> 如果您需要在某个地方注入 `LocalValidatorFactoryBean`,请创建一个 bean 并用 `@Primary` 标记它,以避免与 MVC 配置中声明的 bean 发生冲突。
### 1.12.5 拦截器
在 Java 配置中,您可以注册拦截器以应用于传入请求,如下例所示:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
> 映射拦截器不适合作为安全层,因为它可能与带注解的控制器路径匹配不匹配,后者也可以透明地匹配尾部斜线和路径扩展,以及其他路径匹配选项。其中许多选项已被弃用,但不匹配的可能性仍然存在。通常,我们建议使用 Spring Security,它包括一个专用的 MvcRequestMatcher 来与 Spring MVC 路径匹配对齐,并且还具有一个安全防火墙,可以阻止 URL 路径中的许多不需要的字符。
### 1.12.6 内容类型
您可以配置 Spring MVC 如何根据请求确定请求的媒体类型(例如,`Accept` 标头、URL 路径扩展、查询参数等)。
默认情况下,仅检查 `Accept` 标头。
如果必须使用基于 URL 的内容类型解析,请考虑在路径扩展上使用查询参数策略。有关更多详细信息,请参见后缀匹配和后缀匹配以及 RFD。
在 Java 配置中,您可以自定义请求的内容类型解析,如下例所示:
```java
@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);
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
json=application/json
xml=application/xml
```
### 1.12.7 消息转换器
您可以在 Java 配置中通过重写 `configureMessageConverters()`(替换 Spring MVC 创建的默认转换器)或重写 `extendMessageConverter()`(自定义默认转换器或在默认转换器上添加其他转换器)来自定义 `HttpMessageConverter`。
以下示例使用自定义的 `ObjectMapper`(而不是默认的)添加 XML 和 Jackson JSON 转换器:
```java
@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()));
}
}
```
在前面的示例中,`Jackson2ObjectMapperBuilder` 用于为 `MappingJackson2HttpMessageConverter` 和 `MappingJackson2XmlHttpMessageConverter` 创建一个通用配置,启用缩进、自定义日期格式和注册 `jackson-module-parameter-names`,这增加了对访问参数名称的支持(Java 8 中添加的一项功能)。
此生成器自定义 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) 已禁用。
如果在类路径上检测到以下众所周知的模块,它还会自动注册这些模块:
- jackson-datetype-joda:支持 joda-Time 类型。
- jackson-datatype-jsr310:支持 Java 8 日期和时间 API 类型。
- jackson-datatype-jdk8:支持其他 Java 8类型,例如 `Optional`。
- `jackson-module-kotlin`:支持 Kotlin 类和数据类。
> 使用 Jackson XML 支持启用缩进需要 `woodstox-core-asl` 依赖项,此外还需要 `jackson-dataformat-xml` 依赖项。
还有其他有趣的 Jackson 模块:
- jackson-datatype-money:支持 `javax.money` 类型(非官方模块)。
- jackson-datatype-hibernate:支持特定于 Hibernate 的类型和财产(包括延迟加载方面)。
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
### 1.12.8 视图控制器
这是定义 `ParameterizableViewController` 的快捷方式,该控制器在调用时立即转发到视图。当视图生成响应之前没有 Java 控制器逻辑要运行时,您可以在静态情况下使用它。
以下 Java 配置示例将 `/` 请求转发到一个名为 `home` 的视图:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
```
以下示例通过使用 `` 元素,实现了与前面示例相同的功能,但使用了 XML:
```xml
```
如果 `@RequestMapping` 方法映射到任何 HTTP 方法的 URL,则视图控制器不能用于处理同一 URL。这是因为 URL 与带注解的控制器的匹配被认为是端点所有权的足够强的指示,因此 405(METHOD_NOT_ALLOWED)、415(UNSUPPORTED_MEDIA_TYPE)或类似的响应可以被发送到客户端以帮助调试。因此,建议避免在带注释的控制器和视图控制器之间拆分 URL 处理。
### 1.12.9 视图解析器
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();
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
然而,请注意,FreeMarker、Groovy Markup 和脚本模板也需要配置底层视图技术。
MVC 命名空间提供了专用的元素。以下示例适用于 FreeMarker:
```xml
```
在 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;
}
}
```
### 1.12.10 静态资源
此选项提供了一种方便的方式,可以从基于 `Resource` 的位置列表中提供静态资源。
在下一个示例中,给定一个以 `/resources` 开头的请求,相对路径用于在 web 应用程序根目录下或在 `/static` 下的类路径上查找和服务相对于 `/public` 的静态资源。这些资源的使用期限为一年,以确保最大限度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求。`Last-Modified` 信息是从 `Resource#lastModified` 推导出来的,因此 HTTP 条件请求支持 `"Last-Modified"` 标头。
以下列表显示了如何使用 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)));
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
另请参阅对静态资源的 HTTP 缓存支持。
资源处理程序还支持一系列 `ResourceResolver` 实现和 `ResourceTransformer` 实现,您可以使用它们来创建用于处理优化资源的工具链。
您可以基于从内容、固定应用程序版本或其他版本计算的 MD5 哈希,对版本化的资源 URL 使用 `VersionResourceResolver`。`ContentVersionStrategy`(MD5 散列)是一个不错的选择 — 除了一些显著的例外,例如与模块加载程序一起使用的 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("/**"));
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
然后,您可以使用 `ResourceUrlProvider` 重写 URL,并应用完整的解析器和转换器链 — 例如插入版本。MVC 配置提供了一个 `ResourceUrlProvider` bean,这样它就可以被注入到其他 bean 中。您还可以使用 `ResourceUrlEncodingFilter` 为Thymelaf、JSP、FreeMarker 和其他具有依赖 `HttpServletResponse#encodeURL` 的 URL 标记的应用程序进行透明重写。
请注意,当同时使用 `EncodedResourceResolver`(例如,用于提供 gzip 了的或 brotli 编码的资源)和 `VersionResourceResolver` 时,必须按此顺序注册它们。这确保了基于内容的版本总是基于未编码的文件进行可靠的计算。
对于 WebJars,像 `/webJars/jquery/1.2.0/jquery.min.js` 这样的版本化 URL 是推荐的也是最有效的使用方式。相关的资源位置是通过 Spring Boot 开箱即用配置的(或者可以通过 `ResourceHandlerRegistry` 手动配置),不需要添加 `org.webjars:webjars-locator-core` 依赖项。
`WebJarsResourceResolver` 支持像 `/webjars/jquery/jquery.min.js` 这样的无版本 URL,当 `org.webjars:webjars-locator-core` 库出现在类路径上时,`WebJarsResourceResolver` 会自动注册,代价是类路径扫描可能会减慢应用程序的启动。解析器可以重写 URL 以包含 jar 的版本,也可以与没有版本的传入 URL 进行匹配 — 例如,从 `/webjars/jquery/jquery.min.js` 到 `/webjars/jquery/1.2.0/jquery.min.js`。
> 基于 `ResourceHandlerRegistry` 的 Java 配置为细粒度控制提供了进一步的选项,例如上次修改的行为和优化的资源解析。
### 1.12.11 默认 Servlet
Spring MVC 允许将 `DispatcherServlet` 映射到 `/`(从而覆盖容器默认 Servlet 的映射),同时仍然允许容器的默认 Servlet 处理静态资源请求。它配置了一个 `DefaultServletHttpRequestHandler`,其 URL 映射为 `/**`,并且相对于其他 URL 映射具有最低优先级。
此处理程序将所有请求转发到默认 Servlet。因此,它必须保持在所有其他 URL 句柄映射顺序的最后一个。如果您使用 ``,就会出现这种情况。或者,如果您设置了自己的自定义 `HandlerMapping` 实例,请确保将其 `order` 属性设置为低于 `DefaultServletHttpRequestHandler` 的值,即 `Integer.MAX_VALUE`。
以下示例显示了如何使用默认设置启用该功能:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```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");
}
}
```
以下示例显示了如何在 XML 中实现相同的配置:
```xml
```
### 1.12.12 路径匹配
您可以自定义与路径匹配和 URL 处理相关的选项。有关各个选项的详细信息,请参阅 `PathMatchConfigurer` javadoc。
以下示例显示了如何在 Java 配置中自定义路径匹配:
```java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
// ...
}
}
```
以下示例显示了如何在 XML 配置中自定义路径匹配:
```xml
```
### 1.12.13 高级 Java 配置
`@EnableWebMvc` 导入 `DelegatingWebMvcConfiguration`,该配置:
为Spring MVC应用程序提供默认的Spring配置
检测并委托WebMvcConfigurer实现以自定义该配置。
对于高级模式,您可以删除 `@EnableWebMvc` 并直接从 `DelegatingWebMvcConfiguration` 进行扩展,而不是实现 `WebMvcConfigurer`,如下例所示:
```java
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
```
您可以将现有方法保留在 `WebConfig` 中,但现在也可以覆盖基类中的 bean 声明,并且类路径上仍然可以有任意数量的其他 `WebMvcConfigurer` 实现。
### 1.12.14 高级 XML 配置
MVC 命名空间没有高级模式。如果您需要在 bean 上自定义一个无法更改的属性,则可以使用 Spring `ApplicationContext` 的 `BeanPostProcessor` 生命周期挂钩,如下例所示:
```java
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
```
请注意,您需要将 `MyPostProcessor` 声明为 bean,可以用 XML 显式声明,也可以通过 `` 声明来检测它。
## 1.13 HTTP/2
Servlet 4 容器需要支持 HTTP/2,Spring Framework 5 与 Servlet API 4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置有关的注意事项。有关更多详细信息,请参阅 HTTP/2 wiki 页面。
Servlet API 确实公开了一个与 HTTP/2 相关的构造。您可以使用 `jakarta.servlet.http.PushBuilder` 主动向客户端推送资源,并且它被支持作为 `@RequestMapping` 方法的方法参数。
# 2 REST 客户端
本节介绍客户端访问 REST 端点的选项。
## 2.1 RestTemplate
`RestTemplate` 是一个用于执行 HTTP 请求的同步客户端。它是最初的 Spring REST 客户端,并通过底层 HTTP 客户端库公开了一个简单的模板方法 API。
> 从 5.0 开始,`RestTemplate` 处于维护模式,只接受微小更改和错误的请求。请考虑使用 WebClient,它提供了更现代的 API,并支持同步、异步和流式传输场景。
有关详细信息,请参阅 REST 端点。
## 2.2 WebClient
WebClient 是一个非阻塞、响应式客户端,用于执行 HTTP 请求。它在 5.0 中引入,提供了 `RestTemplate` 的现代替代方案,有效支持同步和异步以及流媒体场景。
与 `RestTemplate` 相比,`WebClient` 支持以下功能:
- 非阻塞I/O。
- 反应式流背压。
- 硬件资源较少的高并发性。
- 函数式、流畅的 API,充分利用了 Java 8 lambdas。
- 同步和异步交互。
- 流式传输到服务器或从服务器流式传输。
有关详细信息,请参阅 WebClient。
## 2.3 HTTP 接口
Spring Frameworks 允许您将 HTTP 服务定义为具有 HTTP 交换方法的 Java 接口。然后,您可以生成一个实现该接口并执行交换的代理。这有助于简化 HTTP 远程访问,并为选择 API 样式(如同步或响应式)提供了额外的灵活性。
有关详细信息,请参阅 REST 端点。
# 3 测试
本节总结了 Spring MVC 应用程序 `spring-test` 中可用的选项。
- Servlet API 模拟:用于单元测试控制器、过滤器和其他 web 组件的 Servlet API 合同的模拟实现。有关更多详细信息,请参阅 Servlet API mock 对象。
- TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法高效缓存加载的配置,并支持使用 `MockServletContext` 加载 `WebApplicationContext`。有关更多详细信息,请参阅 TestContext Framework。
- Spring MVC 测试:一个框架,也称为 `MockMvc`,用于通过 `DispatcherServlet` 测试带注解的控制器(即支持注解),配有 Spring MVC 基础设施,但没有 HTTP 服务器。有关更多详细信息,请参阅 Spring MVC 测试。
- 客户端 REST: `spring-test` 提供了一个 `MockRestServiceServer`,您可以将其用作模拟服务器,用于测试内部使用 `RestTemplate` 的客户端代码。有关更多详细信息,请参阅客户端 REST 测试。
- `WebTestClient`:专为测试 WebFlux 应用程序而构建,但它也可以用于通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个无阻塞、响应式的客户端,非常适合测试异步和流式场景。有关更多详细信息,请参阅 `WebTestClient`。
# 4 WebSockets
这部分参考文档涵盖了对 Servlet 栈的支持、包括原始 WebSocket 交互的 WebSocket 消息传递、通过 SockJS 的 WebSocket 仿真,以及通过 STOMP 作为 WebSocket 上的子协议发布订阅消息传递。
## 4.1 介绍 WebSocket
WebSocket 协议,RFC 6455,提供了一种标准化的方法,可以通过单个 TCP 连接在客户端和服务器之间建立全双工、双向的通信信道。它是一种不同于 HTTP 的 TCP 协议,但被设计为通过 HTTP 工作,使用端口 80 和 443,并允许重用现有的防火墙规则。
WebSocket 交互以 HTTP 请求开始,该请求使用 HTTP `Upgrade` 标头进行升级,或者在本例中切换到 WebSocket 协议。以下示例显示了这样的交互:
```yaml
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 状态代码:
```yaml
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
```
(1) 协议切换
成功握手后,HTTP 升级请求的 TCP 套接字将保持打开状态,以便客户端和服务器继续发送和接收消息。
关于 WebSockets 如何工作的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节,或 Web 上的许多介绍和教程中的任何一个。
请注意,如果 WebSocket 服务器在 web 服务器(例如 nginx)后面运行,则可能需要对其进行配置,以便将 WebSocket 升级请求传递到 WebSocket server。同样,如果应用程序在云环境中运行,请检查云提供商与 WebSocket 支持相关的说明。
### 4.1.1 HTTP 对比 WebSocket
尽管 WebSocket 被设计为与 HTTP 兼容,并且从 HTTP 请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和应用程序编程模型。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。
相比之下,在 WebSockets 中,通常只有一个 URL 用于初始连接。随后,所有应用程序消息都在同一个 TCP 连接上流动。这指向一个完全不同的异步、事件驱动的消息传递体系结构。
WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息的内容规定任何语义。这意味着,除非客户端和服务器在消息语义上达成一致,否则无法路由或处理消息。
WebSocket 客户端和服务器可以通过 HTTP 握手请求上的 `Sec-WebSocket-Protocol` 头协商使用更高级别的消息传递协议(例如 STOMP)。如果没有这一点,他们需要制定自己的惯例。
### 4.1.2 什么时候使用 WebSockets
WebSockets 可以使网页具有动态性和交互性。然而,在许多情况下,AJAX 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。
例如,新闻、邮件和社交订阅源需要动态更新,但每隔几分钟更新一次可能是完全可以的。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如,监视网络故障),HTTP 流或轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 的最佳条件。
还要记住,在 Internet 上,您无法控制的限制性代理可能会阻止 WebSocket 交互,原因可能是它们没有配置为传递 `Upgrade` 标头,也可能是因为它们关闭了看起来空闲的长期连接。这意味着,将 WebSocket 用于防火墙内的内部应用程序比用于面向公众的应用程序更简单。
## 4.2 WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的客户端和服务器端应用程序。
### 4.2.1 WebSocketHandler
创建 WebSocket 服务器就像实现 `WebSocketHandler` 一样简单,或者更可能的是扩展 `TextWebSocketHandler` 或 `BinaryWebSocketHandler`。以下示例使用 `TextWebSocketHandler`:
```java
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,如下例所示:
```java
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 配置:
```xml
```
前面的例子用于 SpringMVC 应用程序,应该包含在 `DispatcherServlet` 的配置中。然而,Spring 的 WebSocket 支持并不依赖于 Spring MVC。在 `WebSocketHttpRequestHandler` 的帮助下,将 `WebSocketHandler` 集成到其他 HTTP 服务环境中相对简单。
当直接或间接使用 `WebSocketHandler` API 时,例如通过 STOMP 消息传递,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话(JSR-356)不允许并发发送。一种选择是用 `ConcurrentWebSocketSessionDecorator` 包装 `WebSocketSession`。
### 4.2.2 WebSocket 握手
自定义初始 HTTP WebSocket 握手请求的最简单方法是通过 `HandshakeInterceptor`,它公开握手之前和之后的方法。您可以使用这样的拦截器来排除握手或使任何属性可用于 `WebSocketSession`。以下示例使用内置的拦截器将 HTTP 会话属性传递给 WebSocket 会话:
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
```
以下示例显示了与前面示例等效的 XML 配置:
```xml
```
一个更高级的选项是扩展 `DefaultHandshakeHandler`,它执行 WebSocket 握手的步骤,包括验证客户端来源、协商子协议以及其他细节。如果应用程序需要配置自定义 `RequestUpgradeStrategy` 以适应尚未支持的 WebSocket 服务器引擎和版本,则可能还需要使用此选项(有关此主题的更多信息,请参阅部署)。Java 配置和 XML 命名空间都可以配置自定义 `HandshakeHandler`。
> Spring 提供了一个 `WebSocketHandlerDecorator` 基类,您可以使用它来用其他行为装饰 `WebSocketHandler`。当使用 WebSocket Java 配置或 XML 命名空间时,默认情况下会提供和添加日志记录和异常处理实现。`ExceptionWebSocketHandlerRecorder` 捕获任何 `WebSocketHandler` 方法产生的所有未捕获的异常,并关闭状态为 `1011` 的 WebSocket 会话,这表示服务器错误。
### 4.2.3 部署
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,`DispatcherServlet` 同时为 HTTP WebSocket 握手和其他 HTTP 请求提供服务。通过调用 `WebSocketHttpRequestHandler` 也可以很容易地集成到其他 HTTP 处理场景中。这既方便又易于理解。然而,对于 JSR-356 运行时,需要特别注意。
Jakarta WebSocket API(JSR-356)提供了两种部署机制。第一个是在启动时进行 Servlet 容器类路径扫描(Servlet 3 的一个特性)。另一个是在 Servlet 容器初始化时使用的注册 API。这两种机制都不可能对所有 HTTP 处理使用单个“前端控制器” — 包括 WebSocket 握手和所有其他 HTTP 请求 — 例如 Spring MVC 的 `DispatcherServlet`。
这是 JSR-356 的一个重要限制,Spring 的 WebSocket 支持通过特定于服务器的 `RequestUpgradeStrategy` 实现来解决这个问题,即使在 JSR-356 运行时运行也是如此。Tomcat、Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及WildFly)目前都有这样的策略。从 Jakarta WebSocket 2.1 开始,Spring 可以在基于 Jakarta EE 10 的 web 容器(如 Tomcat 10.1 和 Jetty 12)上选择标准请求升级策略。
第二个考虑因素是,支持 JSR-356 的 Servlet 容器预计将执行 `ServletContainerInitializer`(SCI)扫描,这可能会减慢应用程序的启动速度 — 在某些情况下,是戏剧性的。如果在升级到支持 JSR-356 的 Servlet 容器版本后观察到重大影响,那么应该可以通过使用 `web.xml` 中的 `` 元素来选择性地启用或禁用 web 片段(和 SCI 扫描),如下例所示:
```xml
```
然后,您可以按名称选择性地启用 web 片段,例如 Spring 自己的 `SpringServletContainerInitializer`,它提供了对 Servlet 3 Java 初始化 API 的支持。以下示例显示了如何执行此操作:
```xml
spring_web
```
### 4.2.4 服务器配置
每个底层 WebSocket 引擎都公开控制运行时特征的配置属性,例如消息缓冲区大小、空闲超时等。
对于 Tomcat、WildFly 和 GlassFish,您可以将 `ServletServerContainerFactoryBean` 添加到WebSocket Java配置中,如下例所示:
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
```
以下示例显示了与前面示例等效的 XML 配置:
```xml
```
> 对于客户端 WebSocket 配置,您应该使用 `WebSocketContainerFactoryBean`(XML)或 `ContainerProvider.getWebSocketContent()`(Java配置)。
对于 Jetty,您需要提供一个预先配置的 Jetty `WebSocketServerFactory`,并通过 WebSocket Java 配置将其插入 Spring 的 `DefaultHandshakeHandler`。以下示例显示了如何执行此操作:
```java
@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 配置:
```xml
```
### 4.2.5 允许的来源
从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是只接受同源请求。也可以允许所有或指定的起源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户端修改 `Origin` 头值(更多详细信息,请参阅RFC 6454:Web Origin 概念)。
三种可能的行为是:
- 仅允许相同的来源请求(默认值):在此模式下,当 SockJS 启用时,Iframe HTTP 响应标头 `X-Frame-Options` 设置为 `SAMEORIGIN`,并且 JSONP 传输被禁用,因为它不允许检查请求的来源。因此,当启用此模式时,不支持 IE6 和 IE7。
- 允许指定的来源列表:每个允许的来源必须以 `http://` 或 `https://` 开头。在这种模式下,当 SockJS 被启用时,IFrame 传输被禁用。因此,当启用此模式时,不支持 IE6 到 IE9。
- 允许所有来源:要启用此模式,您应该提供 `*` 作为允许的原点值。在此模式下,所有传输都可用。
您可以配置 WebSocket 和 SockJS 允许的起源,如下例所示:
```java
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 配置:
```xml
```
## 4.3 SockJS 回退
在公共 Internet 上,您无法控制的限制性代理可能会阻止 WebSocket 交互,原因可能是它们没有配置为传递 `Upgrade` 标头,也可能是因为它们关闭了看似空闲的长期连接。
这个问题的解决方案是 WebSocket 仿真 — 也就是说,先尝试使用 WebSocket,然后依靠基于 HTTP 的技术来模拟 WebSocket 交互并公开相同的应用程序级 API。
在 Servlet 栈上,Spring Framework 为 SockJS 协议提供了服务器(以及客户端)支持。
### 4.3.1 总览
SockJS 的目标是让应用程序使用 WebSocket API,但在运行时必要时可以使用非 WebSocket 替代方案,而无需更改应用程序代码。
SockJS 包括:
- 以可执行叙述测试的形式定义的 SockJS 协议。
- SockJS JavaScript 客户端 — 用于浏览器的客户端库。
- SockJS 服务器实现,包括 Spring Framework `spring-websocket` 模块中的一个。
- `spring-websocket` 模块中的 SockJS Java 客户端(自 4.1 版起)。
SockJS 是为在浏览器中使用而设计的。它使用各种技术来支持各种各样的浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS 客户端页面。传输分为三大类:WebSocket、HTTP 流式传输和 HTTP 长轮询。有关这些类别的概述,请参阅此博客文章。
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 客户端还提供了一个调试标志,它可以在浏览器控制台中启用有用的消息。在服务器端,您可以为 `org.springframework.web.socket` 启用 `TRACE` 日志记录。有关更多详细信息,请参阅 SockJS 协议叙述测试。
### 4.3.2 启用 SockJS
您可以通过 Java 配置启用 SockJS,如下例所示:
```java
@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 配置:
```xml
```
前面的例子用于 SpringMVC 应用程序,应该包含在 `DispatcherServlet` 的配置中。然而,Spring 的 WebSocket 和 SockJS 支持并不依赖于 Spring MVC。借助 `SockJsHttpRequestHandler` 集成到其他 HTTP 服务环境中相对简单。
在浏览器端,应用程序可以使用 `sockjs-client`(版本1.0.x)。它模拟 W3C WebSocket API,并与服务器通信,根据运行的浏览器选择最佳传输选项。请参阅 sockjs-client 页面和浏览器支持的传输类型列表。客户端还提供了几个配置选项 — 例如指定要包括哪些传输。
### 4.3.3 IE 8 和 9
Internet Explorer 8 和 9 仍在使用中。它们是拥有 SockJS 的一个关键原因。本节介绍了在这些浏览器中运行的重要注意事项。
SockJS 客户端通过使用 Microsoft 的 `XDomainRequest` 在 IE 8 和 9 中支持 Ajax/XHR 流。这可以跨域工作,但不支持发送 cookie。Cookie 对于 Java 应用程序来说通常是必不可少的。然而,由于 SockJS 客户端可以与许多服务器类型(而不仅仅是 Java 服务器类型)一起使用,因此它需要知道 cookie 是否重要。如果是这样,那么 SockJS 客户端更喜欢 Ajax/XHR 进行流式传输。否则,它依赖于基于 iframe 的技术。
来自 SockJS 客户端的第一个 `/info` 请求是对可能影响客户端选择传输的信息的请求。其中一个细节是服务器应用程序是否依赖 cookie(例如,用于身份验证或使用粘性会话进行集群)。Spring 的 SockJS 支持包括一个名为 `sessionCookieNeeded` 的属性。它在默认情况下是启用的,因为大多数 Java 应用程序都依赖于 `JSESSIONID` cookie。如果你的应用程序不需要它,你可以关闭这个选项,然后 SockJS 客户端应该在 IE 8 和 9 中选择 `xdr-streaming`。
如果您确实使用基于 iframe 的传输,请记住,可以指示浏览器通过将 HTTP 响应标头 `X-Frame-Options` 设置为 `DENY`、`SAMEORIGIN` 或 `ALLOW-FROM ` 来阻止在给定页面上使用 iframe。这是用来防止点击劫持的。
> Spring Security 3.2+ 支持在每个响应上设置 `X-Frame-Options`。默认情况下,Spring Security Java 配置将其设置为 `DENY`。在 3.2 中,Spring Security XML 命名空间默认不设置该标头,但可以配置为这样做。将来,它可能会默认设置该标头。
>
> 有关如何配置 `X-Frame-Options` 标头设置的详细信息,请参阅 Spring Security 文档的 Default Security Headers。你也可以看到 gh-2718 的额外背景。
如果您的应用程序添加了 `X-Frame-Options` 响应标头(应该如此!)并依赖于基于 iframe 的传输,则需要将标头值设置为 `SAMEORIGIN` 或 `ALLOW-FROM `。Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJS 客户端。将此选项配置为使用与应用程序来源相同的 URL 是一个好主意。
以下示例显示了如何在 Java 配置中执行此操作:
```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 客户端开发模式,以防止浏览器缓存原本会缓存的 SockJS 请求(如 iframe)。有关如何启用它的详细信息,请参阅 SockJS 客户端页面。
### 4.3.4 心跳
SockJS 协议要求服务器发送心跳消息,以防止代理得出连接挂起的结论。Spring SockJS 配置有一个名为 `heartbeatTime` 的属性,您可以使用它来自定义频率。默认情况下,心跳会在 25 秒后发送,假设该连接上没有发送其他消息。这个 25 秒的值符合 IETF 针对公共互联网应用的以下建议。
> 在 WebSocket 和 SockJS 上使用 STOMP 时,如果 STOMP 客户端和服务器协商要交换的检测信号,则 SockJS 检测信号将被禁用。
Spring SockJS 支持还允许您配置 `TaskScheduler` 来安排检测信号任务。任务调度程序由线程池支持,默认设置基于可用处理器的数量。您应该考虑根据您的具体需求自定义设置。
### 4.3.5 客户端断连
HTTP 流和 HTTP 长轮询 SockJS 传输要求连接保持打开的时间比平时更长。有关这些技术的概述,请参阅这篇博客文章。
在 Servlet 容器中,这是通过 Servlet 3 异步支持实现的,该异步支持允许退出 Servlet 容器线程,处理请求,并继续从另一个线程写入响应。
一个特定的问题是 Servlet API 不会为已经离开的客户端提供通知。请参阅 eclipse-ee4j/servlet-api#44。但是,Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的检测信号(默认情况下每 25 秒一次),这意味着通常会在该时间段内检测到客户端断开连接(如果消息发送频率更高,则会更早)。
> 因此,由于客户端已断开连接,可能会发生网络 I/O 故障,这可能会在日志中填充不必要的堆栈跟踪。Spring 尽最大努力识别代表客户端断开连接(特定于每台服务器)的此类网络故障,并使用专用日志类别 `DISCONNECTED_CLIENT_LOG_CATEGORY`(在 `AbstractSockJsSession` 中定义)记录最小消息。如果需要查看堆栈跟踪,可以将该日志类别设置为 TRACE。
### 4.3.6 SockJS 和 CORS
如果允许跨域请求(请参阅允许的源),SockJS 协议将使用 CORS 在 XHR 流和轮询传输中提供跨域支持。因此,除非检测到响应中存在 CORS 标头,否则会自动添加 CORS 标头。因此,如果应用程序已经配置为提供 CORS 支持(例如,通过 Servlet Filter),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 年)。
有关确切的实现,请参阅 `AbstractSockJsService` 中的 `addCorsHeaders` 和源代码中的 `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`:
- JSR-356 运行时中的 `StandardWebSocketClient`。
- `JettyWebSocketClient` 通过使用 Jetty 9+ 本地 WebSocket API。
- Spring 的 `WebSocketClient` 的任何实现。
根据定义,`XhrTransport` 同时支持 `xhr-streaming` 和 `xhr-polling`,因为从客户端的角度来看,除了用于连接服务器的 URL 之外,没有其他区别。目前有两种实现方式:
- `RestTemplateXhrTransport` 将 Spring 的 `RestTemplate` 用于 HTTP 请求。
- `JettyXhrTransport` 使用 Jetty 的 `HttpClient` 进行 HTTP 请求。
以下示例显示了如何创建 SockJS 客户端并连接到 SockJS 端点:
```java
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 格式的数组。默认情况下,使用 Jackson 2,并且需要位于类路径上。或者,您可以配置 `SockJsMessageCodec` 的自定义实现,并在 `SockJsClient` 上进行配置。
要使用 SockJsClient 模拟大量并发用户,您需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。以下示例显示了如何使用 Jetty 执行此操作:
```java
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
```
以下示例显示了服务器端 SockJS 相关的财产(有关详细信息,请参阅 javadoc),您还应该考虑自定义这些属性:
```java
@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` 属性设置为 1000(默认值为 100)。
// (3) 将 `disconnectDelay` 属性设置为 30 属性秒(默认值为 5 秒 — 5 * 1000).
```
## 4.4 STOMP
WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,让客户端和服务器协商一个子协议(即更高级别的消息传递协议),在 WebSocket 之上使用该协议来定义每个消息可以发送什么类型的消息、格式是什么、每个消息的内容等等。子协议的使用是可选的,但无论哪种方式,客户端和服务器需要就一些定义消息内容的协议达成一致。
### 4.4.1 总览
STOMP(简单面向文本的消息传递协议)最初是为脚本语言(如 Ruby、Python 和 Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。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` 标头匹配。
上述概述旨在提供对 STOMP 协议的最基本理解。我们建议全面审查协议规范。
### 4.4.2 优势
与使用原始 WebSockets 相比,使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 提供更丰富的编程模型。关于 HTTP 与原始 TCP 以及它如何让 SpringMVC 和其他 web 框架提供丰富的功能,也可以提出同样的观点。以下是好处列表:
- 无需发明自定义消息传递协议和消息格式。
- STOMP 客户端,包括 Spring Framework 中的 Java 客户端,都是可用的。
- 您可以(可选)使用消息代理(如 RabbitMQ、ActiveMQ 和其他)来管理订阅和广播消息。
- 应用程序逻辑可以组织在任意数量的 `@Controller` 实例中,消息可以基于 STOMP 目的地标头路由到它们,而不是使用给定连接的单个 `WebSocketHandler` 处理原始 WebSocket 消息。
- 您可以使用 Spring Security 来保护基于 STOMP 目的地和消息类型的消息。
### 4.4.3 启用 STOMP
`spring-messaging` 和 `spring-websocket` 模块中提供了基于 WebSocket 的 STOMP 支持。一旦有了这些依赖项,就可以通过 SockJS Fallback 在 WebSocket 上公开 STOMP 端点,如下例所示:
```java
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)客户端需要连接到的端点的 HTTP URL,以便进行 WebSocket 握手。
// (2) 目标标头以 `/app` 开头的 STOMP 消息被路由到 `@Controller` 类中的 `@MessageMapping` 方法。
// (3) 使用内置消息代理进行订阅和广播,并将目标标头以 `/topic` 或 `/queue` 开头的消息路由到代理。
```
以下示例显示了与前面示例等效的 XML 配置:
```xml
```
> 对于内置的简单代理,`/topic` 和 `/queue` 前缀没有任何特殊含义。它们只是区分 pub-sub 和点对点消息传递(即,多个订户和一个消费者)的一种惯例。当您使用外部 broker 时,请查看 broker 的 STOMP 页面,以了解它支持什么类型的 STOMP 目的地和前缀。
要从浏览器进行连接,对于 SockJS,您可以使用 SockJS 客户端。对于 STOMP,许多应用程序都使用了 jmesnil/stomp-websocket 库(也称为 stomp.js),该库功能齐全,已在生产中使用多年,但不再维护。目前,JSteunou/webstomp-client 是该库维护最积极、发展最快的继任者。以下示例代码基于此:
```javascript
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
```
或者,如果您通过 WebSocket(不带 SockJS)进行连接,则可以使用以下代码:
```javascript
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
```
请注意,前面示例中的 `stompClient` 不需要指定 `login` 和 `passcode` 头。即使是这样,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参阅连接到 Broker 和身份验证。
有关更多示例代码,请参阅:
- 使用 WebSocket 构建交互式 web 应用程序 — 入门指南。
- 股票投资组合 — 示例应用程序。
### 4.4.4 WebSocket 服务器
要配置底层 WebSocket 服务器,请应用“服务器配置”中的信息。然而,对于 Jetty,您需要通过 `StompEndpointRegistry` 设置 `HandshakeHandler` 和 `WebSocketPolicy`:
```java
@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 信息流