Spring-快速参考指南-全-

Spring 快速参考指南(全)

原文:Spring Quick Reference Guide

协议:CC BY-NC-SA 4.0

一、简介

Spring 已经从 2004 年推出(1.0 版)时的一个小型开源项目一飞冲天,成为今天基于 Java 和 JVM 的项目的一个几乎普遍的需求。什么开始作为一个更轻量级的替代 JEE 已经演变成更多,同时仍然保持基本原则。

尽管一些 Spring 子项目,比如 Spring Roo,并没有变得非常受欢迎,但是许多其他项目已经找到了观众并蓬勃发展。“Spring”名下有大量的项目,帮助开发人员完成从云应用到关系数据库查询等各种工作。

Note

我们已经尽了最大努力来确保本书中的信息是准确的,但是由于 Spring 的复杂性以及 Spring 在过去和未来的许多版本,可能会有一些不准确的地方,例如在编写本书时不存在的新功能,这取决于您正在使用的版本。

谁应该读这本书

这本书是为每一个想了解更多关于 Spring 框架、Spring Boot 和相关技术的 Java 开发人员准备的。这本书涵盖了从基础到一些高级主题的所有内容。历史上不会花太多的话;取而代之的是,现在它将关注于开发应用的有用信息。

无论你是初学者还是经验丰富的 Java 专家,这本书都会有用。

关于这本书

这本书是有组织的,所以它可以按顺序阅读,对于那些不熟悉 Spring 的人来说,或者作为未来许多年的参考。每章将涵盖一个 Spring 项目或核心 Spring 框架,并被分成许多有标题的部分。它将涉及配置 Spring Beans、XML、Java 配置类和组件扫描的所有三种方式,但主要关注后两种方式。

这本书将关注核心概念并提供代码示例。例子将是实际的,来自真实世界的经验。

特别重要的信息将概述如下:

img/498572_1_En_1_Figa_HTML.jpgTips

像这样样式的文本提供了额外的信息,您可能会发现非常有用。

img/498572_1_En_1_Figb_HTML.jpgInfo

这种风格的文本通常会让好奇的读者参考本书之外的其他信息。

img/498572_1_En_1_Figc_HTML.jpgWarnings

诸如此类的文字提醒谨慎的读者注意他们可能遇到的常见问题。

img/498572_1_En_1_Figd_HTML.jpgExercises

这是一个练习。我们在实践中学习得最好,所以尝试这些是很重要的。

二、概览

Spring 最初是作为企业应用(如 J2EE 标准)的一种替代方案而出现的。通过允许配置POJO(普通的旧 Java 对象)而不是强制类扩展某个类或实现某个接口,它使得将框架与代码完全分离成为可能。

Spring 随着时间的推移不断成长和发展,是当今构建应用最流行的 Java 框架。

核心 Spring

Core Spring 包括 Spring 的依赖注入(DI)框架和配置。DI 设计模式是一种将依赖关系的细节具体化的方式,允许它们被注入。这一点,再加上接口的使用,允许您解耦代码,并使软件更易于管理和扩展。DI 是反转控制 (IoC)的一个子集,其中应用的流程被反转或颠倒。

Core Spring 提供了 Spring 容器,主要是接口BeanFactory及其子接口ApplicationContext的实现。ApplicationContext有很多种实现方式,使用哪一种取决于应用的类型。大多数时候,你的应用代码不需要知道BeanFactory或ApplicationContext的具体类型;每个应用只应定义一次。

img/498572_1_En_2_Fig1_HTML.jpg

图 2-1

WebApplicationContext 的简化类图

ApplicationContext 提供了用于访问应用组件的 Bean 工厂方法(从ListableBeanFactory interface)继承而来)、以通用方式加载文件资源的能力、向注册的侦听器发布事件的能力(从ApplicationEventPublisher接口继承而来)、解析支持国际化的消息的能力(从MessageSource接口继承而来)以及从父 ApplicationContext 继承而来的可能性。ApplicationContext 有许多不同的子类,其中一个是 WebApplicationContext,顾名思义,它对 web 应用很有用。

POJO 中的 Beans 可以通过三种方式之一进行配置:XML、在用@ Configuration注释的配置 Java 类中用@ Bean注释的方法,或者在使用组件扫描时,可以在 POJO 类本身上添加一个注释,如@ Component或@ Service。最推荐的方法是对基础设施使用一个或多个 Java 配置类,对业务类使用组件扫描。

Spring 模块

Spring 有许多模块,根据应用的需要,可以包含或不包含这些模块。以下是 Spring 保护伞下的一些模块和项目:

  • 面向切面编程(AOP)——通过运行时代码交织实现横切关注点。

  • spring Security——认证和授权,支持一系列标准、协议、工具和实践的可配置安全性。

  • spring Data——使用 Java 数据库连接(JDBC)、对象关系映射(orm)工具、反应式关系数据库连接(R2DBC)和 NoSQL 数据库,在 Java 平台上使用关系数据库管理系统的模板和工具。

  • 核心——控制容器的反转、应用组件的配置和 Beans 的生命周期管理。

  • 消息传递——注册消息侦听器对象,以实现透明的消息消费,并通过多个传输层(包括 Java 消息服务(JMS)、AMQP、Kafka 等)向/从消息队列发送消息。

  • Spring MVC(模型-视图-控制器)——一个基于 HTTP 和 servlet 的框架,为 web 应用和 RESTful(表述性状态转移)web 服务的扩展和定制提供钩子。

  • 事务管理——统一多个事务管理 API,协调支持 JTA 和 JXA 的事务。

  • 测试——支持编写单元测试和集成测试的类,比如 Spring MVC Test,它支持测试 Spring MVC 应用的控制器。

  • Spring Boot——简化应用开发的配置框架公约。它包括自动配置,并具有“初始”依赖项,包括许多开源依赖项和每个依赖项的兼容版本。

  • spring web flux——一个使用反应流规范的反应式 web 框架,可以在 Netty、Tomcat 或 Jetty 上运行(使用 Servlet 3.0 异步线程)。

img/498572_1_En_2_Fig2_HTML.jpg

图 2-2

Spring 模块

三、依赖注入

依赖注入(DI)是 Spring 的核心。它指的是在运行时在许多不同的对象之间插入引用,或者通过构造函数、设置器,或者甚至使用运行时反射直接到一个字段。这实现了 IOC(控制反转),其中一个类可以使用另一个类的实例,而不知道该对象是如何构造的或其确切的实现类的任何细节。

Spring 的设计允许使用 POJOs(普通旧 Java 对象)。换句话说,你不需要实现一个特定的接口或者扩展一个类来使用 Spring 的 DI。由 Spring 配置的类的实例被称为 Spring Bean ,或者有时简称为 bean 。

退耦

例如,您可以在 Spring Bean 上用@ Autowired注释 setter 或字段,Spring 将在运行时找到与该字段或 setter 最匹配的类。默认情况下,它将搜索与该类型匹配的类。如果它找不到匹配的 bean 或者有不止一个可能的匹配(在考虑任何@ Qualifier注释和名称之后),Spring 将抛出一个异常,无法启动。

您应该使用接口来进一步分离不同的类。这样,不同的组件可以独立测试,而不依赖于其他组件的实现。企业应用中的紧密耦合会导致脆弱的代码,并且很难在不破坏任何东西的情况下进行更改。

您可以使用@ Qualifier指定一个实例的特定名称,以帮助@ Autowired在可能存在同一个类或接口的多个实例时找到正确的实例。我们将在下一节展示一个这样的例子。

配置

可以用三种方式之一配置 Bean:XML,一个用@Configuration注释的配置 Java 类和用@Bean注释的方法,或者在 Bean 类本身上用一个像@Component这样的注释。最推荐的方法是使用一个或多个 Java 配置类。

用@Configuration注释的配置 Java 类可能如下所示:

@Configuration
public class Configuration {
  @Bean
  public MyService myService() {
    return new MyActualService();
  }

这个配置创建了配置类本身的一个 bean 实例和实现了MyService接口的类MyActualService的一个名为myService的 bean 实例(来自用@Bean标注的方法)。

任何配置类都必须是非最终的和非局部的(公共的),并且有一个无参数构造函数。默认情况下,Spring 使用 CGLIB 代理该类,以便实施 Spring bean 依赖规则(这就是该类不能是 final 的原因)。例如,这允许方法调用总是返回单例 Bean 实例,而不是每次都创建一个新实例。如果不需要这种行为,可以像下面这样提供proxyBeanMethods=false:

@Configuration(proxyBeanMethods = false)

img/498572_1_En_3_Figa_HTML.jpg默认范围是“singleton”,这意味着应用将存在一个类实例或“singleton”。web 应用中还存在其他作用域,如“应用”、“请求”和“会话”。“原型”范围意味着每次请求时都会为 bean 创建一个新的实例。可以使用@Scope注释来改变 bean 的作用域。例如,@Scope("prototype") @Bean public MyService myService() {...}

用@Bean标注的方法的每个参数都将被 Spring 自动连接(使用适用于@Autowired 的相同规则)。例如,在以下配置中,service2 bean 将连接到 myService bean:

@Configuration
public class Configuration {
  @Bean
  public MyService myService() {
    return new MyActualService();
  }
  @Bean
  public OtherService service2(final MyService myService) {
    return new MyActualOtherService(myService);
  }

Listing 3-1Configuration.java

默认情况下,Spring 使用方法名作为 Bean 的名称。因此,前面的示例创建了一个名为“myService”的 Bean 和一个名为“service2”的 Bean。您可以通过向@Bean 注释提供一个值来覆盖它(如@Bean("myname"))。

使用@Qualifier,“service 2”方法可以重写如下(结果相同):

  @Bean
  public OtherService service2(@Qualifier("myService") MyService s) {
    return new MyActualOtherService(s);
  }

这样,即使存在多个实现 MyService 的 beans,Spring 也会知道选择名为“myService”的那个。

img/498572_1_En_3_Figb_HTML.jpg您还可以将一个 bean 配置为具有多个名称。例如,使用@Bean(name={"myname1", "myname2"})将在两个名称下注册同一个 bean,myname1 和 myname2。

应用上下文

ApplicationContext 是直接公开所有由 Spring 配置的 beans 的接口。

根据应用的类型,它有不同的具体类。例如,web 应用将具有 WebApplicationContext 的实现。

组件扫描

您可以在 Spring 中使用组件扫描来扫描类声明中的某些注释。那些注解是@Component、@Controller、@Service和@Repository(和@Configuration)。如果找到它们,Spring 会将 POJO 初始化为 Spring Bean。

可以通过 XML 配置组件扫描,如下所示:

<context:component-scan base-package="com.example"/>

或者在这样的配置类中:

@Configuration
@ComponentScan("com.example")
public class Configuration {

Listing 3-2Configuration.java

在这些示例中,将扫描“com.example”包及其所有子包中的 Spring 注释来创建 beans。注意不要扫描太多的类,因为这会降低初始化时间。

导入

您可以使用@Import导入其他配置文件。使用@ComponentScan也可以用来扫描配置类(标有@Configuration的类)。

如果您真的需要,您也可以使用一个@ImportResource注释来加载 XML 配置文件,例如:

@Import({WebConfig.class, ServiceConfig.class})
@ImportResource("dao.xml")

这将导入WebConfig和ServiceConfig配置类以及dao.xml Spring 配置文件(参见下一章了解更多关于 XML 的内容)。

怠惰

默认情况下,Beans 是被急切地创建的——这意味着 Spring 会在启动时实例化它们并连接它们。这样可以更快地发现任何潜在的问题。如果您不希望 Bean 在必要时才加载(当使用 application context . get Bean(String)方法请求或由(例如)autowiring 请求时),可以使用@Lazy 注释使 Bean 延迟加载。

关闭应用上下文

在 web 应用中,Spring 已经优雅地关闭了 ApplicationContext。但是,在非 web 应用中,您需要注册一个关闭挂钩。

public static void main(final String[] args) throws Exception {
  AbstractApplicationContext ctx
  = new ClassPathXmlApplicationContext(new String []{"beans.xml"});
  // add a shutdown hook for the above context...
  ctx.registerShutdownHook();
  // app runs here...
}

Listing 3-3App.java

这样,当应用退出时,Spring 将优雅地关闭。

BeanFactoryPostProcessors

可以实现 BeanFactoryPostProcessor 接口,以便在创建 bean(所有其他 bean)之前更改 bean 配置。例如,这对于添加定制配置很有用(尽管 Spring 自己处理大多数有用的情况)。BeanFactoryPostProcessor 接口有一个方法来定义,postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory). Spring 自动检测实现这个接口的 beans。

BeanPostProcessors

一个ApplicationContext还自动检测它接收到的实现BeanPostProcessor接口的配置元数据中定义的任何 beans。这些 bean 是特殊的,因为它们是与ApplicationContext同时创建的,并且在任何其他 bean 之前创建,因此它们可以处理其他 bean 定义。

org.springframework.beans.factory.config.BeanPostProcessor接口正好由两个回调方法组成:

Object postProcessBeforeInitialization(Object bean, String beanName)
  throws BeansException

Object postProcessAfterInitialization(Object bean, String beanName)
  throws BeansException

我们将在后面介绍的 Spring AOP 是使用 BeanPostProcessor 接口实现的。它可以用该 bean 的代理替换每个 bean。

初始化和销毁方法

您可以在 Spring 中使用 commannotationbeanpostprocessor 来启用@ PostConstruct和@ PreDestroy这样的 JSR-250 注释。它由组件扫描激活,但也可以在 Spring 配置中直接激活。

另一种方法是使用 Spring 的内置配置。例如,Bean 注释,@Bean(initMethod = "up", destroyMethod = "down")会导致 Spring 在初始化类之后调用“up ”,并在销毁它之前注入所有依赖项和“down”。

性能

默认情况下,Spring Boot 将从名为 application.properties(对于标准属性)或 application.yml(对于 YAML 格式的属性)的文件中加载属性。

可以使用@PropertySource注释将附加属性加载到环境中。

例如,以下代码从/com/acme/目录下的类路径中加载名为 app.properties 的属性文件:

@Configuration
@PropertySource("classpath:/com/acme/app.properties")
public class AppConfig {
  //configuration code...
}

Listing 3-4AppConfig.java

然后,您可以使用环境中的属性,并使用@Value注释注入它们:

@Value("${bean.name}") String beanName;
@Bean
public MyBean myBean() {
  return new MyBean(beanName);
}

名为 app.properties 的文件可能具有以下值:

bean.name=Bob

这将把“Bob”注入前面提到的 beanName 字段。

环境

使用@Value 注释的替代方法是使用org.springframework.core.env.Environment类。它可以自动连接到任何类中(例如,使用@Autowired)。它有以下方法用于在运行时访问已定义的属性:

  • String getProperty(String key)–获取给定属性键的值,如果未解析,则为 null

  • String getProperty(String key,String defaultValue)-获取给定属性键的值,如果找不到,则获取给定的 default value

  • String getRequiredProperty(String key)–获取给定属性键的值,如果未找到,则抛出 IllegalStateException

轮廓

Spring 概要文件允许您配置不同的属性,甚至根据活动概要文件在运行时初始化 Beans。当将同一个应用部署到不同的环境时,例如“试运行”、“测试”和“生产”,它们会很有用您可以拥有任意数量、任意名称的配置文件。

您可以使用spring.profiles.active系统属性或 spring_profiles_active 环境变量将当前配置文件设置为活动的。您可以激活任意数量的配置文件(用逗号分隔)。

@Profile注释可以注释一个@Component bean 类(或者原型注释、@Service, @Repository和@Controller)或者一个@Bean注释的方法,甚至一个@Configuration注释的配置类。

例如,下面的配置类定义了两个不同的数据库。哪个是激活的取决于激活的配置文件。

@Configuration
public class ProfileDatabaseConfig {

  @Bean("dataSource")
  @Profile("development")
  public DataSource embeddedDatabase() { ... }

  @Bean("dataSource")
  @Profile("production")
  public DataSource productionDatabase() { ... }
}

Listing 3-5ProfileDatabaseConfig.java

确保为配置类中的每个@Bean 方法使用不同的名称,即使这些 Bean 被标记为不同的概要文件。否则,您可能会从 Spring 获得意外的行为,因为它使用方法名称作为 bean 名称。

拼写

什么是 Spring 表达式语言(SpEL)?Spring 表达式语言(简称 SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。

可以使用带有#{}语法的@Value注释来注入 SpEL。与只被解释为环境属性的使用${}不同,使用#{}允许您使用嵌入式语言(SpEL)的全部表达能力。

@Value("#{ T(java.lang.Math).random() * 100.0 }")
int randomNumber;

T 语法用于引用 Java 类型(前面的 java.lang.Math 类)。

您也可以使用内置变量systemProperties来引用系统属性:

@Value("#{ systemProperties['user.region'] }")
String region;

SpEL 还有 Elvis 操作符和安全导航器(很像 Kotlin、Groovy 和其他语言),例如:

@Value("#{systemProperties['pop3.port'] ?: 25}")

如果没有给pop3.port.赋值,默认为 25

您也可以使用单引号指定字符串文字,例如:

@Value("#{ 'Hello '.concat('World!') }")
String hello;

这将导致 hello 的值为“Hello World!”。

SpEL 对于 Spring Security 注释也很有用,我们将在下一章中介绍。

测试

作为 Spring-test 的一部分,spring 提供了测试支持。对于 JUnit 4 测试,您可以使用 Spring 的 SpringRunner 和@ContextConfiguration注释来指定如何为 JUnit 单元或集成测试创建 ApplicationContext,例如:

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
public class MyTest {
// class body...
}

Listing 3-6MyTest.java

JUnit 5 测试与此类似,但是使用了@ExtendWith(SpringExtension.class)而不是@RunWith:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
public class MyTest5 {
// class body...
}

Listing 3-7MyTest5.java

编写一个包含 JUnit 测试的 Spring 应用。

四、XML 配置

Spring 配置可以通过 XML 完成。事实上,在引入 Java 配置之前,这是配置 Spring beans 的唯一方法。我们将介绍一些 Spring XML 作为参考资料,并在遗留应用中使用。

可扩展置标语言

XML 标准由基本语法、名称空间和 XML 模式定义组成。简而言之,语法是基于元素和名称的,通常是小写,用大于号和小于号包围(比如);可以在这些符号中设置并使用双引号的属性(如)。

为了清楚起见,让我们看看任何 Spring 配置 XML 文件中常见的前三行,并分析它们的含义:

  1. <?xml...声明这是一个 XML 文件。

  2. <beans是根元素(包装整个文档的元素),xmlns:= " ... "声明根命名空间。例如,这允许您在不指定名称空间的情况下引用。

  3. xmlns:xsi=声明代表 XML 模式实例的“xsi”名称空间。这允许文档随后使用xsi:schemaLocation=来定义在哪里定位相应的 XML 模式。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

Spring XML 配置组织

为了使事情更有条理、更容易理解,在大中型应用中使用多个 XML 文件并在它们之间划分配置是有意义的。您可以决定用许多不同的方式来分离文件:水平切片(控件、服务和存储库或 Dao(数据访问对象))、垂直切片(按特性)或按功能(web 服务、前端和后端)。

XML 应用上下文

要开始使用,请使用以下应用上下文之一:

对于ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,您需要指定 XML 文件。

类路径

例如,这里有一个应用入口类 App,它使用了一个ClassPathXmlApplicationContext:

package com.apress.spring_quick.di;

import com.apress.spring_quick.config.AppSpringConfig;
import com.apress.spring_quick.di.model.Message;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {

    public static void main(String[] args) {
        final ApplicationContext applicationContext =
          new ClassPathXmlApplicationContext("classpath:/application.xml");
        final MyBeanInterface myBean = applicationContext
             .getBean(MyBeanInterface.class);
        //...
    }
}

Listing 4-1App.java

在这个例子中,"classpath:/application.xml"指的是类路径根目录下名为application. xml的文件(通常包含在例如 JAR 文件中)。在典型的构建中,您应该将这个文件放在src/main/resources/目录中,Maven 或 Gradle 会在构建过程中自动将它添加到 JAR 文件中。尽管这里我们提供了一个文件,但是也可以使用多个 XML 文件。

网

对于XmlWebApplicationContext,根上下文(可能是多个 servlet 上下文的父上下文的应用上下文)的默认位置是“/WEB-INF/applicationContext.xml”,对于名称空间为“ -servlet”的上下文,默认位置是“/WEB-INF/<name>-servlet.xml”。例如,对于 Servlet 名称为“products”的 DispatcherServlet 实例,它将查找“/WEB-INF/products-servlet.xml”。

XML Beans

在 Spring XML 中最基本的事情是创建 beans。下面是一个关于application.xml文件的例子:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations"
              value="classpath:db/datasource.properties"/>
    </bean>
    <bean id="dataSource1"
       class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${db.driverClassName}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
    </bean>
</beans>

Listing 4-2application.xml

这个例子展示了如何定义 Spring beans 并在这些 bean 上设置属性。注意,您可以使用\({}语法引用属性,比如`\){db.driverClassName}如何引用db.driverClassName`属性。

初始化并销毁

initialize 方法(在 Spring 实例化并解析了 bean 上的所有依赖项之后立即调用)可以通过设置 bean 定义上的init-method属性来配置,如下面的 XML 配置所示:

<bean name="userService"
  class="com.apress.spring_quick.service.UserService"
  init-method="doInitialization" />

destroy 方法(在 Spring 丢弃 Spring bean 之前调用)可以通过设置destroy-method属性来配置,如下面的 XML 配置所示:

<bean name="userService"
  class="com.apress.spring_quick.service.UserService"
  destroy-method="doCleanup" />

当 bean 被销毁时,这可以用来删除任何不再需要的资源。这将调用如下定义的方法:

public void doCleanup() {
 // do clean up
}

init-method 和 destroy-method 都应该是公共的,并将 void 作为返回类型。

启用 AOP

在 XML 中,在与应用方面的对象相同的应用上下文中使用<aop:aspectj-autoproxy>(特别是在典型的 Spring Web MVC 应用 applicationContext.xml 和...-servlet.xml)。

AOP 配置

以下示例 XML 配置使用 Spring AOP 和 Spring Retry 1 项目来重复对名为remoteCall in any class or interface ending with "Service"的方法的服务调用:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
   xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xmlns:aop = "http://www.springframework.org/schema/aop"
   xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/aop
   http://www.springframework.org/schema/aop/spring-aop.xsd">

<aop:config>
    <aop:pointcut id="remote"
        expression="execution(* com..*Service.remoteCall(..))" />
    <aop:advisor pointcut-ref="remote"
        advice-ref="retryAdvice" />
</aop:config>

<bean id="retryAdvice"
class="org.springframework.retry.interceptor.RetryOperationsInterceptor"
/>
<!-- other bean definitions... -->

</beans>

注意,切入点引用了之前定义的名为“remote”的切入点。更多详情请参见第五章。

启用 Spring Data JPA

Spring Data JPA 允许您使用 ORM(对象关系映射)与数据库进行交互,比如 Hibernate 或 EclipseLink。要在 XML 中启用 Spring Data JPA,请使用以下 XML:

<beans xmlns:="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:jpa="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <jpa:repositories base-package="com.acme.repositories"/>
</beans>

这将扫描“com.acme.repositories”包及其下面的任何 JPA 存储库。更多信息参见第六章。

混合 XML 和 Java 配置

没有理由不能混合使用 XML 配置和 Java 配置。事实上,您可以从 XML 激活 Java 配置,并从 Java 导入 XML 配置文件。

例如,下面的 Spring XML 文件支持 Java 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.apress.spring.config" />

</beans>

这个 XML 在“com.apress.spring.config”包和任何子包中开始组件扫描。任何标有@ Configuration、@ Component或许多其他注释的文件都会被 Spring 拾取。

从 Spring Java 配置文件中,您可以使用@ ImportResource来导入 Spring XML 文件,例如:

@Configuration
@ImportResource( { "spring-context1.xml", "spring-context2.xml" } )
public class ConfigClass { }

如果在 Spring 应用中作为配置启用(通过组件扫描或其他方式),这个类将使 Spring 读取两个文件“spring-context1.xml”和“spring-context2.xml”,作为 Spring XML 配置。

img/498572_1_En_4_Figa_HTML.jpgExercise: Use Both XML and JAVA CONFIG

创建一个新的应用,同时使用 Spring XML 和 Spring Java 配置。尝试将它们以不同的方式结合起来。

五、面向切面编程

AOP 代表面向切面编程。AOP 允许您解决横切关注点,例如日志记录、事务管理、安全性和缓存,而无需一遍又一遍地重复相同的代码。它允许你应用干(不要重复自己)原则。

Spring 在很多方面使用 AOP 本身,但也直接向开发人员公开工具。

简而言之,您通过定义切入点(添加额外特性的地方)和通知(您正在添加的特性)来使用 Spring AOP。

Spring 创建了两种类型的代理,要么是 JDK 1 (当实现一个接口时,这是内置到 JDK 中的)要么是 CGLIB 2 (当没有接口时操作字节码是必要的)。Final 类或方法不能被代理,因为它们不能被扩展。此外,由于代理实现,Spring AOP 只适用于 Spring Beans 上的公共、非静态方法。

术语

Spring AOP 使用以下术语:

  • 方面——作为横切关注点的关注点的模块化。

  • 连接点——它是方法执行过程中的一个点。

  • 建议——一个方面在连接点采取的行动。

  • 切入点——匹配一个或多个连接点的谓词称为切入点。

  • 编织——向切入点添加建议的过程。

  • 简介–为类型定义额外的方法字段。

  • 目标对象——那些被方面建议的对象是目标对象。

  • AOP 代理 Spring AOP 创建的对象,用于满足方面契约。它在应该应用通知的地方执行通知,并委托给被代理的对象(目标对象)。

建议

有五种类型的建议(每种都有相应的注释):

  1. before–在方法执行之前运行

  2. after–总是在方法执行后运行,不管结果如何(类似于 Java 中的关键字finally)

  3. after throwing–仅在方法引发异常时运行

  4. after returning–仅在方法返回值并且可以使用该值时运行

  5. around——包装方法的执行,并给出一个类型为ProceedingJoinPoint的参数,您必须调用proceed()才能实际调用包装的方法

如何启用 AOP

您可以在配置中使用@EnableAspectJAutoProxy注释来启用 Spring AOP。

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

每个方面类都应该用@Aspect注释进行注释。在该类中,您可以指定切入点和通知。还应该用@Component对其进行注释,以便通过注释扫描拾取(或者用另一种方式配置为 Spring bean)。

如何定义切入点

您使用一个切入点表达式(由 AspectJ 项目定义的表达式的子集)来定义一个切入点。在使用注释的方面中,您在一个空方法上使用@Pointcut注释,切入点的名称就是方法的名称,例如:

@Pointcut("execution(* save(..))")
private void dataSave() {}

这里,切入点的名称是“dataSave()”。切入点方法的返回类型必须是void。它可以是任何可见度。

在这个例子中,切入点是execution(* save(..)),它指的是在任何类上执行任何名为save的方法。第一个*是通配符(匹配所有内容),指的是方法的返回类型。..是指方法参数,表示“零到多个参数”,而“保存”是方法名称。切入点方法本身(dataSave)不需要任何代码。通过使用切入点的名称注释另一个方法来使用切入点,例如,假设保存方法返回值或抛出异常:

@AfterReturning(value = "dataSave()", returning = "entity")
public void logSave(JoinPoint jp, Object entity) throws Throwable {
  // log the entity here
}
@AfterThrowing(pointcut = "dataSave()", throwing = "ex")
public void doAfterThrowing(Exception ex) {
  // you can intercept thrown exception here.
}
@Around("execution(* save(..))")
public Object aroundSave(ProceedingJoinPoint jp) throws Throwable {
  return jp.proceed();
}

Listing 5-1Advice examples

如前面的方法所示,aroundSave通知也可以直接声明切入点表达式。

Spring AOP 中的切入点表达式可以使用关键字execution和within以及& &、||、和!(与,或,非)。这里还有一些例子:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz..*)")
private void inXyz() {}

@Pointcut("anyPublicOperation() && inXyz()")
private void xyzOperation() {}

Listing 5-2Pointcut examples

切入点指的是每一个公共方法。

inXyz切入点指的是 com.xyz 包中每个类的每个方法。

xyzOperation切入点结合了另外两个切入点,意味着 com.xyz 包中的每个公共方法。

释文

您也可以使用@target, @annotation或者直接使用注释在切入点表达式中指定注释,例如:

@Pointcut("@target(org.springframework.stereotype.Repository)")
public void allRepositories() {}
// use an annotation on each actual method to advise:
@Pointcut("@annotation(com.apress.spring_quick.aop.LogMe)")
public void logMes() {}

在前面的例子中,第一个切入点应用于用@Repository注释的目标类,第二个切入点引用用@LogMe注释的方法。您也可以使用@args 来指定方法的参数具有特定的注释。

切入点表达式

Spring AOP 切入点表达式支持很多关键字。尽管其中一些表达式匹配 Spring AOP 中的相同连接点,但是它们可以用于不同的绑定(我们将在接下来讨论)。作为参考,这里有许多可能的表达方式:

| 执行(公开* *(..)) | 每个公共方法。 | | 执行(* set*(..)) | 名称以“set”开头的每个方法。 | | 执行(* com . XYZ . service . accountservice . *(..)) | AccountService 接口中定义的每个方法。 | | 执行(* com.xyz.service.*)。*(..)) | “com.xyz.service”包中每个类或接口中定义的每个非私有方法。 | | 执行(* com.xyz.service..*.*(..)) | 在“com.xyz.service”包和子包中的每个类或接口中定义的每个非私有方法。 | | 在(com.xyz.service.*)内 | “com.xyz.service”包的每个连接点。 | | 在(com.xyz.service..*) | “com.xyz.service”包和子包的每个连接点。 | | this(com . XYZ . service . accountservice) | 代理实现 com.xyz.service.AccountService 接口的每个连接点。 | | 目标(com.xyz.service.AccountService) | 目标对象实现 com.xyz.service.AccountService 接口的每个连接点。 | | args(java.io.Serializable) | 任何在运行时有一个类型为 java.io.Serializable 的参数的方法。 | | @ target(org . spring framework . transaction . annotation . transactional) | 目标对象用@Transactional 注释的任何连接点。 | | @ within(org . spring framework . transaction . annotation . transactional) | 目标对象的声明类型用@Transactional 注释的任何连接点。 | | @ annotation(org . spring framework . transaction . annotation . transactional) | 任何用@Transactional 注释的方法。 | | @ args(com . XYZ . security . my annotation) | 用 com.xyz.security.MyAnnotation 批注了单个参数的任何方法。 | | 执行(@com.xyz.security.LogMe void *(..)) | 任何用@com.xyz.security.LogMe 注释并具有`void`返回类型的方法。 |

Spring AOP 中的绑定

任何通知方法都可以声明一个类型为org.aspectj.lang.JoinPoint的参数作为它的第一个参数(请注意,around advice 是要求声明类型为ProceedingJoinPoint的第一个参数,它是JoinPoint)的子接口)。JoinPoint接口提供了许多有用的方法,如下所示:

| `getArgs`() | 返回方法参数。 | | `getThis`() | 返回代理对象。 | | `getSignature`() | 返回所建议方法的说明。 | | `toString`() | 返回所建议方法的有用描述。 | | `getTarget`() | 返回目标对象。 |

您还可以使用切入点表达式将参数传递给通知,以使用args、this、@ annotation或其他关键字链接参数,例如:

@Before("dataSave() && args(course,..)")
public void validateCourse(Course course) {
  // ...
}

这有两个目的:它验证第一个参数的类型为 Course,并向 advice 方法提供该参数。

Spring AOP 的局限性

Spring AOP 只能在 Spring Beans 上建议公共的、非静态的方法。

用代理编织有一些限制。例如,如果在同一个类中有一个从一个方法到另一个方法的内部方法调用,那么对于这个内部方法调用,通知将永远不会被执行。请参见“Spring AOP 代理”图,了解这一概念的说明。

img/498572_1_En_5_Fig1_HTML.jpg

图 5-1

Spring AOP 代理

在此图中,当 method()调用类内部的 methodTwo()时,它从不调用 ProxyClass,因此不会调用 AOP 通知。然而,任何外部类(其他 Spring beans)都将调用应用任何 before、after 或 around 逻辑的代理方法。

可供选择的事物

如果需要额外的功能,可以直接使用 AspectJ。此外,像 Lombok 3 这样的项目提供了用于不同目的的字节码操作,比如创建不可变的数据类,我们将在后续章节中探讨。

六、Spring Data

Spring Data 的任务是为数据访问提供一个熟悉的、一致的基于 Spring 的编程模型,同时仍然保留底层数据存储的特性。

Spring Data 支持各种类型的数据存储,包括关系型和非关系型(SQL 和 NoSQL),从 JPA (Hibernate 或 TopLink)到 Apache Cassandra,从 MongoDB 到 Redis。在本章中,我们将探索 Spring Data JPA、JDBC 和 R2DBC,以及它们如何支持关系数据库,如 MariaDB、Oracle 或 PostgreSQL。

域实体

要开始任何 Spring Data 项目,我们需要定义项目所依赖的域类或实体。这些是映射到我们的数据库表的域对象,并且高度依赖于应用的业务域。

Spring Data 与“javax.persistence”包下的标准注释集成得很好。对于本章,假设客户实体类定义如下:

import javax.persistence.*;
import lombok.*;

@Data
@Entity
@RequiredArgsConstructor
@Table("customers")
public class Customer {

  private @GeneratedValue @Id Long id;
  private final String firstname;
  @Column("surname")
  private final String lastname;

}

为了将 Spring Data 的这个类标记为一个实体类,@Entity注释是必要的。@Id注释标记了表示表的主键的字段。像@Table和@Column这样的可选注释可以用来分别使用与字段名类别不匹配的名称。注意,我们使用 Lombok 的@Data和@RequiredArgsConstructor来消除对重复代码的需求,比如 getters、setters、toString、equals、hashCode 和 constructors。在这种情况下,因为 firstname 和 lastname 是 final,@RequiredArgsConstructor创建一个构造函数,从两个参数中设置这两个值。

此外,我们将使用以下课程:

import org.springframework.data.jpa.domain.AbstractPersistable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.NamedQuery;

@Entity
@NamedQuery(name = "Course.findByTheName",
    query = "from Course c where c.name = ?1")
public class Course extends AbstractPersistable<Long> {

    @Column(unique = true)
    private String name;
    private String subtitle;
    private String description;

    public Course() { this(null); }

    public Course(Long id) {
        this.setId(id);
    }
  // getters and setters omitted
}

Listing 6-1Course.java

注意,我们在这里使用@NamedQuery直接在实体类上定义定制查询。我们还使用@Column(unique = true)来指定 name 列是惟一的。此外,通过扩展AbstractPersistable<Long>,我们继承了一个 Long 类型的 Id 属性,并将该类表示为一个针对 Spring Data 的数据库持久化实体。这是可选的。

Course 和 Customer 故意以不同的方式定义,以展示在 Spring Data 中定义域实体的一些可能的方法。

这本书会经常提到拥有课程和客户的在线学习应用领域。

JDBC Spring Data

Spring Data JDBC 类似于 Spring Data JPA,建立在许多相同的抽象之上,除了 Spring Data JDBC

  • 没有延迟加载

  • 没有内置缓存

  • 更简单

  • 使用聚合根的概念

  • 支持仅将查询手动定义为@Query注释中的字符串,而不是通过方法名

聚合根是一个根实体,当您保存它时,它也保存它的所有引用,并且在编辑时,它的所有引用将被删除并重新插入。

入门指南

首先,在项目中包含 spring-data-jdbc jar 依赖项。

在 Maven 项目中,将以下内容放在依赖项下:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jdbc</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>

或者在梯度构建中,在依赖项下包括以下内容:

implementation 'org.springframework.data:spring-data-jdbc:2.0.1.RELEASE'

使用@EnableJdbcRepositories注释使它们能够使用 Java 配置,例如,使用名为 CategoryConfiguration 的 Java 配置类:

@Configuration
@EnableJdbcRepositories
public class CategoryConfiguration {}

定义存储库

CRUD 代表“创建、读取、更新、删除”在 Spring Data 中,CrudRepository<T,ID>接口提供了与持久数据存储交互的内置方法,比如关系数据库。

要定义您的存储库,创建一个接口并用定义的通用类型扩展CrudRepository<T,ID>,其中 T 是您的实体类的类型,ID 是它的标识符的类型。Spring 将自动为您实现存储库,包括以下方法(这不是一个详尽的列表):

  • S save(S entity)-将实体保存到数据库

  • find all()–返回所有这些内容

  • S findById(ID)

  • 计数()

  • 删除(T)

  • existing sbyid(id)

自定义查询

当使用 Spring Data JDBC 并且您需要定制查询,或者使用 Spring Data JPA 并且内置 Spring Data 约定不能满足需求时,您可以使用@Query注释指定定制 SQL(或者当使用 Spring Data JPA 时指定 JPQL)查询,例如:

@Query("SELECT * FROM customer WHERE lastname = :lastname")
List<Customer> findAllByLastname(@Param("lastname") String lastname);

@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Customer findFirstByLastname(String lastname);

findAllByLastname 查询将按姓氏查找所有客户实体。Spring Data JDBC 只支持命名参数(如前面的:lastname),而 Spring Data JPA 也支持索引参数(如前面的?1)。@Param注释告诉 Spring Data 查询参数的名称。

img/498572_1_En_6_Figa_HTML.jpg Spring 支持基于-parameters编译器标志的 Java 8 及以上版本的参数名发现。通过在您的构建中使用这个标志(Spring Boot 为您处理),您可以省略命名参数的@Param注释。

您还可以定义修改语句,如下所示:

@Query("delete from Customer c where c.active = false")
void deleteInactiveCustomers();

JPA 中的自定义查询

您还可以使用基本语法定义方法签名,Spring 将在 Spring Data JPA 中实现它们。例子如下:

  • find byx–根据一个或多个给定值查找一个实体;x 是一个条件(我们将讨论什么类型的条件是允许的)。

  • findByFirstname(字符串名称)-在本例中,Firstname 是要搜索的属性。

  • findByFirstnameAndLastname–可以使用“And”、“Or”和“Not”。

  • 排序

  • findAllByX–查找符合条件的所有记录。

  • countryx

  • find topn–仅返回前 N 条记录。

情况

以下是自定义查询方法表达式中允许的条件示例:

  • 支持 Is 或 Equals,但默认情况下也是隐含的。

  • IdGreaterThan(Long num) –其中 id 大于给定的 num。

  • IdLessThan(Long num) –其中 id 小于给定的 num。

  • DateLessThan(Date d) –当日期小于给定日期时,d。

  • DateGreaterThan(Date d) –当日期大于给定日期时,d

  • DateBetween(Date d1, Date d2) –其中日期大于等于 d1 且小于等于 d2。

  • 类似于 LessThan 和 GreaterThan 的工作,但只用于日期。

  • NameLike(String string) –其中 name 就像给定值,string。

  • NameStartingWith(String string) –其中 name 以给定值开始,string。

  • NameEndingWith(String string) –其中 name 以给定值结束,string。

  • NameContaining(String string) –其中 name 包含给定的字符串。

  • NameIgnoreCase(String string) –其中名称等于给定值,忽略大小写(不区分大小写的匹配)。

  • 其中 age 匹配给定集合中的任何值。

  • AgeNotIn(Collection<Long> ages) –年龄与给定集合中的任何值都不匹配。

使用

为了更直接地连接到数据库,您可以使用org.springframework.jdbc.core.JdbcTemplate<T>。

  • 这个方法接受一个 SQL 查询,任意数量的参数作为一个对象数组,以及一个为每行结果调用的回调函数。

  • 这个方法与前一个方法相同,只是它采用了一个 RowMapper <T>,将行转换成 POJO 并返回这些 POJO 的列表。

Spring Data JPA

您可以通过 Java 或 XML 启用 Spring Data 存储库代理创建,例如:

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
@Configuration
class DataConfig { //... }

Listing 6-2DataConfig.java

然后,Spring 将在运行时自动创建所有声明的存储库接口的代理实例(在 DataConfig 类的包下)。前面的@EnableJpaRepositories注释将启用 JPA 还有其他类似@ EnableMongoRepositories的口味。

要在 Spring Data 中启用 XML 格式的 JPA:

<beans xmlns:="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:jpa="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

  <jpa:repositories base-package="com.acme.repositories"/>
</beans>

分页和排序

您可以创建一个存储库接口并扩展CrudRepository<T,ID>,Spring 将为您生成来自CrudRepository<T,ID>的内置方法的实现,以及您使用 Spring Data 的命名约定定义的自定义查询方法。

例如,下面的接口扩展了CrudRepository<T,ID>并添加了一个通过姓氏查找客户实体的方法:

@Repository
public interface PersonRepository extends CrudRepository<Customer, Long> {
  List<Customer> findByLastname(String lastname);
  // additional custom query methods go here
}

Listing 6-3PersonRepository.java

这里没有必要使用@Repository注释,但是您可能希望添加它来提醒每个人这个接口是代理的,并且代理是作为 Spring bean 存在的。

在CrudRepository<T,ID>之上,有一个PagingAndSortingRepository<T,ID>抽象,它添加了额外的方法来简化对实体的分页访问。它看起来像下面这样:

public interface PagingAndSortingRepository<T, ID>
        extends CrudRepository<T, ID> {
  Iterable<T> findAll(Sort sort);
  Page<T> findAll(Pageable pageable);
}

您还可以向自定义方法添加排序或可分页参数,以实现结果的排序和分页。

处理

事务是工作的原子单位——通常是在数据库上——它们要么完全完成,要么在出现故障时完全回滚(取消),并且可以包含任意数量的语句。Spring 可以帮助以编程方式或通过注释处理来处理事务——后者是首选。

首先,确保包含项目所需的依赖项,然后启用 Spring 的事务注释处理。请注意,在 XML 中您可以使用<tx:annotation-driven/>或者在 Java 中使用@EnableTransactionManagement来让基于注释的配置工作。

然后你可以用@ Transactional来注释一个方法(或者类)。当注释一个类时,该类的所有方法都将继承那些事务设置。它将使用您的类的 Spring 代理将每个方法包装在一个事务中。

使用代理的另一个后果是,方法只有在外部调用时才被包装在事务中。换句话说,如果一个类的一个方法直接调用同一个类中带有@ Transactional的另一个方法,它将不会调用代理,因此事务将不会被启动(或者根据注释设置进行处理)。

你也可以注释一个接口来影响每个方法;然而,Spring 团队并不建议这样做,因为只有当代理直接实现接口时,它才会起作用。

事务可以被赋予以秒为单位的超时。它们也可以标记为“只读”,并具有不同的隔离级别、传播设置和其他不同的事务设置。

例如,下面是一个带注释的查询方法定义:

@Transactional(timeout = 10, readOnly = true,
    propagation = Propagation.REQUIRES_NEW)
Customer findByBirthdateAndLastname(LocalDate date, String lastname);

这将有十秒钟的超时。只读限定符为 JDBC 驱动程序提供了提示,可能会提高性能,但行为取决于驱动程序。传播被设置为 REQUIRES_NEW,这将在下面解释。

img/498572_1_En_6_Figc_HTML.jpg交易默认只对未勾选的异常进行回退。您可以通过设置@ Transactional批注的 rollbackFor 属性来更改这一点。

可用的不同传播设置如下:

  • REQUIRED–如果在没有事务的情况下调用该方法,则加入一个活动的事务或启动一个新的事务(这是默认行为)。

  • 支持–如果存在活动事务,则加入活动事务,否则不加入事务上下文。

  • MANDATORY–如果存在活动事务,则加入活动事务;如果在没有活动事务的情况下调用该方法,则抛出异常。

  • NEVER–如果在活动事务的上下文中调用该方法,则抛出异常。

  • NOT _ SUPPORTED–挂起活动事务(如果存在)并在没有任何事务上下文的情况下执行该方法。

  • REQUIRES _ NEW–总是为此方法启动新的事务。如果使用活动事务调用该方法,则在执行该方法时,该事务将被挂起。

  • NESTED–如果在没有活动事务的情况下调用该方法,则启动一个新事务,如果在有活动事务的情况下调用该方法,则创建一个新事务,仅包装该方法的执行。

您还可以将事务的隔离级别设置为五个不同值之一(例如,使用@Transaction(isolation = Isolation.READ_COMMITTED)):

  • DEFAULT–这是默认值,取决于数据库的默认隔离级别。

  • READ _ UNCOMMITTED——这是最低级别,允许最大的并发性;但是,它会遭受脏读取、不可重复读取和幻像读取。

  • READ _ COMMITTED–这是第二低的级别,可以防止脏读,但是仍然会遭受不可重复读和幻像读。

  • REPEATABLE _ READ——这个级别防止脏读和不可重复读,代价是允许更少的并发性,但仍然会遭受幻像读。

  • SERIALIZABLE——这是最高级别的隔离,可以防止所有并发副作用,代价是非常低的并发性(一次只能发生一个可序列化的操作)。

    为了理解这些隔离级别,您需要理解并发事务的挫折(脏读、不可重复读和幻像读)。脏读是指单个事务从另一个尚未提交的并发事务中读取数据。不可重复读取是指另一个事务在之前已经读取了不同的数据之后提交了新的数据。当您由于另一个事务在当前事务期间添加或删除行而获得不同的行时,会发生幻像读取。

Spring Data R2DBC

R2DBC 代表反应式关系数据库连接。它是一个 API,使用反应类型与关系数据库如 PostgreSQL、H2 和 Microsoft SQL 异步交互。

Spring Data R2DBC 1 包含了广泛的特性:

  • Spring 配置支持

  • 一个带有构建器的DatabaseClient助手接口,通过行和 POJOs 之间的集成对象映射来帮助执行常见的 R2DBC 操作

  • 异常转换成 Spring 的数据访问异常

  • 功能丰富的对象映射与 Spring 的转换服务相集成

  • 基于注释的映射元数据,可扩展以支持其他元数据格式

  • 自动实现Repository<T,ID>接口,包括对自定义查询方法的支持

尽管该项目相对较新,但在撰写本文时,现有的驱动因素包括以下几个(带有groupId:artifactId名称):

  • posters(io . R2 DBC:R2 DBC-PostgreSQL)

  • H2 (io.r2dbc:r2dbc-h2)

  • Microsoft SQL Server(io . r2dbc:r2dbc-MSSQL)

  • MySQL (dev.miku:r2dbc-mysql)

Spring Data 有一个 R2DBC 集成,有一个 spring-boot-starter-data-r2dbc。

Spring Data R2DBC 以熟悉的方式包装 R2DBC。您可以创建一个存储库接口并扩展ReactiveCrudRepository<T,ID>,Spring 将为您生成实现。

public interface PersonRepository
     extends ReactiveCrudRepository<Customer, Long> {
// additional custom query methods go here
}

与普通的CrudRepository<T,ID>不同,ReactiveCrudRepository<T,ID>方法都返回无功类型,如 Mono 和 Flux(参见第十二章了解更多关于这些类型的信息)。例如,以下是一些方法:

  • Mono<Void> delete(T entity)–从数据库中删除给定的实体

  • Flux<T> findAll()–返回该类型的所有实例

  • Mono<T> findById(org.reactivestreams.Publisher<ID> id)–按 ID 检索实体,ID 由发布者提供

  • Mono<S> save(S entity)–保存给定的实体

  • Flux<S> saveAll(Iterable<S> entities)–保存所有给定的实体

  • Flux<S> saveAll(org.reactivestreams.Publisher<S> entityStream)–保存来自给定发布者的所有给定实体

自定义反应式查询

您还可以使用@Query注释指定定制的 SQL 查询,就像 JPA 或 JDBC 一样,例如:

@Query("SELECT * FROM customer WHERE lastname = :lastname")
Flux<Customer> findByLastname(String lastname);

@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Mono<Customer> findFirstByLastname(String lastname);

科特林支架

Spring Data R2DBC 在很多方面支持 Kotlin 1.3.x。

它要求 kotlin-stdlib(或其变体之一,如 kotlin-stdlib-jdk8)和 kotlin-reflect 出现在类路径中(如果您通过 https://start.spring.io 引导 kotlin 项目,默认情况下会提供)。

有关更多信息,请参见 Spring Data R2DBC 的文档。 2

七、Spring MVC

Spring web MVC 是一个用于构建 Web 服务或 Web 应用的框架,通常简称为 Spring MVC 或只是 MVC。 MVC 代表模型-视图-控制器,是 OO(面向对象)编程中常见的设计模式之一。

核心概念

Spring MVC 是按照开放-封闭原则设计的(开放用于扩展,封闭用于修改)。DispatchServlet 是 Spring MVC 的核心,它包含一个 Servlet WebApplicationContext(包含控制器、ViewResolver、HandlerMapping 和其他解析器),该 Servlet 委托给一个根 WebApplicationContext(包含应用的服务和存储库 beans)。

它检测下列类型的 beans,如果找到就使用它们;否则,将使用默认值:HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver、LocaleResolver、ThemeResolver、MultipartResolver 和 FlashMapManager。

您可以直接与视图技术(如 JSP、Velocity 或 FreeMarker)集成,也可以通过 ViewResolver 或内置的对象映射器(使用 dto(数据传输对象)或任何 POJO)返回类似 JSON 的序列化响应。

img/498572_1_En_7_Fig1_HTML.jpg

图 7-1

前端控制器

入门指南

首先,将依赖项添加到项目中。

然后使用 Java config 或 XML(或自动配置),在项目中启用 Web MVC,例如:

import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.context.annotation.Configuration;
@EnableWebMvc
public class WebConfig {}

Listing 7-1WebConfig.java

还要确保使用@ComponentScan或@Bean或 XML bean 定义来定义您的 Spring Beans。在一个典型的应用中,你可能也有服务和存储库,但是对于这一章,我们将只关注 Spring Web MVC 组件。

控制器

用@Controller注释对一个类进行注释,将其标记为控制器。使用@RestController类似,但是用于 RESTful web 服务控制器——它假设每个方法的返回值都被转换成一个返回值,比如 JSON(类似于用@ResponseBody注释方法的时候)。使用这些注释中的任何一个都将允许组件扫描选择您的类。

您还可以用@RequestMapping注释该类,以设置一个将应用于该类中每个方法的 URL 前缀。例如,用@RequestMapping("/api/v1/")注释控制器类会将“/api/v1/”前缀添加到控制器中每个方法的 URL 映射中。

请求映射

用@RequestMapping或者一个相应的 HTTP 方法类型注释来注释一个方法,比如@GetMapping。每个方法请求映射应该匹配一个特定的传入请求。如果有多个方法匹配同一个 HTTP 请求,Spring 将在初始化控制器时抛出一个错误(通常在启动时)。

该类的每个方法都应使用以下内容之一进行注释,以将其映射到相应的 URL 路径:

  • @ request mapping–需要设置 HTTP 方法和路径属性,例如@RequestMapping(method = RequestMethod.PUT, path = "/courses/{id}")。

  • @ GET mapping("/path")–映射到 HTTP GET。

  • @ POST mapping("/path")–映射到 HTTP POST。

  • @ DELETE mapping("/path")–映射到 HTTP DELETE。

  • @ PUT mapping("/path")–映射到 HTTP PUT。

  • @ PATCH mapping("/path")–映射到 HTTP 补丁。

您可以提供一个内嵌值的 URL 来定义可以映射到参数的路径变量。例如,在 URimg/{ filename }/raw”中,文件名对应于一个路径变量:

@GetMapping(value =img/{filename}/raw",
  produces = MediaType.IMAGE_JPEG_VALUE)
public void getImage(@PathVariable String filename, OutputStream output) {
  // code to send image
}

Listing 7-2Example get-mapping using path variable and produces

在本例中,给定的OutputStream参数可用于提供输出数据(本例中为图像)。您可以使用produces来设置响应的内容类型(本例中为“image/jpeg”)。

您还可以用@ ResponseStatus注释一个方法,将成功的 HTTP 状态更改为默认值(200)以外的值。例如,以下代码会将响应状态代码更改为 201:

@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value = "/courses",
  consumes = MediaType.APPLICATION_JSON_VALUE)
public void create(@RequestBody final CourseDto course) {
   // code to save
}

Listing 7-3Create POST Mapping with custom response status

您还可以指定请求参数或头值,以使请求映射更加具体。例如,@PostMapping(value = "/courses", params = "lang=java", headers = "X-custom-header")将只匹配带有名为“lang”的查询参数、值为“java”和名为 X-custom-header 的头的 POST 请求。

路径正则表达式

还可以在路径变量定义中使用正则表达式来限制路径匹配。例如,以下内容仅匹配以数字结尾的路径:

@GetMapping("/courses/{id:\\d+}")
public CourseDto course(@PathVariable final Long id) {
  // code to get Course
}

Listing 7-4Get Course by Id mapping

映射方法参数

控制器中映射方法的参数的有效注释如下:

  • @RequestParam–一个查询参数。

  • @PathVariable–路径的一部分。

  • @MatrixVariable–这些变量可以出现在路径的任何部分,字符等号(" = ")用于给出值,分号(";")来限定每个矩阵变量。在同一路径上,我们还可以重复相同的变量名,或者使用逗号(“,”)字符分隔不同的值。

  • @RequestHeader–来自请求的 HTTP 头。

  • @CookieValue–来自 cookie 的值。

  • @RequestPart–可用于将“multipart/form-data”请求的一部分与方法参数相关联的注释。支持的方法参数类型包括与 Spring 的MultipartResolver抽象结合的MultipartFile和与 Servlet 3.0 多部分请求结合的javax.servlet.http.Part,或者对于任何其他方法参数,部分的内容通过一个HttpMessageConverter传递,考虑请求部分的“内容类型”头。

  • @ModelAttribute–可用于从模型中访问对象。例如,public String handleCustomer(@ModelAttribute("customer") Customer customer)将使用键"customer获得客户对象

  • @SessionAttribute–会话的属性。

  • @RequestAttribute–请求的属性。

    虽然 Java 在编译后的字节码中没有默认保留参数名,但是你可以通过一个设置来实现这一点——Spring Boot 在默认情况下会这样做,不需要任何干预,允许你自由使用与参数名关联的路径变量等等。

响应正文

用@ResponseBody注释一个方法,告诉 Spring 使用该方法的返回值作为 HTTP 响应的主体。

或者,如果用@RestController注释类,这意味着响应体是每个方法的返回值。

Spring 将使用HttpMessageConverter的实现自动将响应转换成适当的值。Spring MVC 自带内置转换器。

其他允许的响应类型有

  • 实体

  • response entity——包含由 Spring 的转换逻辑序列化的实体和 HTTP 值,比如 HTTP 状态

  • HttpHeaders

  • 字符串(要解析的视图的名称)

  • View

  • 地图或模型

  • 对象

  • DeferredResult ,Callable ,ListenableFuture ,或 CompletableFuture–异步结果

  • ResponseBodyEmitter

  • SSE 发射器

  • streamongresponsebody

  • 反应型,如助焊剂

视图

Spring Web MVC 支持几种不同的视图呈现器,比如 JSP、FreeMarker、Groovy 模板和 Velocity。基于所选择的视图技术,所选择的 ViewResolver 将适当地公开模型、会话和请求属性。

Spring MVC 还包括一个 JSP 标记库来帮助构建 JSP 页面。

这里有一个总结 Spring MVC 如何工作的总体图,缺少一些细节,比如处理异常(我们将在后面讨论):

img/498572_1_En_7_Fig2_HTML.jpg

图 7-2

Spring Web MVC 请求/响应

查看解析器

Spring 提供了几种不同的视图解析器:

|

视图解析器

|

描述

|
| --- | --- |
| AbstractCachingViewResolver | 缓存视图的抽象视图解析器。通常视图在使用之前需要准备;扩展此视图解析程序可提供缓存。 |
| XmlViewResolver | ViewResolver的实现,它接受用 XML 编写的配置文件,使用与 Spring 的 XML bean 工厂相同的 DTD。默认的配置文件是/WEB-INF/views.xml。 |
| ResourceBundleViewResolver | 使用由包基本名称指定的ResourceBundle中的 bean 定义的ViewResolver的实现。通常,您在位于类路径中的属性文件中定义包。默认文件名是views.properties。 |
| UrlBasedViewResolver | 简单实现了ViewResolver接口,实现了逻辑视图名称到 URL 的直接解析,没有显式的映射定义。如果您的逻辑名称以直接的方式匹配视图资源的名称,而不需要任意的映射,那么这是合适的。 |
| InternalResourceViewResolver | 支持InternalResourceView(实际上是 Servlets 和 JSP)的UrlBasedViewResolver的方便子类,以及诸如JstlView和TilesView的子类。您可以使用setViewClass(..)为这个解析器生成的所有视图指定视图类。 |
| VelocityViewResolver / FreeMarkerViewResolver / GroovyMarkupViewResolver | 分别支持VelocityView(实际上是速度模板)、FreeMarkerView或 GroovyMarkupView 的AbstractTemplateViewResolver的方便子类,以及它们的自定义子类。 |
| ContentNegotiatingViewResolver | 基于请求文件名或Accept头解析视图的ViewResolver接口的实现。 |

例如,要配置一个 Spring 应用来使用 JSP 视图,并提供来自/images 和/styles 的静态资源,您应该创建一个名为 WebConfig.java 的 Java 配置类,如下所示:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.spring_quick.web"})
public class WebConfig extends WebMvcConfigurerAdapter {
    // Declare our static resources
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry {
      registry.addResourceHandlerimg/**")
          .addResourceLocationsimg/");
      registry.addResourceHandler("/styles/**")
          .addResourceLocations("/styles/");
    }
    @Override
    public void configureDefaultServletHandling(
                DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    // Serves up /WEB-INF/home.jsp for both "/" and "/home" paths:
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/home").setViewName("home");
    }
    @Bean
    InternalResourceViewResolver getViewResolver() {
        InternalResourceViewResolver resolver =
           new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/");
        resolver.setSuffix(".jsp" );
        resolver.setRequestContextAttribute("requestContext");
        return resolver;
    }

Listing 7-5WebConfig.java

这将使用 InternalResourceViewResolver 设置一个 web 应用,该应用使用。jsp”文件扩展名。

错误处理

您可以使用@ExceptionHandler注释声明一个定制的错误处理方法。当请求处理程序方法抛出任何指定的异常时,Spring 调用这个方法。

捕获的异常可以作为参数传递给方法。例如,请参见以下方法:

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public void handleArgException(IllegalArgumentException exception) {
    // Log the exception
}

Listing 7-6Custom handleArgException method

注意我们如何使用@ResponseStatus将 HTTP 状态代码更改为 400(错误请求),在这种情况下返回。然而,这并不改变异常呈现的结果视图。您可以通过用@ ResponseBody注释方法并返回值来直接覆盖内容,例如:

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ExceptionDto handleArgException(IllegalArgumentException ex) {
    return new ExceptionDto(ex);
}

Listing 7-7Custom handleArgException method which returns a DTO

要处理应用中所有控制器的异常,可以使用一个用@ControllerAdvice标注的类,并将所有用@ExceptionHandler标注的方法放在那里.

Web 范围

web 应用中还存在其他作用域:

  • “应用”——应用范围为 ServletContext 的生命周期创建 bean 实例,它可以跨越多个基于 servlet 的应用。

  • “请求”—请求范围为单个 HTTP 请求创建一个 bean 实例。

  • “会话”—会话作用域为 HTTP 会话创建一个 bean。

测试

Spring 通常为它的所有项目提供测试支持。因为一切都是 POJO,所以编写单元测试很简单。对于 Spring MVC,Spring Boot 提供了@WebMvcTest注释来放置一个测试类和MvcMock类型,帮助测试控制器而不会有太多的性能开销。

例如,要开始使用基于 Spring MVC 梯度构建的 Spring Boot,从下面的梯度构建文件开始:

plugins {
  id 'org.springframework.boot' version '2.2.6.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.apress.spring-quick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenLocal()
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
test {
  useJUnitPlatform()
}

Listing 7-8build.gradle

然后在与应用的主配置文件相同的包中(或在一个子包中)创建一个名为 ControllerTest 的基于 JUnit 5 的测试类,如下所示:

  1. 将@ExtendWith 与 SpringExtension 一起使用可以使 Spring 的测试助手扫描类似@MockBean 注释的东西,该注释创建一个 Bean,它是一个(mockito)模拟实例。

  2. 使用@WebMvcTest 会导致 Spring 只自动配置应用的 MVC 层,包括控制器。

  3. 在 MockMvc 的实例上调用 perform 会调用 HandlerMapping 逻辑,并有一个用于验证响应的 fluent 接口。在这种情况下,我们使用“.andDo(print())”将其打印出来,然后期望 HTTP 状态为 OK (200),并使用“content().string(containsString(String))”来验证响应字符串是否具有预期的结果。

import com.apress.spring_quick.jpa.simple.Course;
import com.apress.spring_quick.jpa.simple.SimpleCourseRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(SpringExtension.class) //(1)
@WebMvcTest                        //(2)
public class ControllerTest {

    @Autowired

    private MockMvc mockMvc;
    @MockBean
    private SimpleCourseRepository courseRepository;

    @Test
    public void coursesShouldReturnAllCourses() throws Exception {
        Course course = new Course();
        course.setName("Java Professional");
        course.setSubtitle("Java 11");
        course.setDescription("");
        when(courseRepository.findAll()).thenReturn(List.of(course));
        mockMvc.perform(get("/api/v1/courses"))           //(3)
                .andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(
        containsString("[{\"id\":null,\"title\":\"Java Professional\"" +
          ",\"subtitle\":\"Java 11\",\"description\":\"\"}]")));
    }
}

Listing 7-9ControllerTest.java

如果没有 Spring Boot 自动配置,您也可以使用MockMvcBuilders. webAppContextSetup(WebApplicationContext)方法创建一个 MockMvc 实例,例如:

// imports:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
// ...code:
WebApplicationContext wac = /** Create the application context */;
MockMvc mockMvc = webAppContextSetup(wac).build();

八、Spring Mobile

Spring Mobile 是 Spring MVC 的扩展,旨在简化移动 web 应用的开发。它包括一个模块,用于在服务器上检测发出请求的设备类型,是手机、平板电脑还是台式机。

入门指南

将项目包含在您的依赖项中,例如,在 Maven pom 中:

<dependency>
    <groupId>org.springframework.mobile</groupId>
    <artifactId>spring-mobile-device</artifactId>
    <version>${org.springframework.mobile-version}</version>
</dependency>

在 Gradle 构建文件中,在“依赖项”下添加以下内容:

implementation
  "org.springframework.mobile:spring-mobile-device:$mobileVersion"

然后在你的gradle.properties文件中设置版本 1 :

mobileVersion=1.1.5.RELEASE

接下来,将DeviceResolverHandlerInterceptor或DeviceResolverRequestFilter添加到您的 web 应用中。第一个与 Spring 框架的耦合更紧密,而第二个是 servlet 过滤器的实现,因此与 Spring 的耦合更少。

DeviceResolverHandlerInterceptor

Spring Mobile 配有一个HandlerInterceptor,在preHandle上,委托给一个DeviceResolver。被解析的Device被设置为一个名为currentDevice的请求属性,使它在整个请求处理过程中对处理程序可用。

要启用它,请将DeviceResolverHandlerInterceptor添加到您的DispatcherServlet配置 XML 中定义的拦截器列表中:

<interceptors>
  <bean class="org.springframework.mobile.device.DeviceResolverHandlerInterceptor" />
</interceptors>

或者,您可以使用 Spring 的基于 Java 的配置来添加DeviceResolverHandlerInterceptor:

@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
//...
  @Bean
  public DeviceResolverHandlerInterceptor drhInterceptor() {
      return new DeviceResolverHandlerInterceptor();
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(drhInterceptor());
  }
}

DeviceResolverRequestFilter

作为DeviceResolverHandlerInterceptor的替代,Spring Mobile 还附带了一个 servlet 过滤器,它委托给一个DeviceResolver。与HandlerInterceptor一样,被解析的Device被设置在一个名为currentDevice”的请求属性下。

要启用,请将DeviceResolverRequestFilter添加到您的 web.xml,如下所示:

<filter>
  <filter-name>deviceResolverRequestFilter</filter-name>
  <filter-class>
org.springframework.mobile.device.DeviceResolverRequestFilter
  </filter-class>
</filter>

访问设备

要在代码中查找当前的Device,可以用几种方法。如果您已经引用了一个ServletRequest或 SpringWebRequest,只需使用DeviceUtils:

//imports
import org.springframework.mobile.device.DeviceUtils;
// code...
Device currentDevice = DeviceUtils.getCurrentDevice(servletRequest);

这将获得当前设备,如果没有为请求解析设备,则为 null。如果当前设备没有被解析,还有一个getRequiredCurrentDevice(HttpServletRequest request)方法抛出运行时异常。

设备接口有以下可用方法:

|

返回类型

|

方法

|
| --- | --- |
| DevicePlatform | getDevicePlatform()–返回一个枚举,可以是 IOS、ANDROID 或 UNKNOWN。 |
| Boolean | isMobile()–如果该设备是苹果 iPhone 或 Nexus One Android 等移动设备,则为 True。 |
| Boolean | isNormal()–如果该设备不是移动或平板设备,则为 True。 |
| Boolean | isTablet()–如果该设备是苹果 iPad 或摩托罗拉 Xoom 等平板设备,则为 True。 |

DeviceWebArgumentResolver

如果您想将当前的Device automatically作为参数传递给一个或多个控制器方法,请配置一个DeviceWebArgumentResolver using XML:

<annotation-driven>
  <argument-resolvers>
    <bean class="org.springframework.mobile.device.DeviceWebArgumentResolver" />
  </argument-resolvers>
</annotation-driven>

您也可以使用基于 Java 的配置来配置DeviceHandlerMethodArgumentResolver,如下所示:

@Bean
public DeviceHandlerMethodArgumentResolver deviceHMAR() {
    return new DeviceHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
  argumentResolvers.add(deviceHMAR());
}

LiteDeviceResolver

Spring 允许不同的DeviceResolver实现,但是默认情况下只提供一个名为LiteDeviceResolver的移动或平板设备。

您还可以通过添加额外的关键字来自定义LiteDeviceResolver,如果这些关键字包含在请求的用户代理中,将被解析为“普通”设备,例如,使用 Java 配置:

@Bean
public LiteDeviceResolver liteDeviceResolver() {
    List<String> keywords = new ArrayList<String>();
    keywords.add("vivaldi");
    keywords.add("yandex");
    return new LiteDeviceResolver(keywords);
}
@Bean
public DeviceResolverHandlerInterceptor deviceResolverHandlerInt() {
    return new DeviceResolverHandlerInterceptor(liteDeviceResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(deviceResolverHandlerInt());
}

站点偏好管理

Spring Mobile 提供了一个名为StandardSitePreferenceHandler的单一SitePreferenceHandler实现,这应该适合大多数需求。它支持基于查询参数的站点偏好指示(site_preference)和可插拔的SitePreference存储,并且可以在使用the SitePreferenceHandlerInterceptor的 Spring MVC 应用中启用。此外,如果用户没有明确指示SitePreference,将基于检测到的用户设备导出默认值。

因此,除了前面的拦截器之外,还要添加以下内容:

    @Bean
    public SitePreferenceHandlerInterceptor
         sitePreferenceHandlerInterceptor() {
        return new SitePreferenceHandlerInterceptor();
    }

然后将addInterceptors方法更新如下:

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(drhInterceptor());
        registry.addInterceptor(sitePreferenceHandlerInterceptor());
    }

与设备分辨率类似,您可以使用SitePreferenceUtils或SitePreferenceHandlerMethodArgumentResolver来访问当前的SitePreference。然而,将移动用户重定向到不同的站点可能更有意义。在这种情况下,您可以使用SiteSwitcherHandlerInterceptor将移动用户重定向到专用的移动站点。

SitePreferenceHandlerInterceptor的mDot、dotMobi, urlPath, and standard工厂方法配置基于 cookie 的SitePreference存储。cookie 值将在移动和普通站点域之间共享。在内部,拦截器委托给一个SitePreferenceHandler,所以在使用切换器时不需要注册一个SitePreferenceHandlerInterceptor。例如,以下拦截器会将移动用户重定向到 mobile.app.com,将平板电脑重定向到 tablet.app.com,否则只重定向 app.com:

@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.standard("app.com",
       "mobile.app.com", "tablet.app.com", ".app.com");
}
// standard(normalName, mobileServerName, tabletServerName, cookieDomain)

一种不需要额外 DNS 条目的更简单的方法是 urlPath 工厂:

@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
}

这个拦截器会将移动用户重定向到 /mobile/ paths。例如,如果正常的 URL 是“myapp.com/courses”,那么移动站点将是“myapp.com/mobile/courses”。

Spring 移动示例

这个例子将基于前几章的 Spring Web MVC 和 Spring Data 内容,并利用 Spring Boot。有关这两个主题的更多信息,请参见相关章节。这个示例项目可以在网上找到。 2

首先,创建一个名为“spring-mobile”的新目录,并创建一个 Gradle“build . Gradle”文件,如下所示:

plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id "java"
}
group = 'com.apress.spring-quick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

ext {
    mobileVersion = '1.1.5.RELEASE'
}
repositories {
    mavenLocal()
    mavenCentral()
}
dependencies {
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "org.springframework.boot:spring-boot-starter-web"
    implementation "org.springframework.boot:spring-boot-starter-groovy-templates"
    implementation "org.springframework.mobile:spring-mobile-device:$mobileVersion"
    implementation "com.apress.spring-quick:spring-data-jpa:0.0.1"

    implementation "com.h2database:h2:1.4.192" // database
}

Listing 8-1build.gradle

这使用了 Spring Boot Gradle 插件和依赖管理来简化项目的设置。注意,我们包含了第六章中的“spring-data-jpa”项目作为依赖项。这使得存储库可以在运行时作为 Spring beans 包含(取决于配置)。

接下来,创建一个主类,如下所示:

@SpringBootApplication
@Import({WebConfig.class, ServiceConfig.class})
public class SpringMobileWebApp {
    public static void main(String[] args) throws IOException {
        SpringApplication.run(SpringMobileWebApp.class, args);
    }
}

Listing 8-2SpringMobileWebApp.java

接下来,设置 ServiceConfig,它包括来自第六章的“spring-data-jpa”项目的特定包:

@Configuration
@EnableJpaRepositories(basePackages =
        {"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"},
        enableDefaultTransactions = true)
@ComponentScan(basePackages = {"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"})
public class ServiceConfig {
}

接下来,我们指定 WebConfig 类,该类定义了本章前面描述的拦截器,以及 GroovyMarkupConfigurer 和 GroovyMarkupViewResolver:

import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.mobile.device.*;
import org.springframework.mobile.device.site.*;
import org.springframework.mobile.device.switcher.*;
import org.springframework.web.method.support.*;
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public SitePreferenceHandlerMethodArgumentResolver sitePrefMAR() {
        return new SitePreferenceHandlerMethodArgumentResolver();
    }
    @Bean
    public DeviceHandlerMethodArgumentResolver deviceHMAR() {
        return new DeviceHandlerMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(deviceHMAR());
        argumentResolvers.add(sitePrefMAR());
    }

    @Bean
    public DeviceResolverHandlerInterceptor drhInterceptor() {
        return new DeviceResolverHandlerInterceptor();
    }

    @Bean
    public SitePreferenceHandlerInterceptor sitePreferenceHandlerInterceptor() {
        return new SitePreferenceHandlerInterceptor();
    }

    @Bean
    public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor(){
        return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(drhInterceptor());
        registry.addInterceptor(sitePreferenceHandlerInterceptor());
        registry.addInterceptor(siteSwitcherHandlerInterceptor());
    }

    @Bean
    public GroovyMarkupConfigurer groovyMarkupConfigurer() {
        GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
        configurer.setResourceLoaderPath("classpath:/templates/");
        return configurer;
    }

    @Bean
    public GroovyMarkupViewResolver groovyMarkupViewResolver() {
        GroovyMarkupViewResolver resolver = new GroovyMarkupViewResolver();
        resolver.setSuffix(".groovy");
        resolver.setRequestContextAttribute("requestContext");
        return resolver;
    }
}

Listing 8-3WebConfig.java

注意,我们已经使用“/mobile”路径定义了一个SiteSwitcherHandlerInterceptor。Groovy 相关的配置告诉 Spring 在类路径中的/templates/下查找以“.”结尾的文件。太棒了。

最后,我们需要为 MVC 应用定义控制器。为了给移动站点启用完全不同的逻辑,我们可以为移动和普通请求定义一个单独的控制器。或者,我们可以将SitePreference作为方法参数注入到每个控制器方法中,并使用它,因为我们设置了一个SitePreferenceHandlerMethodArgumentResolver。

在本例中,我们创建了一个 CourseController 和 MobileCourseController,如下所示:

@Controller
@RequestMapping("/mobile")
public class MobileCourseController {
    @GetMapping("/")
    public String home() {
        return "mobile/home";
    }
    // additional methods...

Listing 8-5MobileCourseController.java

@Controller
@RequestMapping
public class CourseController {
    @GetMapping("/")
    public String home() {
        return "home";
    }
    // additional methods...

Listing 8-4CourseController.java

请注意,由于 mobileCourseController 是用@RequestMapping("/mobile ")注释的,它将匹配所有以"/mobile "开头的路径,因此匹配所有 SitePreference 为 Mobile 的用户。同样,我们也可以对平板电脑做同样的事情。

Groovy 标记模板应该放在 src/main/resources/templates 目录下。“home.groovy”模板应该如下所示:

yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
    head {
        meta('http-equiv':'"Content-Type" content: "text/html; charset: utf-8"')
        title('Courses Demo')
        link(rel: 'stylesheet', href: '/styles/main.css', type: 'text/css')
    }
    body {
        h3('Normal Home page')
        div(class: 'site_pref') {
            a(href: '/?site_preference=mobile', 'Mobile')
            yieldUnescaped '|'
            a(href: '/?site_preference=normal', 'Desktop')
        }
        div(class: 'content') {
            div {
                a(href: '/courses', 'Courses')
            }
        }
    }
}

使用 URL ?site_preference=mobile (或者点击具有相同 URL 路径的网页上的“移动”链接)触发 SiteSwitcherHandlerInterceptor 来改变 SitePreference。在这种情况下,用户将被重定向到由文件src/main/resources/templates/mobile/home.groovy呈现的“移动/家庭”视图。

img/498572_1_En_8_Fig1_HTML.jpg

图 8-1

移动/普通主页

img/498572_1_En_8_Figa_HTML.jpg Exercise: Add Tablets

从本章的代码开始(可在网上获得 3 ),添加对平板电脑的支持。

九、Spring Security

Spring Security 是一个高度可定制的认证和访问控制框架。它是保护基于 Spring 的应用的标准。它支持多种安全标准,如 LDAP 和 OAuth2。

此外,它与其他 Spring 模块和项目集成得很好,并且可以利用基于注释的代理。此外,它与 SpEL (Spring Expression Language)配合得很好,这一点我们将在本章中介绍。

特征

Spring Security 很容易扩展,并且有许多内置特性:

  • 对身份验证和授权的全面和可扩展的支持

  • 防范诸如会话固定、点击劫持、跨站点请求伪造等攻击

  • Servlet API 集成

  • 与 Spring Web MVC 的可选集成

  • 支持 OAuth 和 OAuth2

  • 支持 SAML

概观

Spring Security 的核心分为两件事:认证,它决定用户(主体)的身份,以及访问控制,它决定什么用户可以访问什么资源。

Spring Security 认证基于一个AuthenticationManager接口,该接口有一个方法Authentication authenticate(Authentication)。它由拥有一个或多个认证提供者的ProviderManager实现。AuthenticationProvider接口有两个方法,Authentication authenticate(Authentication)和boolean supports(Class),如果这个AuthenticationProvider支持指定的Authentication对象,则返回true。

Spring Security 访问控制(也称为授权)基于一个拥有一个或多个AccessDecisionVoter的AccessDecisionManager。AccessDecisionVoter<S>的主要实现是基于角色做出访问决策的RoleVoter。

配置

Spring Security 性可以通过典型的方式配置,XML 或 Java 配置。

  1. 我们声明 AuthenticationManager 应该使用内存中的用户数据库,并添加一个默认的“admin”用户。在生产中,您可能应该将用户存储在数据库中或使用其他方法。出于演示目的,我们使用带有 DefaultPasswordEncoder()的用户构建器方法创建一个用户。这也不应该在生产中进行。用户还被赋予用户和管理员角色。

  2. configure(HttpSecurity http)提供了一个流畅的界面,用于使用 URL 匹配器、登录和注销页面以及其他 web 安全设置来配置访问控制。第一个方法名为 httpBasic(),支持基于标头的 HTTP 基本身份验证。后续的方法,以及()。authorizeRequests()设置授权(访问控制)设置。

  3. 代码antMatchers("/courses").hasRole("USER")只为“/courses”路径创建一个过滤器,并指定用户必须拥有用户角色才能获得访问权限。

import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // from WebSecurityConfigurerAdapter
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // here you could configure a JDBC database
        // auth.jdbcAuthentication().usersByUsernameQuery(...)
        auth.inMemoryAuthentication()
                .withUser(User.builder().withDefaultPasswordEncoder() //(1)
                        .username("admin").password("123")
                        .roles("USER", "ADMIN")
                        .build());
    }

    // from WebSecurityConfigurerAdapter
    @Override
    protected void configure(HttpSecurity http) throws Exception {   //(2)
        http.httpBasic().and()
                .authorizeRequests()
                .antMatchers("/courses").hasRole("USER")  //(3)
                .antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()

                .logout()
                .permitAll()
                .and().csrf().disable();
    }
}

Listing 9-1SecurityConfig.java

代替antMatchers,你可以使用mvcMatchers,主要区别是后者匹配 URL 的方式与 MVC @RequestMapping完全相同,这允许更多的灵活性,比如不同的扩展名(例如.json或.xml)。

第一个匹配的 URL 决定了访问,因此您应该按照从最具体到最不具体的顺序排列 URL 匹配器。

密码安全性

Spring Security 中的密码是通过PasswordEncoder接口的实现来加密的,Spring 提供了几个实现(没有解码器,因为密码编码应该是单向算法)。

| NoOpPasswordEncoder | 一个什么都不做的密码编码器。适用于使用明文密码的测试。 | | BCryptPasswordEncoder | 使用 BCrypt 强哈希函数的 PasswordEncoder 实现。 | | pbkdf 2 密码编码器 | 使用 PBKDF2 的`PasswordEncoder`实现,具有可配置的迭代次数和随机的 8 字节随机 salt 值。 | | SCryptPasswordEncoder | 使用 SCrypt 哈希函数的 PasswordEncoder 实现。 | | 标准密码编码 | 一个标准的`PasswordEncoder`实现,使用 1024 次迭代的 SHA-256 散列和一个随机的 8 字节随机 salt 值。 | | 委派密码编码 | 基于前缀标识符委托给另一个 password encoder 的密码编码器。这使得加密升级更加容易。 |

如果你正在开发一个新系统,Spring 团队建议你使用BCryptPasswordEncoder来获得更好的安全性和与其他语言的互操作性。

你可以使用PasswordEncoderFactories 1 或者通过构造函数来创建一个DelegatingPasswordEncoder。

img/498572_1_En_9_Figa_HTML.jpg为了确保适当的安全性,您应该调整您的密码编码,使其在系统上的处理时间大约为一秒钟。这有助于密码更难被暴力破解。例如,BCryptPasswordEncoder的构造函数可以接受一个强度参数,该参数指定了要使用的对数轮次(在 4 和 31 之间),您应该测试一下,看看哪个数字在一个像样的处理器上需要大约一秒的编码时间。

访问身份验证

SecurityContext接口可用于通过getAuthentication()方法访问当前登录的用户,该方法获得当前认证的主体或认证请求令牌。可以从SecurityContextHolder.getContext()静态方法中访问SecurityContext。默认情况下,SecurityContextHolder使用ThreadLocal来存储当前的SecurityContext,它为每个线程存储一个值。

Spring 将在控制器方法中注入任何 Principal 或 Authentication 类型的参数值。

注释安全性

您可以通过注释启用访问控制。根据项目的配置,可以使用几种不同的注释,包括来自javax.annotation.security package、@Secured、@PreAuthorize和@PostAuthorize.的@RolesAllowed

首先,要使用 Spring 方法安全,我们需要添加spring-security-config依赖,例如,使用 Maven:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.3.2.RELEASE</version>
</dependency>

如果我们想使用 Spring Boot,我们可以使用spring-boot-starter-security依赖项,它包含了 spring-security-config(更多信息参见第 15 章 )。

通过 XML 启用:

<beans ...
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">
    <security:global-method-security
        secured-annotations="enabled"
        pre-post-annotations="enabled" />

Listing 9-2security.xml

使用 Java 配置:

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, // (1)
  securedEnabled = true, // (2)
  jsr250Enabled = true) //  (3)
public class MethodSecurityConfig
  extends GlobalMethodSecurityConfiguration {
}

Listing 9-3MethodSecurityConfig.java

  1. prePostEnabled属性决定@PreAuthorize and @PostAuthorize should be enabled是否。

  2. securedEnabled属性确定是否应该启用@Secured注释。

  3. jsr250Enabled属性允许我们使用@RolesAllowed、、??、、、@DenyAll注释。

使用@Secured

使用全局方法安全性,您可以在任何 Spring bean 上的任何方法上添加@Secured,它将被 Spring Security 拦截以执行适当的授权。

例如:

@Secured("ROLE_USER")
// method1
@Secured({"ROLE_ADMIN", "ROLE_USER"}) //either ADMIN or USER
// method2

@Secured可以为多个角色接受一个字符串数组,在这种情况下,如果有任何角色匹配,这是允许的。它不支持 SpEL (Spring Expression Language),所以对于更复杂的访问逻辑,需要使用不同的注释。

使用预授权

使用@PreAuthorize和@PostAuthorize可以实现更复杂的逻辑(包括随后描述的 SpEL)来确定哪些用户可以访问。

例如,您可以如下使用@PreAuthorize:

//imports
import org.springframework.security.access.prepost.PreAuthorize;
// code...
@PreAuthorize("hasRole('ADMIN') && hasRole('USER')")
public void deleteById(Long id) {

在这种情况下,当前认证必须同时具有管理员和用户角色才能访问deleteById方法。

提供给@PreAuthorize和@PostAuthorize注释的表达式也可以引用附加变量、@PostAuthorize的returnObject和@PreAuthorize中的方法参数(使用 #name 语法)。

假设主体是具有用户属性name的User,并且Course具有可能与User的属性username相匹配的属性owner,例如:

@PostAuthorize("returnObject.owner == authentication.principal.username")
public Course getCourse(Long id) {
  //method definition
}

@PreAuthorize("#course.owner == authentication.principal.username")
public void removeCourse(Course course) {
  //method definition
}

在第一个例子中,表达式将验证返回的课程对象的所有者是否等于 Spring Security 的认证主体的用户名(当前登录的用户名User)。如果不是这样,用户将会得到一个身份验证异常。

第二个例子在调用方法之前(本例中为removeCourse)验证给定课程的所有者是否等于 Spring Security 的认证主体的用户名。

全局方法安全性

拼写

SpEL (Spring Expression Language)是一种基于文本的表达式语言,由 Spring 解释,通常用于简化值注入。它可以直接在@PreAuthorize和@PostAuthorize值内使用,并具有 Spring Security 提供的附加功能。

Spring Expression Language

SpEL 可以使用#{ 表达式 }语法在任何@Value注释值中使用。SpEL 支持标准操作(+ - / % < > <= >= == != && || !)以及它们的英文单词对等词(加号、减号、div、mod、lt、gt、le、ge、eq、ne、and、or、not)。它还支持 Elvis 运算符(?:)和空安全取消引用(?。).它还支持正则表达式匹配的“匹配”。

可以使用单引号(')来指定字符串值。

SpEL 支持使用 T( Type )语法引用 Java 类型。

SpEL 支持使用{key:value}语法定义映射,例如,{'key': 1,' key2': 2}。

它还支持使用 list[n]语法通过索引访问列表值;例如,list[0]将访问第一个元素。

在 Spring Security 上下文中,hasRole 函数是可用的,因此只有当当前用户拥有 ADMIN 角色时,hasRole('ADMIN ')才会返回 true。

十、Spring Web 服务

Spring Web Services (Spring WS)专注于构建契约优先的 SOAP web 服务,具有灵活的 XML 映射、契约和实现之间的松散耦合以及与 Spring 的轻松集成。它的架构类似于 Spring MVC。

特征

Spring WS 具有以下特性:

  • 强大的映射——可以将传入的 XML 请求分发到任何对象,这取决于消息有效负载、SOAP 操作头或 XPath 表达式。

  • XML API 支持——传入的 XML 消息可以用标准的 JAXP API 来处理,比如 DOM、SAX 和 StAX,还有 JDOM、dom4j、XOM,甚至编组技术。

  • 灵活的 XML 编组 Spring Web Services 发行版中的对象/XML 映射模块支持 JAXB 1 和 2、Castor、XMLBeans、JiBX 和 XStream。

  • 支持 WS-Security——WS-Security 允许您对 SOAP 消息进行签名、加密和解密,或者根据它们进行身份验证。

  • 与 Spring Security 集成 Spring Web 服务的 WS-Security 实现提供了与 Spring Security 的集成。

入门指南

首先,将以下依赖项添加到 Maven pom 文件中:

<dependencies>
    <dependency>
        <groupId>org.springframework.ws</groupId>
        <artifactId>spring-ws-core</artifactId>
        <version>3.0.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>jdom</groupId>
        <artifactId>jdom</artifactId>
        <version>2.0.2</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.2.0</version>
    </dependency>
</dependencies>

或者如果使用 Gradle,添加以下内容:

implementation 'org.springframework.ws:spring-ws-core:3.0.9.RELEASE'
implementation 'org.jdom:jdom:2.0.2'
implementation 'jaxen:jaxen:1.2.0'

在 Java 配置类上使用@EnableWs注释,使 spring-ws 能够注册默认的EndpointMappings、EndpointAdapter和EndpointExceptionResolver。

您需要创建一个 web.xml 文件,如下所示:

<web-app xmlns:="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
             http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">
    <display-name>MyCompany Web Service</display-name>

    <servlet>
        <servlet-name>no-boot-spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet
        </servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

Listing 10-1WEB-INF/web.xml

基于 servlet 的名称,Spring 将寻找一个对应的名为<servlet_name>-servlet.xml的 Spring XML 配置文件。在这种情况下,它将寻找一个WEB-INF/no-boot-spring-ws-servlet.xml文件。

Spring Boot 配置

要在 Spring Boot Gradle 项目中包含 Spring-WS,请添加以下依赖项:

implementation 'org.springframework.boot:spring-boot-starter-web-services'
implementation 'org.jdom:jdom:2.0.2'
implementation 'jaxen:jaxen:1.2.0'

Spring Boot WS 启动器(spring-boot-starter-web-services)将自动执行以下操作:

  • 在 servlet 容器中配置一个MessageDispatcherServlet

  • 扫描所有的.wsdl和. xsd文档,查找 WSDL 和模式定义的 beans

先合同

首先编写契约启用实际模式的更多特性(比如限制字符串值的允许值),允许将来更容易升级,并允许与非 Java 系统更好的互操作性。

有四种不同的方法来定义这样的 XML 契约:

  • 文档类型定义

  • XML 模式(XSD)

  • 放松 ng

  • 图式【2】

对于本书,我们将使用课程领域的 XML 模式。例如(假设您想要使用名称空间," http://mycompany.com/schemas "),创建一个名为“my.xsd”的文件,并将其放在项目的“src/main/resources”目录中,内容如下:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           elementFormDefault="qualified"
           targetNamespace="http://mycompany.com/schemas"
           xmlns:my="http://mycompany.com/schemas">
    <xs:element name="Course">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="my:Number"/>
                <xs:element ref="my:Title"/>
                <xs:element ref="my:Subtitle"/>
                <xs:element ref="my:Description"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="Number" type="xs:integer"/>
    <xs:element name="Title" type="xs:string"/>
    <xs:element name="Subtitle" type="xs:string"/>
    <xs:element name="Description" type="xs:string"/>
</xs:schema>

在 Spring-WS 中,不需要手工编写 WSDL。我们将在后面的部分展示如何生成 WSDL。

编写端点

在 Spring-WS 中,您将实现端点来处理传入的 XML 消息。端点通常是通过用一个或多个处理传入请求的方法用@Endpoint注释来注释类而创建的。方法签名非常灵活:您可以包含与传入的 XML 消息相关的任何类型的参数,这将在后面解释。

首先创建一个用@Endpoint 注释的类,该类要么被组件扫描(@Endpoint 将其标记为特殊的@Component),要么直接使用 Java configuration 将其配置为 Spring Bean。然后添加一个或多个方法来处理 XML 请求的不同元素,例如:

  1. 因为我们使用 JDOM2,所以我们定义了要在 Xpath 定义中使用的Namespace。

  2. 我们定义了XPathExpression实例,稍后我们将使用这些实例来评估 XML 有效负载的各个部分。

  3. 我们使用@ PayloadRoot来定义我们希望与该方法匹配的 SOAP 有效负载的名称空间和元素。在Element参数上的@ RequestPayload注释被注入了匹配的有效载荷,然后我们可以在这个方法中处理它。

import org.jdom2.*;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;

@Endpoint

public class CourseEndpoint {

    private XPathExpression<Element> numberExpression;

    private XPathExpression<Element> titleExpression;

    private XPathExpression<Element> subtitleExpression;

    private XPathExpression<Element> descriptionExpression;

    @Autowired
    public CourseEndpoint() throws JDOMException {
        Namespace namespace = Namespace.getNamespace("my",
          "http://mycompany.com/my/schemas");               //1
        XPathFactory xPathFactory = XPathFactory.instance();
        numberExpression = xPathFactory.compile("//my:Number", Filters.element(), null, namespace);    //2
        titleExpression = xPathFactory.compile("//my:Title", Filters.element(), null, namespace);
        subtitleExpression = xPathFactory.compile("//my:Subtitle", Filters.element(), null, namespace);
        descriptionExpression = xPathFactory.compile("//my:Description", Filters.element(), null, namespace);
    }

    @PayloadRoot(namespace = "http://mycompany.com/my/schemas",
                              localPart = "CourseRequest")  //3
    public void handleRequest(@RequestPayload Element courseRequest) throws Exception {
        Long number = Long.parseLong(numberExpression.evaluateFirst(courseRequest).getText());
        String description = descriptionExpression.evaluateFirst(courseRequest).getText();
        String fullTitle = titleExpression.evaluateFirst(courseRequest).getText() + ":"
                + subtitleExpression.evaluateFirst(courseRequest).getText();

        // handleCourse(number, fullTitle, description)
    }

}

生成 WSDL

下面是我们如何在 XML 配置中定义 WSDL 生成:

  1. 首先,id 决定了 wsdl 资源的名称(courses.wsdl)。

  2. portTypeName 确定 WSDL 端口类型的名称。

  3. locationUri 描述了服务本身的相对位置。

  4. targetNamespace 是可选的,但是在 WSDL 本身中定义了命名空间。

<sws:dynamic-wsdl id="courses"
    portTypeName="CourseResource"
    locationUri="/courseService/"
    targetNamespace="http://mycompany.com/definitions">
  <sws:xsd location="/WEB-INF/my.xsd"/>
</sws:dynamic-wsdl>

EndpointMappings 和 EndpointExceptionResolvers

默认情况下,Spring-WS(通过WsConfigurationSupport类)注册以下端点映射:

  • PayloadRootAnnotationMethodEndpointMapping按 0 排序,用于将请求映射到@PayloadRoot带注释的控制器方法

  • SoapActionAnnotationMethodEndpointMapping排序为 1,用于将请求映射到@SoapAction带注释的控制器方法

  • AnnotationActionEndpointMapping在 2 排序,用于将请求映射到@Action带注释的控制器方法

它还注册了一个EndpointAdapter、DefaultMethodEndpointAdapter,用于处理带有注释的端点方法和以下 EndpointExceptionResolvers 的请求:

  • SoapFaultAnnotationExceptionResolver用于处理标注有@SoapFault的异常

  • SimpleSoapExceptionResolver用于创建默认例外

定制的

您可以通过实现WsConfigurer接口或者扩展WsConfigurerAdapter基类并覆盖单个方法来定制 Spring-WS 配置,例如:

@Configuration
@EnableWs
@ComponentScan
public class CustomWsConfiguration extends WsConfigurerAdapter {
    @Override
    public void addInterceptors(List<EndpointInterceptor> interceptors)  {
        interceptors.add(new MyInterceptor());
    }

    @Override
    public void addArgumentResolvers(
         List<MethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(myArgumentResolver());
    }

    @Bean
    public MethodArgumentResolver myArgumentResolver() {
        return new MyArgumentResolver();
    }
 }

Listing 10-2CustomWsConfiguration.java

可重写的 WsConfigurerAdapter 方法:

| `void addArgumentResolvers(``List argumentResolvers)` | 添加冲突解决程序以支持自定义终结点方法参数类型。 | | `void addInterceptors(``List interceptors)` | 为端点方法调用的预处理和后处理添加端点拦截器。 | | `void addReturnValueHandlers(``List returnValueHandlers)` | 添加处理程序以支持自定义控制器方法返回值类型。 |

端点拦截器

EndpointInterceptor 接口具有为请求、响应、错误和完成后调用的方法,并且能够清除响应、修改响应、给出完全不同的响应或停止处理。

| `void afterCompletion(``MessageContext messageContext, Object endpoint, Exception ex)` | 请求和响应(或故障,如果有的话)处理完成后的回调。 | | `boolean handleFault(``MessageContext messageContext, Object endpoint)` | 处理传出响应错误。 | | `boolean handleRequest(``MessageContext messageContext, Object endpoint)` | 处理传入的请求消息。 | | `boolean handleResponse(``MessageContext messageContext, Object endpoint)` | 处理传出的响应消息。 |

每个“handle”方法被作为一个链调用,返回值决定处理是否应该停止。True 表示继续处理;false 表示此时阻止处理。如果 handleRequest 方法从任何 EndpointInterceptor 返回 false,端点本身将不会被处理。

十一、Spring REST

REST(表述性状态转移)概述了一种使用 HTTP 方法(如 GET、POST、PUT 和 PATCH)围绕资源和元数据设计 web 服务的方法,以映射到定义良好的动作。Roy Fielding 在 2000 年加州大学欧文分校的博士论文“基于网络的软件架构的架构风格和设计”中首次定义了它。遵循这些原则的 web 服务被称为 RESTful 。

这一章主要是关于两个 Spring 项目,Spring REST Docs 和 Spring HATEOAS。 2 它建立在第七章的内容之上,所以在阅读本章之前一定要先阅读它。尽管构建 RESTful web 服务并不需要使用这些项目,但是将它们与 Spring MVC 一起使用可以让您使用 Spring 构建一个全功能的 web API。

Spring 休息文档

Spring REST Docs 3 使用 Asciidoctor 语法基于测试结合文本文档生成文档,尽管您也可以使用 Markdown。这种方法旨在生成 API 文档,类似于 Swagger,但更灵活。

Spring REST Docs 使用用 Spring MVC 的 MockMvc、Spring WebFlux 的 WebTestClient 或 REST Assured 3 编写的测试产生的片段。这种测试驱动的方法有助于保证 web 服务文档的准确性。如果代码片段不正确,生成它的测试就会失败。

入门指南

首先,将 Spring REST Docs 依赖项添加到您的项目中。如果使用 Maven ,添加以下依赖关系:

<dependency>
  <groupId>org.springframework.restdocs</groupId>
  <artifactId>spring-restdocs-mockmvc</artifactId>
  <version>2.0.4.RELEASE</version>
  <scope>test</scope>
</dependency>

另外,添加以下 Maven 插件,它将在准备包阶段处理 asciidoctor 文本:

<build>
  <plugins>
   <plugin>
      <groupId>org.asciidoctor</groupId>
      <artifactId>asciidoctor-maven-plugin</artifactId>
      <version>1.5.8</version>
      <executions>
        <execution>
         <id>generate-docs</id>
         <phase>prepare-package</phase>
         <goals>

           <goal>process-asciidoc</goal>

         </goals>

         <configuration>

           <backend>html</backend>

           <doctype>book</doctype>

         </configuration>

       </execution>

     </executions>

     <dependencies>

       <dependency>

         <groupId>org.springframework.restdocs</groupId>
         <artifactId>spring-restdocs-asciidoctor</artifactId>

         <version>2.0.4.RELEASE</version>

       </dependency>

      </dependencies>

    </plugin>

  </plugins>
</build>

如果使用 Gradle 构建,请使用以下构建文件:

plugins {
    id "org.asciidoctor.convert" version "2.4.0"
    id "java"
}
ext {
    snippetsDir = file('build/generated-snippets')
    ver = '2.0.4.RELEASE'
}
dependencies {
asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$ver"
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$ver"
}
test {
    outputs.dir snippetsDir
}
asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

REST 文档生成

为了从一个现有的基于 Spring MVC 的项目中生成 REST 文档,您需要为您想要记录的每个请求/响应编写单元或集成测试,并在测试中包含JUnitRestDocumentation规则。

例如,使用@SpringBootTest定义一个测试,或者在测试的设置方法中设置应用上下文,并使用@Rule定义一个JUnitRestDocumentation的实例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GettingStartedDocumentation {
  @Rule
  public final JUnitRestDocumentation restDocumentation =
                new JUnitRestDocumentation();

然后设置 MockMvc 实例

private MockMvc mockMvc;

@Before
public void setUp() {
  this.mockMvc =
  MockMvcBuilders.webAppContextSetup(this.context)
  .apply(documentationConfiguration(this.restDocumentation))
  .alwaysDo(document("{method-name}/{step}/",
        preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint())))
        .build();
}

使用以下静态导入

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static
org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
.documentationConfiguration;

对于使用mockMvc的 JUnit 测试中的每个测试方法,Spring REST Docs 现在将(在构建期间)为每个 HTTP 请求创建一个目录,该目录通过将测试名称从 CamelCase 转换为破折号分隔的名称来命名(例如,creatingACourse变成 creating-a-course)和一个数字索引目录。例如,如果一个测试中有四个请求,那么您将拥有目录1/ 2/ 3/和4/。每个 HTTP 请求依次获得以下生成的代码片段:

  • curl-request.adoc

  • httpie-request.adoc

  • http-request.adoc

  • http-response.adoc

  • request-body.adoc

  • response-body.adoc

然后,您可以在src/docs/asciidoc/目录下编写 Asciidoctor 文档,并将生成的片段包含到您的输出中,例如:

include::{snippets}/creating-a-course/1/curl-request.adoc[]

This text is included in output.

include::{snippets}/creating-a-course/1/http-response.adoc[]

这将包括您的文档输出(通常是 HTML5 输出)中的每个前面的片段。

在 Spring Boot 提供文件服务

要在基于 Spring Boot 的项目中提供 HTML5 生成的文档,请将以下内容添加到 Gradle 构建文件中:

bootJar {
  dependsOn asciidoctor
  from ("${asciidoctor.outputDir}/html5") {
    into 'static/docs'
  }
}

Spring 的海涛

与 REST 密切相关的是作为应用状态引擎的超媒体的概念 ( HATEOAS ),5,它概述了来自 web 服务的每个响应应该如何提供描述其他端点的信息或链接,就像网站如何工作一样。spring hate OAS6有助于启用这些类型的 RESTful web 服务。

入门指南

首先,将 Spring HATEOAS 依赖项添加到您的项目中。如果使用 Spring Boot 和 Maven,请添加以下依赖项:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

如果将 Spring Boot 与 Gradle 一起使用,请使用以下依赖关系:

implementation 'org.springframework.boot:spring-boot-starter-hateoas'

创建链接

HATEOAS 的关键部分是链接,它可以包含 URI 或 URI 模板,允许客户端轻松导航 REST API,并提供未来的兼容性——客户端可以使用链接,允许服务器更改链接指向的位置。

Spring HATEOAS 提供了轻松创建链接的方法,比如LinkBuilder and WebMvcLinkBuilder。它还提供了在响应中表示链接的模型,比如EntityModel, PagedModel, CollectionModel, and RepresentationModel。使用哪种模型取决于返回哪种类型的数据一个实体(EntityModel),数据页(PagedModel),或其他。

让我们举一个使用WebMvcLinkBuilder和EntityModel:的例子

package com.apress.spring_quick.rest;

import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;

@RestController
public class GettingStartedController {
    @GetMapping("/")
    public EntityModel<Customer> getCustomer() {
        return EntityModel.of(new Customer("John", "Doe"))
.add(linkTo(GettingStartedController.class).withSelfRel())
    .add(linkTo(GettingStartedController.class)
    .slash("next").withRel("next"));
    }
}

在运行时,此端点将以 JSON 的形式返回以下内容(在本地运行时):

{
  "firstname":"John",
  "lastname":"Doe",
  "_links":{
    "self":{"href":"http://localhost:8080"},
    "next":{"href":"http://localhost:8080/next"}
  }
}

Hypertext Application Language

超文本应用语言(HAL) 7 是一个用于定义超媒体的标准草案,比如在 JSON 或 XML 代码中到外部资源的链接。该标准最初于 2012 年 6 月提出,专门用于 JSON,此后出现了两个版本,JSON 和 XML。两个关联的 MIME 类型是媒体类型:application/hal+xml和媒体类型:application/hal+json。HAL 由资源和链接组成。它可以有嵌入的资源,这些资源也有链接。例如,如果一门课程有许多测试,您可能会看到如下 HAL JSON:

{
    "_links": {
        "self": { "href": "http://localhost:8080/courses" },
        "next": { "href": "http://localhost:8080/courses?page=2" },
        "my:find": {
            "href": "http://localhost:8080/courses/{?name}",
            "templated": true
        }
    },
    "total": 14,
    "_embedded": {}
}

测试

测试 HATEOAS 输出的方法类似于测试任何生成 XML 或 JSON 的 web 服务。

在服务生成 JSON 的常见情况下,使用一个库来导航 JSON 会很有帮助,就像 XPath 导航 XML 文档一样, JsonPath 。一个用 Java 实现 JsonPath 的库是 Jayway JsonPath 。 8 虽然你可以直接使用它,但是 Spring 用静态MockMvcResultMatchers.jsonPath方法包装了 JsonPath 的用法,以便于使用 Hamcrest 匹配器。

要使用 JsonPath,我们只需要在 Maven pom 中包含一个依赖项:

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
    <scope>test</scope>
</dependency>

或者,如果使用 Gradle,包括

testCompile 'com.jayway.jsonpath:json-path:2.4.0'

例如,参见下面的 JUnit 测试类,它使用 JsonPath 来验证_links.self和_links.next不为空:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import static org.hamcrest.Matchers.*;

@ExtendWith(SpringExtension.class) // JUnit 5
@SpringBootTest
public class GettingStartedDocumentation {

  @Autowired
  private WebApplicationContext context;

  private MockMvc mockMvc;

  @BeforeEach
  public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .build();
  }

  @Test
  public void index() throws Exception {
    this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("_links.self", is(notNullValue())))
        .andExpect(jsonPath("_links.next", is(notNullValue())));
  }
}

Listing 11-1GettingStartedDocumentation.java

十二、反应器

反应器 1 是 Spring 的 reactive streams 实现(在版本 3 及以后)。它有两种主要的发布者类型,Flux<T>和Mono<T>。它使用调度程序来决定在哪个线程上运行每个操作。

Spring 框架在许多方面与 Reactor 集成,使其更容易与其他 Spring 项目(如 Spring Data 和 Spring Security)一起使用。Spring WebFlux 是一个 web 框架,很像 Spring MVC,但它是围绕反应流构建的,能够在 Netty 上运行,Netty 是一个非阻塞 I/O 客户机-服务器框架。

为什么使用 Reactor?

Reactor 和 reactive streams 的目的通常是使对大量数据的操作能够以最高效、可伸缩和最快的方式分解并在许多不同的线程(多线程)上执行。虽然使用 Java 8 的并行流可以简单地实现并行处理,但是反应式流增加了大量额外的功能和定制,比如错误处理、重试、缓存和重放流、处理背压等等。

您可以将一个反应流想象成有三个轨道,数据轨道、完成轨道(不管流是否已经完成)和错误轨道。此外,每个 rails 都可以转换成另一个 rails:完整的流可以被替换,操作可以抛出异常,或者异常可以被处理并用更多的数据替换。

此外,Reactor 还增加了上下文的概念,我们将在本章的后面探讨。

入门指南

如果您有一个 Maven 版本,请将以下内容添加到 pom 文件中:

<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-core</artifactId>
  <version>3.3.7.RELEASE</version>
</dependency>
<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-test</artifactId>
  <version>3.3.7.RELEASE</version>
  <scope>test</scope>
</dependency>

对于 Gradle 构建,将以下内容添加到 Gradle 构建文件的依赖项中:

implementation 'io.projectreactor:reactor-core:3.3.7.RELEASE'
testImplementation 'io.projectreactor:reactor-test:3.3.7.RELEASE'

流量

Flux<T>是反应器反应物流的主要入口。 2 Mono<T>就像是一个Flux<T>除了零或一个元素。Mono<T>和Flux<T>都执行org.reactivestreams.Publisher<T>。

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

像任何反应流实现一样,Reactor 使用调度器来决定每个操作运行哪个线程。

Flux.range(1, 100)
  .publishOn(Schedulers.parallel())
  .subscribe(v -> doSomething(v));

Reactor 中的错误处理是通过流上的方法调用来实现的。以下方法可用于通量<T>或单声道<T>(为简洁起见,省略通用类型):

  • onErrorResume(Function)–接受异常并返回不同的发布者作为回退流或辅助流

  • onErrorMap(函数)–接受异常并允许您修改它,或者如果您愿意,返回一个全新的异常

  • onErrorReturn(T)-提供出现错误时使用的默认值

  • dooner error(Consumer extends Throwable>)——允许您在不影响底层流的情况下处理错误

错误(抛出的异常)总是结束一个流的事件,应该由订阅者来处理。然而,很多时候,如在前面的例子中,错误是不可能的,因此不需要处理。

单声道的

为什么有一个单独的类,称为 Mono,只有一个或零个元素?可以把它想象成 Java 8 的可选类到反应流世界的翻译。

Mono 与 Flux 非常相似,只是它有如下方法

  • justOrEmpty(T)–采用可空值并转换成单声道。如果为 null,结果与 Mono.empty()相同。

  • justOrEmpty(Optional<? extends T>)–取一个可选,直接转换成单声道。

与 Java 的可选不同,Mono 可以处理错误和其他事情。例如,返回 Mono 的方法可能会执行以下操作:

return Mono.error(new RuntimeException("your error"))

相应的代码可以像处理 Flux 一样处理来自单声道的错误(使用 onErrorResume、onErrorMap 或 onErrorReturn)。

创建通量或单声道

您可以从固定数据(冷数据)或以编程方式从动态数据(热数据)创建通量。

以下是产生冷流的一些不同方法:

  1. 从值列表中创建通量。

  2. 从一个迭代中产生一个通量。

  3. 创建一个从 1 到 64 的范围。

Flux<String> flux1 = Flux.just("a", "b", "foobar");       //1
List<String> iterable = Arrays.asList("a", "b", "foobar");
Flux<String> flux2 = Flux.fromIterable(iterable);         //2
Flux<Integer> numbers = Flux.range(1, 64);                //3

您可以创建一个空的或只有一个元素的简单单声道,如下所示:

  1. 创建一个空的单声道。

  2. 用一个元素创建一个单声道。

  3. 创建一个包装 RuntimeException 的单声道。

Mono<String> noData = Mono.empty();   //1
Mono<String> data = Mono.just("foo"); //2
Mono<String> monoError = Mono.error(new RuntimeException("error")); //3

您可以使用 generate、create 或 push 方法之一以编程方式创建通量。

generate 方法有多个重载定义,但是为了简单起见,让我们把重点放在接受一个 Supplier 和一个 BiFunction 的方法上。该函数将当前状态和 SynchronousSink 作为参数,后者可用于发布流的下一个状态。例如,以下代码使用 AtomicLong 实例从 0 到 10 递增数字,并提供每个数字的平方:

  1. AtomicLong 的构造器被用作提供者。

  2. 递增后,将数字的平方提供给接收器。

  3. 10 之后,调用 complete,它调用任何订阅者的 onComplete,关闭流。create 方法使用 next、error 和 complete 方法公开一个 FluxSink。这允许您任意地将数据发布到一个 Flux 上。

Flux<Long> squares = Flux.generate(
  AtomicLong::new, //1
  (state, sink) -> {
    long i = state.getAndIncrement();
    sink.next(i * i); //2
    if (i == 10) sink.complete(); //3
    return state;
});

例如,下面演示了如何注册一个处理消息列表的 MessageListener:

Flux<String> bridge = Flux.create(sink -> {
 messageProcessor.register(
  new MessageListener<String>() {
  public void handle(List<String> chunks) {
  for(String s : chunks) {
   sink.next(s);
  }
 }
 public void processComplete() {
  sink.complete();
 }
 public void processError(Throwable e) {
  sink.error(e);
 }
});
});

如果这里处理的消息有单线程源,可以用 push 方法代替 create 。

调度程序

reactor.core.scheduler 包下的 Schedulers 类提供了许多静态方法来提供调度程序,这些调度程序确定您的代码将在哪个或哪些线程上运行。

下面是一些静态方法及其含义:

  • schedulers . immediate()–当前线程。

  • schedulers . single()–单个可重用的线程。请注意,该方法对所有调用方重用同一个线程,直到调度程序被释放。如果您想要一个针对每个调用的专用线程,请对每个调用使用 Schedulers.newSingle()。

  • schedulers . elastic()–一个弹性线程池。它根据需要创建新的工作池,并重用空闲的工作池。闲置时间过长(默认值为 60 秒)的工作池将被释放。例如,对于 I/O 阻塞工作,这是一个很好的选择。Schedulers.elastic()是一种为阻塞进程提供自己的线程的简便方法,这样它就不会占用其他资源。

  • schedulers . parallel()–一个固定的工作池。它会创建与 CPU 核心数量一样多的工作线程。

  • schedulers . from Executor(Executor)–创建一个调度程序来使用给定的执行器,允许您使用 Java 执行器的丰富知识。

例如,让我们以生成正方形为例,让它并行运行:

  1. 首先,我们使用 Flux.range 获取从 1 到 64 的范围,并调用 flatMap(它采用一个 lambda 表达式,将范围内的每个值转换为一个新的反应器类型,在本例中为 Mono)。

  2. 使用 Schedulers.newSingle(name),我们为每个值创建一个新的单线程,传递给 subscribeOn 将导致映射表达式在该单线程上执行。请记住,我们在这里描述的是单声道的执行,而不是初始流量。

  3. 为了以防万一,我们提供了使用 doOnError 的异常处理代码。

  4. 使用 doOnComplete,当整个执行完成时,我们打印出“Completed”。

  5. 最后,我们订阅通量(没有这一步,什么都不会发生)并将结果添加到我们的正方形列表中。

List<Integer> squares = new ArrayList<>();
Flux.range(1, 64).flatMap(v -> // 1
Mono.just(v)
 .subscribeOn(Schedulers.newSingle("comp"))           //2
 .map(w -> w * w))
 .doOnError(ex -> ex.printStackTrace())               //3
 .doOnComplete(() -> System.out.println("Completed")) //4
 .subscribeOn(Schedulers.immediate())
 .subscribe(squares::add);                            //5

这里我们再次看到在反应流中,任何东西都可以变成一个流,甚至是一个值。通过为范围内的每个值创建一个 Mono,我们能够使用 Reactor 来声明我们希望每个计算使用哪种线程。在这种情况下,由于我们使用了newSingle,所有的处理都将通过一个新的线程对 64 个值中的每一个进行并行处理。

然而,这可能不是最有效的实现,因为创建大量线程会导致大量开销。相反,我们应该使用Schedulers.parallel(),这样就可以精确地计算出 CPU 可以处理的线程数量。这样,Reactor 会为您处理细节。

拉事件

如果您有更多的“拉”的情况(事件是通过轮询一个源创建的),您可以使用FluxSink<T>的 onRequest 方法。例如,以下代码创建了一个 Flux,用于轮询信道中的新事件:

  1. 当使用给定的数目发出请求时,轮询来自通道的事件。这个“n”是请求的项目数。

  2. 当通量被取消时,调用通道的cancel方法。

  3. channel.close()方法提供给onDispose用于完成、出错或取消调用。

  4. 最后,将接收器的next方法注册为通道的侦听器。

Flux<String> bridge = Flux.create(sink -> {
sink.onRequest(n -> channel.poll(n)) // 1
  .onCancel(channel::cancel)         // 2
  .onDispose(channel::close);        // 3
  channel.register(sink::next);      // 4
});

请记住,onRequest 不会无缘无故被调用多次。反应器非常精确。

它将使用某个数字(比如 32)调用 onRequest,然后直到有大量的项目被发布到 Flux 时才再次调用它(即在 sink.next 被调用 32 次之后)。

本章中使用的代码示例可以在网上找到。3

处理背压

像所有反应流的实现一样,Reactor 具有处理背压的能力。只需在通量(或其他未列出的通量)上使用以下方法之一来指定您想要使用的背压策略:

  • onBackpressureBuffer()–缓冲所有项目,直到它们可以被下游处理。

  • onBackpressureBuffer(maxSize)–最多缓冲给定计数的项目。

  • onBackpressureBuffer(maxSize,BufferOverflowStrategy)-将项目缓冲到给定的计数,并允许您指定 BufferOverflowStrategy,例如 onBackpressureBuffer(100,bufferoverflow strategy。DROP_OLDEST)。

  • onbackpressurelast()–类似于只保存最后添加的项目的缓冲区。如果下游没有跟上上游,那么只会给下游最新的元素。

  • onBackpressureError()–如果上游生成的项目多于下游请求的项目,则通过 Exceptions.failWithOverflow()中的 IllegalStateException 错误(调用下游订阅者的 onError)结束流量。

  • onBackpressureDrop()–删除超出请求范围的所有项目。

  • onBackpressureDrop(Consumer)–丢弃超出请求的任何项目,并为每个丢弃的项目调用给定的使用者。

对于这些方法中的每一种,只有当项目在流上产生的速度快于下游(订户)可以处理的速度时,该策略才适用。如果不是这种情况,例如,对于冷流,没有背压策略是必要的。

还要记住,反应器并不神奇,在考虑背压策略时应该小心。

img/498572_1_En_12_Figb_HTML.jpg背压是指当流中的事件/数据过多,下游无法处理时所发生的情况。打个比方,想想在一些城市的高峰时段,当交通陷入停滞时,或者当地铁列车满员时,会发生什么。反压力是一种减慢速度的反馈机制。4

语境

Reactor 附带了一个与ThreadLocal有些类似的高级特性,但它应用于一个Flux或一个Mono,而不是一个线程:??。

Reactor 的Context很像一个不可变的映射或键/值存储。它是从订阅者到订阅者透明地存储的。

上下文是特定于反应器的,不与其他反应流实现一起工作。

当设置上下文时,您不应该在流程开始时定义它。例如,不要这样做(如您所料,上下文在下游将不可用):

// this is WRONG!
Flux<Integer> flux = Flux.just(1).subscriberContext(Context.of("pid", 12));

相反,您应该将它定义到末尾,因为它会沿着链“向后”传播,例如:

Flux<Integer> flux = Flux.just(1);
Flux<String> stringFlux = flux.flatMap(i ->
Mono.subscriberContext().map(ctx -> i + " pid: " +
                           ctx.getOrDefault("pid", 0)));
// supply context here:
StepVerifier.create(stringFlux.subscriberContext(Context.of("pid", 123)))
  .expectNext("1 pid: 123")
  .verifyComplete();

前面的代码使用 StepVerifier (我们将在接下来介绍)来验证我们是否获得了预期的值。

注意我们如何在 Mono 上使用静态方法Mono.subscriberContext()来访问上下文。

Reactor 拥有出色的在线文档。 5

测试

自动化测试总是一个好主意,如果有工具来直接测试反应流就更好了。幸运的是,Reactor 附带了一些专门用于测试的元素,这些元素被收集到我们在本章开始时包含的它们自己的工件中:reactor-test。

反应器测试的两个主要用途如下:

  • 使用 StepVerifier 测试序列是否遵循给定的场景

  • 生成数据,以便用 TestPublisher 测试下游操作符(包括您自己的操作符)的行为。

步骤验证器

Reactor 的 StepVerifier 可以用来验证 Reactor 发布者的行为(Flux 或 Mono)。

下面是一个利用 StepVerifier 进行 JUnit 测试的简单示例:

  1. 创建一个Mono包装一个RuntimeException模仿一个实际的错误状态。

  2. 创建一个StepVerifier包装单声道。

  3. 声明一个onError事件是预期的,并且异常的错误消息是“error”。

  4. 我们称之为verify()结尾。如果有任何期望没有实现,这将抛出一个AssertionError。

@Test
public void testStepVerifier_Mono_error() {
  Mono<String> monoError = Mono.error(new RuntimeException("error")); //1
  StepVerifier.create(monoError) //2
    .expectErrorMessage("error") //3
    .verify(); //4
}

接下来,我们将创建一个只有一个字符串的单声道并验证它:

  1. 创建一个单声道包装一个值,“foo”。

  2. 创建一个包装单声道的 StepVerifier。

  3. 用“foo”调用 Expect onNext。

  4. 调用 verifyComplete()的效果与 verify()相同,但也要求调用 onComplete。

@Test public void testStepVerifier_Mono_foo() {
  Mono<String> foo = Mono.just("foo"); //1
  StepVerifier.create(foo)             //2
    .expectNext("foo")                 //3
    .verifyComplete();                 //4
}

这里,我们将使用三个值测试流量,如果测试时间过长,将会超时:

  1. 创造一个只有三个数字的通量。

  2. 创建包裹焊剂步进检验器。

  3. 为每个预期值调用 expectNext。

  4. 调用 expectComplete 以期望调用 onComplete。

  5. 最后,必须在最后调用 verify()。这种验证变化采用持续时间超时值。在这里,是 10 秒。在发布者可能永远不会调用 onComplete 的情况下,这有助于防止测试挂起。

@Test public void testStepVerifier_Flux() {
  Flux<Integer> flux = Flux.just(1, 4, 9); //1
  StepVerifier.create(flux)                //2
    .expectNext(1)                         //3
    .expectNext(4)
    .expectNext(9)
    .expectComplete()                      //4
    .verify(Duration.ofSeconds(10));       //5
}

测试发布者

TestPublisher<T>类提供了为测试目的提供微调数据的能力。TestPublisher<T>是一个反应流发布者,但是可以使用 flux()或 mono()方法转换成 Flux 或 Mono。

TextPublisher 有以下方法:

  • 下一个(T)和下一个(T,T...)–触发 1-n onNext 信号

  • 发出(T...)-与 next 相同,也以 onComplete 信号结束

  • 完成

  • error(Throwable)-以 onError 信号终止。

下面演示了如何使用TestPublisher<T>:

  1. 创建 TestPublisher 实例。

  2. 将其转化为通量。

  3. 创建新列表。出于测试目的,我们将使用该列表从发布者处收集值。

  4. 使用 onNext 和 onError 的两个 lambda 表达式订阅发布服务器。这将把发布者发出的每个值添加到列表中。

  5. 最后,从 TestPublisher 发出值“foo”和“bar”。

  6. 断言列表的大小是预期的 2。

TestPublisher<Object> publisher = TestPublisher.create(); //1
Flux<Object> stringFlux = publisher.flux();               //2
List list = new ArrayList();                              //3
stringFlux.subscribe(next -> list.add(next),
                     ex -> ex.printStackTrace());         //4
publisher.emit("foo", "bar");                             //5
assertEquals(2, list.size());                             //6
assertEquals("foo", list.get(0));
assertEquals("bar", list.get(1));

注意,在发出任何值之前,您必须订阅TestPublisher(在前面的例子中是通过订阅stringFlux来完成的)。

元组和 Zip

元组是两个或更多元素的强类型集合,Reactor 内置了它们。一些操作如zipWith返回元组的反应流。

Flux 有一个实例方法zipWith(Publisher<? extends T2> source2),它的返回类型为Flux<Tuple2<T,T2>>。它等待两个通量(初始通量和源 2)发射一个元素,然后将两者组合成一组 2。还有一个静态方法 Flux.zip,它被重载以接受 2 到 8 个发布者,并将它们压缩成元组。

当您想要执行返回反应结果(通量或单声道)的多个操作并组合它们时,压缩非常有用。

Mono 有两种主要的压缩方式(非静态方法,都有一个返回类型Mono<Tuple2<T,T2>>):

  • zipWith(Mono<? extends T2> other)–用另一个流压缩当前流,以元组 2 的形式给出每个对应元素的组合。

  • zipWhen(Function<T,Mono<? extends T2>> rightGenerator)–将当前单声道与另一个单声道压缩,以 Tuple2 的形式给出每个对应元素的组合,但仅在第一个流的操作完成后,从而允许您使用第一个单声道的结果来生成第二个单声道。

例如,假设您有两个执行异步操作的方法 Mono getCourse(Long id)和 MonogetStudentCount(Course Course ),假设您想从课程 id 中获取学生人数,您可以执行以下操作:

Mono<Integer>  getStudentCount(Long id) {
  return getCourse(id)
   .zipWhen(course -> getStudentCount(course))
   .map(tuple2 -> tuple2.getT2());
}

这是一个简单的例子,但是您可以想象组合两个不同的实体,或者在返回之前对它们执行逻辑,或者调用另一个带有两个参数的方法,等等。

反应器附件

Project Reactor 在io.projectreactor.addons groupId 下提供额外的功能。Reactor extra 包括额外的数学函数,不同的重试方式,包括抖动和后退,以及 TupleUtils。

<dependency>
    <groupId>io.projectreactor.addons</groupId>
    <artifactId>reactor-extra</artifactId>
    <version>3.3.3.RELEASE</version>
</dependency>

对于 Gradle 构建,将以下内容添加到 Gradle 构建文件的依赖项中:

implementation 'io.projectreactor.addons:reactor-extra:3.3.3.RELEASE'

当您的应用在一个集成点失败时,比如调用另一个 RESTful 服务时,为了使您的整个系统可靠,您可能需要重试调用几次。但是,为了防止失败的服务过载,您应该采用回退或增加每次重试之间的时间,以及抖动,随机修改时间,以便来自许多不同实例的重试不会同时发生(相关)。例如,看一下下面的代码:

  1. 我们用 IOException 的异常值创建 Retry,这意味着只有在抛出异常时才会重试(这里可以提供任何异常类;例子只有 IOException)。

  2. 我们将指数回退定义为初始值为 100 毫秒,最大值为 60 秒。

  3. 我们添加了随机抖动,并将重试最大值设置为 5,这意味着它最多重试五次。

  4. 我们添加了 Spring ApplicationContext,并使用它在每次失败后应用回滚。

  5. 最后,我们在一个 Flux 实例上调用 retryWhen(retry ),对该 Flux 应用重试。

 var retry = Retry.anyOf(IOException.class)           \\1
         .exponentialBackoff(Duration.ofMillis(100),  \\2
                        Duration.ofSeconds(60))
         .jitter(Jitter.random())                     \\3
         .retryMax(5)
         .withApplicationContext(appContext)          \\4
         .doOnRetry(context ->
             context.applicationContext().rollback());
    return flux.retryWhen(retry);                     \\5

img/498572_1_En_12_Figd_HTML.jpg关于重试、退避和抖动的更多信息,请参见亚马逊构建者图书馆的这篇优秀的文章。 6

十三、Spring Integration

Spring Integration 是一个支持众所周知的企业集成模式的编程模型。

特征

Spring Integration 实现了许多常见的企业集成模式, 1 比如通道、聚合器、过滤器和转换器,并提供了许多不同消息传递实现的抽象。

img/498572_1_En_13_Fig1_HTML.jpg

图 13-1

企业集成

Spring Integration 提供了一个消息传递范例,将一个应用分成多个组件,这些组件在相互不了解的情况下进行通信。除了将细粒度组件连接在一起,Spring Integration 还提供了许多通道适配器和网关来与外部系统通信。通道适配器用于单向集成(发送或接收),网关用于请求/回复场景(入站或出站)。

支持的消息传递包括但不限于

  • REST/HTTP

  • FTP/SFTP

  • 推特

  • Web 服务(SOAP)

  • TCP/UDP

  • (同 JavaMessageService)Java 消息服务

  • 拉比特

  • 电子邮件

img/498572_1_En_13_Figa_HTML.jpgSpringCloud 集成

Spring Cloud Stream 项目建立在 Spring Integration 之上,Spring Integration 被用作消息驱动微服务的引擎。这在第十八章中有所涉及。

入门指南

最简单的开始方式是使用 Spring Initializr 或 Spring Boot CLI 创建一个新项目(它们将在第十五章中介绍)。在现有项目中,添加以下依赖项:

implementation 'org.springframework.boot:spring-boot-starter-integration'
testImplementation 'org.springframework.integration:spring-integration-test'

然后还包括您的项目需要的任何其他 Spring Boot 启动器或其他库,例如:

implementation 'org.springframework.boot:spring-boot-starter-amqp'
testImplementation 'org.springframework.amqp:spring-rabbit-test'

这带来了用于 AMQP 的 Spring Boot 启动器和用于测试与 RabbitMQ 集成的 spring-rabbit-test。

然后,在 Spring Boot 应用中,将@ EnableIntegration注释添加到您的一个配置类中,这将执行以下操作:

  • 注册一些内置的 beans,比如errorChannel及其LoggingHandler、taskScheduler用于轮询器、jsonPath SpEL-function 等等。

  • 添加几个BeanFactoryPostProcessor实例。

  • 添加几个BeanPostProcessor实例来增强或转换和包装特定的 beans,以便进行集成。

  • 添加注释处理器来解析消息传递注释,并在应用上下文中为它们注册组件。

您还可以使用@IntegrationComponentScan来扫描类路径,寻找特定于 Spring Integration 的注释,比如@MessagingGateway注释。

综上所述,您的主应用类可能如下所示:

@EnableIntegration
@IntegrationComponentScan
@SpringBootApplication
public class SpringIntApplication {

        public static void main(String[] args) {
                SpringApplication.run(SpringIntApplication.class, args);
        }
}

添加附加支持

一般来说,当您想要将 Spring Integration 与特定的技术(比如 JPA)结合使用时,您可以在 org . Spring framework . Integration groupId 下包含名为 spring-integration- X 的附加构件,例如,对于 Kafka: 2

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-kafka</artifactId>
    <version>3.3.0.RELEASE</version>
</dependency>

一些可用的支持:

| 作业的装配区(JobPackArea) | 超文本传送协议 | 数据库编程 | (同 JavaMessageService)Java 消息服务 | | 邮件 | MongoDB | 卡夫卡 | 使用心得 | | 资源 | 无线电磁指示器(Radio Magnetic Indicator 的缩写) | 窝 | science for the people 为人类服务的科学 | | 跺脚 | 溪流 | 系统记录 | TCP 和 UDP (ip) | | webflux | 网络服务 | 可扩展置标语言 | XMPP |

消息网关

消息网关是在现有消息传递技术上使用的抽象 Spring Integration,它允许您的代码与接口进行交互,而无需了解底层通道。当您用@ MessagingGateway注释一个接口,用@ Gateway注释一个或多个方法时,Spring 在运行时使用来自您包含的支持工件的底层技术用代理实现接口。

例如,对于 Kafka 消息网关:

  1. 定义 requestChannel 来发送数据,在本例中是一个字符串负载。

  2. 使用@Header定义一个头,在本例中,消息有效负载将被发送到 Kafka 主题。

  3. 定义 replyChannel,可用于从 Kafka 获取消息。注意,返回类型是 Spring 的 Message 接口,这是一个抽象,可以用于任何消息传递系统。replyTimeout 以毫秒为单位,所以这里是 10 秒。

//Use the following imports:
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.Gateway;
import org.springframework.kafka.support.KafkaHeaders;
@MessagingGateway

public interface KafkaGateway {
    @Gateway(requestChannel = "toKafka.input")              \\1
    void sendToKafka(String payload,
                     @Header(KafkaHeaders.TOPIC) String topic);                                \\2

    @Gateway(replyChannel = "fromKafka", replyTimeout = 10000)  \\3
    Message<?> receiveFromKafka();
}

假设一切都设置正确,Spring Integration 在运行时将 KafkaGateway 接口实现为 Spring Bean,因此可以通过以下方式调用它:

KafkaGateway kafkaGateway = context.getBean(KafkaGateway.class);
String message = "any message";
String topic = "topic";
kafkaGateway.sendToKafka(message, topic);

集成流程

创建流有两种主要方法(实现 IntegrationFlow 接口),要么使用 lambda 表达式,要么使用从IntegrationFlows类开始的 fluent builder DSL。

在第一个实例中,我们利用了IntegrationFlow是一个 SAM(单一抽象方法)接口的事实,因此可以提供一个带有一个参数的 lambda 表达式,Java 将知道它从返回类型实现了该接口,例如:

    @Bean
    public IntegrationFlow toKafka(KafkaTemplate<?, ?> kafkaTemplate) {
        return flowDefinition -> flowDefinition
                .handle(Kafka.outboundChannelAdapter(kafkaTemplate)
                        .messageKey("si.key"));
    }

IntegrationFlows类可以用来创建一个IntegrationFlow,例如:

    @Bean
    public IntegrationFlow fromKafkaFlow(
                   ConsumerFactory<?, ?> consumerFactory) {
        return IntegrationFlows
          .from(Kafka.messageDrivenChannelAdapter(consumerFactory, topic))
          .channel((Channels c) -> c.queue("fromKafka"))
          .get();
    }

静态方法IntegrationFlows.from返回一个扩展了IntegrationFlowDefinition的IntegrationFlowBuilder,并有一个“get()”方法返回一个新的StandardIntegrationFlow实例。IntegrationFlowDefinition上的方法可以让你流畅地构建一个IntegrationFlow,包括以下内容:

  • aggregate——abstractcorrelationmessagehandler 的特定于聚合器的实现,abstractcorrelationmessagehandler 是一个消息处理程序,它在 MessageStore 中保存相关消息的缓冲区。它负责可以批量完成的相关消息组。

  • barrier——一个消息处理程序,它挂起线程,直到具有相应相关性的消息被传递到触发器方法或超时发生。

  • bridge——一个简单的 MessageHandler 实现,它将请求消息直接传递到输出通道,而不修改它。该处理程序的主要目的是将 PollableChannel 连接到 SubscribableChannel,反之亦然。

  • 通道–定义发送消息的方法。

  • claimcheck in–使用提供的 MessageStore 填充 ClaimCheckInTransformer 的 MessageTransformingHandler。

  • claimcheck out–使用提供的 MessageStore 填充 ClaimCheckOutTransformer 的 MessageTransformingHandler。

  • Control Bus–在当前 IntegrationFlow 链位置填充特定于控制总线 EI 模式的 MessageHandler 实现。

  • convert–为运行时要转换的提供的 payloadType 填充 MessageTransformingHandler 实例。

  • delay–将 DelayHandler 填充到当前的集成流位置。

  • enrich–使用提供的选项将 ContentEnricher 填充到当前集成流位置。ContentEnricher 是一个消息转换器,可以用动态或静态值增加消息的有效负载。

  • enrich headers–填充 MessageTransformingHandler,将静态配置的头值添加到消息中。

  • filter–如果消息通过给定的 MessageSelector,则 MessageFilter 仅传递到过滤器的输出通道。

  • fixedSubscriberChannel–在当前 IntegrationFlow 链位置填充 fixedSubscriberChannel 的一个实例(在 bean 实例化过程中为单个最终订户设置的专用 SubscribableChannel)。

  • Flux transform–填充一个 FluxMessageChannel 以启动对上游数据的反应式处理,将其包装到一个 Flux,通过 Flux.transform(Function)应用所提供的函数,并将结果发送到下游流中订阅的另一个 FluxMessageChannel。

  • gateway–为提供的子流或通道填充“人工”GatewayMessageHandler。

  • handle–为提供的 MessageHandler 或 MessageProcessorSpec bean 和方法名填充 ServiceActivatingHandler。

  • headerFilter–为当前的 StandardIntegrationFlow 提供 header filter。

  • log–填充当前消息通道的窃听,并使用 LoggingHandler,这是一个简单记录消息或其有效负载的 MessageHandler 实现。

  • Logan reply——该操作符只能在流的末尾使用。与“日志”方法相同。返回 IntegrationFlow 。

  • null channel–将 bean 作为终端操作符添加到该流定义中。返回 IntegrationFlow 。

  • publishSubscribeChannel——PublishSubscribeChannel(向每个订户发送消息的通道)BaseIntegrationFlowDefinition.channel(java.lang.String)方法——允许使用“子流”订户功能的特定实现。

  • 重新排序–填充一个重新排序 MessageHandler,它使用 MessageStore 中相关消息的缓冲区对消息进行重新排序。

  • 路线——这种方法有许多不同的变体。它们填充 MethodInvokingRouter,或者如果提供了 SpEL 表达式,则填充 ExpressionEvaluatingRouter,然后确定要使用的 MessageChannel 或通道名称。

  • routeByException–可以按异常类型路由消息。

  • route torecipients–使用 RecipientListRouterSpec 中的选项填充 RecipientListRouter,RecipientListRouterSpec 在多个通道上发送消息。

  • scatterGather 根据为分散函数提供的 MessageChannel 和为收集函数提供的 AggregatorSpec,将 ScatterGatherHandler 填充到当前的集成流位置。

  • split–使用提供的 SpEL 表达式填充 MethodInvokingSplitter 以在运行时评估服务提供的方法,或者填充 ExpressionEvaluatingSplitter。分割器将消息分割成多个消息。

  • transform–为提供的 GenericTransformer 填充 MessageTransformingHandler 实例。

  • trigger–填充 ServiceActivatingHandler 实例以执行 MessageTriggerAction。

  • wireTap–将 Wire Tap EI 模式特定的 ChannelInterceptor 实现填充到 currentMessageChannel。

这绝不是详尽无遗的。这些方法中的大多数都有几个重载的变体。

Kafka 配置

然后,您可以在spring.kafka.consumer和spring.kafka.producer下的application.yml中配置 Spring Integration Kafka 的特定设置,例如:

spring:
  kafka:
    consumer:
      group-id: siTestGroup
      auto-offset-reset: earliest
      enable-auto-commit: false
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      batch-size: 16384
      buffer-memory: 33554432
      retries: 0
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

Listing 13-1application.yml

img/498572_1_En_13_Figb_HTML.jpg安装卡夫卡留给读者作为练习。前往 https://kafka.apache.org/quickstart 并按照指示进行操作。然后按照本章的内容设置 Spring Integration。

主题

因为我们使用的是卡夫卡,所以我们也需要首先创建主题。

由于我们使用的是带自动配置的 Spring Boot,如果我们提供 NewTopic Spring Beans,Spring Boot 的自动配置 KafkaAdmin(来自spring-integration-kafka))将为我们提供主题,例如:

    @Bean
    public NewTopic topic() {
        return new NewTopic("topic", 5, (short) 1);
    }
    @Bean
    public NewTopic newTopic() {
        return new NewTopic("topic2", 5, (short) 1);
    }

这将创建名为“topic”和“topic2”的两个主题,复制 1 个(意味着只存储一个副本)和 5 个分区,这意味着数据将被分成 5 个分区。

监控

默认情况下,如果存在一个千分尺meterRegistry bean,这将是一个包括 Spring 致动器的 Spring Boot 项目的情况,Spring Integration 度量将由千分尺管理。如果您希望使用遗留的 Spring Integration 指标,可以向应用上下文添加一个DefaultMetricsFactory(来自 Spring Integration) bean。

十四、SpringBatch

Spring Batch 是一个为企业系统支持长时间运行的数据转换或类似的长时间运行过程的项目。它有大量的特性,其中一些我们将会谈到。

特征

Spring Batch 提供了分区和处理大量数据的特性。它还提供了在处理大量记录时必不可少的可重用功能,包括事务管理、作业处理统计、作业重启、重试和跳过、日志记录和跟踪以及资源管理。

概观

在大图中,Spring Batch 由 JobLauncher、JobRepository、Jobs、Steps、ItemReaders、ItemProcessors 和 ItemWriters 组成。

JobLauncher 使用给定的作业参数运行作业。每个作业可以有多个步骤。每个步骤通常由一个 ItemReader、ItemProcessor 和 ItemWriter 组成。使用 JobRepository 保存和加载元数据,或关于每个实体状态的信息。

img/498572_1_En_14_Figa_HTML.jpg

这个例子

为了演示 Spring Batch,我们将使用一个示例。在本例中,我们将使用一个简单的课程定义。Spring Batch 将用于加载定义课程的 CSV 文件,转换值,并将新的课程行保存到数据库中。

建设

为了简单起见,我们将使用 Spring Boot(这将在下一章更全面地介绍)。首先,我们将使用 spring-batch 定义一个 Gradle 构建,然后我们将讨论 Maven 构建。

Gradle Build

创建一个名为build. gradle的文件,内容如下:

plugins {
  id 'org.springframework.boot' version '2.3.0.RELEASE'                 //1
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-batch' //2
  runtimeOnly 'org.hsqldb:hsqldb'
  testImplementation('org.springframework.boot:spring-boot-starter-test')
  {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
  testImplementation 'org.springframework.batch:spring-batch-test' //3
}
test {
  useJUnitPlatform() //4
}

  1. 我们为 Spring Boot 和 Spring 依赖管理应用插件,这允许我们在依赖块中删除版本。

  2. 这一行定义了 spring-boot-starter-batch,它引入了 Spring Batch 所需的所有 jar。在下一行,我们包含 hsqldb 1 作为数据库。

  3. 还有一个专门用于测试 Spring Batch 的库,spring-batch-test。

  4. 这一行告诉 Gradle 使用 JUnit 5 进行测试。

Maven 构建

使用以下内容创建一个名为“pom.xml”的文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.3.0.RELEASE</version>
                <relativePath/>
        </parent>
        <groupId>com.example</groupId>
        <artifactId>batch-processing</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>batch-processing</name>
        <description>Demo project for Spring Boot, Batch</description>

        <properties>
                <java.version>1.8</java.version>
        </properties>

        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-batch</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.hsqldb</groupId>
                        <artifactId>hsqldb</artifactId>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                        <exclusions>
                                <exclusion>
                            <groupId>org.junit.vintage</groupId>
                            <artifactId>junit-vintage-engine</artifactId>
                                </exclusion>
                        </exclusions>
                </dependency>
                <dependency>
                        <groupId>org.springframework.batch</groupId>
                        <artifactId>spring-batch-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>
        <build>
                <plugins>
                        <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
</project>

除了标准的 Spring Boot Maven 构建,我们还包括 hsqldb(数据库)、spring-boot-starter-batch 和 spring-batch-test。

img/498572_1_En_14_Figb_HTML.jpg由于 Spring Batch 通常涉及到与数据库的交互,并且默认情况下将元数据保存到数据库中,因此 Spring Batch 的启动依赖于spring-boot-starter-jdbc。

计划

由于 spring-boot-starter-jdbc 位于类路径中,并且我们已经包含了一个数据库(hsqldb ),所以初始化我们的数据库唯一需要做的就是在 src/main/resources/下包含一个名为 schema-all.sql 的文件。创建此文件并添加以下内容:

DROP TABLE course IF EXISTS

;

CREATE TABLE course  (
    course_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
    title VARCHAR(200),
    description VARCHAR(250)
);

课程

我们将课程实体定义为具有标题和描述的典型领域类(POJO ):

public class Course {
        private String title;
        private String description;

        public Course() {
        }
        public Course(String title, String description) {
                this.title = title;
                this.description = description;
        }

        //getters and setters...
        @Override
        public String toString() {
                return "title: " + title + ", description: " + description;
        }
}

课程处理器

Spring Batch 提供了ItemProcessor<I,O>接口(I 代表输入,O 代表输出),用于在需要以某种方式修改或处理实体时实现逻辑。

在这种情况下,我们定义了一个实现ItemProcessor<I,O>的CourseProcessor,它用一个空格替换任意数量的空格,并修剪任何前导或尾随空格:

  1. 我们声明 CourseProcessor 实现了 ItemProcessor 接口,当然,in 和 out 类型是相同的。如果它们不同,第一个声明的类型将声明要处理的参数的类型,第二个类型将是返回类型。

  2. 这里,我们在标题和描述中都使用 replaceAll(使用正则表达式\s+)将任何空格替换为一个空格。我们创建一个新对象,这样处理器就是幂等的——它不应该修改输入对象。

  3. 最后,我们从 process 方法返回新的课程实例。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;

public class CourseProcessor implements ItemProcessor<Course, Course> { //1

  private static final Logger log =
     LoggerFactory.getLogger(CourseProcessor.class);

  @Override
  public Course process(final Course course) throws Exception {
    final String title = course.getTitle()
                               .replaceAll("\\s+", " ").trim(); //2
    final String description = course.getDescription()
                                     .replaceAll("\\s+", " ").trim();
    final Course transformedCourse = new Course(title, description);

    log.info("Converting (" + course + ") into (" + transformedCourse + ")");

    return transformedCourse; //3
  }
}

批量配置

最后,我们定义了一个@Configuration,它定义了 Spring Batch 将自动运行的步骤和作业。虽然在这种情况下我们有一个作业和一个步骤,但是也可能有多个作业和每个作业的一个或多个步骤。如果存在多个作业,您可以指定哪个或哪些作业作为属性运行(spring.batch.job.names)。

  1. @ EnableBatchProcessing启用 Spring Batch 的自动配置,提供默认的JobRepository、JobBuilderFactory、StepBuilderFactory等 Spring beans。

  2. 我们创建一个FlatFileItemReader<T>,它是 Spring Batch 提供的众多助手类之一。这里,我们定义从哪个文件中读取,并使用一个BeanWrapperFieldSetMapper<T>,我们定义在Course上设置哪些字段(使用 Java Bean 标准)。

  3. 我们创建一个JdbcBatchItemWriter<T>,它将记录插入到我们的数据库中。

  4. 使用StepBuilderFactory,我们创建一个步骤,该步骤将分十个过程进行处理(一次十个)。为了提高效率和性能,数据以块的形式进行处理。如果块中发生任何错误,整个块都将回滚。

  5. 我们使用 JobBuilderFactory 定义作业。

import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.*;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing                  //1
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Bean
    public FlatFileItemReader<Course> reader() {            //2
        return new FlatFileItemReaderBuilder<Course>()
               .name("personItemReader")
               .resource(new ClassPathResource("sample-data.csv"))
               .delimited()
               .names(new String[]{"title", "description"})
               .fieldSetMapper(new BeanWrapperFieldSetMapper<Course>() {{
                    setTargetType(Course.class);
               }})
               .build();
    }

    @Bean
    public CourseProcessor processor() {
        return new CourseProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Course> writer(DataSource dataSource) { //3
        return new JdbcBatchItemWriterBuilder<Course>()
           .itemSqlParameterSourceProvider(new
                  BeanPropertyItemSqlParameterSourceProvider<>())
                .sql("INSERT INTO course (title, description) VALUES" +
                     " (:title, :description)")
                .dataSource(dataSource)
                .build();
    }

    @Bean
    public Step readAndSaveStep(JdbcBatchItemWriter<Course> writer,  //4
                                CourseProcessor processor) {
        return stepBuilderFactory.get("saveStep")
                .<Course, Course>chunk(10)
                .reader(reader())
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    public Job importCourseJob(JobCompletionListener listener, Step step) {
        return jobBuilderFactory.get("importCourseJob")     //5
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step)
                .end()
                .build();
    }
}

Listing 14-1BatchConfiguration.java

对于本例,文件 sample-data.csv 可能如下所示(注意将被删除的多余空格):

Java   11,   Java 11 for beginners
Java    Advanced,  Advanced Java course
Spring    ,   Course for Spring Framework

JobExecutionListener

Spring Batch 发布可以使用JobExecutionListener监听的事件。例如,下面的类JobCompletionListener实现了afterJob方法,并仅在作业完成时打印出一条消息:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;

import org.springframework.stereotype.Component;

@Component
public class JobCompletionListener extends JobExecutionListenerSupport {

  private static final Logger log =
          LoggerFactory.getLogger(JobCompletionListener.class);

  @Override
  public void afterJob(JobExecution jobExecution) {
    if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
      log.info("JOB FINISHED!");
    }
  }
}

JobExecutionListenerSupport类实现了JobExecutionListener。这允许我们实现接口,并且只定义afterJob方法。

Spring 批处理元数据

Spring Batch 可以自动存储关于每个批处理执行的元数据作为审计记录,并帮助重启或事后分析错误。

Spring 批处理元数据表与用 Java 表示它们的域对象非常匹配。例如,JobInstance、JobExecution、JobParameters 和 StepExecution 分别映射到BATCH_JOB_INSTANCE、BATCH_JOB_EXECUTION、BATCH_JOB_EXECUTION_PARAMS和BATCH_STEP_EXECUTION。执行上下文映射到BATCH_JOB_EXECUTION_CONTEXT和BATCH_STEP_EXECUTION_CONTEXT.

img/498572_1_En_14_Figc_HTML.jpg使用 Spring Boot,您可以确保使用以下属性创建该模式(创建表):

spring.batch.initialize-schema=always

默认情况下,只有当您使用嵌入式数据库时,它才会创建表。同样,您甚至可以使用

spring.batch.initialize-schema=never

Spring 重试

通常,在运行批处理过程时,如果某个操作失败,您可能希望自动重试,多次尝试相同的操作。例如,可能会出现暂时的网络故障,或者数据库出现暂时的问题。这是一个普遍期望的特征;Spring 开发了Spring Retry2项目,通过 AOP 或者编程来实现这个横切特性。

要开始使用 spring-retry,首先将其包含在构建中:

| `Maven` | ```org.springframework.retry``spring-retry``1.3.0``` | | `Gradle` | `implementation 'org.springframework.retry:spring-retry:jar:1.3.0'` |

然后,为了使用声明式/AOP 方法,将@EnableRetry注释添加到您的一个 Java 配置类中(这告诉 Spring 扫描@Retryable 注释):

@Configuration
@EnableBatchProcessing
@EnableRetry
public class BatchConfiguration {

或者在命令式(编程式)方法中使用 Spring Retry,直接使用 RetryTemplate,例如:

RetryTemplate template = RetryTemplate.builder()
                                .maxAttempts(3)
                                .fixedBackoff(1000)
                                .retryOn(RemoteAccessException.class)
                                .build();

template.execute(ctx -> {
    // ... some code
});

在本例中,只有在抛出 RemoteAccessException 时,执行的代码才会重试三次,并且每次都会后退一秒(1000 毫秒)。

重试条款

| 最大尝试次数 | 最大重试次数。 | | 固定补偿 | 增加重试之间暂停的时间(毫秒)。 | | 指数后退 | 当有问题的系统由于过饱和而停机时,用于以指数方式增加重试之间的暂停时间(以毫秒为单位)的参数可以更好地解决问题。 | | 随机后退 | 最好包含随机性(从 0%到 200%的延迟时间)以避免重试的相关性(这样一堆节点就不会同时重试)。 |

可重试注释

使用 AOP 方法,您可以用@Retryable(在配置类上使用@EnableRetry之后)注释 Spring 自省的任何方法(在 Spring bean 的公共方法上)。例如,让我们修改之前的 CourseProcessor,最多重试四次:

@Retryable(maxAttempts = 4, backoff =
        @Backoff(random = true, delay = 100))
@Override
public Course process(final Course course) throws Exception {
  // code...
  return transformedCourse;

}

注意我们是如何使用@Backoff 注释设置回退的。

十五、Spring Boot

Spring Boot 消除了与构建 Spring 应用相关的大量样板文件,从而提高了开发人员的工作效率。除了注释扫描等普通的 Spring 特性之外,它还使用了约定和“starter”依赖,极大地简化了应用的开发。

同时,它非常灵活,给了开发人员很大的权限来决定包含什么,并且随着时间的推移,您可以根据需要修改配置。

Spring Boot 简介

Spring Boot 极大地简化了基于 Spring 的应用或微服务的创建。它简化了软件开发人员的生活,通过自动配置一些东西,消除了为每个依赖项指定版本的需要。

使用 Spring Boot,开发人员可以包含许多“起始依赖项”,每个“起始依赖项”本身都包含许多库和项目的自动配置。这极大地改善了启动和添加项目的体验。在许多情况下,Spring Boot 还包括合理的默认配置。随着项目的增长,开发人员可以覆盖默认设置,并以许多不同的方式定制项目。

您可以将 web 项目打包成 WAR 或单个 JAR 文件。如果使用 jar 文件方法(这是推荐的方法),Spring Boot 将使用一个嵌入式 web 容器,如 Tomcat 或 Jetty,并在 JAR 文件中包含所有的依赖项(称为“胖 JAR”)。

创建新项目

启动 Spring Boot 项目有几种方式:

  1. 转到 Spring Initializr1网站,在那里创建一个项目模板。还有像 Spring Tool Suite 这样的工具,它们利用了 IDE 中的 Spring Initializr。

  2. 创建自己的基于 Maven 的项目。

  3. 创建自己的基于 Gradle 的项目。

  4. 使用 Spring Boot 命令行界面。

您可以通过在浏览器中进入 https://start.spring.io 或者在命令行中使用 Spring Boot CLI,轻松创建一个包含任意数量 Spring Boot 启动器的新项目。它提供了一些选项,比如是使用 Maven 还是 Gradle 构建,使用什么测试框架,使用哪种语言(Java、Groovy 或 Kotlin)等等。然后创建构建文件,主应用类,一个基本 Spring Boot 应用的测试。

Spring Boot 命令行界面

您可以使用 SDKMAN: 2 轻松安装 Spring Boot CLI

$ sdk install springboot
$ spring --version
Spring Boot v2.3.0.RELEASE

首先,使用spring init --list查看所有可用的依赖项:

img/498572_1_En_15_Fig1_HTML.jpg

图 15-1

Spring 初始化列表:输出

$ spring init –list

然后,一旦您理解了您需要哪些依赖项,您就可以使用spring init命令来创建您的项目;例如:

img/498572_1_En_15_Fig2_HTML.jpg

图 15-2

新 Spring Boot 项目文件结构

$ spring init –d actuator,web,data-jpa,security my-project
Using service at https://start.spring.io
Project extracted to '/Users/developer/example/my-project'

回弹应用

@SpringBootApplication注释告诉 Spring 许多事情:

  1. 使用自动配置。

  2. 使用组件扫描。扫描所有的包(从类的包和所有的子包开始),寻找用 Spring 注释标注的类,比如@Component。

  3. 这个类是一个基于 Java 的配置类(与@Configuration相同),所以您可以在这里使用返回 bean 的方法上的@Bean注释来定义 bean。

  4. 它将该类标记为应用的主配置类(与@SpringBootConfiguration相同),并允许 Spring Boot 测试找到它(标记为@ SpringBootTest,这将在后面介绍)。

每个应用只能使用一个@SpringBootApplication。

| `@Configuration` | 将该类标记为可以定义 beans 的 Java 配置类。 | 可以有任何数字。 | | `@SpringBootConfiguration` | 与@Configuration 相同,并且可以通过 Spring Boot 测试发现。 | 每个应用只能有一个。 | | `@SpringBootApplication` | 与`@SpringBootConfiguration`相同,此外它还支持自动配置和组件扫描。 | 每个应用只能有一个。 |

自动配置

Spring Boot 自动配置考虑应用的运行时,并根据许多因素(如类路径上的库)自动配置应用。

它遵循的格言是:“如果每个人都必须做,那么为什么每个人都必须做?”

例如,要创建一个典型的 MVC web 应用,您过去需要添加一个配置类和多个依赖项,并配置一个 Tomcat 容器。使用 Spring Boot,您需要添加的只是一个依赖项和一个控制器类,它会自动添加一个嵌入式 Tomcat 实例。

配置文件可以定义为属性文件、yaml 和其他方式。首先,在“src/main/resources”下创建一个名为“application.properties”的文件,并添加以下内容:

server.port=8003
app.name=Humble Code

这会将服务器设置为在端口 8003 上运行,并设置一个用户定义的属性 app.name,该属性可以是任何值。

启用自动配置

Spring Boot 背后的大部分“魔力”是自动配置——然而,一旦你知道自动配置是如何工作的,你就可以揭开它的神秘面纱。

通过在你的一个@Configuration类中包含@EnableAutoConfiguration或@SpringBootApplication注释,你启动了 Spring 的自动配置。这使得 Spring 能够为您的整个项目自动配置。它可以做从创建嵌入式数据库到启动 Tomcat 实例的事情。

为了定位自动配置类(和其他东西),Spring Boot 检查您发布的 jar 中是否存在一个META-INF/spring.factories文件。该文件应该在EnableAutoConfiguration键下列出您的配置类,如下例所示:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.libx.autoconfigure.LibXAutoConfiguration,\
com.example.libx.autoconfigure.LibXWebAutoConfiguration

然后,这些自动配置类使用注释来描述它们应用的条件和规则。例如,看看下面名为MyReactiveRepositoriesAutoConfiguration的示例类声明:

@Configuration
@ConditionalOnClass({ MyClient.class, ReactiveMyRepository.class })
@ConditionalOnMissingBean({
ReactiveMyRepositoryFactoryBean.class,
ReactiveMyRepositoryConfigurationExtension.class })
@ConditionalOnProperty(
prefix = "spring.data.mydb.reactive-repositories",
name = "enabled", havingValue = "true", matchIfMissing = true)
@Import(MyReactiveRepositoriesAutoConfigureRegistrar.class)
@AutoConfigureAfter(MyReactiveDataAutoConfiguration.class)
public class

MyReactiveRepositoriesAutoConfiguration {

我们将一次检查一个注释。

  1. @Configuration–声明这个类是一个 Spring 配置类。

  2. @ConditionalOnClass–告诉 Spring 仅当这些类在运行时类路径中时才使用这个配置。

  3. @ConditionalOnMissingBean–仅当 ApplicationContext 中不存在这些类型的 Beans 时,此配置才有效。

  4. @ConditionalOnProperty–属性spring.data.mydb.reactive-repositories.enabled必须设置为 true 或 missing ( matchIfMissing)才能调用此配置。

  5. @Import–导入将由 Spring 处理的另一个配置类。

  6. @AutoConfigureAfter–告诉 Spring 只在MyReactiveDataAutoConfiguration已经被处理之后才处理这个配置。

不包括自动配置

您可以通过使用EnableAutoConfiguration注释的 exclude 属性来排除某些自动配置类,例如:

@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Application {
/* main method */
}

这将停止对DataSourceAutoConfiguration类的评估,而不管它上面的其他条件。

您还可以通过属性排除自动配置类,例如,在 yaml:

spring:
  autoconfigure.exclude:
    org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

或者在您的 application.properties 文件中:

spring.autoconfigure.exclude=\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

应用属性

默认情况下,Spring Boot 将从名为application.properties(对于标准属性)或application.yml(对于 YAML 格式的属性)的文件,以及那些为每个活动概要文件附加了 -PROFILE_NAME 的文件中加载属性。

例如,如果您激活了测试概要文件,Spring 将按照这个顺序加载以下内容(后面的属性可以覆盖前面的属性):

  1. 应用.属性

  2. 应用程式. yml

  3. 应用测试属性

  4. 应用测试. yml

覆盖属性

环境变量也可以覆盖任何属性。Spring 自动将大写下划线语法转换为属性语法。例如,名为SPRING_PROFILES_ACTIVE的环境变量将覆盖属性文件中的spring.profiles.active。

优先级顺序是(列表中较早的条目优先于较晚的条目)

  1. 命令行参数

  2. 来自SPRING_APPLICATION_JSON的属性(嵌入在环境变量或系统属性中的内联 JSON)

  3. web.xml中的 Servlet 上下文参数

  4. JNDI 属性来自java:comp/env

  5. Java 系统属性(来自 System.getProperties())

  6. 环境变量

  7. 从application-{profile}.properties文件或类似的 YAML 文件加载的特定于配置文件的属性

  8. 从application.properties文件或application.yml加载的属性

自动 Spring AOP

如果包含在类路径中,Spring AOP 将被自动配置。如果您设置了spring.aop.auto=false,配置将不会被激活。

Spring Boot 执行器

Spring Boot 执行器是 Spring Boot 的一个子项目,它为应用增加了几个生产级服务,将在下一章介绍。

Spring Boot 测试

Spring Boot 为测试提供了全面的内置支持。例如,用@RunWith(SpringRunner.class)和@SpringBootTest注释一个 JUnit 4 测试类,我们可以在整个应用如下运行的情况下运行集成测试:

表 15-1

web 环境枚举

| `DEFINED_PORT` | 使用配置的任何端口创建 web 应用上下文。 | | `MOCK (the default)` | 如果 servlet APIs 位于类路径上,则使用模拟 servlet 环境创建 WebApplicationContext,如果 Spring WebFlux 位于类路径上,则创建 ReactiveWebApplicationContext,否则创建常规 ApplicationContext。 | | `RANDOM_PORT` | 通常与测试中的`@LocalServerPort`注入字段(设置为 Spring 分配的 HTTP 端口)一起使用。 | | `NONE` | 创建一个 ApplicationContext 并将 spring application . setwebapplicationtype(web applicationtype)设置为`WebApplicationType.NONE`,这对于在没有 web 逻辑的情况下测试服务器端逻辑非常有用。 |
  1. 在 JUnit 4 测试中,使用@RunWith( SpringRunner.class )来启用 Spring 测试对测试中自动连接字段的支持(对于 JUnit 5 测试,使用@ExtendWith(SpringExtension.class))。

  2. 使用@ SpringBootTest告诉 Spring 在当前包或更高的包中查找用@ SpringBootConfiguration或@ SpringBootApplication注释的类,并从该配置开始启动 ApplicationContext。使用"webEnvironment = WebEnvironment.RANDOM_PORT"指定每次运行测试时,Spring Boot 应用应该随机选择一个端口在本地运行。这有助于避免与任何其他正在运行的应用发生冲突。这些是可能的值:

@RunWith(SpringRunner.class)                                \\1
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)\\2
public class BootApplicationTests {
  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testFreeMarkerTemplate() {
    ResponseEntity<String> entity = testRestTemplate
          .getForEntity("/", String.class);
    assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(entity.getBody()).contains("Welcome to");
  }
}

Listing 15-1BootApplicationTests.java

这个简单的测试启动了我们的 Spring Boot 应用,并验证了根页面返回 HTTP OK (200)状态代码,并且正文包含文本“Welcome to”。

TestRestTemplate 是自动创建的,并被设置为向正在运行的 Spring Boot 应用发出 HTTP 请求。

Spring 还提供了@ WebMvcTest注释,用于创建一个只实例化和测试 MVC 控制器的测试。所需的任何其他 beans 都需要通过您提供的配置或者作为使用@ MockBean注释的模拟来提供,例如:

  1. 为了只测试一个控制器(CourseController)),我们可以使用@WebMvcTest并指定一个类(您可以指定多个控制器)。Spring 将自动配置 Spring MVC 基础设施,刚好足以测试这个控制器。实际上没有网络 I/O 发生,所以测试可以非常快速地运行。

  2. 自动创建一个 MockMvc 的实例,该实例可用于直接测试任何使用 Spring MVC 的控制器(如第七章所述)。

  3. 用@MockBean标记一个字段告诉 spring-test 基础设施创建该接口的一个模拟实例(使用 mockito)(在本例中为CourseService)。

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(CourseController.class) //1
public class WebMockTest {

        @Autowired
        private MockMvc mockMvc;       //2

        @MockBean
        private CourseService service; //3

        // tests...
}

十六、Spring Boot 执行器

Spring Boot 执行器是 Spring Boot 的子项目,它为应用添加了几个生产级服务,如健康端点和指标。

默认端点

Spring Boot 执行器有助于实现 web 服务的常见非功能需求,如健康检查、配置、审计、调试和度量。

默认情况下,Actuator 提供许多端点(默认情况下映射到/actuator/*)。可以通过 HTTP 或 JMX 协议访问它们。例如,您可以检查默认运行状况检查端点,如下所示:

$ curl localhost:8080/actuator/health
{"status":"UP"}

每个端点可以单独启用或禁用,并且通常以 JSON 响应。以下是所有默认端点:

  • auditevents–显示当前应用的审计事件信息。需要 AuditEventRepository bean

  • beans–显示应用中所有 Spring beans 的完整列表

  • caches–显示可用的缓存

  • conditions–显示在配置和自动配置类上评估的条件,以及它们匹配或不匹配的原因

  • configprops–显示所有@ConfigurationProperties的列表

  • env–显示 Spring 环境的属性

  • flyway–显示已经应用的任何 Flyway 数据库迁移。需要一个或多个 Flyway beans

  • health –显示应用健康信息

  • httptrace–显示 HTTP 跟踪信息(默认情况下,最后 100 次 HTTP 请求-响应交换)。需要一个 HttpTraceRepository bean

  • info–显示任意应用信息

  • integrationgraph–显示 Spring 积分图。需要依赖 spring-integration-core

  • loggers–显示和修改应用中记录器的配置

  • liquibase–显示已经应用的任何 Liquibase 数据库迁移。需要一个或多个 Liquibase beans

  • metrics–显示当前应用的指标信息

  • mappings–显示所有@ RequestMapping路径的整理列表

  • scheduledtasks–显示应用中的计划任务

  • sessions–允许从 Spring 会话支持的会话存储中检索和删除用户会话。需要使用 Spring 会话的基于 Servlet 的 web 应用

  • shutdown–让应用正常关闭(默认禁用;有关如何启用它,请参见下一节)

  • threaddump–执行线程转储

配置执行器

要启用(或禁用)端点,请使用management.endpoint.[name].enabled属性,例如:

management.endpoint.shutdown.enabled=true

如果希望在不同的端口或路径上为管理端点提供服务,可以使用management.server.port and management.endpoints.web.base-path进行设置,例如:

management.endpoints.web.base-path=/manage
management.server.port=8070

请记住,这将启动一个新的嵌入式容器(如 Tomcat)来支持它。

公开端点

默认情况下,大多数端点都不公开。您可以指定哪些端点应该通过 HTTP (web)公开,哪些不应该使用management.endpoints.web.exposure.include,例如(application.properties 中的):

management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans

您还可以确定通过 JMX 公开哪些端点,例如,公开除了bean之外的所有端点:

management.endpoints.jmx.exposure.include=*
management.endpoints.jmx.exposure.exclude=beans
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=beans

信息

您可以通过创建一个实现了InfoContributor接口的 Spring bean 来添加这个端点公开的定制信息。例如,您可以使用下面的InfoContributor,它公开了来自SimpleCourseRepository的课程数:

package com.apress.springquick.springbootmvc;

import com.apress.spring_quick.jpa.simple.SimpleCourseRepository;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
/**
* Contributes to Actuator's /info end-point with additional info
* (count of courses in this case).
 */
@Component
public class CustomInfoContributor implements InfoContributor {

    final SimpleCourseRepository courseRepository;

    public CustomInfoContributor(SimpleCourseRepository courseRepository) {
        this.courseRepository = courseRepository;
    }

    @Override
    public void contribute(final Info.Builder builder) {
        builder.withDetail("count_courses", courseRepository.count());
    }
}

Listing 16-1CustomInfoContributor.java

默认情况下,Info 作为 JSON 提供,所以这个例子将从位于/actuator/info端点的 HTTP get 产生类似于{"count_courses": 0}的内容。

健康

自动配置的健康指示器

以下健康指示器由 Spring Boot 在适当的时候自动配置(例如,CassandraHealthIndicator 仅在配置了 Cassandra 数据库的情况下创建):

|

名称

|

描述

|
| --- | --- |
| CassandraHealthIndicator | 检查 Cassandra 数据库是否启动。 |
| CouchbaseHealthIndicator | 检查 Couchbase 集群是否启动。 |
| DataSourceHealthIndicator | 检查是否可以获得到数据源的连接。 |
| DiskSpaceHealthIndicator | 检查磁盘空间是否不足。 |
| ElasticSearchRestHealthIndicator | 检查 Elasticsearch 集群是否启动。 |
| HazelcastHealthIndicator | 检查 Hazelcast 服务器是否启动。 |
| InfluxDbHealthIndicator | 检查 InfluxDB 服务器是否已启动。 |
| JmsHealthIndicator | 检查 JMS 代理是否已启动。 |
| LdapHealthIndicator | 检查 LDAP 服务器是否启动。 |
| MailHealthIndicator | 检查邮件服务器是否启动。 |
| MongoHealthIndicator | 检查 Mongo 数据库是否启动。 |
| Neo4jHealthIndicator | 检查 Neo4j 数据库是否启动。 |
| PingHealthIndicator | 总是用 UP 来回应。 |
| RabbitHealthIndicator | 检查 Rabbit 服务器是否启动。 |
| RedisHealthIndicator | 检查 Redis 服务器是否启动。 |
| SolrHealthIndicator | 检查 Solr 服务器是否启动。 |

定制健康

包含的健康指标很多,可以自己写。通过实现HealthIndicator接口创建自己的接口,例如:

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
  @Override
  public Health health() {
   int errorCode = check(); // perform some specific health check
   if (errorCode != 0) {
     return Health.down().withDetail("Error Code", errorCode).build();
   }
   return Health.up().build();
  }
}

Listing 16-2MyHealthIndicator.class

可能的健康状态如下:

|

状态

|

养生法

|

意为

|
| --- | --- | --- |
| 起来 | 向上( ) | 一切正常。 |
| 停止服务 | 服务中断( ) | 不可用,也许是故意的。 |
| 未知的 | 未知( ) | 状态未知。 |
| 向下 | 向下( ) | 有东西坏了。 |

即使有一个健康指标不正常,其状态也会传播到最高级别。换句话说,只有当一切正常时,健康端点才会返回{"status":"UP"}。

韵律学

Spring Boot 执行器为千分尺、 1 提供依赖管理和自动配置,是支持众多监控系统的应用度量门面。这些系统对于深入了解您的应用在运行时的行为非常有用,无论是在登台环境中还是在生产环境中,并使您能够尽早发现性能问题。

千分尺支持许多外部系统,包括

  • 应用

  • 阿特拉斯

  • Datadog

  • 动态跟踪

  • 弹性的

  • 神经中枢

  • 石墨

  • 潮湿的

  • 流入

  • 管理扩展

  • KairosDB

  • 新遗迹

  • 普罗米修斯

  • 信号 Fx

  • 简单(内存中)

  • 堆栈驱动程序

  • 状态:状态

  • 波阵面

哪些指标可用取决于应用的类型。

导航至/actuator/metrics显示可用血糖仪名称列表。通过以选择器的形式提供特定血糖仪的名称,例如/actuator/metrics/jvm.memory.max,您可以深入查看关于该血糖仪的信息。

您还可以添加自定义指标。将MeterRegistry注入到您的 bean 中,然后使用它来添加您的度量,例如:

import io.micrometer.core.instrument.MeterRegistry;

class MyBean {
  private final List<String> nameList = new ArrayList<>();
  MyBean(final MeterRegistry registry) {
  registry.gaugeCollectionSize("nameList.size",
       Tags.empty(), nameList);
  }
  //...code
}

Listing 16-3MyBean.java

这个例子创建了一个标尺(一个可以上升或下降的值),用来跟踪nameList集合的大小。标签可用于标记您的过滤指标。

审计

Spring Boot 执行器有一个灵活的审计框架,将事件发布到一个AuditEventRepository。默认情况下,Spring Security 自动发布身份验证事件。例如,这对于报告和实现基于身份验证失败的锁定策略非常有用。

记录

/actuator/loggers端点提供关于系统日志配置的信息(在 JSON 中),并提供在运行时修改配置的方法。对于每个记录器,它提供了configuredLevel和effectiveLevel。您可以修改configuredLevel(这通常也会改变effectiveLevel)。

您可以通过在/ loggers/{name}端点使用 HTTP GET 来请求信息或特定的记录器,例如:

$ curl http://localhost:8081/actuator/loggers/com.apress
{"configuredLevel":null,"effectiveLevel":"INFO"}

img/498572_1_En_16_Figa_HTML.jpg确保你已经将致动器配置为暴露(如本章前面所解释的)并且将 Spring Security 配置为允许你访问该端点(参见第九章)。

然后,您可以使用带有 JSON 的 HTTP POST 在运行时修改记录器的级别,确保正确设置内容类型头。

$ curl -H "Content-Type: application/json" -d "{\"configuredLevel\":\"DEBUG\"}" http://localhost:8081/actuator/loggers/com.apress

然后,您可以再次请求该特定记录器的配置,以确保您的更改得到反映,例如:

$ curl http://localhost:8081/actuator/loggers/com.apress
{"configuredLevel":"DEBUG","effectiveLevel":"DEBUG"}

十七、Spring Webflux

Spring WebFlux 类似于 Spring MVC,但允许您使用反应式流,并且是异步和非阻塞的,如果使用正确,可以让您的应用具有更好的性能。

通过 WebFlux,我们可以使用 HTTP 或 WebSocket 连接快速创建异步、非阻塞和事件驱动的应用。Spring 在它的许多 API 中使用自己的 Reactive Streams 实现,Reactor 1 (带有Flux<T>和Mono<T>)。您可以在应用中使用另一种实现,如 RxJava,但 project Reactor 具有与 WebFlux 的最佳集成。

默认情况下,Spring WebFlux 应用使用 Netty, 2 的嵌入式实例,这是一个异步事件驱动的应用框架,尽管您可以将其配置为使用嵌入式 Tomcat、Jetty 或 Undertow。

在这一章中,我们将看看如何使用 Spring Boot、WebFlux 和 Reactor 以及 MongoDB 持久层来实现一个完整的项目。

使用 Spring WebFlux,我们可以非常容易地创建一个非阻塞的异步应用,它支持 MongoDB、Redis 或 Cassandra 数据库或任何实现了 R2DBC 驱动程序的关系数据库。

入门指南

为了本章的目的,我们将创建一个基于 Java 的梯度构建的 Spring Boot 项目。

Spring Boot 是高度可定制的,你可以为你的项目添加任何你想要的“启动器”(网络、邮件、freemarker、安全等)。).这使得它尽可能的轻便。

我们将创建一个基于 WebFlux 的项目,它使用 Spring 的 Reactor 项目以及 MongoDB 3 来创建一个完全反应式的 web 应用。

这个项目的代码可以在 adamldavis/humblecode 的 GitHub 上获得。 4

Gradle Plugin

Spring Boot 的基本 Gradle build 如下所示:

  1. 您可能注意到的第一件事是缺少指定的版本;Spring Boot 为你提供这些,并确保一切都是兼容的。

  2. 我们为构建工件指定 groupId 和版本。我们还将 Java 源代码版本指定为 11。您不需要指定主类。这是由 Spring Boot 通过注释确定的。

  3. 我们包含了“webflux”启动器来启用 Spring 的 WebFlux。

  4. 我们在这里包含 lombok 项目只是为了简化模型类。Lombok 提供注释,并根据每个类使用的注释自动生成样板代码,如 getters 和 setters。

  5. 这里我们包括了 spring-data starter,用于将反应式 MongoDB 与反应器集成在一起。

  6. 最后,我们包括“spring-boot-starter-test”和“reactor-test”来使用 spring 提供的测试支持。

plugins {                                                       //1
  id 'org.springframework.boot' version '2.3.0.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.humblecode'                                             //2
version = '0.0.2-SNAPSHOT'
sourceCompatibility = 11

repositories {
  mavenCentral()
}
dependencies {
  compile('org.springframework.boot:spring-boot-starter-webflux')     //3

  compileOnly('org.projectlombok:lombok')                   //4
  compile(
'org.springframework.boot:spring-boot-starter-data-mongodb-reactive') //5
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('io.projectreactor:reactor-test')             //6
}

请记住,为了使后端完全反应性,我们与数据库的集成需要异步。这不是每种类型的数据库都能做到的。在这种情况下,我们使用支持异步操作的 MongoDB。

截至本文撰写之时,Spring Data 为 Redis、MongoDB 和 Cassandra 提供了直接的反应式集成。要做到这一点,只需在“starter”编译依赖项中为您想要的数据库切换“mongodb”即可。Spring Data 还为关系数据库提供了对 R2DBC 的支持(详见第六章)。

任务

Spring Boot 插件为构建添加了几个任务。

要运行该项目,请运行“gradle bootRun”(默认情况下在端口 8080 上运行)。查看命令行输出,了解有用的信息,比如应用运行在哪个端口上。

当您准备好部署时,运行"gradle bootRepackage”,这将构建一个胖 jar,在一个 jar 中包含运行整个应用所需的一切。

spring boot 应用

主类是通过用@SpringBootApplication标注来指定的。在com.example.demo包中创建一个名为DemoApplication.java的文件,并放入以下内容:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
  @Bean
  public Service sampleService() {
    return new SampleService(); } //2
}

稍后,我们可以添加自己的配置类来更好地配置应用中的安全性等内容。例如,下面是 SecurityConfig 类的开头,它将在我们的应用中启用 Spring Security 性:

@EnableWebFluxSecurity
public class SecurityConfig

稍后,我们将探索如何为 WebFlux 项目增加安全性。

我们的领域模型

在这一节中,我们将实现一个非常简单的网站,使用 RESTful API 进行在线学习。每门课程都有一个价格(以美分计)、一个名称和一个部分列表。

我们将使用以下领域模型课程类定义:

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.*;
@Data //1
@AllArgsConstructor
@Document //2
public class Course {
  @Id UUID id = UUID.randomUUID(); //3

  public String name;
  public long price = 2000; // $20.00 is default price

  public final List<Segment> segments = new ArrayList<>();

  public Course(String name) {this.name = name;}

  public void setSegments(List<Segment> segments) {
    this.segments.clear();
    this.segments.addAll(segments);
  }
  // custom toString method

}

  1. 前两个注释是 Lombok 注释。告诉 Lombok 为每个字段添加 getters 和 setters、equals 和 hashCode 方法、一个构造函数和一个 toString 方法。 5

  2. @Document注释是 spring-data-mongodb 注释,声明这个类代表一个 mongodb 文档。

  3. @Id 注释表示该文档的 Id 属性。

安装 mongodb 后,可以使用以下命令启动它(在基于 Unix 的系统上):

mongod –dbpath data/ --fork --logpath ∼/mongodb/logs/mongodb.log

反应免疫储存

首先,我们需要创建一个到后端数据库的接口,在本例中是 MongoDB。

使用我们包含的spring-boot-starter-data-mongodb-reactive依赖项,我们可以简单地创建一个扩展ReactiveMongoRepository<T,ID>的新接口,并且(除了这个接口上已经有的方法之外)Spring 将生成代码,支持我们使用标准命名方案定义的任何方法(正如我们在第六章中了解到的)。通过返回反应器类,像Flux<T>或Mono<T>,这些方法将自动反应。

例如,我们可以创建一个课程存储库:

  1. 第一个泛型类型是这个存储库存储的类型(课程),第二个是课程 ID 的类型。

  2. 该方法查找名称与给定搜索字符串匹配的所有球场,并返回一个 Flux 。

  3. 该方法查找具有给定名称的所有课程。如果我们确信名字是唯一的,我们可以使用Mono<Course> findByName(String name)。

public interface CourseRepository extends
    ReactiveMongoRepository<Course, UUID> {            //1
  Flux<Course> findAllByNameLike(String searchString); //2
  Flux<Course> findAllByName(String name);             //3
}

简单地通过扩展ReactiveMongoRepository<T,ID>接口,我们的库将拥有大量有用的方法,如 findById、insert 和保存所有返回的反应器类型(Mono <T>或 Flux <T>)。

控制器

接下来,我们需要制作一个基本的控制器来呈现我们的视图模板。

用@ Controller注释一个类以创建一个 web 控制器,例如:

@Controller
public class WebController {
  @GetMapping("/hello")
  public String hello() { return "home"; }
}

由于前面的方法返回字符串“home ”,它将呈现相应的视图模板。

GetMapping 注释等同于使用@RequestMapping(path="/hello", method = RequestMethod.GET)。它还没有反应,我们将在本章的后面补充。

默认情况下,Spring WebFlux 使用嵌入式 Netty 实例。使用嵌入式容器意味着容器只是另一个“bean ”,这使得配置更加容易。可以使用application.properties和其他应用配置文件进行配置。

接下来,我们想向我们的存储库添加一些初始数据,这样就有东西可看了。我们可以通过添加一个用@PostConstruct注释的方法来实现这一点,该方法只在计数为零时向 courseRepository 添加数据:

@PostConstruct
public void setup() {
  courseRepository.count().blockOptional().filter(count -> count == 0)
  .ifPresent(it ->
    Flux.just(
      new Course("Beginning Java"),
      new Course("Advanced Java"),
      new Course("Reactive Streams in Java"))
    .doOnNext(c -> System.out.println(c.toString()))
    .flatMap(courseRepository::save)
    .subscribeOn(Schedulers.single())
    .subscribe() // need to do this to actually execute save
  );
}

这里的代码混合使用了 Java 8 的Optional<T>和 Reactor。请注意,我们必须调用 Flux 上的 subscribe,否则它永远不会执行。我们在这里通过调用不带参数的 subscribe()来实现这一点。由于 count()返回一个Mono<Long>,我们调用blockOptional(),它将阻塞(等待单声道完成)然后使用给定值;如果是零,那么我们将三个课程对象保存到courseRepository。关于使用 Flux 和 Mono 的复习,请参见第十二章。

查看模板

在任何 Spring Boot 项目中,我们可以使用许多视图模板渲染器中的一个。在这种情况下,我们将 freemarker spring starter 包含到依赖项下的构建文件中:

compile('org.springframework.boot:spring-boot-starter-freemarker')

我们将模板放在src/main/resources/templates下。这里是文件的重要部分,home.ftl:

<div class="page-header">
    <h1>Welcome to ${applicationName}!</h1>
</div>
<article id="content" class="jumbotron center"></article>
<script type="application/javascript">
jQuery(document).ready(HC.loadCourses);
</script>

这将调用相应的 JavaScript 从我们的 RestController 获取课程列表。loadCourses 函数的定义如下:

  1. 首先,我们调用我们的 RESTful API,我们将在后面定义它。

  2. 由于我们使用的是 jQuery,它会自动确定响应是 JSON 并解析返回的数据。

  3. 使用 forEach,我们构建一个 HTML 列表来显示每门课程,并提供一个链接来加载每门课程。

  4. 我们更新 DOM 以包含我们构建的列表。

  5. 这里我们指定了错误处理函数,以防 HTTP 请求出错。

jQuery.ajax({method: 'get', url: '/api/courses'}).done( //1
function(data) {
  var list = data;                                      //2
  var ul = jQuery('<ul class="courses btn-group"></ul>');
  list.forEach((crs) => {                               //3
    ul.append('<li class="btn-link" onclick="HC.loadCourse(\''+
    crs.id+'\'); return false">'
    + crs.name + ': <i>' + crs.price + '</i></li>')
  });
  jQuery('#content').html(ul);                          //4
}
).fail( errorHandler );                                 //5

尽管我们在这里使用 jQuery,但是我们也可以选择任何 JavaScript 库/框架。对于 Spring Boot,JavaScript 文件应该存储在 src/main/resources/static/js。

约定接口规范

默认情况下,Spring 将来自@ RestController的数据编码到 JSON 中,因此相应的CourseController是这样定义的:

@RestController
public class CourseController {
      final CourseRepository courseRepository;

      public CourseControl(CourseRepository courseRepository) {
            this.courseRepository = courseRepository;
      }

      @GetMapping("/api/courses")
      public Flux<Course> getCourses() {
            return courseRepository.findAll();
      }

      @GetMapping("/api/courses/{id}")
      public Mono<Course> getCourse(@PathVariable("id") String id) {
            return courseRepository.findById(UUID.fromString(id));
      }
}

Listing 17-1CourseController.java

注意我们如何直接从一个RestController返回像通量这样的反应器数据类型,因为我们使用的是 WebFlux。这意味着每个 HTTP 请求都是非阻塞的,并使用 Reactor 来确定在哪些线程上运行操作。

注意,在这个例子中,我们直接从控制器调用存储库。在生产系统中,最佳实践是在控制器和存储库之间添加一个“服务”层来保存业务逻辑。

现在我们有了阅读课程的能力,但我们还需要保存和更新课程的能力。

因为我们正在制作一个 RESTful API,所以我们使用@ PostMapping来处理保存新实体的 HTTP POST,使用@ PutMapping来处理更新的 PUT。

下面是保存方法:

@PostMapping(value = "/api/courses",
      consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<Course> saveCourse(@RequestBody Map<String,Object> body) {
      Course

course = new Course((String) body.get("name"));
      course.price = Long.parseLong(body.get("price").toString());

      return courseRepository.insert(course);
}

注意,insert 方法返回一个反应器 Mono;您可能还记得,Mono 只能返回零个或一个实例,或者失败并出现错误。

下面是更新方法:

@PutMapping(value = "/api/courses/{id}",
      consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<Course> updateCourse(@PathVariable("id") String id,
                                 @RequestBody Map<String,Object> body) {

  Mono<Course> courseMono = courseRepository.findById(UUID.fromString(id));

  return courseMono.flatMap(course -> {
      if (body.containsKey("price")) course.price =
      Long.parseLong(body.get("price").toString());
      if (body.containsKey("name")) course.name =
            (String) body.get("name");
      return courseRepository.save(course);
    });
}

注意我们在这里如何使用flatMap来更新课程并返回 save 方法的结果,该方法也返回单声道。如果我们使用 map,返回类型将是 Mono<Mono<Course>>。通过使用flatMap,我们将它“展平”为我们想要的返回类型Mono<Course>。

关于反应器的更多信息,参见第十二章。

进一步配置

在实际的应用中,我们很可能想要覆盖应用的许多默认配置。例如,我们希望实现自定义的错误处理和安全性。

首先,为了定制 WebFlux,我们添加了一个扩展WebFluxConfigurationSupport的类(这里它被命名为 WebFluxConfig,但是它可以被命名为任何东西):

@EnableWebFlux
public class WebFluxConfig extends WebFluxConfigurationSupport {

  @Override
  public WebExceptionHandler responseStatusExceptionHandler() {
    return (exchange, ex) -> Mono.create(callback -> {
            exchange.getResponse().setStatusCode(HttpStatus.I_AM_A_TEAPOT);
      System.err.println(ex.getMessage());
      callback.success(null);
    });
  }
}

这里我们覆盖了responseStatusExceptionHandler来设置状态代码为418(我是茶壶 6 ),这是一个实际存在的 HTTP 状态代码(只是为了演示)。您可以重写许多方法来提供自己的自定义逻辑。

最后,没有某种形式的安全性,任何应用都是不完整的。首先,确保将 spring-security 依赖项添加到您的构建文件中:

compile('org.springframework.boot:spring-boot-starter-security')

接下来,添加一个类并用@EnableWebFluxSecurity对其进行注释,并如下定义 beans:

  1. 这个注释告诉 Spring Security 保护您的 WebFlux 应用。

  2. 我们使用 ant 模式定义了允许所有用户使用的路径,其中“**”表示任何一个或多个目录。这使得每个人都可以访问主页和静态文件。

  3. 在这里,我们确保用户必须登录才能访问“/user/”路径下的任何路径。

  4. 这一行将 UserRepository 中的所有用户转换成一个列表。然后将它传递给 MapReactiveUserDetailsService,该服务为用户提供 Spring Security 性。

  5. 您必须定义一个密码编码。这里我们定义一个明文编码只是为了演示的目的。在实际系统中,您应该使用 StandardPasswordEncoder 或 BCryptPasswordEncoder。

@EnableWebFluxSecurity //1
public class SecurityConfig {

  @Bean
  public SecurityWebFilterChain
         springSecurityFilterChain(ServerHttpSecurity http) {
    http
      .authorizeExchange()
      .pathMatchers("/api/**", "/css/**", "/js/**",img/**", "/")
      .permitAll() //2
      .pathMatchers("/user/**").hasAuthority("user") //3
      .and()
      .formLogin();
      return http.build();
    }

    @Bean
    public MapReactiveUserDetailsService
        userDetailsService(@Autowired UserRepository userRepository) {
      List<UserDetails> userDetails = new ArrayList<>();
      userDetails.addAll(
            userRepository.findAll().collectList().block());//4
      return new MapReactiveUserDetailsService(userDetails);
  }

  @Bean
  public PasswordEncoder myPasswordEncoder() { //5
      // never do this in production of course
      return new PasswordEncoder() {/*plaintext encoder*/};
  }
}

测试

Spring Boot 为测试提供了全面的内置支持。例如,用@RunWith(SpringRunner.class)和@SpringBootTest注释一个 JUnit (4)测试类,我们可以如下运行整个应用来运行集成测试:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HumblecodeApplicationTests {
  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testFreeMarkerTemplate() {
    ResponseEntity<String> entity = testRestTemplate
          .getForEntity("/", String.class);
    assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(entity.getBody()).contains("Welcome to");
  }
}

这个简单的测试启动了我们的 Spring Boot 应用,并验证了根页面返回 HTTP OK (200)状态代码,并且正文包含文本“Welcome to”。使用“webEnvironment = WebEnvironment.RANDOM_PORT”指定每次运行测试时,Spring Boot 应用应该随机选择一个端口在本地运行。

Spring Data R2DBC

R2DBC 7 (反应式关系数据库连接)是一个标准的编程接口,用于以反应式、非阻塞的方式与关系数据库(如 MySQL)集成。

尽管仍处于早期阶段,但已经有一些驱动程序的实现,包括 MySQL、H2、微软 SQL Server 和 PostgreSQL。

R2DBC 在第六章中有更全面的介绍。

Netty 还是 Tomcat

默认情况下,Spring WebFlux 将使用嵌入式 Netty web 容器。但是,如果 spring-web 包含在您的类路径中,您的应用将使用 Tomcat 运行。Spring Web 引入 Tomcat 作为依赖项,这是自动配置的默认设置。

WebFlux 支持使用 Netty(一种异步、非阻塞、事件驱动的网络应用框架)或 Servlet 3.1 非阻塞标准(使用 Tomcat 或 Jetty)运行。

为了确保在 Netty 上运行,应该从依赖项中排除 Tomcat。

web 客户端

如果您的类路径中有 Spring WebFlux,您也可以使用WebClient来调用远程 web 服务。与RestTemplate相比,这个客户端有更多的功能感,并且是完全反应式的,使用 Netty 作为并发引擎。

您可以从静态的WebClient.builder()方法开始使用构建器模式创建 WebClient 的实例,例如:

import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
// later on...
WebClient myWebClient = WebClient.builder()
    .baseUrl("http://localhost:8080")
    .defaultCookie("cookieKey", "cookieValue")
    .defaultHeader(HttpHeaders.CONTENT_TYPE,
               MediaType.APPLICATION_JSON_VALUE)
  .build();

这将使用给定的 baseUrl 构建一个 WebClient,这样所有的请求都将从这个 Url 开始。它还为每个请求提供了一个 cookie 和头部。配置 WebClient 的方法还有很多。 8

每个请求从定义 HTTP 方法开始,然后您可以指定一个额外的 URL 路径(有或没有路径变量)并调用返回一个Mono<ClientResponse>的 exchange,例如:

// get the Course with ID=1 and print it out:
myWebClient.get()
          .uri("/api/courses/{id}", 1L)
          .exchange()
          .flatMap((ClientResponse response) ->
                       response.bodyToMono(Course.class))
          .subscribe(course -> System.out.println("course = " + course));

十八、SpringCloud

SpringCloud 1 是一个伞形项目,由几个与构建基于云的应用和微服务相关的项目组成。这些项目包括 Spring Cloud 网飞、Spring Cloud Config、Spring Cloud Vault、Spring Cloud OpenFeign、Spring Cloud for Amazon Web Services、Spring Cloud Stream 和 Spring Cloud Bus。有太多的内容要在一章中涵盖,所以我们将涵盖一些重点。

特征

Spring Cloud 专注于为基于云的微服务的典型用例提供良好的开箱即用体验,以及覆盖其他的可扩展性机制。

  • 分布式/版本化配置

  • 服务注册和发现

  • 选择途径

  • 服务对服务呼叫

  • 负载平衡

  • 断路器

  • 全局锁

  • 领导选举与集群国家

  • 分布式消息传递

Spring Cloud 采用了一种非常声明性的方法,通常只需更改一个类路径和/或一个注释就可以获得很多特性。例如,这个示例应用启用了 Eureka discovery 客户端(一个与 Eureka 服务器通信以定位服务的客户端):

@SpringBootApplication
@EnableDiscoveryClient
public class DiscoveryApplication

{
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Listing 18-1DiscoveryApplication.java

入门指南

首先,您需要将 spring-cloud-dependencies 构件添加到您的构建中。对于 Gradle 来说,这看起来像下面这样:

dependencyManagement {
 imports {
  mavenBom "org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR6"
 }
}

对于 Maven,它看起来是这样的:

<dependencyManagement>
 <dependencies>
  <dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-dependencies</artifactId>
   <version>Hoxton.SR6</version>
   <type>pom</type>
   <scope>import</scope>
  </dependency>
 </dependencies>
</dependencyManagement>

接下来,您需要包含 spring-cloud-starter 以及您想要使用的任何其他特定启动器,这取决于您的需求。作为参考,下面是 Spring Cloud 的依赖工件及其描述(groupId 是org.springframework.cloud):

|

ArtifactId

|

描述

|
| --- | --- |
| spring-cloud-starter | SpringCloud 基地首发。 |
| spring-cloud-starter-eureka-server | 尤里卡发现服务器,它是网飞 Spring 云的一部分。 |
| spring-cloud-starter-aws | Spring Cloud AWS 支持的启动程序。 |
| spring-cloud-starter-aws-jdbc | 允许您轻松连接到 AWS RDS 数据库。 |
| spring-cloud-starter-aws-messaging | 支持消息传递的 AWS SQS。 |
| spring-cloud-starter-config | 支持云配置解决方案。 |
| spring-cloud-starter-consul-config | 使用 Consul 支持云配置。 |
| spring-cloud-starter-gateway | 提供了一种简单而有效的方法来路由到 API,并提供跨领域的关注点,如安全性、指标和弹性。 |
| spring-cloud-gcp-starter | 支持谷歌云平台(GCP)。 |
| spring-cloud-gcp-starter-pubsub | 支持 Google Cloud PubSub。 |
| spring-cloud-gcp-starter-storage | 支持谷歌云存储。 |
| spring-cloud-starter-netflix-eureka-client | 用于 Eureka 的基于 REST 的 API,用于负载平衡和故障转移的发现客户机。 |
| spring-cloud-starter-netflix-hystrix | 带有网飞 Hystrix 的断路器。 |
| spring-cloud-starter-netflix-ribbon | 用网飞的丝带实现客户端负载平衡。 |
| spring-cloud-starter-netflix-zuul | 使用 Zuul 支持智能和可编程路由。 |
| spring-cloud-starter-openfeign | Feign 是一个 REST 客户端,允许您在接口上使用 JAX RS 或 Spring MVC 注释来定义客户端的代理。 |
| spring-cloud-starter-vault-config | 为使用 Vault 提供客户端支持,Vault 是 HashiCorp 的外部机密配置存储。 |
| spring-cloud-starter-zipkin | 支持使用 Zipkin 的分布式跟踪。 |
| spring-cloud-starter-zookeeper-config | 使用 ZooKeeper 支持分布式配置。 |
| spring-cloud-stream | 用于构建高度可伸缩的事件驱动微服务的框架(需要 RabbitMQ 或 Kafka 这样的消息系统)。 |

例如,对于 spring-cloud-stream,在 Maven pom 中添加以下内容:

<dependency>

  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-stream</artifactId>
</dependency>

为了更容易地开始,您可以使用第十五章中描述的 Spring Boot CLI。

SpringCloud 网飞

Spring Cloud 网飞包括对来自网飞的 OSS(开源软件)的支持,包括 Eureka、Hystrix、ZooKeeper 等。这些项目中的每一个都有不同的用途,它们可以单独使用,也可以一起使用。

换句话说,Eureka 用于云发现,以便服务可以在云环境中轻松找到彼此,而不需要知道彼此的确切 IP 地址。Spring 使得启动 Eureka 服务器变得非常容易,例如:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Listing 18-2DiscoveryServer.java

然后,您可以像使用任何 Spring 配置一样配置 Eureka 服务器,例如:

# Configure this Discovery Server
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false # do not auto-register as client
    fetchRegistry: false
server:
  port: 3000   # where this discovery server is accessible

Listing 18-3application.yml

然后,在使用@ EnableEurekaClient的其他 Spring Boot 应用中,您可以将它们配置为向 Eureka 服务器注册:

# Application name
spring:
  application:
    name: SERVICE1
# Discovery Server Access
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: false
    serviceUrl:
      defaultZone: ${EUREKA_URI:http://localhost:3000/eureka/}

根据您的设置,您应该用尤里卡服务器的实际主机的位置替换 localhost ,或者在运行时设置尤里卡 _URI 环境变量。

Exercise: Implement a Eureka Server and Client

使用前面的说明,创建两个应用——一个是 Eureka 服务器,一个是客户端服务。也许,使用现有的 Spring Boot 应用作为客户端。

寻找服务

一旦有了 Eureka 服务器和配置了 Eureka 客户端的应用,有几种方法可以“发现”或定位注册到 Eureka 服务器的服务:

  • 使用尤里卡客户端

  • 使用发现客户端

  • 通过使用 Eureka 服务标识符代替实际的 URL 来使用 RestTemplate

  • 使用 Feign(这将在本章后面介绍)

您可以使用更通用的(不是特定于网飞的)选项org.springframework.cloud.client.discovery.DiscoveryClient,它为发现客户端提供了一个简单的 API,如下例所示:

@Autowired
private DiscoveryClient discoveryClient;

public String serviceUrl() {
    List<ServiceInstance> list = discoveryClient.getInstances("SERVICE1");
    if (list != null && list.size() > 0 ) {
        return list.get(0).getUri();
    }
    return null;
}

要使用的标识符基于应用名称;在本例中,它是 SERVICE1。在这个简单的例子中,我们使用找到的第一个服务的 URI(通用资源标识符),如果有的话;否则,我们返回 null (URI 比 URL 更通用,但含义相似)。

Exercise: Use the Discovery Client

创建另一个 Spring Boot 应用,该应用使用 DiscoveryClient 来定位和调用您之前定义的服务。记得使用spring.application.name属性设置每个应用的名称。

Spring 云配置

Spring Cloud Config 包含了一个针对云原生应用的集中式配置的抽象。有了配置服务器,您就有了一个中央位置来管理所有环境中应用的外部属性。客户机和服务器上的概念完全对应于 Spring Environment和PropertySource抽象,因此它们非常适合 Spring 应用。服务器存储后端的默认实现使用 git 存储库来存储配置。

只要 Spring Boot 执行器和 Spring 配置客户端在类路径上,任何 Spring Boot 应用都会尝试联系一个在http://localhost:8888(默认值为spring.cloud.config.uri)上的配置服务器。如果你想改变这个默认设置,你可以在bootstrap.yml or bootstrap.properties中或者通过系统属性或环境变量设置spring.cloud.config.uri。然后你可以添加@EnableAutoConfiguration注释,例如:

@Configuration
@EnableAutoConfiguration
@RestController
public class Application {

  @Value("${config.name}") // can come from config server
  String name = "";

  //more code...
}

要运行自己的服务器,使用spring-cloud-config-server依赖项,并在 Java 配置类或主@SpringBootApplication注释类上添加@EnableConfigServer注释。如果您设置属性spring.config.name=configserver,,应用将在端口8888上运行,并提供来自样本存储库的数据。将spring.cloud.config.server.git.uri属性设置为 git 存储库的位置(它可以以file:开头,表示它是文件系统上的本地路径)。

SpringCloud 开佯

该项目支持 OpenFeign 2 (有时也称为 Feign),这是一种用于包装 REST 应用消息和构建声明性 REST 客户端的抽象。Feign 创建了一个用 JAX RS 或 Spring MVC 注释修饰的接口的动态实现,例如:

@SpringBootApplication
@EnableFeignClients                    //1
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }

    @FeignClient("service1")        //2
    static interface NameService {
        @GetMapping("/")          //3
        public String getName();
    }
}

  1. 使用@EnableFeignClients来启用 feign 客户端的自动配置,这是我们在这个类中定义的一个客户端。

  2. 在这里,我们用 FeignClient 对接口进行了注释,并将其命名为 service1。您还可以添加自定义配置,如下所示:@FeignClient(name = "service1", configuration = FooConfiguration.class)。

  3. 对于 HTTP,我们使用 Spring MVC 的@GetMapping注释来定义映射到这个方法的路径。您可以使用任何类似的注释,如@RequestMapping或@PostMapping或JAX-RS注释。

在@ FeignClient注释中,给定的字符串值(如前面示例中的“service1”所示)是一个客户端名称,用于创建带状负载平衡器或 Spring Cloud 负载平衡器。您可以使用 url 属性指定一个绝对 URL。负载平衡器将在运行时发现“service1”服务的物理地址。如果您的应用是一个 Eureka 客户机,那么它将在 Eureka 服务注册中心解析服务。

AWS 的 Spring 云

该项目支持 AWS(亚马逊 Web 服务 3 ),包括为 SQS 4 (简单队列服务)实现 Spring Messaging API,为 ElastiCache 实现 Spring Cache API, 5 ,以及基于 RDS 6 实例的逻辑名自动创建 JDBC 数据源。

例如,要在您的应用中开始监听 AWS SQS 消息队列,您需要在构建文件中包含spring-cloud-starter-aws-messaging依赖项,然后使用@ MessageMapping注释,如下所示:

@MessageMapping("logicalQueueName")
private void receiveMessage(Course course,
                            @Header("SenderId") String senderId) {
    // handle message...

}

img/498572_1_En_18_Figb_HTML.jpg有关更多信息,请参见用于 AWS 的 Spring Cloud 的 Spring 文档。 7

SpringCloudStream

Spring Cloud Stream 类似于 Spring Integration 的消息支持,但仅适用于发布和消费消息。Spring Cloud Stream 支持 RabbitMQ、Apache Kafka、Amazon Kinesis 等多种绑定器实现。

Spring Cloud Stream 的核心构件是

  • 目的地绑定器——负责提供与外部消息传递系统(如 RabbitMQ)集成的组件。

  • 输入和输出/生产者和消费者——从 Spring Cloud Stream 2.1 开始,内置了对使用 Java 8 函数接口来定义接收器(输入)、源(输出)和处理器(两者)的支持,分别使用java.util.function.Consumer<T>、Supplier<T>和Function<T,R>(请参见下面的 EnableBinding 以了解遗留替代方案)。

  • 消息——生产者和消费者用来与目的地绑定者通信的规范数据结构。

启用绑定

遗留的 Spring CloudStream 使用@ EnableBinding注释,该注释带有 Sink、Source、Processor 或您自己定制的带注释接口的值。接收器接口有一个 input()方法,源接口有一个 output()方法,处理器接口扩展接收器和源,例如:

@EnableBinding(Source.class)
public class TimerSource {
  @Bean
  @InboundChannelAdapter(value = Source.OUTPUT, poller =
     @Poller(fixedDelay = "10", maxMessagesPerPoll = "1"))
  public MessageSource<String> timerMessageSource() {
    return () -> new GenericMessage<>("Hello Spring Cloud Stream");
  }
}

在这种情况下,它支持将此方法作为输出/源进行轮询。

Install Rabbitmq

在 Ubuntu Linux 上,使用以下命令:

$ sudo apt install rabbitmq 伺服器

在 MacOS 上,使用“brew install rabbitmq”,其他系统请参见在线指南。 8

下面是使用Supplier<T>的源语义的例子:

@SpringBootApplication
public static class SourceFromSupplier {
        @Bean

public Supplier<String> source1() {
                return () -> "" + new Date();
        }
        // other beans...
}

默认情况下,类似前面的简单源每秒轮询一次。这可以通过设置spring.cloud.stream.poller.fixed-delay属性(毫秒)和spring.cloud.stream.poller.max-messages-per-poll属性(默认为 1)来改变。

下面是一个使用Consumer<T>的接收器语义的例子(它只打印出传入的消息):

        @Bean

        public Consumer<String> sink() {
                return System.out::println;
        }

然后,配置 Spring Cloud Stream 以绑定到您的函数,如下所示(在 application.yml 中):

spring:
  cloud:
    stream:
      bindings:
        source1-out-0:
          destination: test1
        sink-in-0:
          destination: test1
      function:
        definition: source1;source2;sink

如果使用 RabbitMQ,这将在运行时创建一个名为“test1”的队列,并将源和接收器都链接到它。默认情况下,每秒钟轮询一次源。确保在您的构建依赖关系中包含一个消息绑定,比如与 RabbitMQ 通信的"spring-cloud-stream-binder-rabbit"。

Spring Cloud 函数构建在 Project Reactor 之上,因此您可以在实现供应商、函数或消费者时轻松受益于反应式编程模型(例如,通过将您的源函数更改为返回类型为供应商 >)。在这种情况下,轮询是不必要的,您可以控制数据的供应,例如:

@SpringBootApplication
public static class FluxSupplierConfiguration {
  @Bean
  public Supplier<Flux<String>> stringSupplier() {
    return () -> Flux.from(emitter -> {
        while (true) {
          try {
            emitter.onNext("Hello from Supplier");
            Thread.sleep(2000); //sleep two seconds
          } catch (Exception ignore) {}
        }
    });
  }
}

Listing 18-4FluxSupplierConfiguration.java

Build a Pair of Spring Cloud Stream Apps

在最后的练习中,构建两个使用 RabbitMQ 的应用:一个提供消息,一个打印消息。对于消息,请使用“Hello from”+新日期()。对 Maven 构建使用以下依赖关系(或使用第十五章中的 Spring Initializr):

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>
 spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

有关提示,请参见前面的示例代码。尝试轮询方法,然后尝试之前显示的反应方式。

posted @ 2024-10-02 14:13  绝不原创的飞龙  阅读(204)  评论(0)    收藏  举报