SpringBoot2-高级教程-全-

SpringBoot2 高级教程(全)

原文:Pro Spring Boot 2

协议:CC BY-NC-SA 4.0

一、Spring 框架 5

欢迎阅读本书的第一章,在这一章中,我将向您介绍 Spring 框架,一点历史,以及它自诞生以来是如何发展的。本章适用于不熟悉 Spring 框架的开发人员。如果你是一个有经验的 Spring 框架开发者,你可以跳过这一章。

也许你在想,“我想学 Spring Boot。为什么我需要了解 Spring Framework?”嗯,让我告诉你,Spring Boot 的 Spring 是。Spring Boot 有不同的机制来运行 Spring 应用;要理解 Spring Boot 真正是如何工作的,有必要了解更多关于 Spring 框架的知识。

一点历史

Spring 框架是 Rod Johnson 在 2003 年创建的,他是《没有 EJB 的 J2EE 发展》一书的作者。Spring 框架是对当时 J2EE 规范的所有复杂性的回应。今天,它已经得到改善,但你需要有一个完整的基础设施来运行 J2EE 生态系统的某些方面。

我们可以说 Spring 是 Java EE 的补充技术。Spring 框架集成了几种技术,如 Servlet API、WebSocket API、并发实用程序、JSON 绑定 API、bean 验证、JPA、JMS 和 JTA/JCA。

Spring 框架支持依赖注入公共注释规范,使得开发更加容易。

本章说明了 Spring Framework 版本 5.x 至少需要 Java EE 7 级别(Servlet 3.1+和 JPA 2.1)。Spring 仍然支持 Tomcat 8 和 9、WebSphere 8 和 JBoss EAP 7。此外,我还向您展示了 Spring Framework 5 的新特性——反应式支持!

如今,Spring 是 Java 社区中使用最多、最受认可的框架之一,不仅因为它能够工作,还因为它继续与其他令人惊叹的项目一起创新,包括 Spring Boot、Spring Security、Spring Data、Spring Cloud、Spring Batch 和 Spring Integration 等等。

设计原则和模式

要了解 Spring Boot,你需要了解一个框架;重要的是不仅要知道它做什么,还要知道它遵循哪些原则。以下是 Spring 框架的一些原则。

  • 在每个级别提供选择。Spring 允许您尽可能推迟设计决策。例如,您可以通过配置切换持久性提供者,而无需更改代码。许多其他基础设施问题以及与第三方 API 的集成也是如此。您将会看到,这甚至会在您将应用部署到云中时发生。

  • 容纳不同的观点。Spring 拥抱灵活性,对事情应该如何做并不固执己见。它从不同的角度支持广泛的应用需求。

  • 保持强大的向后兼容性。Spring 的发展已经被小心地管理,使得版本之间几乎没有突破性的变化。Spring 支持精心选择的一系列 JDK 版本和第三方库,以便于维护依赖于 Spring 的应用和库。

  • 关心 API 设计。Spring 团队投入了大量的思想和时间来制作直观的、跨多个版本和多年的 API。

  • 为代码质量设定高标准。Spring 框架非常强调有意义的、当前的和准确的 Javadocs。这是极少数可以宣称代码结构清晰,包之间没有循环依赖的项目之一。

那么,运行 Spring 应用需要什么呢?Spring 使用普通的旧 Java 对象(POJOs ),使其易于扩展。Spring 是非侵入性的,使您的应用企业就绪;但是你需要通过添加一个配置来帮助 Spring 连接所有的依赖项,并注入创建 Springbean来执行你的应用所需要的东西(见图 1-1 )。

img/340891_2_En_1_Fig1_HTML.jpg

图 1-1

Spring 语境

图 1-1 显示了创建所有 Spring beans 的 Spring 上下文——感谢引用您的类的配置,这使得您的应用运行。您将在接下来的小节中找到更多信息,在这些小节中,您将创建一个完整的 REST API 应用。

Spring 框架 5

Spring 使得创建 Java 企业应用变得容易,因为它提供了开发人员在企业环境中使用 Java 语言所需的一切。它在 JVM (Java 虚拟机)上提供了对 Groovy 和 Kotlin 作为替代语言的出色支持。

Spring Framework 5 需要 JDK 8+版本,并为 Java 开发工具包(JDK) 9、10 和 11 提供现成的支持。Spring 团队对 11 和 17 版本有相同的长期维护支持,这与 JDK 团队相关。这个新版本于 2017 年推出,采用了一种新的方法来使用反应式流进行函数式编程。

Spring Web MVC 是为 Servlet API 和 Servlet 容器服务的。这是可以的,直到有更多的服务需求,这发现了一个特殊的问题:每个请求都有一些阻塞;随着高需求,有必要做些别的事情。结果是:反应器,一个网络框架。在版本 5 中引入了 Spring WebFlux 模块,具有完全非阻塞的堆栈,支持反应流背压,并运行在 Netty、Undertow 和 Servlet 3.1+容器等服务器上。这是用少量线程处理并发性的非阻塞堆栈的部分答案,这些线程可以用更少的硬件进行扩展。

WebFlux 模块依赖于另一个 Spring 项目:项目反应器。Reactor 是 Spring WebFlux 的反应库选择。它提供了 Mono 和 Flux API 类型,通过与操作符的react vex词汇表一致的一组丰富的操作符来处理0..10..N的数据序列。Reactor 是一个反应流库,因此,它的所有操作符都支持非阻塞背压。Reactor 非常关注服务器端 Java。它是与 Spring 密切合作开发的。

我不想深入讨论 Spring 的很多特性,因为我可以用一个简单的 web 应用来展示它们。你怎么想呢?所有这些很酷的 WebFlux 特性都在它自己的章节中进行了回顾。

简单的 Spring Web 应用

让我们首先创建一个 Spring web 应用——一个 ToDo 应用,它提供了一个可以执行 CRUD(创建、读取、更新和删除)的 REST API。要创建一个新的 Spring 应用,您需要安装 Maven。在接下来的章节中,你可以选择 Maven 或者 Gradle。

使用 Maven 创建项目

让我们从使用 Maven 中的以下命令创建 ToDo Spring 项目开始。

$ mvn archetype:generate -DgroupId=com.apress.todo -DartifactId=todo -Dversion=0.0.1-SNAPSHOT -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-webapp

该命令为 web 应用生成基本模板和结构。通常,它会生成webappresources文件夹,但不会生成java文件夹,后者需要手动创建。

todo
├── pom.xml
└── src
    └── main
        ├── resources
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            └── index.jsp

您可以在您喜欢的 IDE 中导入代码;这将使识别任何问题变得更加容易。

添加依赖关系

打开pom.xml,用清单 1-1 替换所有内容。

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.apress.todo</groupId>
    <artifactId>todo</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>
    <name>todo Webapp</name>

    <properties>

        <!-- Generic properties -->
        <java.version>1.8</java.version>

        <!-- Web -->
        <jsp.version>2.2</jsp.version>
        <jstl.version>1.2</jstl.version>
        <servlet.version>3.1.0</servlet.version>
        <bootstrap.version>3.3.7</bootstrap.version>
        <jackson.version>2.9.2</jackson.version>
        <webjars.version>0.32</webjars.version>

        <!-- Spring -->
        <spring-framework.version>5.0.3.RELEASE</spring-framework.version>

        <!-- JPA -->
        <spring-data-jpa>1.11.4.RELEASE</spring-data-jpa>
        <hibernate-jpa.version>1.0.0.Final</hibernate-jpa.version>
        <hibernate.version>4.3.11.Final</hibernate.version>

        <!-- Drivers -->
        <h2.version>1.4.197</h2.version>

        <!-- Logs -->
        <slf4j.version>1.7.25</slf4j.version>
        <logback.version>1.2.3</logback.version>
    </properties>

    <dependencies>
        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>${spring-data-jpa}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
            <version>${hibernate-jpa.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate.version}</version>
        </dependency>

        <!-- Logs -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!-- Drivers -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${h2.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Java EE Web dependencies -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>${jstl.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${servlet.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>${jsp.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Web UI -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator</artifactId>
            <version>${webjars.version}</version>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>${bootstrap.version}</version>
        </dependency>

        <!-- Web - JSON/XML Response -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-joda</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>todo</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Listing 1-1todo/pom.xml

清单 1-1 显示了pom.xml文件以及创建一个简单的 Spring web 应用所需的所有依赖项。

Spring 腹板配置

接下来,我们从 Spring 配置开始。Spring 需要开发人员决定类在哪里,它们如何相互交互,以及 web 应用的一些额外配置。

让我们从修改web.xml文件开始,如清单 1-2 所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
    <display-name>ToDo Web Application</display-name>
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Listing 1-2todo/src/main/webapp/WEB-INF/web.xml

需要设置DispatcherServlet,它是任何 Spring web app 的主要入口。这个类根据上下文配置连接所有东西。如您所见,这是一个非常简单的配置。

接下来,让我们通过创建一个dispatcherServlet-servlet.xml文件来配置 Spring 上下文。有一个命名约定;如果 servlet 在web.xml文件中被命名为todo,那么 Spring 上下文文件应该被命名为todo-servlet.xml。在本例中,servlet 被命名为dispatcherServlet,因此它寻找一个dispatcherServlet-servlet.xml文件(参见清单 1-3 )。

<?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"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
            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-4.3.xsd
            http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.8.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

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

    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="objectMapper" ref="jsonMapper"/>
            </bean>
            <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
                <property name="objectMapper" ref="xmlMapper"/>
            </bean>

        </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="jsonMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
        <property name="simpleDateFormat" value="yyyy-MM-dd HH:mm:ss" />
    </bean>

    <bean id="xmlMapper" parent="jsonMapper">
        <property name="createXmlMapper" value="true"/>
    </bean>

    <mvc:resources mapping="/webjars/**" location="classpath:META-INF/resources/webjars/" />

    <jpa:repositories base-package="com.apress.todo.repository" />

    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:META-INF/sql/schema.sql" />
        <jdbc:script location="classpath:META-INF/sql/data.sql" />
    </jdbc:embedded-database>

    <bean id="jpaVendorAdapter"
          class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="showSql" value="true" />
    </bean>

    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
    </bean>

    <bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager" />

    <bean
            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".jsp" />
    </bean>

    <bean id="h2WebServer" class="org.h2.tools.Server" factory-method="createWebServer"
          init-method="start" destroy-method="stop">
        <constructor-arg value="-web,-webAllowOthers,-webDaemon,-webPort,8082" />
    </bean>

</beans>

Listing 1-3todo/src/main/webapp/WEB-INF/dispatcherServlet-servlet.xml

清单 1-3 显示了 Spring web 配置。看看它使用的所有 XML 名称空间。这很有帮助,因为如果您使用带有代码完成功能的 IDE,它会为您提供每个条目的组件及其属性。我们来分析一下。

  • <context:component-scan/>。这个标签告诉 Spring 容器它需要扫描所有的类;它寻找注释,包括@Service@Configuration。这有助于 Spring 连接所有的 Spring beans,以便您的应用可以运行。在这种情况下,它在com.apress.todo.*包级别扫描标记的类和所有的子包。

  • <mvc:annotation-driven/>。这个标签告诉 Spring 容器这是一个 web 应用,它需要寻找每一个@Controller@RestController类及其具有@RequestMapping或其他 Spring MVC 注释的方法,因此它可以创建必要的 MVC beans 来接受来自用户的请求。

  • <mvc:message-converters/>。这个标签通知 MVC beans 在有请求时使用什么来进行消息转换。例如,如果有一个带有 HTTP 头Accept: application/xml的请求,它会以 XML 的形式响应,就像带有application/json一样。

  • jsonMapperxmlMapper豆。这些类是 Spring beans,帮助格式化数据和创建正确的映射器。

  • <mvc:resources/>。这个标签告诉 Spring MVC 使用哪些资源,以及在哪里可以找到它们。在这种情况下,这个应用正在使用 WebJars (在pom.xml文件中声明)。

  • <jpa:repositories/>。这个标签告诉 Spring 容器和 Spring Data 模块扩展CrudRepository接口的接口在哪里。在这种情况下,它会在com.apress.todo.repository中寻找它们。*包装级别。

  • <jdbc:embedded-database/>。因为这个应用使用 JPA 和内存数据库的 H2 驱动程序,所以这个标记只是一个声明,使用一个可以在启动时执行 SQL 脚本的实用程序;在本例中,它创建了todo表并插入了一些记录。

  • jpaVendorAdapter比恩。使用 JPA 实现需要这个 bean 声明;在本例中,它是 Hibernate(在pom.xml文件中使用的一个依赖项)。换句话说,Hibernate 框架被用作 Java 持久性 API (JPA)的实现。

  • EntityManagerFactory比恩。对于每个 JPA 实现,有必要创建一个实体管理器来保存所有会话,并代表应用执行所有 SQL 语句。

  • TransactionManager比恩。应用需要有一个交易,因为我们不想有重复或坏数据,对不对?我们需要应用并符合 ACID(原子性、一致性、隔离性、持久性),所以我们需要事务。

  • <tx:annotation-driven/>。这个注释基于前面的声明设置所有的事务。

  • viewResolver比恩。有必要说明 web 应用将使用哪种视图引擎,因为有很多选项,比如 Java Server Faces、JSP 等等。

  • h2WebServer比恩。这个 bean 设置 H2 引擎,以便可以在应用中访问它。

正如你所看到的,这部分需要一点关于如何装配 Spring 的知识。如果你想了解更多,我推荐几本出版社的书,包括 I. Cosmina 等人的《Pro Spring 5 》。

我想向您展示运行一个更简单的 REST API 需要做些什么;相信我,如果你认为这太多了,试着用 Java EE 做同样的事情,用这个应用的所有特性(MVC,JPA,SQL 初始化,JSP,事务)。

让我们回顾一下启动时执行的 SQL 脚本。在resources/META-INF/sql文件夹中创建两个文件(参见清单 1-4 和清单 1-5 )。

create table todo (
  id varchar(36) not null,
  description varchar(255) not null,
  created timestamp,
  modified timestamp,
  completed boolean,
  primary key (id)
);

Listing 1-4todo/src/main/resources/META-INF/sql/schema.sql

如您所见,创建 SQL 表非常简单。

insert into todo values ('7fd921cfd2b64dc7b995633e8209f385','Buy Milk','2018-09-23 15:00:01','2018-09-23 15:00:01',false);
insert into todo values ('5820a4c2abe74f409da89217bf905a0c','Read a Book','2018-09-02 16:00:01','2018-09-02 16:00:01',false);
insert into todo values ('a44b6db26aef49e39d1b68622f55c347','Go to Spring One 2018','2018-09-18 12:00:00','2018-09-18 12:00:00',false);

Listing 1-5todo/src/main/resources/META-INF/sql/data.sql

当然,还有一些 SQL 语句和 ToDo。

重要的是要知道 JPA 需要一个持久性单元,您可以在其中配置一些东西,比如哪些托管类是持久性单元的一部分、类如何映射到数据库表、数据源连接等等。因此,有必要创建一个。您可以在resources/META-INF/文件夹中创建persistence.xml文件(参见清单 1-6 )。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns:="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
    <persistence-unit name="toDo">
        <description>My Persistence Unit</description>
    </persistence-unit>
</persistence>

Listing 1-6todo/src/main/resources/META-INF/persistence.xml

这里没有必要声明映射的类或连接,因为 Spring Data 模块会处理它;你只需要声明一个persistence-unit名。

接下来,重要的是必须为应用登录,不仅是为了调试,而且你可以用它来了解你的应用正在发生什么。在resources文件夹中创建logback.xml文件(参见清单 1-7 )。

<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns:="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback http://ch.qos.logback/xml/ns/logback/logback.xsd">

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
            </Pattern>
        </layout>
    </appender>

    <logger name="org.springframework" level="info" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

    <logger name="org.springframework.jdbc" level="debug" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

    <logger name="com.apress.todo" level="debug" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

    <root level="error">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>

Listing 1-7todo/src/main/resources/logback.xml

再说一次,这里没什么特别的。请注意,com.apress.todo的记录级别被设置为DEBUG

班级

接下来,是时候为 ToDo REST API 创建实际代码了。让我们从创建域模型开始:ToDo域类。在src/main/java文件夹中创建类。请记住,Maven 工具并没有创建这种结构;我们需要手动创建它(参见清单 1-8 )。

package com.apress.todo.domain;

import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.sql.Timestamp;

@Entity

public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    private String description;

    private Timestamp created;
    private Timestamp modified;

    private boolean completed;

    public ToDo() {
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Timestamp getCreated() {
        return created;
    }

    public void setCreated(Timestamp created) {
        this.created = created;
    }

    public Timestamp getModified() {
        return modified;
    }

    public void setModified(Timestamp modified) {
        this.modified = modified;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }
}

Listing 1-8todo/src/main/java/com/apress/todo/domain/ToDo.java

正如你所看到的,它只是一个普通的 Java 类,但是因为这个应用持久化数据(在本例中是 ToDo ),所以有必要用@Entity注释标记这个类,并用@Id注释声明主键。这个类还使用额外的注释为主键生成一个 36 个随机字符的 GUID。

接下来,让我们创建一个包含所有 CRUD 操作的存储库。在这里,应用使用了 Spring Data 模块的强大功能,它隐藏了所有带有表和 keep 会话的样板映射类,甚至还进行事务处理。Spring Data 实现了所有的 CRUD 换句话说,您不需要担心如何保存、更新、删除和查找记录。

创建从CrudRepository接口扩展而来的ToDoRepository接口(参见清单 1-9 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> {
}

Listing 1-9todo/src/main/java/com/apress/todo/repository/ToDoRepository.java

清单 1-9 显示了一个接口。这个ToDoRepository接口从CrudRepository<T,K>扩展而来,是一个通用接口。CrudRepository需要一个域类和主键类型;在这种情况下,域类是ToDo类,主键类型是String(标有@Id注释的那个)。

在 XML 配置中,您使用了<jpa:repositories/>标签。该标签指向ToDoRepository包,这意味着 Spring Data 保持记录,并且它连接了与扩展CrudRepository接口的接口相关的一切。

接下来,让我们创建接受用户请求的 web 控制器。创建ToDoController类(参见清单 1-10 )。

package com.apress.todo.controller;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/")
public class ToDoController {

    private ToDoRepository repository;

    @Autowired
    public ToDoController(ToDoRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public ModelAndView index(ModelAndView modelAndView, HttpServletRequest request) {
        modelAndView.setViewName("index");
        return modelAndView;
    }

    @RequestMapping(value = "/toDos", method = { RequestMethod.GET }, produces = {
            MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE})
    public ResponseEntity<Iterable<ToDo>> getToDos(@RequestHeader HttpHeaders headers) {
        return new ResponseEntity<Iterable<ToDo>>(this.repository.findAll(), headers, HttpStatus.OK);
    }

}

Listing 1-10todo/src/main/java/com/apress/todo/controller/ToDoController.java

清单 1-10 显示了网络控制器。花点时间复习一下。这里我们需要一整本书来描述所有的 Spring MVC 模块和每个特性。

这里重要的是这个类用@Controller注释进行了标记。还记得<mv:annotation-driven/>标签吗?该标签查找每个标记为@Controller的类,并向所有具有@GetMapping@RequestMapping@PostMapping注释的方法注册控制器,以基于定义的路径接受请求。在这种情况下,仅定义了//toDos路径。

这个类使用了一个将ToDoRepository作为参数的构造函数。这是由 Spring 容器通过@Autowired注释注入的。如果您使用的是 Spring 4.3 版本,可以省略这个注释;默认情况下,Spring container 识别出构造函数需要依赖项,并自动注入它们。这就像说,“嘿,Spring container,我需要注入ToDoRepository豆,因为我将使用它“这就是 Spring 使用依赖注入的方式(还有方法注入、字段注入和 setter 注入)。

@GetMapping ( @RequestMapping默认情况下也是如此)响应/路径和视图名称;在这种情况下,它返回对应于the WEB-INF/view/index.jsp JSP 页面的索引名。@RequestMapping是做同样事情的另一种方式(@GetMapping,但是这次它声明的是/toDos路径。这个方法响应取决于请求者发送的头的种类,比如application/jsonapplication/xml。它使用ResponseEntity作为响应;它使用存储库实例来调用从数据库返回所有 ToDo 的findAll方法,因为在 JSON 和 XML 映射器中声明的配置表明引擎会处理这种转换。

再次,花时间分析正在发生的事情。运行应用后,您可以使用所有这些注释。

接下来,让我们创建视图,这是在请求/路径时调用的 JSP。在WEB-INF/views文件夹中创建index.jsp(参见清单 1-11 )。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Simple Directory Web App</title>
    <link rel="stylesheet" type="text/css"
          href="webjars/bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css"
          href="webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css">
</head>
<body>
<div class="container theme-showcase" role="main">
    <div class="jumbotron">
        <h1>ToDo Application</h1>
        <p>A simple Rest API Spring MVC application</p>
    </div>

    <div class="page-header">
        <h1>API</h1>
        <a href="toDos">Current ToDos</a>
    </div>

</div>
</body>
</html>

Listing 1-11todo/src/main/webapp/WEB-INF/views/index.jsp

我认为这里唯一需要注意的是资源的使用,比如 WebJars。该应用正在使用引导 CSS。但是这些资源从哪里来呢?首先,声明pom.xml中的org.webjars:bootstrap依赖项。其次,在配置中使用了<mvc:resources/>标签来说明在哪里可以找到这些资源。

运行应用

您已经完成了运行应用所需的所有配置和代码。现在,是应用服务器的时候了。要运行该应用,请按照下列步骤操作。

  1. 打开一个终端,转到您的根项目(todo/)。执行下一个 maven 命令。

    $ mvn clean package
    
    

    该命令将您的应用打包到一个 WAR 文件(web 归档文件)中,准备在应用服务器中部署。文件在target/文件夹中,命名为todo.war

img/340891_2_En_1_Fig2_HTML.jpg

图 1-2

http://localhost:8080/all/

  1. 下载 Tomcat 应用服务器。(运行这个 app 不需要很重的应用服务器;一只轻盈的雄猫就可以了)。可以从 https://tomcat.apache.org/download-90.cgi 下载。

  2. 解压并安装在任何目录下。

  3. target/todo.war复制到<tomcat-installation>/webapps/文件夹中。

  4. 运行你的雄猫。进入浏览器,点击http://localhost:8080/todo网址(见图 1-2 )。

如果你点击链接,你应该有一个 XML 响应(见图 1-3 )。

img/340891_2_En_1_Fig3_HTML.jpg

图 1-3

http://localhost:8080/all/all

如何获得 JSON 响应?打开终端并执行以下命令。

$ curl -H "Accept: application/json" localhost:8080/todo/toDos
[ {
  "id" : "7fd921cfd2b64dc7b995633e8209f385",
  "description" : "Buy Milk",
  "created" : "2018-09-23 15:00:01",
  "modified" : "2018-09-23 15:00:01",
  "completed" : false
}, {
  "id" : "5820a4c2abe74f409da89217bf905a0c",
  "description" : "Read a Book",
  "created" : "2018-09-02 16:00:01",
  "modified" : "2018-09-02 16:00:01",
  "completed" : false
}, {
  "id" : "a44b6db26aef49e39d1b68622f55c347",
  "description" : "Go to Spring One 2018",
  "created" : "2018-09-18 12:00:00",
  "modified" : "2018-09-18 12:00:00",
  "completed" : false
} ]

你可以用application/xml来测试,看到和浏览器一样的结果。

恭喜你!您已经创建了第一个 Spring MVC REST API 应用。

使用 Java 配置

您可能认为 XML 对于创建配置来说太冗长了。嗯,有时候可以,但是 Spring 有另一种方式来配置 Spring 容器,那就是通过注释和 Java config 类。

如果您想尝试一下,您可以创建ToDoConfig类并添加清单 1-12 中所示的代码。

package com.apress.todo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import javax.sql.DataSource;
import java.text.SimpleDateFormat;
import java.util.List;

@Configuration

@EnableJpaRepositories(basePackages="com.apress.todo.repository")

@EnableTransactionManagement

@EnableWebMvc

public class ToDoConfig implements WebMvcConfigurer {

      @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
          .addResourceHandler("/webjars/**")
          .addResourceLocations("classpath:/META-INF/resources/webjars/","/resources/","/webjars/")
              .resourceChain(true).addResolver(new WebJarsResourceResolver());
    }

      @Override
      public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
            builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
            converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
      }

      @Bean
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver bean = new InternalResourceViewResolver();
        bean.setPrefix("/WEB-INF/views/");
        bean.setSuffix(".jsp");
        return bean;
    }

      @Bean
      public DataSource dataSource() {
            EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
            return builder.setType(EmbeddedDatabaseType.H2).addScript("META-INF/sql/schema.sql")
                        .addScript("META-INF/sql/data.sql").build();
      }

      @Bean
      public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

            HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
            vendorAdapter.setShowSql(true);

            LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
            factory.setJpaVendorAdapter(vendorAdapter);
            factory.setDataSource(dataSource());
            return factory;
      }

      @Bean
      public PlatformTransactionManager transactionManager() {
            JpaTransactionManager txManager = new JpaTransactionManager();
            txManager.setEntityManagerFactory(entityManagerFactory().getNativeEntityManagerFactory());
            return txManager;
      }
}

Listing 1-12todo/src/main/java/com/apress/todo/config/ToDoConfig.java

清单 1-12 实际上与 XML 配置相同,但是这一次,它使用了 Java Config类,其中我们以编程方式声明了 Spring beans,并且有必要覆盖一些 web 配置。

如果你想运行它来测试这个JavaConfig类,你需要做一些事情。打开dispatcherServlet-servlet.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"
       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-4.3.xsd">

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

</beans>

最后,需要告诉 Spring 在哪里可以找到@Configuration标记的类(另一个替代方法是使用WebApplicationInitializer类);一旦找到它,它就根据 Java Config类的声明连接一切。

记得用mvn clean包清理并重新打包你的应用,再次生成WAR文件。您可以运行它,并获得与使用 XML 配置相同的结果。

那么,你觉得 Spring 框架怎么样?是的,你需要了解发生了什么。您需要知道 Spring beans 生命周期是如何工作的,以及依赖注入是如何使用的。此外,了解一点 AOP(面向方面编程)也很重要,因为这是连接一切为我们工作的魔法的一部分。

你认为这太过分了吗?好吧,如果你试图用一个普通的 Java 2 EE 概要文件来制作同一个应用,那就更麻烦了。记住,它不仅仅是公开一个 REST API,而是使用数据库、事务、消息转换器、视图解析器等等;这就是为什么使用 Spring,web 应用更容易创建。

但是你猜怎么着?Spring Boot 为你做了所有的样板配置,通过创建企业 Spring 应用加快了开发速度!

注意

记住你可以从 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 获得这本书的源代码。

摘要

关于 Spring 框架以及它在 Spring Boot 中扮演的角色,还有很多需要学习的地方。一章是不够的。所以,如果你想了解更多,我鼓励你在 https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/ 查阅 Spring 文档。

在下一章中,我们从 Spring Boot 开始,了解创建与本章相同的应用是多么容易,只是“需要启动”

二、Spring Boot 简介

在前一章中,我向您展示了什么是 Spring 框架,它的一些主要特性(比如依赖注入设计模式的实现),以及如何使用它(通过创建一个简单的 web/数据应用并将其部署到 Tomcat 服务器)。我还向您展示了创建 Spring 应用所需的每个步骤(例如,添加各种 XML 文件的配置选项,以及如何运行应用)。

在这一章中,我将向您展示什么是 Spring 它的主要组件,如何使用它来创建 Spring 应用,以及如何运行或部署它。这是创建 Spring 应用的一种更简单的方式。本书的其余部分涵盖了更多的细节;这只是对 Spring Boot 技术的一个小介绍。

Spring Boot

我可以说 Spring Boot 是 Spring 框架的下一个篇章,但是不要误解我:Spring Boot 不会取代 Spring 框架,因为 Spring Boot 是 Spring 框架的!您可以将 Spring Boot 视为轻松创建 Spring 应用的新方法。

Spring Boot 简化了我们的开发方式,因为它使创建生产就绪的基于 Spring 的应用变得容易,您可以“直接运行”你会发现,使用 Spring Boot,你可以创建带有嵌入式服务器的独立应用(默认情况下是 Tomcat,如果你使用新的 web-reactive 模块,则是 Netty),使它们 100%可运行和可部署。应用。

Spring Boot 最重要的特性之一是一个固执己见的运行时,它帮助你遵循创建健壮的、可扩展的、可伸缩的 Spring 应用的最佳实践。

你可以在 https://projects.spring.io/spring-boot/ 找到 Spring Boot 项目。非常广泛的文档在 https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/ 。Spring Boot 主页如图 2-1 所示。

img/340891_2_En_2_Fig1_HTML.jpg

图 2-1

Spring Boot 首页( http://projects.spring.io/spring-boot/ )

Spring Boot 来了

要创建 Spring 应用,您需要了解所有的配置技术和/或技术需求。运行最简单的 Spring 应用也需要很多步骤。四年前,Spring 团队推出了第一个测试版,我有幸测试了它。结果是惊人的。现在,随着更多的特性加入到技术中,它已经真正成为创建 Spring 应用的“事实上的”方式。Spring Boot 让创建企业级应用变得更加容易。

如果你看一下 Spring Boot 项目的网页,会发现这样一句话:绝对不需要代码生成,也不需要 XML 配置。也许您想知道如何创建 Spring 应用并在没有任何配置的情况下运行它们。Spring 容器至少需要知道如何连接你的类,对吗?或者 Spring 容器需要知道如何使用你添加到应用中的技术。别担心。我会告诉你这项惊人技术背后的所有秘密。但是首先,让我们尽可能创建最简单的 Spring web 应用(参见清单 2-1 )。

@RestController
class WebApp{
      @GetMapping("/")
      String welcome(){
        "<h1><font face="verdana">Spring Boot Rocks!</font></h1>"
     }
}

Listing 2-1
app.groovy

清单 2-1 是一个 Groovy 应用,也是最简单的 Spring web 应用。为什么这么棒?我总是告诉我的学生,如果你懂 Java,那么你就懂 Groovy。Groovy 去掉了所有的 Java 样板文件,加上几行代码,你就有了一个 web app(不过不用担心,这本书大部分都是讲 Java 的;除了在最后一章我谈到了 Groovy 和 Kotlin,这是 Spring 语言支持的一个新的补充。你是怎么经营的?就像执行一样简单

$ spring run app.groovy

然后,您应该会看到带有 Spring Boot 标语的输出日志、Tomcat 容器初始化以及应用已经在端口 8080 上启动的提示。如果你打开浏览器并点击http://localhost:8080,那么你应该会看到文本 Spring Boot 摇滚!

你可能会说,“等一下!这个spring run命令是什么?怎么安装?我还需要什么?这是 Spring Boot 吗?”这是创建和运行 Spring 应用的众多方法之一。这是我第一次尝试展示这项技术的威力(四年前),一个简单的脚本可以运行一个完整的 Spring web 应用。Spring Boot 团队创建了 Spring Boot CLI

Spring Boot 命令行界面

Spring Boot CLI(命令行界面)是创建 Spring 应用的许多方法之一,但这种方法通常用于原型应用。你可以把它当成 Spring Boot 的游乐场。参考模型将在以下章节中介绍。我只是想使用简单的 Groovy 或 Java 脚本让您感受一下 Spring Boot 的强大。对我来说,Spring Boot CLI 是 Spring Boot 生态系统的重要组成部分。

现在,让我们回到前面的代码。您是否注意到清单 2-1 中没有导入?Spring Boot CLI 如何了解 web 应用以及如何运行它?

Spring Boot CLI 检查您的代码,并基于 Spring MVC 注释(@RestController@GetMapping),它尝试使用嵌入式 Tomcat 服务器将您的代码作为 web 应用执行,并从内部运行 web 应用。幕后的神奇之处在于,Groovy 编程语言提供了一种简单的方法,通过使用 AST(抽象语法树)来截取语句和创建动态代码;因此,很容易注入缺失的 Spring 代码并运行它。换句话说,Spring Boot CLI 发现了您的应用,并注入了缺失的部分,以使一个完整的 Spring web 应用启动并运行。

还记得我说过它也可以运行 Java 脚本吗?让我们看一下同一个 web 应用的 Java 版本。我将暂时向您展示代码;如果你想运行这些应用,你可以阅读附录,在那里我解释了如何安装 Spring Boot CLI 及其特性(见清单 2-2 )。

package com.apress.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SimpleWebApp {

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

    @RequestMapping("/")
    public String greetings(){
        return "<h1>Spring Boot Rocks in Java too!</h1>";
    }
}

Listing 2-2
SimpleWebApp.java

清单 2-2 显示了 Java 中 Spring Boot 应用的入口点。首先,它在执行应用的 main 方法中使用了一个@SpringBootApplication注释和SpringApplication singleton 类。SpringApplication.run方法调用接受两个参数。第一个参数是包含@Configuration注释的主配置类(恰好是这个类的名字;但稍后将对此进行更多讨论)。第二个参数是应用参数(我们将在后面的章节中讨论)。从这个 Java 版本中可以看出,我们使用了 Spring MVC 注释:@RestController@GetMapping

您可以通过执行以下命令来运行此示例

$ spring run SimpleWebApp.java

如果你打开浏览器,点击http://localhost:8080/,你会看到“Spring Boot 也用 Java 摇滚了!”。

如果您想设置您的 Spring Boot CLI,您可以跳转到附录,在那里我包括一个分步安装,其所有功能,以及 Spring Boot CLI 的好处。对于 Spring 云应用的快速原型,Spring Boot CLI 是完美的播放器;这就是我在本书中包含 Spring Boot CLI 的原因。

Spring Boot 应用模型

Spring Boot 定义了一种轻松创建 Spring 应用的方法,以及一种遵循 Spring 应用最佳实践的编程模型。要创建 Spring Boot 应用,您需要以下组件:

  • 一个构建/依赖管理工具,比如 Maven 或者 Gradle (Spring Boot 也支持 AntIvy;在本书中,每个例子只需要 Maven 或 Gradle)。

  • 构建工具中正确的依赖管理和插件。如果你使用 Maven,需要一个<parent/>标签(当然,有更多的方法来配置 Spring Boot,但是添加一个<parent/>标签是最简单的)和spring-boot-maven-plugin。如果你正在使用 Gradle,你需要应用org.springframework.bootio.spring.dependency-management插件。

    • 使用spring-boot-starters 添加所需的依赖项。
  • 创建包含以下内容的主类

    • @SpringBootApplication注解

    • main 方法中的SpringApplication.run语句。

在下一节中,我们将创建我们的第一个 Spring Boot 应用,我将解释所有前面的组件。这非常简单,但是我们如何开始呢?有什么工具可以帮助我们开始一个 Spring Boot 项目吗?答案是肯定的!我们实际上可以使用 Spring Boot CLI,因为它提供了一种创建 Spring Boot 项目的方法。我们还有 ide(集成开发环境),比如STS(Spring Tool Suitehttps://spring.io/tools)、来自 JetBrains(https://www.jetbrains.com/idea/)NetBeans(https://netbeans.org)、GitHubAtom(https://atom.io)、微软 VSCode ( https://code.visualstudio.com )。Atom 和 VSCode 都有插件,可以以非常简单的方式处理 Spring Boot 应用;但是Spring Initializr(http://start.spring.io)是我启动 Spring Boot 项目的偏好。在本书中,我使用 IntelliJ IDEA。

让我们通过创建第一个 Spring Boot 应用来看看如何使用 Spring Boot Initializr web 服务。

我的第一份 Spring Boot 申请

要创建我们的第一个 Spring Boot 应用,打开您的浏览器并进入 http://start.spring.io (见图 2-2 )。

img/340891_2_En_2_Fig2_HTML.jpg

图 2-2

http://start.spring.io

图 2-2 显示了 Spring Boot Initializr 的主页,这是 Pivotal 提供的一项网络服务,可以帮助您轻松创建 Spring Boot 项目。

  1. Let’s start by filling out the fields.

    • 组:com.apress

    • 神器:demo

    • 依赖关系:web

    您可以选择 Maven 或 Gradle 项目类型。您可以选择编程语言(Java、Groovy 或 Kotlin)和 Spring Boot 版本。在 Generate Project 按钮下面,有一个链接,上面写着“切换到完整版本”链接,它显示了您需要的依赖项。在这种情况下,您可以在依赖项字段中输入 Web 并点击回车,如图 2-2 所示。

  2. 单击 Generate Project 按钮保存一个名为demo.zip的文件。

  3. 解压缩demo.zip文件并将项目导入到您喜欢的 IDE 中(我使用 IntelliJ IDEA)。如果你仔细观察,你会发现。zip 文件有一个包装器,这取决于您选择的项目类型。如果是 Gradle 项目,那么有一个grad le(grad le wrapper);如果它是一个 Maven 项目,那么它应该是一个 mvnw (Maven 包装器)。这意味着你不需要安装任何这些构建/管理工具,因为 Spring Boot Initializr 会把它们带给你。

  4. 检查构建/依赖项管理文件。打开pom.xmlbuild.gradle文件。

    如果您选择了 Maven,请参见清单 2-3 。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>

      <groupId>com.apress</groupId>
      <artifactId>demo</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>jar</packaging>

      <name>demo</name>
      <description>Demo project for Spring Boot</description>

      <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
         <version>2.0.0.RELEASE</version>
         <relativePath/> <!-- lookup parent from repository -->
      </parent>

      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
      </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-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>

Listing 2-3
Maven pom.xml

如您所见,我们拥有主要组件的一部分:<parent/>标签、spring-boot-starter-web依赖项和spring-boot-maven-plugin

如果您选择了 Gradle,请参见清单 2-4 。

buildscript {

      ext {
            springBootVersion = '2.0.0.RELEASE'
      }
      repositories {
            mavenCentral()
      }
      dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
      }

}

apply plugin: 'java'
apply plugin: 'eclipse'

apply plugin: 'org.springframework.boot'

apply plugin: 'io.spring.dependency-management'

group = 'com.apress'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
      mavenCentral()
}

dependencies {
   compile('org.springframework.boot:spring-boot-starter-web')
   testCompile('org.springframework.boot:spring-boot-starter-test')
}

Listing 2-4
build.gradle

build.gradle文件显示了一些必需的组件:org.springframework.bootio.spring.dependency-management插件,以及spring-boot-starter-web依赖项。

  1. 打开com.apress.demo.DemoApplication.java类(参见清单 2-5 )。
package com.apress.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);
      }
}

Listing 2-5com.apress.demo.DemoApplication.java

如您所见,我们有运行应用的其他组件:@SpringBootApplication注释和SpringApplication.run语句。

  1. 为 web 控制器添加一个显示为文本的新类。创建com.apress.demo.WebController.java类(参见清单 2-6 )。
package com.apress.demo;

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

@RestController
public class WebController {

    @GetMapping
    public String index(){
        return "Hello Spring Boot";
    }
}

Listing 2-6com.apress.demo.WebController.java

这与脚本非常相似——一个简单的返回字符串的@RestController

  1. 要运行您的应用,您可以使用 IDE,也可以转到项目的根目录并执行以下命令。对于 maven: ./mvnw spring-boot:run,对于 Gradle: ./gradlew bootRun

然后你可以去你的浏览器点击http://localhost:8080。你会看到“你好,Spring Boot”的短信。

恭喜你!您刚刚创建了第一个 Spring Boot 应用。

注意

这本书的所有配套代码都在 press 网站上。在本例中,我创建了两个项目:一个用 Maven,另一个用 Gradle。

为什么是 Spring Boot?

我们为什么要用 Spring Boot?这是一项令人惊叹的技术,适用于

  • 遵循网飞工程团队开发的 12 个因素模式的云原生应用( http://12factor.net )

  • 通过减少开发和部署时间提高生产效率。

  • 企业生产就绪的 Spring 应用。

  • 非功能性需求,如 Spring Boot 执行器(一个模块,提供与新平台无关的千分尺 ( https://micrometer.io )、健康检查和管理)和用于运行 web 应用的嵌入式容器(Tomcat、Netty、Undertow、Jetty 等)。).

  • 微服务,它因创建可伸缩、高可用性和健壮的应用而受到关注。Spring Boot 允许开发人员只关注业务逻辑,把重担留给 Spring 框架。

Spring Boot 特色

Spring Boot 有很多特色,我会在接下来的章节中向你展示,但我可以在这一节描述其中的一些。

Spring Boot

  • 提供了SpringApplication类。我向您展示了在 Java Spring Boot 应用中,main 方法执行这个单例类。这个特殊的类提供了一种便捷的方式来启动 Spring 应用。

  • 允许您创建应用,而不需要任何 XML 配置。Spring Boot 不做任何代码生成。

  • 通过SpringApplicationBuilder singleton 类提供了一个 fluent builder API,它允许您创建具有多个应用上下文的层次结构。这个特性与 Spring 框架及其内部工作方式更相关。如果你是一个 Spring 开发者,我会在接下来的章节中解释这个特性,但是如果你是 Spring 和 Spring Boot 的新手,那么你只需要知道你可以扩展 Spring Boot 来获得对你的应用更多的控制。

  • 提供了更多配置 Spring 应用事件和监听器的方法。

  • 提供“固执己见”的技术;这个特性试图创建正确类型的应用,既可以是 web 应用(嵌入式 Tomcat、Netty、Undertow 或 Jetty 容器),也可以是单个应用。

  • 提供了org.springframework.boot.ApplicationArguments接口,允许访问任何应用参数。当您尝试使用参数运行应用时,这是一个有用的特性。

  • 允许您在应用启动后执行代码。你唯一需要做的就是实现CommandLineRunner接口,并提供run(String ...args)方法的实现。一个特殊的例子是在启动时初始化数据库中的记录,或者您可能希望在应用执行之前检查服务是否正在运行。

  • 允许您通过使用application.propertiesapplication.yml文件来具体化配置。在接下来的章节中会有更多的介绍。

  • 允许您通过启用application.properties or application.yml文件中的spring.application.admin.enabled属性来添加与管理相关的功能,通常是通过 JMX。

  • 允许您拥有配置文件,帮助您的应用在不同的环境中运行。

  • 允许您以非常简单的方式配置和使用日志记录。

  • 通过使用 starter poms,提供了一种配置和管理依赖项的简单方法。换句话说,如果你要创建一个 web 应用,你只需要在你的 Maven pom.xmlbuild.gradle文件中包含spring-boot-start-web依赖项。

  • 通过使用具有新的测微计平台无关框架的 Spring Boot 致动器,提供开箱即用的非功能性要求,这允许您对您的应用进行仪器化。

  • 提供@Enable<feature>注释,帮助您包含、配置和使用数据库(SQL 和 NoSQL)、缓存调度消息传递 Spring Integration Spring 批处理 Spring Cloud 等等技术。

Spring Boot 拥有所有这些特征,甚至更多。我将在接下来的章节中详细介绍这些特性。现在,是时候通过了解 Spring Boot 的内部运作来开始了解它了。

摘要

在这一章中,我向您简要介绍了 Spring Boot 技术,该技术专门用于轻松创建 Spring 企业级应用。

在接下来的章节中,我将向您展示 Spring Boot 的内部原理,以及根据您的依赖项和代码创建正确应用的幕后魔术。当你创建不同的项目时,我会谈到 Spring Boot 所有的酷功能。

三、Spring Boot 内部结构和特点

在前一章,我简要介绍了 Spring Boot,创建 Spring Boot 应用的主要组件,并讨论了使用 Spring Initializr 创建 Spring Boot 项目是多么容易。

在这一章中,我将向您展示当 Spring Boot 启动您的应用时,在幕后发生了什么。一切都是关于自动配置!我从 Groovy 脚本开始(同样,您可以跳到附录部分并安装 Spring Boot CLI)。我使用一个普通的 Java 项目,就像第二章中的 Spring Boot 应用一样。让我们从学习自动配置是如何工作的开始。

自动配置

自动配置是 Spring Boot 的重要特性之一,因为它根据类路径、注释和任何其他配置声明(如 JavaConfig 类或 XML)来配置您的 Spring Boot 应用。

清单 3-1 是前几章中的同一个例子,但是在这种情况下,我用它来解释当 Spring Boot 运行它时在幕后发生了什么。

@RestController
class WebApp{

    @GetMapping('/')
    String index(){
        "Spring Boot Rocks"
   }
}

Listing 3-1app.groovy

您可以使用 Spring Boot CLI(命令行界面)运行此程序

$ spring run app.groovy

Spring Boot 不会生成任何源代码,但它会动态添加一些。这是 Groovy 的优势之一:您可以在运行时访问 AST(抽象语法树)。Spring Boot 从导入缺失的依赖项开始,比如org.springframework.web.bind.annotation.RestController注释,以及其他导入。

接下来,它确定您需要一个 spring-boot-starter-web (我将在接下来的章节中详细讨论),因为您分别用@RestController@GetMapping注释标记了您的类和方法。它给代码添加了@Grab("spring-boot-web-starter")注释(对于 Groovy 脚本中的导入很有用)。

接下来,它添加了触发自动配置的必要注释,即@EnableAutoConfiguration注释(稍后,我会谈到这个注释,它恰好是 Spring Boot 背后的魔法),然后它添加了作为应用入口点的 main 方法。您可以在清单 3-2 中看到结果代码。

import org.springframework.web.bind.annotation.RestController

// Other Imports

@Grab("spring-boot-web-starter")

@EnableAutoConfiguration

@RestController
class WebApp{
   @GetMapping("/")
    String greetings(){
        "Spring Boot Rocks"
   }

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

Listing 3-2app.groovy Modified by Spring Boot

清单 3-2 显示了 Spring Boot 运行的实际修改后的程序。您可以看到自动配置是如何工作的,但是通过运行带有- debug 参数的清单 3-1 。让我们来看看。

$ spring run app.groovy --debug
...
DEBUG 49009 --- [] autoConfigurationReportLoggingInitializer :
=========================
AUTO-CONFIGURATION REPORT
=========================

Positive matches:
-----------------
//You will see all the conditions that were met to enable a Web application. And this is because you have the //@RestController annotation.

Negative matches:
-----------------
//You will find all the conditions that failed. For example you will find that the ActiveMQAutoConfiguration class did //not match, because you don't have any reference of the ActiveMQConnectionFactory.

在终端中查看该命令的输出。请注意 Spring Boot 在运行这个简单的应用之前所做的所有正面和负面匹配。因为您正在运行 Spring Boot CLI,所以它会尝试猜测您想要运行哪种应用。当你创建一个 Maven 或者 Gradle 项目,并且你指定了依赖关系(pom.xml或者build.gradle),你就在帮助 Spring Boot 根据你的依赖关系做出决定。

禁用特定的自动配置

在第二章中,我谈到了@SpringBootApplication注释,它是 Spring Boot 应用的主要组件之一。这个注释相当于声明了@Configuration@ComponentScan@EnableAutoConfiguration注释。我为什么要提这个?因为您可以通过在类中使用@EnableAutoConfiguration@SpringBootApplication注释添加exclude参数来禁用特定的自动配置。让我们看看清单 3-3 中 Groovy 脚本的一个例子。

import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration

@RestController
@EnableAutoConfiguration(exclude=[ActiveMQAutoConfiguration.class])
class WebApp{

    @RequestMapping("/")
        String greetings(){
             "Spring Boot Rocks"
        }
}

Listing 3-3
app.groovy

清单 3-3 显示了具有 exclude 参数的@EnableAutoConfiguration注释。此参数接收自动配置类的数组。如果您使用下面的代码再次运行此操作,您会看到您所做的操作被排除。

$ spring run app.groovy --debug
...
Exclusions:
-----------

   org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration
...

当您希望 Spring Boot 跳过某些不必要的自动配置 s 时,这对于 Groovy 脚本来说是一项非常有用的技术。

让我们看看如何在 Java Spring Boot 应用上使用它(参见清单 3-4 )。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration;

@SpringBootApplication(exclude={ActiveMQAutoConfiguration.class,DataSourceAutoConfiguration.class})

public class DemoApplication {

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

Listing 3-4DemoApplication.java: Spring Boot Snippet

清单 3-4 显示了一个 Java 版本;在这个例子中,主类只声明了@SpringBootApplication注释,在这个注释中,您可以排除自动配置类。清单 3-4 显示了两个被排除的类:ActiveMQAutoConfigurationDataSourceAutoConfiguration。为什么不使用@EnableAutoConfiguration标注?记住,@SpringBootApplication注释继承了@EnableAutoConfiguration@Configuration@ComponentScan,这就是为什么您可以在@SpringBootApplication注释中使用 exclude 参数。

当使用 debug 选项运行 Maven 或 Gradle 项目(使用示例清单 3-4 )时,您会在控制台输出中看到如下内容:

...
Exclusions:
-----------

   org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration
   org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
...

@EnableAutoConfiguration 和@Enable 批注

Spring 框架及其部分模块,如 Spring Data、Spring AMQP、Spring Integration,提供了@Enable<Technology>注解;例如,@EnableTransactionManagement@EnableRabbit@EnableIntegration就是上述模块的一部分。在 Spring 应用中,您可以使用这些注释来遵循配置模式上的约定,使您的应用更容易开发和维护,而不必太担心它的配置。

Spring Boot 利用这些注释,在@EnableAutoConfiguration注释中使用它们来进行自动配置。让我们仔细看看@EnableAutoConfiguration注释,看看它背后的逻辑以及@Enable<Technology>注释适合在哪里(参见清单 3-5 )。

...
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

      Class<?>[] exclude() default {};

      String[] excludeName() default {};

}

Listing 3-5org.springframework.boot.autoconfigure.EnableAutoConfiguration.java

清单 3-5 显示了@EnableAutoConfiguration标注;正如您已经知道的,这个类试图配置您的应用可能需要的 beans。自动配置类是基于类路径和应用定义的 beans 来应用的,但是这使得寻找所有必要配置类的org.springframework.boot.autoconfigure.AutoConfigurationImportSelector类更加强大。

AutoConfigurationImportSelector类有几个方法,但是对于自动配置最重要的一个是getCandidateConfigurations方法(参见清单 3-6 )。

...
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
                  AnnotationAttributes attributes) {
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
      getSpringFactoriesLoaderFactoryClass(),
                                          getBeanClassLoader());
      Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you
are using a custom packaging, make sure that file is correct.");
      return configurations;
}
...

Listing 3-6org.springframework.boot.autoconfigure.AutoConfigurationImportSelector Snippet

清单 3-6 展示了AutoConfigurationImportSelector类的一个片段,其中getCandidateConfigurations方法返回一个SpringFactoriesLoader.loadFactoryNamesSpringFactoriesLoader.loadFactoryNames寻找spring-boot-autoconfigure jar 中定义的META-INF/spring.factories(参见清单 3-7 了解其内容)。

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
....
....

Listing 3-7spring-boot-autoconfigure-<version>.jar/META-INF/spring.factories Snippet

从清单 3-7 中可以看出,spring.factories定义了所有的自动配置类,用于设置应用运行所需的任何配置。让我们来看看CloudAutoConfiguration级(见清单 3-8 )。

package org.springframework.boot.autoconfigure.cloud;

import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.Cloud;
import org.springframework.cloud.app.ApplicationInstanceInfo;
import org.springframework.cloud.config.java.CloudScan;
import org.springframework.cloud.config.java.CloudScanConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;

@Configuration
@Profile("cloud")
@AutoConfigureOrder(CloudAutoConfiguration.ORDER)
@ConditionalOnClass(CloudScanConfiguration.class)
@ConditionalOnMissingBean(Cloud.class)
@ConditionalOnProperty(prefix = "spring.cloud", name = "enabled", havingValue = "true", matchIfMissing = true)
@Import(CloudScanConfiguration.class)
public class CloudAutoConfiguration {

      // Cloud configuration needs to happen early (before data, mongo etc.)
      public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 20;

}

Listing 3-8org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration.java

清单 3-8 向您展示了CloudAutoConfiguration类。如您所见,这是一个非常短的类,但是如果它在应用类路径中找到 spring-cloud 类,它就配置了一个云应用,但是如何配置呢?它使用@ConditionalOnClass@ConditionalOnMissingBean注释来决定应用是否是云应用。不要太担心这个,因为在本书的章节扩展 Spring Boot 中,当你创建自己的自动配置类时,你会用到这些注释。

在清单 3-8 中要看到的另一件事是@ConditionalOnProperty注释的使用,这仅在属性spring.cloud被启用时才适用。值得一提的是,这种自动配置是在云配置文件中执行的,由@Profile标注表示。只有当其他注释满足它们的条件时,才会应用@Import注释(使用(@Conditional*注释),这意味着如果类路径中有spring-cloud-*类,就会执行CloudScanConfiguration类的导入。我会在第十三章中详细介绍。现在,您需要理解自动配置使用您的类路径来决定为您的应用配置什么。这就是为什么我们说 Spring Boot 是一个固执己见的运行时,还记得吗?

Spring Boot 特色

在这一部分,我将向您展示 Spring Boot 的一些特性。Spring Boot 是高度可定制的,从设置应用的自动配置(基于类路径)到定制它如何启动、显示什么以及基于它自己的属性启用或禁用什么。因此,让我们来了解一些定制您的 Spring 应用的 Spring Boot 功能。

让我们使用 Spring Boot 的 Initializr 创建一个 Spring Boot Java 项目。打开浏览器,进入 https://start.spring.io 。将以下值添加到字段中。请确保单击“切换到完整版本”字段,以便您可以修改软件包名称。

  • 组:com.apress.spring

  • 神器:spring-boot-simple

  • 名称:spring-boot-simple

  • 包名:com.apress.spring

您可以选择 Maven 或 Gradle 项目类型。然后点击 Generate Project 按钮,下载一个 ZIP 文件。将它解压缩到您喜欢的任何地方,并将其导入到您喜欢的 IDE 中(参见图 3-1 )。

img/340891_2_En_3_Fig1_HTML.jpg

图 3-1

Spring Boot 项目

注意

您可以从 Apress 网站下载源代码,在每个项目中您都可以找到 Maven pom.xml 和 Gradle build.gradle 文件,因此您可以选择您想要使用的构建工具。

现在,运行 Spring Boot 应用。如果您使用的是 Maven,请使用您的 IDE 或打开一个终端并执行以下命令。

$ ./mvnw spring-boot:run

如果你使用的是 Gradle,你可以执行

$ ./gradlew bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

INFO 10669 --- [    main] c.a.spring.SpringBootSimpleApplication   : Starting SpringBootSimpleApplication on ...
INFO 10669 --- [    main] c.a.spring.SpringBootSimpleApplication   : No active profile set, falling back to default profiles: default
INFO 10669 --- [    main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation...
INFO 10669 --- [    main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
INFO 10669 --- [    main] c.a.spring.SpringBootSimpleApplication   : Started SpringBootSimpleApplication in 1.582 seconds (JVM running for 4.518)
INFO 10669 --- [Thread-3] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation...
INFO 10669 --- [Thread-3] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

您应该会看到类似于以下输出的内容。它展示了一面旗帜(Spring Boot)和一些日志。让我们看看清单 3-9 中的主要应用。

package com.apress.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSimpleApplication {

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

Listing 3-9src/main/java/com/apress/spring/SpringBootSimpleApplication.java

清单 3-9 显示了主应用。您已经从上一章了解了 Spring Boot 组件,但是让我们再回顾一下。

  • @SpringBootApplication。这个注释实际上是@ComponentScan@Configuration@EnableAutoConfiguration注释。从前面的章节中你已经了解了关于@EnableAutoConfiguration的一切。

  • SpringApplication。这个类为在 main 方法中执行的 Spring Boot 应用提供了引导。您需要传递被执行的类。

现在,您可以开始定制 Spring Boot 应用了。

SpringApplication 类

您可以使用SpringApplication进行更高级的配置,因为您可以用它创建一个实例并做更多的事情(参见清单 3-10 )。

package com.apress.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSimpleApplication {

    public static void main(String[] args) {

      SpringApplication app =
            new SpringApplication(SpringBootSimpleApplication.class);
            //add more features here.
           app.run(args);
      }
}

Listing 3-10src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 2

SpringApplication允许你配置你的应用的行为方式,并且你可以控制所有 Spring beans 使用的主ApplicationContext。如果你需要了解更多关于ApplicationContext以及如何使用它,我推荐Pro Spring Framework 5(a press,2017),其中作者解释了关于 Spring 的一切。在这种情况下,我们重点关注 Spring Boot 的一些特性。让我们从酷的东西开始。

自定义横幅

每次运行应用时,您都会在应用的开头看到一个横幅。它可以用不同的方式定制。

实现org.springframework.boot.Banner接口(参见清单 3-11 )。

package com.apress.spring;

import java.io.PrintStream;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;

@SpringBootApplication
public class SpringBootSimpleApplication {

    public static void main(String[] args) {

      SpringApplication app = new SpringApplication(SpringBootSimpleApplication.class);
      app.setBanner(new Banner() {
            @Override
            public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
            out.print("\n\n\tThis is my own banner!\n\n".toUpperCase());
            }
      });
               app.run(args);
    }
}

Listing 3-11src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 3

当您运行该应用时,您会看到类似这样的内容:

$ ./mvnw spring-boot:run

      THIS IS MY OWN BANNER!

INFO[main] c.a.spring.SpringBootSimpleApplication   : Starting SpringBootSimpleApplication ...
...
...
INFO[main] c.a.spring.SpringBootSimpleApplication   : Started SpringBootSimpleApplication in 0.789seconds (JVM running for 4.295)
INFO[Th-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@203f6b5: startup date [Thu Feb 25 19:00:34 MST 2016]; root of context hierarchy
INFO[Th-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

您可以创建自己的 ASCII 横幅并显示它。怎么做?有一个很酷的站点,从文本( http://patorjk.com )创建 ASCII 艺术,如图 3-2 。

img/340891_2_En_3_Fig2_HTML.jpg

图 3-2

文本到 ASCII 艺术生成器

图 3-2 所示为 http://patorjk.com 的网站。点击文本到 ASCII 艺术生成器链接。然后,在文本字段中添加 Pro Spring Boot 2.0 (或者任何你想要的东西)。然后,点击测试所有以查看所有 ASCII 艺术(参见图 3-3 )。

img/340891_2_En_3_Fig3_HTML.jpg

图 3-3

ASCII 类型

图 3-3 显示了所有的 ASCII 艺术(约 314)。现在,您可以选择一个。点击选择文本按钮,复制(Ctrl+C Windows/Cmd+C macOS)它,在src/main/resources/目录下创建一个名为banner.txt的文件(见图 3-4 )。

img/340891_2_En_3_Fig4_HTML.jpg

图 3-4

src/main/resource/banner.txt 内容

您可以再次运行您的应用。

$ ./mvnw spring-boot:run

您会看到您在banner.txt文件中添加的 ASCII 图片。如果您使用清单 3-11 (您设置横幅的地方)运行您的应用,它会覆盖它并使用您的类路径中的banner.txt文件;这是默认的。

默认情况下,Spring Boot 在类路径中查找banner.txt文件。但是你可以改变它的位置。在src/main/resources/META-INF/目录下创建另一个banner.txt文件(或者复制你已经有的文件)。通过传递一个-D参数来运行应用。如果您使用的是 Maven,请执行以下命令。

$ ./mvnw spring-boot:run -Dspring.banner.location=classpath:/META-INF/banner.txt

如果你正在使用 Gradle,你需要首先在build.gradle文件的末尾添加这个配置。

bootRun {
    systemProperties = System.properties
}

执行以下命令。

$ ./gradlew bootRun -Dspring.banner.location=classpath:/META-INF/banner.txt

这个命令使用标志-D来传递指向新的类路径位置/META-INF/banner.txtspring.banner.location属性。您可以在src/main/resources/application.properties文件中声明这个属性,如下所示。

spring.banner.location=classpath:/META-INF/banner.txt

如果使用 Maven,就像这样运行它:

$ ./mvnw spring-boot:run

如果您使用的是 Gradle,请像这样运行它:

$ ./gradlew bootRun

设置banner.txt文件有几个选项。

您可以完全移除横幅。你可以在src/main/resources/application.properties中这样定义它:

spring.main.banner-mode=off

该命令优先于位于classpath:banner.txt位置的默认banner.txt文件。此外,您可以通过编程来实现(只需记住注释掉该属性)(参见清单 3-12 )。

package com.apress.spring;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSimpleApplication {

      public static void main(String[] args) {
            SpringApplication app = new SpringApplication(SpringBootSimpleApplication.class);
              app.setBannerMode(Banner.Mode.OFF);
              app.run(args);
      }
}

Listing 3-12src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 4

SpringApplicationBuilder

SpringApplicationBuilder类提供了一个流畅的 API,并且是SpringApplicationApplicationContext实例的构建器。它还提供了层次支持,并且我到目前为止向你展示的所有东西(用SpringApplication)都可以用这个构建器来设置(见清单 3-13 )。

package com.apress.spring;

import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class SpringBootSimpleApplication {

      public static void main(String[] args) {
        new SpringApplicationBuilder()
            .bannerMode(Banner.Mode.OFF)
            .sources(SpringBootSimpleApplication.class)
            .run(args);
      }
}

Listing 3-13src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 5

清单 3-13 展示了SpringAplicationBuilder fluent API。接下来,我们来看更多的例子。

创建 Spring 应用时,可以有一个层次结构。(如果想了解更多 Spring 的应用上下文,我推荐 Pro Spring 第五版。)可以用 SpringApplicationBuilder 创建。

new SpringApplicationBuilder(SpringBootSimpleApplication.class)
            .child(OtherConfig.class)
            .run(args);

如果您有一个 web 配置,请确保它被声明为子配置。此外,父节点和子节点必须共享相同的org.springframework.core.env.Environment接口(这代表当前应用运行的环境;它与概要文件和属性声明相关)。

您可以在启动时记录信息;默认情况下,它被设置为 true。

new SpringApplicationBuilder(SpringBootSimpleApplication.class)
            .logStartupInfo(false)
            .run(args);

您可以激活配置文件。

new SpringApplicationBuilder(SpringBootSimpleApplication.class)
            .profiles("prod","cloud")
            .run(args);

稍后,我将向您展示概要文件,以便您能够理解前面的行。

您可以为一些ApplicationEvent事件附加监听器。

Logger log = LoggerFactory.getLogger(SpringBootSimpleApplication.class);
new SpringApplicationBuilder(SpringBootSimpleApplication.class)
      .listeners(new ApplicationListener<ApplicationEvent>() {
      @Override
      public void onApplicationEvent(ApplicationEvent event) {
                   log.info("#### > " + event.getClass().getCanonicalName());
             }

            })
      .run(args);

当您运行应用时,您应该会看到以下输出。

...
#### > org.springframework.boot.context.event.ApplicationPreparedEvent
...
#### > org.springframework.context.event.ContextRefreshedEvent
#### > org.springframework.boot.context.event.ApplicationReadyEvent
...
#### > org.springframework.context.event.ContextClosedEvent
...

您的应用可以添加必要的逻辑来处理这些事件。您还可以拥有这些额外的事件:ApplicationStartedEvent(这在开始时发送)、ApplicationEnvironmentPreparedEvent(这在环境已知时发送)、ApplicationPreparedEvent(这在 bean 定义之后发送)、ApplicationReadyEvent(这在应用准备就绪时发送)、以及ApplicationFailedEvent(这在启动期间出现异常时发送)。我向您展示了输出中的另一个(与 Spring 容器更相关)。

您可以阻止任何 web 环境自动配置的发生。请记住,Spring Boot 根据类路径猜测您正在运行哪种应用。对于一个 web app 来说,算法很简单;但是想象一下,你使用的库实际上是在没有 web 环境的情况下运行的,你的应用不是 web 应用,但是 Spring Boot 把它配置成 web 应用。

new SpringApplicationBuilder(SpringBootSimpleApplication.class)
            .web(WebApplicationType.NONE)
            .run(args);

你发现WebApplicationType是一个枚举。您的应用可以配置为WebApplicationType.NONEWebApplicationType.SERVLETWebApplicationType.REACTIVE。如你所见,它的意思很简单。

应用参数

Spring Boot 允许您将参数传递给应用。当您拥有SpringApplication.run(SpringBootSimpleApplication.class, args)时,您可以访问 beans 中的参数(参见清单 3-14 )。

package com.apress.spring;

import java.io.IOException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;

@SpringBootApplication
public class SpringBootSimpleApplication {

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

@Component
class MyComponent {

   private static final Logger log = LoggerFactory.getLogger(MyComponent.class);

   @Autowired
   public MyComponent(ApplicationArguments args) {
        boolean enable = args.containsOption("enable");
        if(enable)
            log.info("## > You are enabled!");

        List<String> _args = args.getNonOptionArgs();
           log.info("## > extra args ...");
        if(!_args.isEmpty())
           _args.forEach(file -> log.info(file));
   }
}

Listing 3-14src/main/java/com/apress/spring/SpringBootSimpleApplication.java -version 6

当您执行containsOption时,它期望参数是--<arg>。在清单 3-14 中,期待的是--enable的说法;getNonOptionArgs接受其他参数。要测试它,您可以执行下面的命令。

$ ./mvnw spring-boot:run -Dspring-boot.run.arguments="--enable"

你应该看看## > You are enabled

此外,您可以像这样运行它:

$ ./mvnw spring-boot:run -Dspring-boot.run.arguments="arg1,arg2"

如果你正在使用 Gradle(在撰写本文时),你需要等待一段时间,因为传递参数仍然是一个问题(见https://github.com/spring-projects/spring-boot/issues/1176);但是您可以在可执行的 jar 中传递一个参数,我将在下一节中对此进行描述。

使用可执行 JAR 访问参数

您可以选择创建一个独立的应用——一个可执行的 JAR(您将了解到更多相关内容)。要创建一个可执行的 JAR,如果您使用的是 Maven,请执行下面的命令。

$ ./mvnw package

这个命令在target目录中创建可执行的 JAR。

或者,如果您使用的是 Gradle,您可以执行

$./gradlew build

这个命令在build/libs目录中创建一个可执行的 JAR。

现在您可以运行可执行的 JAR 了。

$ java -jar spring-boot-simple-0.0.1-SNAPSHOT.jar

您可以像这样传递参数:

$ java -jar spring-boot-simple-0.0.1-SNAPSHOT.jar --enable arg1 arg2

您应该得到相同的文本作为参数enable和列表arg1arg2

ApplicationRunner 和 CommandLineRunner

如果你在执行完SpringApplication后注意到 Spring Boot,它就结束了。如果您很好奇,在这个类被执行后,您不能使用 beans,但是有一个解决方案。Spring Boot 有公开运行方法的ApplicationRunnerCommandLineRunner接口(参见清单 3-15 )。

package com.apress.spring;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBootSimpleApplication implements CommandLineRunner, ApplicationRunner{
      private static final Logger log = LoggerFactory.getLogger(SpringBootSimpleApplication.class);

      public static void main(String[] args) throws IOException {

            SpringApplication.run(SpringBootSimpleApplication.class, args);

      }

@Bean
      String info(){
            return "Just a simple String bean";
      }

      @Autowired
      String info;

      @Override
      public void run(ApplicationArguments args) throws Exception {
            log.info("## > ApplicationRunner Implementation...");
            log.info("Accessing the Info bean: " + info);
            args.getNonOptionArgs().forEach(file -> log.info(file));
      }
      @Override
      public void run(String... args) throws Exception {
            log.info("## > CommandLineRunner Implementation...");
            log.info("Accessing the Info bean: " + info);
            for(String arg:args)
                  log.info(arg);
      }
}

Listing 3-15src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 7

清单 3-15 显示了CommandLineRunnerApplicationRunner以及它们的实现。CommandLineRunner曝光public void run(String... args)方法,ApplicationRunner曝光public void run(ApplicationArguments args)方法。他们实际上是一样的。没有必要同时实现这两者。清单 3-16 显示了使用CommandLineRunner接口的另一种方式。

package com.apress.spring;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBootSimpleApplication {
      private static final Logger log = LoggerFactory.getLogger(SpringBootSimpleApplication.class);

      public static void main(String[] args) throws IOException {

            SpringApplication.run(SpringBootSimpleApplication.class, args);

      }

      @Bean
      String info(){
            return "Just a simple String bean";
      }

      @Autowired
      String info;

      @Bean
      CommandLineRunner myMethod(){
            return args -> {
                  log.info("## > CommandLineRunner Implementation...");
                  log.info("Accessing the Info bean: " + info);
                  for(String arg:args)
                        log.info(arg);
            };
      }
}

Listing 3-16src/main/java/com/apress/spring/SpringBootSimpleApplication.java Version 8

清单 3-16 显示了一个用@Bean注释的方法,返回一个CommandLineRunner实现。这个例子使用 Java 8 语法(lambda)进行返回。您可以添加任意多的返回CommadLineRunner的方法。如果您想以某种顺序执行,您可以使用@Order注释。

应用配置

我们开发人员知道我们永远不会摆脱应用配置。我们总是寻找持久存储的位置,例如 URL、IP、凭证、数据库信息等等——我们通常在应用中经常使用的数据。我们知道,作为一种最佳实践,我们需要避免对这种配置信息进行硬编码。我们需要外部化,以便它可以安全、易于使用和部署。

使用 Spring,您可以选择使用 XML 和<context:property-placeholder/>标签,或者您可以使用@PropertySource注释来声明您的属性,并指向声明它们的文件。Spring Boot 为你提供了同样的机制,但有更多的改进。

  • Spring Boot 为您提供了不同的选项来保存您的应用配置。
    • 您可以使用名为application.properties的文件,该文件应该位于应用的根类路径中。(您可以在更多选项中添加该文件,稍后我将向您展示。)

    • 您可以使用名为application.yml的 YAML 符号文件,该文件也需要位于根类路径中。(您可以在更多选项中添加该文件,稍后我将向您展示。)

    • 您可以使用环境变量。这正在成为云场景的默认实践。

    • 您可以使用命令行参数。

请记住,Spring Boot 是一种固执己见的技术。它的大部分应用配置都是基于一个通用的application.propertiesapplication.yml文件。如果没有指定,它已经将这些属性值作为默认值。您可以在 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 获得常用应用属性的完整列表。

Spring(和 Spring Boot)最好的特性之一是通过使用@Value注释(带有属性名)来访问属性值。或者可以从从org.springframework.core.env.PropertyResolver接口扩展而来的org.springframework.core.env.Environment接口访问它们。例如,如果您有一个带有

data.server=remoteserver:3030

您可以通过使用@Value注释来访问应用中的data.server属性,如下面的代码片段所示。

//...
@Service
public class MyService {

@Value("${data.server}")
private String server;

      //...
}

这段代码片段展示了@Value注释的用法。Spring Boot 将来自application.properties文件的data.server属性值注入到带有值remoteserver:3030的服务器变量中。

如果您不想使用application.properties,您可以选择通过命令行注入属性。

$ java -jar target/myapp.jar --data.server=remoteserver:3030

你会得到同样的结果。如果您不喜欢application.properties文件或者您讨厌 YAML 语法,您可以使用一个名为SPRING_APPLICATION_JSON的专用环境变量来公开相同的属性及其值。

$ SPRING_APPLICATION_JSON='{ "data":{"server":"remoteserver:3030"}}' java -jar target/myapp.jar

同样,你会得到同样的结果。(Windows 用户应该先使用SET指令设置环境变量。)因此,Spring Boot 为您提供了公开应用属性的选项。

配置属性示例

让我们创建一个简单的项目,帮助您更好地理解应用配置。打开浏览器,进入 https://start.spring.io 。使用下列字段值。

  • 组:com.apress.spring

  • ArtifactId: spring-boot-config

  • 包名:com.apress.spring

  • 名称:spring-boot-config

你可以选择任何你觉得舒服的项目类型。单击 Generate Project 按钮,保存 ZIP 文件,将其解压缩并导入到您喜欢的 IDE 中。

在继续项目之前,如果要覆盖应用配置属性,您必须知道 Spring Boot 使用了一个顺序:

  • 命令行参数

  • SPRING _ 应用 _JSON

  • JNDI (java:comp/env)

  • System.getProperties()

  • 操作系统环境变量

  • RandomValuePropertySource(随机值)。*)

  • 特定于配置文件(应用-{profile})。jar)在包 jar 之外。

  • 特定于配置文件(应用-{profile})。jar)在包装罐的内部。

  • 包 jar 外部的应用属性(application.properties)。

  • 包 jar 中的应用属性(application.properties)。

  • @财产来源

  • spring application . set default properties

如您所见,这是覆盖应用属性的顺序。先说几个例子。

命令行参数

转到您的项目,编辑主类,看起来像清单 3-17 。

package com.apress.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBootConfigApplication {

      private static Logger log = LoggerFactory.getLogger(SpringBootConfigApplication.class);

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

      @Value("${server.ip}")
      String serverIp;

      @Bean
      CommandLineRunner values(){
            return args -> {
                  log.info(" > The Server IP is: " + serverIp);
            };
      }

}

Listing 3-17src/main/java/com/apress/spring/SpringBootConfigApplication.java

清单 3-17 显示了主类。如您所见,它使用了@Value("${server.ip}")注释。这个注释翻译文本"${server.ip}"。它按照我之前提到的顺序查找这个属性及其值。

您可以通过在项目的根目录下执行以下命令来运行此示例。

$ ./mvnw spring-boot:run -Dserver.ip=192.168.12.1

如果您首先打包您的应用(创建一个可执行的 JAR ),然后如下运行它。

$ /.mvnw package
$ java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar --server.ip=192.168.12.1

在这两种情况下,您都会看到类似于下面的输出。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

INFO - [main] c.a.spring.SpringBootConfigApplication   : Starting SpringBootConfigApplication v0.0..
INFO - [main] c.a.spring.SpringBootConfigApplication   : No active profile set, falling back to de..
INFO - [main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.an..
INFO - [main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on sta..
INFO - [main] c.a.spring.SpringBootConfigApplication   : Started SpringBootConfigApplication in 0...
INFO - [main] c.a.spring.SpringBootConfigApplication   :  > The Server IP is: 192.168.34.56
INFO - [main] c.a.spring.SpringBootConfigApplication   :  > App Name: My Config App
INFO - [main] c.a.spring.SpringBootConfigApplication   :  > App Info: This is an example
INFO - [ T-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annot..
INFO - [ T-2] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdo...

从输出中可以看到这个:> The Server IP is: 1921.68.12.1

现在,让我们创建application.properties文件(参见清单 3-18 了解其内容)。

server.ip=192.168.23.4

Listing 3-18src/main/resources/application.properties

如果您使用相同的命令行参数运行应用,您会看到参数优先于来自application.properties文件的参数,但是如果您不使用参数运行它,如下所示。

$ ./mvnw spring-boot:run

或者

$ /.mvnw package
$ java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar

你得到了textThe Server IP is: 192.168.3.4。如果您做了大量的 JSON 格式化工作,也许您会对以这种格式传递属性感兴趣。您可以使用spring.application.json属性。所以,你可以这样运行它:

$ ./mvnw spring-boot:run -Dspring.application.json='{"server":{"ip":"192.168.145.78"}}'

或者

$ java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar --spring.application.json='{"server":{"ip":"192.168.145.78"}}'

您也可以将其作为环境变量添加。

$ SPRING_APPLICATION_JSON='{"server":{"ip":"192.168.145.78"}}' java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar

你看正文> The Server IP is: 192.168.145.78。是的,您可以添加引用您的属性的环境变量,如下所示:

$ SERVER_IP=192.168.150.46 ./mvnw spring-boot:run

或者

$ SERVER_IP=192.168.150.46 java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar

你看正文> The Server IP is: 192.168.150.46

注意

请记住,对于 Windows 用户,您需要使用SET指令来使用环境变量。

Spring Boot 如何知道环境变量与属性server.ip相关?

宽松约束

Spring Boot 使用宽松的装订规则(见表 3-1 )。

表 3-1

Spring Boot 放松了约束

|

财产

|

描述

|
| --- | --- |
| message.destinationName | 标准骆驼箱 |
| 消息.目的地名称 | 虚线表示法,这是在 application.properties 或 YML 文件中添加的推荐方法。 |
| 消息目的地名称 | 大写,这是操作系统环境变量的推荐用法 |

表 3-1 显示了适用于酒店名称的宽松规则。这就是为什么在前面的例子中,server.ip属性也被识别为SERVER_IP。这个宽松的规则与@ConfigurationProperties注释及其前缀有关,您将在后面的章节中看到。

更改位置和名称

Spring Boot 接到命令要找到application.properties或 YAML 的档案。

  1. 它查看位于当前目录中的/config子目录。

  2. 当前目录

  3. 类路径/配置包

  4. 类路径根

您可以通过在当前目录下创建一个/config子目录并添加一个新的application.properties来进行测试,并测试顺序是否正确。记住,您应该已经在类路径根(src/main/resources)中有了一个application.properties文件。

Spring Boot 允许您更改属性文件的名称及其位置。所以举个例子,假设你使用的是/config子目录,属性文件的名字现在是mycfg.properties(其内容:server.ip=127.0.0.1)。然后,您可以使用以下命令运行该应用。

$./mvnw spring-boot:run -Dspring.config.name=mycfg

或者

$ java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar --spring.config.name=mycfg

或者

$ SPRING_CONFIG_NAME=mycfg java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar

你应该看到正文:> The Server IP is: 127.0.0.1。没有必要在名称中包含.properties;它会自动使用它。你也可以改变它的位置。比如创建一个名为app的子目录,添加一个mycfg.properties文件(其内容:server.ip=localhost)。然后,您可以运行或执行您的应用

$ ./mvnw spring-boot:run -Dspring.config.name=mycfg -Dspring.config.location=file:app/

或者

$ java -jar target/spring-boot-config-0.0.1-SNAPSHOT.jar --spring.config.location=file:app/ --spring.config.name=mycfg

或者您可以在src/main/resources/META-INF/conf中添加mycfg.properties(您可以创建它)并执行以下命令:

$ mkdir -p src/main/resources/META-INF/conf
$ cp config/mycfg.properties src/main/resources/META-INF/conf/
$ ./mvnw clean spring-boot:run -Dspring.config.name=mycfg -Dspring.config.location=classpath:META-INF/conf/

您应该会看到文本> The Server IP is: 127.0.0.1。尝试更改该属性值,这样您就可以看到它实际上正在类路径中查找。

Spring Boot 也有搜索属性文件的命令。

  1. 类路径

  2. 类路径:/配置

  3. 文件:

  4. 文件:配置/

除非用spring.config.location属性来改变它,否则要设置哪个环境变量来改变属性文件的位置呢?就是SPRING_CONFIG_LOCATION

基于配置文件

如果我没弄错的话,从 3.1 版本开始,Spring 框架增加了一个很酷的特性,允许开发人员基于概要文件创建自定义属性和 beans。这是一种分离环境的有用方法,无需重新编译或打包 Spring 应用。唯一要做的事情是用@ActiveProfiles注释指定活动概要文件,或者获取当前环境并使用setActiveProfiles方法。或者你可以使用环境变量SPRING_PROFILES_ACTIVE,或者spring.profiles.active属性。

您可以使用以下格式的属性文件:application-{profile}.properties。让我们在你的config/子目录中创建两个文件:application-qa.propertiesapplication-prod.properties。让我们看看每一个的内容。

  • 应用-质量保证.属性

    server.ip=localhost
    
    
  • 应用产品属性

    server.ip=http://my-remote.server.com
    
    

现在,您可以使用

$ ./mvnw clean spring-boot:run -Dspring.profiles.active=prod

当您执行这个命令时,请看一下日志的开头。您应该会看到类似下面的输出。

...
INFO 58270 --- [main] c.a.spring... : The following profiles are active: prod
...
INFO 58270 --- [main] c.a.spring... :  > The Server IP is: http://my-remote.server.com
INFO 58270 --- [main] c.a.spring... :  > App Name: Super App
INFO 58270 --- [main] c.a.spring... :  > App Info: This is production

您应该看到图例、The following profiles are active: prod,当然还有 profile 应用属性 active ( application-prod.properties)值:> The Server IP is: http://my-remote.server.com 。作为练习,尝试将application-prod.properties的名称改为mycfg-prod.properties,将application-qa.properties的名称改为mycfg-qa.properties。使用获得新名称的 Spring 属性。如果您没有设置任何活动的概要文件,它将使用默认值,这意味着它会抓取application.properties

自定义属性前缀

Spring Boot 允许你为你的属性编写和使用你自己的自定义属性前缀。您唯一需要做的事情就是用@ConfigurationPropertie s 注释进行注释,这是一个 Java 类,它的属性是 setters 和 getters。

如果您正在使用 STS IDE,我建议在您的pom.xmlbuild.gradle文件中包含一个依赖项(取决于您正在使用哪个依赖项管理器)。这种依赖关系创建了一个代码洞察,它触发了属性编辑器的代码完成。所以如果你使用 Maven,你可以在你的pom.xml文件中添加下一个依赖项。

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
</dependency>

如果您正在使用 Gradle,您可以将以下依赖项添加到您的build.gradle文件中。

dependencies {
      optional "org.springframework.boot:spring-boot-configuration-processor"
}

compileJava.dependsOn(processResources)

这种依赖关系允许您处理自定义属性并完成代码。现在,让我们看看这个例子。修改您的src/main/resource/application.properties文件,看起来像清单 3-19 。

server.ip=192.168.3.5

myapp.server-ip=192.168.34.56
myapp.name=My Config App
myapp.description=This is an example

Listing 3-19src/main/resources/application.properties

清单 3-19 显示了application.properties文件。第二块是新的。它定义了以myapp为前缀的自定义属性。打开您的主应用类,将其编辑成清单 3-20 所示的样子。

package com.apress.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@SpringBootApplication
public class SpringBootConfigApplication {

      private static Logger log = LoggerFactory.getLogger(SpringBootConfigApplication.class);

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

      @Value("${myapp.server-ip}")
      String serverIp;

      @Autowired
      MyAppProperties props;

      @Bean
      CommandLineRunner values(){
            return args -> {
                  log.info(" > The Server IP is: " + serverIp);
                  log.info(" > App Name: " + props.getName());
                  log.info(" > App Info: " + props.getDescription());
            };
      }

      @Component
      @ConfigurationProperties(prefix="myapp")
      public static class MyAppProperties {
            private String name;
            private String description;
            private String serverIp;

            public String getName() {
                  return name;
            }
            public void setName(String name) {
                  this.name = name;
            }
            public String getDescription() {
                  return description;
            }

            public void setDescription(String description) {
                  this.description = description;
            }
            public String getServerIp() {
                  return serverIp;
            }
            public void setServerIp(String serverIp) {
                  this.serverIp = serverIp;
            }
      }

}

Listing 3-20src/main/java/com/apress/spring/SpringBootConfigApplication.java Version 9

清单 3-19 显示了主应用类。让我们检查一下。

  • @Value("${myapp.server-ip}")。现在注释有了一个 myapp.server-ip,这意味着该值等于 1921.68.34.56。

  • @Autowired MyAppProperties道具。这创建了一个MyAppProperties类型的实例。

  • @Component @ConfigurationProperties(prefix="myapp")@ConfigurationProperties注释告诉 Spring Boot,该类用于前缀为myappapplication.properties文件中定义的所有属性。这意味着当你有myapp.serverIp(或myapp.server-ip)myapp.namemyapp.description时,它会识别。@Component注释只确保该类作为 bean 被拾取。

Spring Boot 使用宽松的规则将Environment属性绑定到@ConfigurationPropertiesbean,所以没有任何冲突名称。

现在,如果您运行您的应用,您应该会看到您所有的myapp属性。

$ ./mvnw clean spring-boot:run
...
> The Server IP is: 192.168.34.56
> App Name: My Config App
> App Info: This is an example
...

正如您所看到的,您有很多使用应用配置属性的选项,如果您想使用 YAML 语法,请参考 Spring Boot 文档中的示例。

通过提供有助于属性完成的元数据信息,可以向 IDE 添加命中。看看 https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html 上关于如何创建元数据的参考文档。

摘要

本章通过解释自动配置特性,包括@EnableAutoConfiguration注释如何在幕后工作,向您展示了 Spring Boot 洞察。您还学习了如何排除一些自动配置类。

您了解了一些 Spring Boot 功能以及如何使用应用配置属性。您还了解了如何通过添加前缀来自定义您的应用配置属性。

在下一章中,你将通过 Spring Boot 学习更多关于 web 应用的知识。

四、Spring Boot 的网络应用

如今,web 是任何类型应用的主要渠道,从桌面到移动设备,从社交和商业应用到游戏,从简单内容到流数据。有了这个想法,Spring Boot 可以帮助您轻松开发下一代网络应用。

本章向您展示了如何轻松创建 Spring Boot web 应用。通过前面章节中的一些例子,你已经了解了你可以用网络做什么。您了解了 Spring Boot 使得用几行代码创建 web 应用变得更加容易,并且您不需要担心配置文件或寻找应用服务器来部署您的 web 应用。通过使用 Spring Boot 及其自动配置,您可以拥有一个嵌入式应用服务器,如 Tomcat、Netty、Undertow 或 Jetty,这使得您的应用非常易于分发和移植。

的实现

让我们开始谈论 Spring MVC 技术和它的一些特性。请记住,Spring 框架由大约 20 个模块或技术组成,web 技术是其中之一。对于 web 技术,Spring 框架有spring-webspring-webmvcspring-webfluxspring-websocket模块。

spring-web模块具有基本的 web 集成特性,比如多部分文件上传功能、Spring 容器的初始化(通过使用 servlet 监听器)以及面向 web 的应用上下文。spring-mvc模块(也称为 web 服务器模块)包含 web 应用的所有 Spring MVC(模型-视图-控制器)和 REST 服务实现。这些模块提供了许多特性,比如非常强大的 JSP 标记库、可定制的绑定和验证、灵活的模型传输、可定制的处理程序和视图解析等等。

Spring MVC 是围绕org.springframework.web.servlet.DispatcherServlet类设计的。这个 servlet 非常灵活,并且具有非常健壮的功能,这在其他任何 MVC web 框架中都是找不到的。使用DispatcherServlet,您有几个现成的解析策略,包括视图解析器、区域解析器、主题解析器和异常处理程序。换句话说,DispatcherServlet接受一个 HTTP 请求,并将其重定向到正确的处理程序(用@Controller@RestController标记的类以及使用@RequestMapping注释的方法)和正确的视图(您的 JSP)。

Spring Boot MVC 自动配置

通过将spring-boot-starter-web依赖项添加到您的pom.xmlbuild.gradle文件中,可以很容易地创建 Web 应用。这个依赖项提供了所有必需的spring-webjar 和一些额外的 jar,比如tomcat-embed*jackson(用于 JSON 和 XML)。这意味着 Spring Boot 使用 Spring MVC 模块的能力,并提供所有必要的自动配置来创建正确的 web 基础设施,例如配置DispatcherServlet,提供默认值(除非您覆盖它),设置嵌入式 Tomcat 服务器(这样您就可以在没有任何应用容器的情况下运行您的应用),等等。

自动配置为您的 web 应用添加了以下功能。

  • 静态内容支持。这意味着您可以在名为/static(默认情况下)或/public/resources或/ META-INF/resources的目录中添加静态内容,比如 HTML、JavaScript、CSS、media 等等,这些目录应该在您的类路径或当前目录中。Spring Boot 拿起它,并应要求为他们服务。您可以通过修改spring.mvc.static-path-patternspring.resources.static-locations属性来轻松改变这一点。Spring Boot 和 web 应用的一个很酷的特性是,如果你创建一个index.html文件,Spring Boot 会自动提供它,不需要注册任何其他 bean,也不需要额外的配置。

  • HttpMessageConverters 。如果您正在使用常规的 Spring MVC 应用,并且想要获得 JSON 响应,那么您需要为HttpMessageConverters bean 创建必要的配置(XML 或 JavaConfig)。Spring Boot 默认添加了这种支持,所以你不必这么做;这意味着默认情况下您会得到 JSON 格式(由于 Jackson 库,spring-boot-starter-web提供了依赖项)。如果 Spring Boot 自动配置发现您的类路径中有 Jackson XML 扩展,它会将一个 XML HttpMessageConverter聚合到转换器,这意味着您的应用可以基于您的content-type请求,或者是application/json或者是application/xml提供服务。

  • JSON 序列化器和反序列化器。如果你想对 JSON 的序列化/反序列化有更多的控制,Spring Boot 提供了一个简单的方法来创建你自己的,从JsonSerializer<T>和/或JsonDeserializer<T>扩展,用@JsonComponent注释你的类,这样它就可以注册使用了。Spring Boot 的另一个特点是杰克逊的支持;默认情况下,Spring Boot 将日期字段序列化为2018-05-01T23:31:38.141+0000,但是您可以通过更改spring.jackson.date-format=yyyy-MM-dd属性来更改这种默认行为(您可以应用任何日期格式模式);前一个值生成输出,如2018-05-01

  • 路径匹配和内容协商。Spring MVC 应用实践之一是能够响应任何后缀来表示内容类型响应及其内容协商。如果你有类似/api/todo.json或者/api/todo.pdf的东西,那么content-type设置为application/jsonapplication/pdf;所以响应分别是 JSON 格式或 PDF 文件。换句话说,Spring MVC 进行.*后缀模式匹配,比如/api/todo.*。默认情况下,Spring Boot 禁用此功能。通过使用spring.mvc.contentnegotiation.favor-parameter=true属性(默认为false,您仍然可以使用一个可以添加参数的特性;所以你可以做一些类似/api/todo?format=xml的事情。(format是默认的参数名;当然可以用spring.mvc.contentnegotiation.parameter-name=myparam来改。这将触发content-typeapplication/xml

  • 错误处理。Spring Boot 使用/error映射创建一个白色标签页面来显示所有的全局错误。您可以通过创建自己的自定义页面来更改行为。您需要在src/main/resources/public/error/位置创建您的定制 HTML 页面,这样您就可以创建500.html404.html页面。如果你正在创建一个 RESTful 应用,Spring Boot 以 JSON 格式响应。Spring Boot 还支持 Spring MVC 来处理使用@ControllerAdvice@ExceptionHandler注释时的错误。您可以通过实现ErrorPageRegistrar并将其声明为 Spring bean 来注册自定义ErrorPages

  • 模板引擎 支持。Spring Boot 支持 FreeMarker,Groovy 模板,百里香叶和小胡子。当包含spring-boot-starter-<template engine>依赖项时,Spring Boot 自动配置对于启用和添加所有必要的视图解析器和文件处理程序是必要的。默认情况下,Spring Boot 会查看src/main/resources/templates/路径。

还有许多其他功能,Spring Boot 网站自动配置提供。现在,我们只关注了 Servlet 技术,但是很快我们就会进入 Spring Boot 家族的最新成员: WebFlux

Spring Boot Web:所有应用

为了更好地理解 Spring Boot 如何使用 web 应用以及 Spring MVC 模块的强大功能,您将创建一个 ToDo 应用来公开一个 RESTful API。这些是要求:

  • 创建具有以下字段和类型的 ToDo 域模型:id(字符串)、description(字符串)、completed(布尔)、created(日期和时间)、modified(日期和时间)。

  • 创建一个 RESTful API,提供基本的 CRUD(创建、读取、更新、删除)操作。使用最常见的 HTTP 方法:POST、PUT、PATCH、GET 和 DELETE。

  • 创建一个存储库来处理多个待办事项的状态。目前,内存中的存储库就足够了。

  • 当有错误请求或提交新的 ToDo 没有必需字段时,添加错误处理程序。唯一的必填字段是描述。

  • 所有的请求和响应都应该是 JSON 格式。

所有应用

打开你的浏览器,进入 https://start.spring.io ,使用以下值创建你的待办事宜应用(另见图 4-1 )。

img/340891_2_En_4_Fig1_HTML.jpg

图 4-1

https://start.spring.io 所有应用

  • 组:com.apress.todo

  • 神器:todo-in-memory

  • 名称:todo-in-memory

  • 包名:com.apress.todo

  • 依赖关系:WebLombok

选择 Lombok 依赖项有助于轻松创建域模型类,并消除样板文件 setters、getters 和其他覆盖。

注意

如果您需要更多关于龙目岛的信息,请参见 https://projectlombok.org 的参考文件。

您可以选择 Maven 或 Gradle 作为项目类型;在本书中,我们不加区分地使用这两个词。按下 Generate Project 按钮并下载 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。一些最好的 ide 是 STS ( https://spring.io/tools/sts/all )、IntelliJ IDEA ( www.jetbrains.com/idea/ )和 VSCode ( https://code.visualstudio.com/ )。我为代码完成特性推荐这些 ide 中的一个,它可以帮助您看到要添加到代码中的方法或参数。

域模型:ToDo

根据需求,您需要创建一个ToDo域模型类(参见清单 4-1 )。

package com.apress.todo.domain;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class ToDo {

    @NotNull
    private String id;
    @NotNull
    @NotBlank
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        LocalDateTime date = LocalDateTime.now();
        this.id = UUID.randomUUID().toString();
        this.created = date;
        this.modified = date;
    }

    public ToDo(String description){
        this();
        this.description = description;
    }
}

Listing 4-1com.apress.todo.domain.ToDo.java

清单 4-1 向您展示了ToDo类,它拥有所有必需的字段。它还使用了@Data注释,这是一个 Lombok 注释,生成一个默认的构造函数(如果你没有的话)和所有的设置器、获取器和覆盖,比如toString方法,使类更干净。还要注意,该类在一些字段中有@NotNull@NotBlank注释;这些注释用于我们稍后进行的验证。默认的构造函数有字段初始化,所以很容易创建一个ToDo实例。

流畅的 API: ToDoBuilder

接下来,让我们创建一个流畅的 API 类来帮助创建一个ToDo实例。您可以看到这个类是一个工厂,它创建一个带有描述或特定 ID 的 ToDo(参见清单 4-2 )。

package com.apress.todo.domain;

public class ToDoBuilder {

    private static ToDoBuilder instance = new ToDoBuilder();
    private String id = null;
    private String description = "";

    private ToDoBuilder(){}

    public static ToDoBuilder create() {
        return instance;
    }

    public ToDoBuilder withDescription(String description){
        this.description = description;
        return instance;
    }

    public ToDoBuilder withId(String id){
        this.id = id;
        return instance;
    }

    public ToDo build(){
        ToDo result = new ToDo(this.description);
        if(id != null)
            result.setId(id);
        return result;
    }

}

Listing 4-2com.apress.todo.domain.ToDoBuilder.java

清单 4-2 是一个简单的工厂类,它创建了一个ToDo实例。您将在接下来的章节中扩展它的功能。

存储库:公共存储库

接下来,创建一个具有通用持久性操作的接口。这个接口是通用的,所以很容易使用任何其他实现,使回购成为一个可扩展的解决方案(参见清单 4-3 )。

package com.apress.todo.repository;

import java.util.Collection;

public interface  CommonRepository<T> {
    public T save(T domain);
    public Iterable<T> save(Collection<T> domains);
    public void delete(T domain);
    public T findById(String id);
    public Iterable<T> findAll();
}

Listing 4-3com.apress.todo.repository.CommonRepository<T>.java

清单 4-3 是一个通用接口,可以用作任何其他持久性实现的基础。当然,您可以随时更改这些签名。这只是一个如何创建可扩展的东西的例子。

存储库:ToDoRepository

让我们创建一个实现CommonRepository<T>接口的具体类。记住规范;现在,只需要在内存中保存待办事项(参见清单 4-4 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.stereotype.Repository;

@Repository
public class ToDoRepository implements CommonRepository<ToDo> {

    private Map<String,ToDo> toDos = new HashMap<>();

    @Override
    public ToDo save(ToDo domain) {
        ToDo result = toDos.get(domain.getId());
        if(result != null) {
            result.setModified(LocalDateTime.now());
            result.setDescription(domain.getDescription());
            result.setCompleted(domain.isCompleted());
            domain = result;
        }
        toDos.put(domain.getId(), domain);
        return toDos.get(domain.getId());

    }

    @Override
    public Iterable<ToDo> save(Collection<ToDo> domains) {
        domains.forEach(this::save);
        return findAll();
    }

    @Override
    public void delete(ToDo domain) {
        toDos.remove(domain.getId());
    }

    @Override
    public ToDo findById(String id) {
        return toDos.get(id);
    }

    @Override
    public Iterable<ToDo> findAll() {
        return toDos.entrySet().stream().sorted(entryComparator).map(Map.Entry::getValue).collect(Collectors.toList());
    }

    private Comparator<Map.Entry<String,ToDo>> entryComparator = (Map.Entry<String, ToDo> o1, Map.Entry<String, ToDo> o2) -> {
        return o1.getValue().getCreated().compareTo(o2.getValue().getCreated());
    };
}

Listing 4-4com.apress.todo.repository.ToDoRepository.java

清单 4-4 展示了CommonRepository<T>接口的实现。审查代码并进行分析。此类正在使用保存所有待办事项的哈希。由于 hash 的性质,所有的操作都得到了简化,使其易于实现。

验证:ToDoValidationError

接下来,让我们创建一个验证类,暴露应用中任何可能的错误,比如没有描述的 ToDo。记住,在ToDo类中,ID 和描述字段被标记为@NotNull。描述字段有一个额外的@NotBlank注释,以确保它从不为空(参见清单 4-5 )。

package com.apress.todo.validation;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.util.ArrayList;
import java.util.List;

public class ToDoValidationError {

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<String> errors = new ArrayList<>();

    private final String errorMessage;

    public ToDoValidationError(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public void addValidationError(String error) {
        errors.add(error);
    }

    public List<String> getErrors() {
        return errors;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}

Listing 4-5com.apress.todo.validation.ToDoValidationError.java

清单 4-5 显示了ToDoValidationError类,它保存任何请求产生的任何错误。它使用了一个额外的@JsonInclude注释,这意味着即使 errors 字段为空,也必须包含它。

验证:ToDoValidationErrorBuilder

让我们创建另一个帮助构建ToDoValidationError实例的工厂(参见清单 4-6 )。

package com.apress.todo.validation;

import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;

public class ToDoValidationErrorBuilder {

    public static ToDoValidationError fromBindingErrors(Errors errors) {
        ToDoValidationError error = new ToDoValidationError("Validation failed. " + errors.getErrorCount() + " error(s)");
        for (ObjectError objectError : errors.getAllErrors()) {
            error.addValidationError(objectError.getDefaultMessage());
        }
        return error;
    }
}

Listing 4-6com.apress.todo.validation.ToDoValidationErrorBuilder.java

清单 4-6 是另一个Factory类,它可以用所有必要的信息轻松创建一个ToDoValidationError实例。

控制器:ToDoController

现在,是时候创建 RESTful API 并使用所有以前的类了。您创建了ToDoController类,在其中您可以看到所有的 Spring MVC 特性、注释、配置端点的方式以及如何处理错误。让我们回顾一下清单 4-7 中的代码。

package com.apress.todo.controller;

import com.apress.todo.domain.ToDo;
import com.apress.todo.domain.ToDoBuilder;
import com.apress.todo.repository.CommonRepository;
import com.apress.todo.validation.ToDoValidationError;
import com.apress.todo.validation.ToDoValidationErrorBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.validation.Valid;
import java.net.URI;

@RestController

@RequestMapping("/api")

public class ToDoController {

    private CommonRepository<ToDo> repository;

    @Autowired
    public ToDoController(CommonRepository<ToDo> repository) {
        this. repository = repository;
    }

    @GetMapping("/todo")
    public ResponseEntity<Iterable<ToDo>> getToDos(){
        return ResponseEntity.ok(repository.findAll());
    }

    @GetMapping("/todo/{id}")
    public ResponseEntity<ToDo> getToDoById(@PathVariable String id){
        return ResponseEntity.ok(repository.findById(id));
    }

    @PatchMapping("/todo/{id}")
    public ResponseEntity<ToDo> setCompleted(@PathVariable String id){
        ToDo result = repository.findById(id);
        result.setCompleted(true);
        repository.save(result);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .buildAndExpand(result.getId()).toUri();

        return ResponseEntity.ok().header("Location",location.toString()).build();
    }

    @RequestMapping(value="/todo", method = {RequestMethod.POST,RequestMethod.PUT})
    public ResponseEntity<?> createToDo(@Valid @RequestBody ToDo toDo, Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(ToDoValidationErrorBuilder.fromBindingErrors(errors));
        }

        ToDo result = repository.save(toDo);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
                .buildAndExpand(result.getId()).toUri();
        return ResponseEntity.created(location).build();
    }

    @DeleteMapping("/todo/{id}")
    public ResponseEntity<ToDo> deleteToDo(@PathVariable String id){
        repository.delete(ToDoBuilder.create().withId(id).build());
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping("/todo")
    public ResponseEntity<ToDo> deleteToDo(@RequestBody ToDo toDo){
        repository.delete(toDo);
        return ResponseEntity.noContent().build();
    }

  @ExceptionHandler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public ToDoValidationError handleException(Exception exception) {
        return new ToDoValidationError(exception.getMessage());
    }

}

Listing 4-7com.apress.todo.controller.ToDoController.java

清单 4-7 是ToDoController类。我们来复习一下。

  • @RestController。Spring MVC 提供了@Controller@RestController来表达请求映射、请求输入、异常处理等等。所有的功能都依赖于这些注释,因此不需要扩展或实现接口特定的接口。

  • @RequestMapping。这个注释将请求映射到控制器方法。有几个属性来匹配 URL、HTTP 方法(GET、PUT、DELETE 等。)、请求参数、报头和媒体类型。它可以在类级(共享映射)使用,也可以在方法级用于特定端点映射。在这种情况下,用"/api"标记,意味着所有的方法都有这个前缀。

  • @Autowired。构造函数用@Autowired进行了注释,这意味着它注入了CommonRepository<ToDo>实现。此注释可以省略;从版本 4.3 开始,Spring 会自动注入任何声明的依赖项。

  • @GetMapping。这是@RequestMapping注释的快捷变体,对HTTP GET方法有用。@GetMapping相当于@RequestMapping(value="/todo", method = {RequestMethod.GET})

  • @PatchMapping。这是@RequestMapping注释的快捷方式变体;在这个类中,它将 ToDo 标记为已完成。

  • @DeleteMapping。这是@RequestMapping注释的快捷方式变体;它用于删除待办事项。有两个重载方法:deleteToDo,一个接受字符串,另一个接受ToDo实例。

  • @PathVariable。当您声明包含 URL 表达式模式的终结点时,此批注非常有用;在本例中是"/api/todo/{id}",其中 ID 必须与方法参数的名称相匹配。

  • @RequestBody。这个注释发送一个带有主体的请求。通常,当你提交一个表单或一个特定的内容时,这个类接收一个 JSON 格式的ToDo,然后HttpMessageConverter将 JSON 反序列化成一个ToDo实例;多亏了 Spring Boot 和它的自动配置,这是自动完成的,因为它默认注册了MappingJackson2HttpMessageConverter

  • ResponseEntity<T>。这个类返回一个完整的响应,包括 HTTP 头,主体通过HttpMessageConverters转换并写入 HTTP 响应。ResponseEntity<T>类支持流畅的 API,因此创建响应很容易。

  • @ResponseStatus。通常,当方法具有 void 返回类型(或 null 返回值)时,使用此注释。该注释发回响应中指定的 HTTP 状态代码。

  • @Valid。该注释验证传入的数据,并用作方法的参数。要触发一个验证器,需要用@NotNull@NotBlank和其他注释来注释您想要验证的数据。在这种情况下,ToDo类在 ID 和 description 字段中使用这些注释。如果验证器发现错误,它们被收集在Errors类中(在本例中,一个spring-webmvc jars 附带的 hibernate 验证器被注册并用作全局验证器;您可以创建自己的自定义验证并覆盖 Spring Boot 的默认值)。然后,您可以检查并添加必要的逻辑来发回错误响应。

  • @ExceptionHandler。Spring MVC 自动声明内置的异常解析器,并为该注释添加支持。在这种情况下,@ExceptionHandler在这个控制器类中声明(或者您可以在@ControllerAdvice拦截器中使用它),任何异常都被重定向到handleException方法。如果需要,您可以说得更具体一些。例如,你可以有一个DataAccessException,并通过一个方法来处理。

        @ExceptionHandler
        @ResponseStatus(value = HttpStatus.BAD_REQUEST)
        public ToDoValidationError
           handleException(DataAccessException exception) {
                         return new
                ToDoValidationError(exception.getMessage());
        }
    
    

在类中有一个方法接受两个 HTTP 方法:POSTPUT@RequestMapping可以接受多个 HTTP 方法,因此很容易分配一个方法来处理它们(例如@RequestMapping(value="/todo", method = {RequestMethod.POST, RequestMethod.PUT}))。

我们已经涵盖了这个应用的所有必要需求,所以是时候运行它并查看结果了。

运行:待办事项应用

现在,您已经准备好运行 ToDo 应用并测试它了。如果您使用的是 IDE (STS 或 IntelliJ),可以右键单击主应用类(TodoInMemoryApplication.java)并选择运行操作。如果您使用的编辑器没有这些功能,您可以通过打开终端窗口并执行清单 4-8 或清单 4-9 中的命令来运行您的 Todo 应用。

./gradlew spring-boot:run

Listing 4-9If you are using Gradle project type

./mvnw spring-boot:run

Listing 4-8If you are using Maven project type

Spring Initializr ( https://start.spring.io )总是提供你选择的项目类型包装器(Maven 或者 Gradle 包装器),所以不需要预装 Maven 或者 Gradle。

Spring Boot web 应用的一个默认设置是,它配置了一个嵌入式 Tomcat 服务器,因此您可以轻松运行您的应用,而无需将其部署到应用 servlet 容器中。默认情况下,它选择端口 8080。

测试:待办事项应用

测试 ToDo 应用应该非常简单。这种测试是通过命令或特定的客户端进行的。如果你正在考虑单元测试或集成测试,我会在另一章解释。这里我们将使用 cURL 命令。默认情况下,这个命令在任何 UNIX 操作系统中都有,但是如果你是 Windows 用户,你可以从 https://curl.haxx.se/download.html 下载它。

第一次运行时,待办事项应用不应该有任何待办事项。您可以通过在另一个终端中执行以下命令来确保这一点。

curl -i http://localhost:8080/api/todo

您应该会看到类似于以下输出的内容:

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 02 May 2018 22:10:19 GMT

[]

你的目标是/api/todo端点,如果你看一下清单 4-7 ,一个getToDos方法返回ResponseEntity<Iterable<ToDo>>,这是 ToDo 的集合。默认响应是 JSON 格式(见Content-Type头)。响应是发回 HTTP 头和状态。

接下来,让我们用下面的命令添加一些 ToDo。

curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read the Pro Spring Boot 2nd Edition Book"}' http://localhost:8080/api/todo

在命令中,post ( -X POST)和 data ( -d)是 JSON 格式。您只发送描述字段。需要添加具有正确内容类型的头(-H),并指向/api/todo端点。执行该命令后,您应该会看到如下输出:

HTTP/1.1 201
Location: http://localhost:8080/api/todo/d8d37c51-10a8-4c82-a7b1-b72b5301cdab
Content-Length: 0
Date: Wed, 02 May 2018 22:21:09 GMT

您获得了位置头,在那里 ToDo 被读取。Location显示您刚刚创建的待办事项的 ID。这个响应是由createToDo方法生成的。添加至少另外两个待办事项,以便我们可以有更多的数据。

如果您再次执行第一个命令来获得所有的 ToDo,您应该会看到类似这样的内容:

curl -i http://localhost:8080/api/todo
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 02 May 2018 22:30:16 GMT

[{"id":"d8d37c51-10a8-4c82-a7b1-b72b5301cdab","description":"Read the Pro Spring Boot 2nd Edition Book","created":"2018-05-02T22:27:26.042+0000","modified":"2018-05-02T22:27:26.042+0000","completed":false},{"id":"fbb20090-19f5-4abc-a8a9-92718c2c4759","description":"Bring Milk after work","created":"2018-05-02T22:28:23.249+0000","modified":"2018-05-02T22:28:23.249+0000","completed":false},{"id":"2d051b67-7716-4ee6-9c45-1de939fa579f","description":"Take the dog for a walk","created":"2018-05-02T22:29:28.319+0000","modified":"2018-05-02T22:29:28.319+0000","completed":false}]

当然,这打印得不是很好,但是你有完整的待办事项列表。您可以使用另一个命令行工具来格式化这个输出:jq ( https://stedolan.github.io/jq/ )。

curl -s http://localhost:8080/api/todo | jq
[
  {
    "id": "d8d37c51-10a8-4c82-a7b1-b72b5301cdab",
    "description": "Read the Pro Spring Boot 2nd Edition Book",
    "created": "2018-05-02T22:27:26.042+0000",
    "modified": "2018-05-02T22:27:26.042+0000",
    "completed": false
  },
  {
    "id": "fbb20090-19f5-4abc-a8a9-92718c2c4759",
    "description": "Bring Milk after work",
    "created": "2018-05-02T22:28:23.249+0000",
    "modified": "2018-05-02T22:28:23.249+0000",
    "completed": false
  },
  {
    "id": "2d051b67-7716-4ee6-9c45-1de939fa579f",
    "description": "Take the dog for a walk",
    "created": "2018-05-02T22:29:28.319+0000",
    "modified": "2018-05-02T22:29:28.319+0000",
    "completed": false
  }

]

接下来,您可以修改其中一个待办事项;例如,

curl -i -X PUT -H "Content-Type: application/json" -d '{ "description":"Take the dog and the cat for a walk", "id":"2d051b67-7716-4ee6-9c45-1de939fa579f"}' http://localhost:8080/api/todo
HTTP/1.1 201
Location: http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
Content-Length: 0
Date: Wed, 02 May 2018 22:38:03 GMT

此处Take the dog for a walk改为Take the dog and the cat for a walk。该命令使用-X PUT,并且需要id字段(我们可以从以前帖子的 location 头或者从访问/api/todo端点获得它)。如果你检查了所有的待办事项,你有一个修改的待办事项。

接下来,让我们完成一个待办事项。您可以执行以下命令。

curl -i -X PATCH http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
HTTP/1.1 200
Location: http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
Content-Length: 0
Date: Wed, 02 May 2018 22:50:27 GMT

该命令使用的是通过setCompleted方法处理的-X PATCH。如果您查看位置链接,ToDo 应该已完成。

curl -s http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f | jq
{
  "id": "2d051b67-7716-4ee6-9c45-1de939fa579f",
  "description": "Take the dog and the cat for a walk",
  "created": "2018-05-02T22:44:57.652+0000",
  "modified": "2018-05-02T22:50:27.691+0000",
  "completed": true
}

completed字段现在是true。如果此待办事项已完成,那么您可以删除它。

curl -i -X DELETE http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
HTTP/1.1 204
Date: Wed, 02 May 2018 22:56:18 GMT

cURL 命令有-X DELETE,它由deleteToDo方法处理,从散列中删除。如果你看一下所有的待办事项,你现在应该比以前少一个。

curl -s http://localhost:8080/api/todo | jq
[
  {
    "id": "d8d37c51-10a8-4c82-a7b1-b72b5301cdab",
    "description": "Read the Pro Spring Boot 2nd Edition Book",
    "created": "2018-05-02T22:27:26.042+0000",
    "modified": "2018-05-02T22:27:26.042+0000",
    "completed": false
  },
  {
    "id": "fbb20090-19f5-4abc-a8a9-92718c2c4759",
    "description": "Bring Milk after work",
    "created": "2018-05-02T22:28:23.249+0000",
    "modified": "2018-05-02T22:28:23.249+0000",
    "completed": false
  }
]

现在,让我们测试验证。执行以下命令。

curl -i -X POST -H "Content-Type: application/json" -d '{"description":""}' http://localhost:8080/api/todo

该命令在描述字段为空的情况下发送数据(-d选项)(通常,当您提交 HTML 表单时会发生这种情况)。您应该会看到以下输出。

HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 03 May 2018 00:01:53 GMT
Connection: close

{"errors":["must not be blank"],"errorMessage":"Validation failed. 1 error(s)"}

400 状态码(错误请求)和errorserrorMessage(由ToDoValidationErrorBuilder类构建)响应。使用以下命令。

curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/todo

HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 03 May 2018 00:07:28 GMT
Connection: close

{"errorMessage":"Required request body is missing: public org.springframework.http.ResponseEntity<?> com.apress.todo.controller.ToDoController.createToDo(com.apress.todo.domain.ToDo,org.springframework.validation.Errors)"}

此命令正在发布,但没有数据,并以错误消息作为响应。这来自于@ExceptionHandler注释和handleException方法。所有的错误(不同于空白的描述)都用这个方法处理。

您可以继续测试更多的 ToDo 或修改一些验证注释,看看它们是如何工作的。

注意

如果没有 cURL 命令或者无法安装,可以使用其他任何 REST 客户端,比如 PostMan ( https://www.getpostman.com )或者失眠( https://insomnia.rest )。如果你喜欢命令行,那么 Httpie ( https://httpie.org )是另一个不错的选择;它使用 Python。

Spring Boot Web:改写默认值

Spring Boot web 自动配置设置默认运行一个 Spring web 应用。在这一节中,我将向您展示如何覆盖其中的一些。

您可以通过创建自己的配置(XML 或 JavaConfig)和/或使用application.properties(或)来覆盖 web 默认值。yml)文件。

服务器覆盖

默认情况下,嵌入式 Tomcat 服务器在端口 8080 上启动,但是您可以通过使用以下属性轻松地更改它。

server.port=8081

Spring 的一个很酷的特性是你可以应用 SpEL (Spring Expression Language)并将其应用于这些属性。例如,当您创建一个可执行的 jar (./mvnw package./gradlew build时,您可以在运行您的应用时传递一些参数。您可以执行以下操作

java -jar todo-in-memory-0.0.1-SNAPSHOT.jar --port=8787

而在你的application.properties中,你有这样的东西:

server.port=${port:8282}

这个表达式意味着如果你传递了--port参数,它就取那个值;如果不是,则设置为8282。这只是你能用 SpEL 做的一点点尝试,但是如果你想知道更多,去 https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions

您还可以更改服务器地址,这在您希望使用特定 IP 运行应用时非常有用。

server.address=10.0.0.7

您可以更改应用的上下文。

server.servlet.context-path=/my-todo-app

你可以像这样卷曲:

curl -I http://localhost:8080/my-todo-app/api/todo

您可以通过使用以下属性让 Tomcat 支持 SSL。

server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=secret
server.ssl.key-password=secret

我们将在后面的章节中重温这些属性,并让我们的应用与 SSL 一起工作。

您可以使用下列属性来管理会话。

server.servlet.session.store-dir=/tmp
server.servlet.session.persistent=true
server.servlet.session.timeout=15

server.servlet.session.cookie.name=todo-cookie.dat
server.servlet.session.cookie.path=/tmp/cookies

如果您的环境支持,您可以启用 HTTP/2 支持。

server.http2.enabled=true

JSON 日期格式

默认情况下,日期类型以长格式显示在 JSON 响应中;但是您可以通过在以下属性中提供您自己的模式来改变这一点。

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=MST7MDT

这些属性格式化日期并使用您指定的时区(如果您想了解更多关于可用 id 的信息,您可以执行java.util.TimeZone#getAvailableIDs)。如果您修改了 ToDo 应用,运行它,添加一些 ToDo,并获得列表。您应该会得到这样的回应:

curl -s http://localhost:8080/api/todo | jq
[
  {
    "id": "f52d1429-432d-43c5-946d-15c7fa5f50eb",
    "description": "Get the Pro Spring Boot 2nd Edition Book",
    "created": "2018-05-03 11:40:37",
    "modified": "2018-05-03 11:40:37",
    "completed": false
  }
]

如果你想知道更多关于哪些属性存在,书签 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

内容类型:JSON/XML

Spring MVC 使用HttpMessageConverters(客户端和服务器端)来协商 HTTP 交换中的内容转换。如果在类路径中找到了 Jackson 库,Spring Boot 会将默认值设置为 JSON。但是,如果您也想公开 XML,并请求 JSON 或 XML 内容,会发生什么呢?

Spring Boot 通过添加一个额外的依赖项和一个属性使这变得非常简单。

如果您使用的是 Maven,请参见清单 4-10 。

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

Listing 4-10Maven: pom.xml

或者,如果您正在使用 Gradle,请参见清单 4-11 。

compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml')

Listing 4-11Gradle: build.gradle

您可以将以下属性添加到 application.properties 文件中。

spring.mvc.contentnegotiation.favor-parameter=true

如果您运行带有这些更改的 ToDo 应用,您可以通过执行以下命令获得 XML 格式的响应。

curl -s http://localhost:8080/api/todo?format=xml
<ArrayList><item><id>b3281340-b1aa-4104-b3d2-77a96a0e41b8</id><description>Read the Pro Spring Boot 2nd Edition Book</description><created>2018-05-03T19:18:30.260+0000</created><modified>2018-05-03T19:18:30.260+0000</modified><completed>false</completed></item></ArrayList>

在前面的命令中,?format=xml被追加到 URL 后面;JSON 响应也是如此。

curl -s http://localhost:8080/api/todo?format=json | jq
[
  {
    "id": "b3281340-b1aa-4104-b3d2-77a96a0e41b8",
    "description": "Read the Pro Spring Boot 2nd Edition Book",
    "created": "2018-05-03T19:18:30.260+0000",
    "modified": "2018-05-03T19:18:30.260+0000",
    "completed": false
  }
]

如果您想漂亮地打印 XML,在 UNIX 环境中有xmllint命令。

curl -s http://localhost:8080/api/todo?format=xml | xmllint --format -

Spring MVC:覆盖默认值

到目前为止,我还没有向您展示如何创建 Web 应用来公开 HTML、JavaScript 等技术的组合,这是因为现在业界倾向于在前端使用 JavaScript/TypeScript 应用。

这并不意味着你不能创建一个具有后端和前端的 Spring Web MVC。Spring Web MVC 为您提供了与模板引擎和其他技术的良好集成。

如果使用任何模板引擎,可以通过使用以下属性来选择视图前缀和后缀:

spring.mvc.view.prefix=/WEB-INF/my-views/
spring.mvc.view.suffix=.jsp

正如你所看到的,Spring Boot 可以帮助你做很多配置,如果你在做常规的 Spring Web 应用,通常你需要做很多。这种新方式有助于你加速发展。如果你需要更多关于如何使用 Spring MVC 及其所有特性的指导,你可以看看参考文档: https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/web.html#mvc

使用不同的应用容器

默认情况下,Spring Boot 使用 Tomcat(用于 web servlet 应用)作为应用容器,并设置一个嵌入式服务器。如果您想覆盖这个缺省值,可以通过修改 Maven pom.xml或 Gradle build.gradle文件来实现。

使用 Jetty 服务器

同样的变化也适用于逆流或净流(见清单 4-12 和清单 4-13 )。

configurations {
 compile.exclude module: "spring-boot-starter-tomcat"
}

dependencies {
 compile("org.springframework.boot:spring-boot-starter-web")
 compile("org.springframework.boot:spring-boot-starter-jetty")
  // ...
}

Listing 4-13Gradle: build.gradle

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
      <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
      </exclusions>
</dependency>
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Listing 4-12Maven - pom.xml

Spring Boot 网站:客户端

创建 Spring Boot web 应用的另一个重要特性是 Spring Web MVC 附带了一个有用的RestTemplate类,可以帮助创建客户端。

任何 App 客户端

打开您的浏览器并转到 https://start.spring.io 站点,使用以下值创建您的 ToDo 客户端应用(另见图 4-2 )。

img/340891_2_En_4_Fig2_HTML.jpg

图 4-2

任何 app 客户端

  • 组:com.apress.todo

  • 神器:todo-client

  • 名称:todo-client

  • 包名:com.apress.todo

  • 依赖关系:WebLombok

按下 Generate Project 按钮并下载 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。

域模型:ToDo

创建 ToDo 域模型,该模型应该与上一个应用中的最小字段相匹配(参见清单 4-14 )。

package com.apress.todo.client.domain;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        LocalDateTime date = LocalDateTime.now();
        this.id = UUID.randomUUID().toString();
        this.created = date;
        this.modified = date;
    }

    public ToDo(String description){
        this();
        this.description = description;
    }
}

Listing 4-14com.apress.todo.client.domain.ToDo.java

清单 4-14 向您展示了 ToDo 域模型类。包名称不同;不需要匹配类名。应用不需要知道要序列化或反序列化到 JSON 中的包是什么。

错误处理函数:两个错误处理函数

接下来,让我们创建一个错误处理程序,处理来自服务器的任何错误响应。创建ToDoErrorHandler类(参见清单 4-15 )。

package com.apress.todo.client.error;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;

import java.io.IOException;
import java.nio.charset.Charset;

public class ToDoErrorHandler extends DefaultResponseErrorHandler {

    private Logger log = LoggerFactory.getLogger(ToDoErrorHandler.class);

    @Override
    public void handleError(ClientHttpResponse response)
throws IOException {
        log.error(response.getStatusCode().toString());
        log.error(StreamUtils.copyToString(
response.getBody(),Charset.defaultCharset()));
    }

}

Listing 4-15com.apress.todo.client.error.ToDoErrorHandler.java

清单 4-15 显示了ToDoErrorHandler类,这是一个从DefaultResponseErrorHandler扩展而来的定制类。因此,如果我们得到一个 400 HTTP 状态(错误请求),我们可以捕捉错误并对其做出反应;但是在这种情况下,该类只是记录错误。

自定义属性:ToDoRestClientProperties

有必要知道 ToDo 应用在哪里运行,以及使用哪个 basePath,这就是为什么有必要保存这些信息。最佳实践之一是从外部获取这些信息。

让我们创建一个保存 URL 和基本路径信息的ToDoRestClientProperties类。这些信息可以保存在application.properties文件中(参见清单 4-16 )。

package com.apress.todo.client;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component

@ConfigurationProperties(prefix="todo")

@Data
public class ToDoRestClientProperties {

    private String url;
    private String basePath;

}

Listing 4-16com.apress.todo.client.ToDoRestClientProperties.java

清单 4-16 显示了保存 URL 和基本路径信息的类。Spring Boot 允许您创建自定义类型的属性,这些属性可以从application.properties文件中访问和映射;唯一的要求是您需要用@ConfigurationProperties注释来标记这个类。这个注释可以接受像prefix这样的参数。

application.properties文件中,添加以下内容。

todo.url=http://localhost:8080
todo.base-path=/api/todo

客户端:ToDoRestClient

让我们创建一个使用RestTemplate类的客户机,它有助于在客户机和服务器之间交换信息。创建ToDoRestClient类(参见清单 4-17 )。

package com.apress.todo.client;

import com.apress.todo.client.domain.ToDo;
import com.apress.todo.client.error.ToDoErrorHandler;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

@Service

public class ToDoRestClient {

    private RestTemplate restTemplate;
    private ToDoRestClientProperties properties;

    public ToDoRestClient(
                        ToDoRestClientProperties properties){
        this.restTemplate = new RestTemplate();
        this.restTemplate.setErrorHandler(
                                        new ToDoErrorHandler());
        this.properties = properties;
    }

    public Iterable<ToDo> findAll() throws URISyntaxException {
        RequestEntity<Iterable<ToDo>> requestEntity = new RequestEntity<Iterable<ToDo>>(HttpMethod.GET,new URI(properties.getUrl() + properties.getBasePath()));
        ResponseEntity<Iterable<ToDo>> response =
                restTemplate.exchange(requestEntity,new ParameterizedTypeReference<Iterable<ToDo>>(){});

        if(response.getStatusCode() == HttpStatus.OK){
            return response.getBody();
        }

        return null;
    }

    public ToDo findById(String id){
        Map<String, String> params = new HashMap<String, String>();
        params.put("id", id);
        return restTemplate.getForObject(properties.getUrl() + properties.getBasePath() + "/{id}",ToDo.class,params);
    }

    public ToDo upsert(ToDo toDo) throws URISyntaxException {
       RequestEntity<?> requestEntity = new RequestEntity<>(toDo,HttpMethod.POST,new URI(properties.getUrl() + properties.getBasePath()));
       ResponseEntity<?> response = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<ToDo>() {});

       if(response.getStatusCode() == HttpStatus.CREATED){
           return restTemplate.getForObject(response.getHeaders().getLocation(),ToDo.class);
       }
       return null;
    }

    public ToDo setCompleted(String id) throws URISyntaxException{
        Map<String, String> params = new HashMap<String, String>();
        params.put("id", id);
        restTemplate.postForObject(properties.getUrl() + properties.getBasePath() + "/{id}?_method=patch",null, ResponseEntity.class, params);

        return findById(id);
    }

    public void delete(String id){
        Map<String, String> params = new HashMap<String, String>();
        params.put("id", id);
        restTemplate.delete(properties.getUrl() + properties.getBasePath() + "/{id}",params);
    }

}

Listing 4-17com.apress.todo.client.ToDoRestClient.java

清单 4-17 显示了与 ToDo 应用交互的客户端。该类正在使用 RestTemplate 类。RestTemplate是 Spring 用于同步客户端 HTTP 访问的中心类。它简化了与 HTTP 服务器的通信,并加强了 RESTful 原则。它处理 HTTP 连接,让应用代码提供 URL(可能有模板变量)并提取结果。许多特性中的一个允许您处理自己的错误响应。所以看一下构造函数,它正在设置ToDoErrorHandler类。

复习课;它包含 ToDo 应用(后端)的所有操作。

注意

默认情况下,RestTemplate 依赖标准的 JDK 设施来建立 HTTP 连接。您可以通过 interceptinghtpaccessor . setrequestfactory(org . spring framework . HTTP . client . clienthttprequestfactory)属性切换到使用不同的 HTTP 库,如 Apache HttpComponents、Netty 和 OkHttp。

运行和测试客户端

为了运行和测试客户端应用,修改TodoClientApplication类(参见清单 4-18 )。

package com.apress.todo;

import com.apress.todo.client.ToDoRestClient;
import com.apress.todo.client.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class TodoClientApplication {

      public static void main(String[] args) {
            SpringApplication app = new
                SpringApplication(TodoClientApplication.class);
            app.setWebApplicationType(WebApplicationType.NONE);
            app.run(args);
      }

      private Logger log = LoggerFactory.getLogger(TodoClientApplication.class);

      @Bean
      public CommandLineRunner process(ToDoRestClient client){
          return args -> {

              Iterable<ToDo> toDos = client.findAll();
              assert toDos != null;
              toDos.forEach( toDo -> log.info(toDo.toString()));

            ToDo newToDo = client.upsert(new ToDo("Drink plenty of Water daily!"));
            assert newToDo != null;
            log.info(newToDo.toString());

            ToDo toDo = client.findById(newToDo.getId());
            assert toDos != null;
            log.info(toDo.toString());

            ToDo completed = client.setCompleted(newToDo.getId());
            assert completed.isCompleted();
            log.info(completed.toString());

            client.delete(newToDo.getId());
            assert client.findById(newToDo.getId()) == null;
        };
    }
}

Listing 4-18com.apress.todo.TodoClientApplication.java

清单 4-18 向您展示了如何测试客户端。首先用WebApplicationType.NONE关闭 web 环境。然后使用CommandLineRunner(作为 bean)在 app 启动前执行代码。

在运行它之前,分析代码,看看发生了什么。您可以使用命令行或通过您的 IDE 运行。确保 ToDo 应用已启动并正在运行。

您将再次使用此客户端。

注意

请记住,所有的源代码都可以从 Apress 网站或 GitHub 的以下位置下载: https://github.com/Apress/pro-spring-boot-2

摘要

在这一章中,你学习了 Spring Boot 如何管理和自动配置 web 应用,以及它如何使用 Spring MVC 的力量。您还了解了可以覆盖自动配置提供的所有合理的缺省值。

通过 ToDo 应用,您学习了 web 应用的一些 Spring Boot 特性,比如 JSON 和 XML 配置,几个 MVC 注释的用法,比如@RequestMapping@ExceptionHandler,等等。

您了解了 Spring Boot 如何使用嵌入式应用容器(在本例中,Tomcat 是默认的)来轻松部署或传输。

您在 ToDo 应用中使用的所有注释都是 Spring MVC 的一部分。在本书的剩余部分,我们实现了更多。如果想了解更多关于 Spring MVC 的知识,可以看看 Pro Spring 5 (Apress,2017)。

在下一章中,您将了解 Spring Boot 如何为持久性引擎自动配置 Spring Data 模块。您还将了解更多关于 Spring JDBC、JPA、REST 和 NoSQL 模块的知识,比如 Mongo。

五、Spring Boot 的数据访问

从尝试访问、保存和分析数据,到使用几个字节到几十亿字节的信息,数据已经成为 IT 世界中最重要的部分。已经有许多尝试来创建框架和库,以便于开发人员与数据进行交互,但是有时这变得太复杂了。

在 3.0 版本之后,Spring 框架创建了不同的团队,专门研究不同的技术。Spring Data 项目组诞生了。这个项目的目标是简化数据访问技术的使用,从关系数据库和非关系数据库,到 map-reduce 框架和基于云的数据服务。这个 Spring Data 项目是特定于给定数据库的子项目的集合。

本章介绍了使用前几章中的 ToDo 应用通过 Spring Boot 进行数据访问。您将使 ToDo 应用与 SQL 和 NoSQL 数据库一起工作。我们开始吧。

SQL 数据库

您还记得(在 Java 世界中)需要处理所有 JDBC (Java 数据库连接)任务的那些日子吗?您必须下载正确的驱动程序和连接字符串,打开和关闭连接、SQL 语句、结果集和事务,并将结果集转换为对象。在我看来,这些都是非常手工的工作。然后,许多 ORM(对象关系映射)框架开始出现来管理这些任务——比如 Castor XML、ObjectStore 和 Hibernate 等框架。它们允许您识别域类并创建与数据库表相关的 XML。在某种程度上,你也需要成为管理这类框架的专家。

Spring 框架通过遵循模板设计模式对那些框架帮助很大。它允许您创建一个抽象类,该类定义了执行方法的方式,并创建了允许您只关注业务逻辑的数据库抽象。它将所有困难留给了 Spring 框架,包括处理连接(打开、关闭和池化)、事务以及与框架交互的方式。

值得一提的是,Spring 框架依赖于几个接口和类(如javax.sql.DataSource接口)来获得关于您将要使用的数据库、如何连接到它(通过提供连接字符串)以及它的凭证的信息。现在,如果您有事务管理要做,那么DataSource接口是必不可少的。通常,DataSource接口需要驱动程序类、JDBC URL、用户名和密码来连接数据库。

Spring Data

Spring Data 团队已经为 Java 和 Spring 社区创建了一些惊人的数据驱动框架。他们的任务是为数据访问提供熟悉且一致的基于 Spring 的编程,并对您想要使用的底层数据存储技术进行全面控制。

Spring Data 项目是几个额外的库和数据框架的保护伞,这使得对关系和非关系数据库使用数据访问技术变得很容易(又名 NoSQL)。

以下是 Spring Data 的一些特性。

  • 支持跨存储持久性

  • 基于存储库和定制的对象映射抽象

  • 基于方法名的动态查询

  • 通过 JavaConfig 和 XML 实现简单的 Spring Integration

  • 支持 Spring MVC 控制器

  • 透明审核的事件(已创建,上次更改)

还有更多的功能——需要一整本书来介绍它们。这一章的内容足以创建强大的数据驱动的应用。请记住,Spring Data 是涵盖所有内容的主要总括项目。

Spring 的 JDBC

在这一节中,我将向您展示如何使用JdbcTemplate类。这个特定的类实现了模板设计模式,这是一个具体的类,它公开了执行其方法的已定义方式或模板。它隐藏了所有的样板算法或一组指令。在 Spring 中,您可以选择不同的方式来构成您的 JDBC 数据库访问的基础;使用JdbcTemplate类是经典的 Spring JDBC 方法,这是最低级别。

当您使用JdbcTemplate类时,您只需要实现回调接口来创建与任何数据库引擎交互的简单方法。JdbcTemplate类需要一个javax.sql.DataSource,可以在任何类中使用,通过在 JavaConfig、XML 或注释中声明它。JdbcTemplate类负责所有的SQLException并得到妥善处理。

您可以使用NamedParameterJdbcTemplate(JDBC template 包装器)来提供命名参数(:parameterName),而不是传统的 JDBC "?"占位符。这是 SQL 查询的另一个选项。

JdbcTemplate类公开了不同的方法。

  • 查询(选择)。您通常使用queryqueryForObject方法调用。

  • 更新(插入/更新/删除)。你使用update方法调用。

  • 操作(数据库/表/函数)。您使用executeupdate方法调用。

使用 Spring JDBC,您可以通过使用SimpleJdbcCall类调用存储过程,并使用特定的RowMapper接口处理结果。JdbcTemplate类使用RowMapper<T>来逐行映射ResultSet的行。

Spring JDBC 支持嵌入式数据库引擎,比如 HSQL、H2 和 Derby。它易于配置,并提供快速启动时间和可测试性。

另一个特性是用脚本初始化数据库的能力;您可以使用嵌入式支持,也可以不使用。您可以添加自己的 SQL 格式的模式和数据。

JDBC 和 Spring Boot

Spring 框架支持使用 JDBC 或 ORMs 处理 SQL 数据库(我将在下面的章节中介绍)。Spring Boot 给数据应用带来了更多。

当 Spring Boot 发现您的应用有一个 JDBC JARs 时,它使用自动配置来设置合理的默认值。Spring Boot 根据类路径中的 SQL 驱动程序自动配置数据源。如果它发现您有任何嵌入式数据库引擎(H2、HSQL 或 Derby),那么它是默认配置的;换句话说,你可以有两个驱动依赖项(例如,MySQL 和 H2),如果 Spring Boot 没有找到任何声明的数据源 bean,它会基于类路径中的嵌入式数据库引擎 JAR 创建它(例如,H2)。默认情况下,Spring Boot 还将 HikariCP 配置为连接池管理器。当然,您可以覆盖这些默认值。

如果您想覆盖缺省值,那么您需要提供自己的 datasource 声明,或者是 JavaConfig、XML,或者是在application.properties文件中。

# Custom DataSource
spring.datasource.username=springboot
spring.datasource.password=rocks!
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?autoReconnect=true&useSSL=false

src/main/resources/application.properties

如果您在应用容器中部署应用,Spring Boot 支持 JNDI 连接。您可以在application.properties文件中设置 JNDI 名称。

spring.datasource.jndi-name=java:jboss/ds/todos

Spring Boot 为数据应用带来的另一个功能是,如果你有一个名为 schema.sqldata.sqlschema- < platform >的文件。sql ,或数据- <平台>。sql 在类路径中,它通过执行这些脚本文件来初始化您的数据库。

因此,如果您想在您的 Spring Boot 应用中使用 JDBC,您需要添加spring-boot-starter-jdbc依赖项和您的 SQL 驱动程序。

ToDo 应用

是时候像我们在上一章所做的那样使用 ToDo 应用了。你可以从头开始,也可以跟着做。如果您是从零开始,那么您可以转到 Spring Initializr ( https://start.spring.io )并将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-jdbc

  • 名称:todo-jdbc

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, JDBC, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-1 )。

img/340891_2_En_5_Fig1_HTML.jpg

图 5-1

spring initializehttps://start.spring.io

你可以复制上一章的所有类,除了ToDoRepository类;这是唯一的新类。还要确保在pom.xmlbuild.gradle文件中有两个驱动程序:H2 和 MySQL。根据我在上一节中讨论的内容,如果我不指定任何数据源(在 JavaConfig、XML 或application.properties中),Spring Boot 自动配置会做什么呢?正确!Spring Boot 默认自动配置H2 嵌入式数据库。

存储库:ToDoRepository

创建一个实现CommonRepository接口的ToDoRepository类(参见清单 5-1 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.time.LocalDateTime;
import java.util.*;

@Repository
public class ToDoRepository implements CommonRepository<ToDo> {

    private static final String SQL_INSERT = "insert into todo (id, description, created, modified, completed) values (:id,:description,:created,:modified,:completed)";
    private static final String SQL_QUERY_FIND_ALL = "select id, description, created, modified, completed from todo";
    private static final String SQL_QUERY_FIND_BY_ID = SQL_QUERY_FIND_ALL + " where id = :id";
    private static final String SQL_UPDATE = "update todo set description = :description, modified = :modified, completed = :completed where id = :id";
    private static final String SQL_DELETE = "delete from todo where id = :id";

    private final NamedParameterJdbcTemplate jdbcTemplate;

    public ToDoRepository(NamedParameterJdbcTemplate jdbcTemplate){
        this.jdbcTemplate = jdbcTemplate;
    }

    private RowMapper<ToDo> toDoRowMapper = (ResultSet rs, int rowNum) -> {
        ToDo toDo = new ToDo();
        toDo.setId(rs.getString("id"));
        toDo.setDescription(rs.getString("description"));
        toDo.setModified(rs.getTimestamp("modified").toLocalDateTime());
        toDo.setCreated(rs.getTimestamp("created").toLocalDateTime());
        toDo.setCompleted(rs.getBoolean("completed"));
        return toDo;
    };

    @Override
    public ToDo save(final ToDo domain) {
        ToDo result = findById(domain.getId());
        if(result != null){
            result.setDescription(domain.getDescription());
            result.setCompleted(domain.isCompleted());
            result.setModified(LocalDateTime.now());
            return upsert(result, SQL_UPDATE);
        }
        return upsert(domain,SQL_INSERT);
    }

    private ToDo upsert(final ToDo toDo, final String sql){ 

        Map<String, Object> namedParameters = new HashMap<>();
        namedParameters.put("id",toDo.getId());
        namedParameters.put("description",toDo.getDescription());
        namedParameters.put("created",java.sql.Timestamp.valueOf(toDo.getCreated()));
        namedParameters.put("modified",java.sql.Timestamp.valueOf(toDo.getModified()));
        namedParameters.put("completed",toDo.isCompleted());

        this.jdbcTemplate.update(sql,namedParameters);

        return findById(toDo.getId());
    }

    @Override
    public Iterable<ToDo> save(Collection<ToDo> domains) {
        domains.forEach( this::save);
        return findAll();
    }

    @Override
    public void delete(final ToDo domain) {
        Map<String, String> namedParameters = Collections.singletonMap("id", domain.getId());
        this.jdbcTemplate.update(SQL_DELETE,namedParameters);
    }

    @Override
    public ToDo findById(String id) {
        try {
            Map<String, String> namedParameters = Collections.singletonMap("id", id);
            return this.jdbcTemplate.queryForObject(SQL_QUERY_FIND_BY_ID, namedParameters, toDoRowMapper);
        } catch (EmptyResultDataAccessException ex) {
            return null;
        }
    }

    @Override
    public Iterable<ToDo> findAll() {
        return this.jdbcTemplate.query(SQL_QUERY_FIND_ALL, toDoRowMapper);
    }

}

Listing 5-1com.apress.todo.respository.ToDoRepository.java

列表 5-1 显示了使用JdbcTemplateToDoRepository类,不是直接强韧的。这个类使用的是帮助所有命名参数的NamedParameterJdbcTemplate(一个JdbcTemplate包装器),这意味着在 SQL 语句中不使用?,而是使用类似:id的名称。

此类还声明了一个行映射器;请记住,JdbcTemplate使用行映射器在每行的基础上映射ResultSet的行。

分析代码并检查每个方法中使用普通 SQL 的每个方法实现。

数据库初始化:schema.sql

请记住,Spring 框架允许您初始化数据库——创建或修改任何表,或者在应用启动时插入/更新数据。要初始化一个 Spring app(不是 Spring Boot),需要添加配置(XML 或者 Java config);但是 ToDo 应用是 Spring Boot。如果 Spring Boot 找到了schema.sql和/或data.sql文件,它会自动执行它们。让我们创建schema.sql(见清单 5-2 )。

DROP TABLE IF EXISTS todo;
CREATE TABLE todo
(
  id varchar(36) not null primary key,
  description varchar(255) not null,
  created timestamp,
  modified timestamp,
  completed boolean
);

Listing 5-2src/main/resources/schema.sql

清单 5-2 显示了应用启动时执行的schema.sql,因为 H2 是配置的默认数据源,所以这个脚本是针对 H2 引擎执行的。

运行和测试:ToDo 应用

现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它,或者如果您使用的是 Maven,请执行

./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

./gradlew bootRun

要测试 ToDo 应用,您可以运行 ToDoClient 应用。它应该没有任何问题。

H2 控制台

既然您已经运行了 ToDo 应用,那么您如何确保应用将数据保存在 H2 引擎中呢?Spring Boot 有一个属性可以启用 H2 控制台,这样你就可以与它进行交互。它对于开发非常有用,但对于生产环境却不太有用。

修改application.properties文件并添加以下属性。

spring.h2.console.enabled=true

src/main/resources/application.properties

重启 ToDo 应用,用 cURL 命令添加值,进入浏览器并点击http://localhost:8080/h2-console。(见图 5-2 )。

img/340891_2_En_5_Fig2_HTML.jpg

图 5-2

http://localhost:8080/h2-console

图 5-2 显示了 H2 控制台,您可以在/h2-console端点到达该控制台。(您也可以覆盖这个端点)。JDBC URL 必须是jdbc:h2:mem:testdb(有时这是不同的,所以把它改成那个值)。默认情况下,数据库名称是testdb(但是您也可以覆盖它)。如果您点击连接按钮,您将获得不同的视图,在该视图中您可以看到正在创建的表格和数据(参见图 5-3 )。

img/340891_2_En_5_Fig3_HTML.jpg

图 5-3

http://localhost:8080/H2-控制台

图 5-3 显示了您可以执行任何 SQL 查询并取回数据。如果您想查看 ToDo 应用中正在执行哪些 SQL 查询,您可以将以下属性添加到application.properties文件中。

logging.level.org.springframework.data=INFO
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG

src/main/resources/application.properties

正如您所看到的,JdbcTemplate类为您提供了许多与任何数据库引擎交互的可能性,但是这个类是“最底层”的方法。

在撰写本文时,有了一种以更统一的方式使用JdbcTemplate类的新方法 Spring Data 方法(我将在下面的章节中描述)。Spring Data 团队已经创建了新的 Spring Data JDBC 项目,它遵循了 Eric Evans 在《?? 领域驱动设计》一书中描述的聚合根概念。它有很多特性,比如 CRUD 操作、支持@Query注释、支持 MyBatis 查询、事件等等,所以请关注这个项目。这是一种新的 JDBC 方式。

Spring Data

JPA (Java 持久性 API)为对象关系映射提供了一个 POJO 持久性模型。Spring Data JPA 促进了这种模型的持久性。

实现数据访问可能会很麻烦,因为我们需要处理连接、会话、异常处理等等,即使对于简单的 CRUD 操作也是如此。这就是为什么 Spring Data JPA 提供了额外级别的功能:直接从接口创建存储库实现,并使用约定从方法名生成查询。

以下是 Spring Data JPA 的一些特性。

  • 不同提供者对 JPA 规范的支持,比如 Hibernate、Eclipse Link、Open JPA 等等。

  • 对存储库的支持(来自领域驱动设计的概念)。

  • 域类的审核。

  • 支持 Quesydsl ( http://www.querydsl.com/ )谓词和类型安全的 JPA 查询。

  • 分页、排序、动态查询执行支持。

  • 支持@Query标注。

  • 支持基于 XML 的实体映射。

  • 使用@EnableJpaRepositories注释进行基于 JavaConfig 的存储库配置。

Spring Boot Spring Data JPA

Spring Data JPA 最重要的一个好处是,我们不需要担心实现基本的 CRUD 功能,因为这就是它要做的。我们只需要创建一个从Repository<T,ID>CrudRepository<T,ID>JpaRepository<T,ID>扩展而来的接口。JpaRepository接口不仅提供了CrudRepository所做的,还从提供额外功能的PagingAndSortingRepository接口扩展而来。如果您查看CrudRepository<T,ID>接口(在您的 ToDo 应用中使用),您可以看到所有的签名方法,如清单 5-3 所示。

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
      <S extends T> S save(S entity);
      <S extends T> Iterable<S> saveAll(Iterable<S> entities);
      Optional<T> findById(ID id);
      boolean existsById(ID id);
      Iterable<T> findAll();
      Iterable<T> findAllById(Iterable<ID> ids);
      long count();
      void deleteById(ID id);
      void delete(T entity);
      void deleteAll(Iterable<? extends T> entities);
      void deleteAll();
}

Listing 5-3org.springframework.data.repository.CrudRepository.java

清单 5-3 展示了CrudRepository<T,ID>接口,其中T表示实体(你的域模型类)和 ID,需要实现Serializable的主键。

在一个简单的 Spring 应用中,需要使用@EnableJpaRepositories注释来触发额外的配置,该配置应用于应用中定义的存储库的生命周期中。好的一面是当你使用 Spring Boot 的时候你不需要这个,因为 Spring Boot 会照顾它。Spring Data JPA 的另一个特性是查询方法,这是用域实体的字段创建 SQL 语句的一种非常强大的方法。

因此,要在 Spring Boot 中使用 Spring Data JPA,您需要spring-boot-starter-data-jpa和 SQL 驱动程序。

当 Spring Boot 执行其自动配置并发现您有 Spring Data JPA JAR 时,它默认配置数据源(如果没有定义的话)。它配置 JPA 提供程序(默认情况下,它使用 Hibernate)。它启用了存储库(通过使用@EnableJpaRepositories配置)。它检查您是否定义了任何查询方法。还有更多。

带有 Spring Data 的 ToDo 应用 JPA

您可以从头开始创建您的 ToDo 应用,或者查看您需要的类,以及您的pom.xmlbuild.gradle文件中必要的依赖项。

从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-jpa

  • 名称:todo-jpa

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, JPA, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-4 )。

img/340891_2_En_5_Fig4_HTML.jpg

图 5-4

spring initializehttps://start.spring.io

您可以复制前一章中的所有类,除了ToDoRepository类,它是唯一新的类;你修改其他人。

存储库:ToDoRepository

创建一个从CrudRepository<T,ID>扩展而来的ToDoRepository接口。TToDo级,ID 是String(见清单 5-4 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends
                              CrudRepository<ToDo,String> {}

Listing 5-4com.apress.todo.repository.ToDoRepository.java

清单 5-4 显示了扩展一个CrudRepositoryToDoRepository接口。没有必要创建一个具体的类或实现任何东西;Spring Data JPA 为我们完成了实现。所有的 CRUD 操作都处理我们需要持久化数据的任何事情。就是这样——在需要的地方使用ToDoRepository,我们不需要做任何其他事情。

域模型:ToDo

要使用 JPA 并符合规范,需要从域模型中声明实体(@Entity)和主键(@Id)。让我们通过添加以下注释和方法来修改ToDo类(参见清单 5-5 )。

package com.apress.todo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity

@Data

@NoArgsConstructor

public class ToDo {

    @NotNull
    @Id
        @GeneratedValue(generator = "system-uuid")
        @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
         void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
        void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 5-5com.apress.todo.domain.Todo.java

清单 5-5 显示了ToDo域模型的修改版本。这个类现在有了额外的元素。

  • @NoArgsConstructor。该注释属于 Lombok 库。它创建一个没有参数的类构造函数。JPA 需要一个不带参数的构造函数。

  • @Entity。此批注指定该类是一个实体,并保存在所选的数据库引擎中。

  • @Id。该注释指定了实体的主键。被注释的字段应该是任何 Java 原语类型和任何原语包装类型。

  • @GeneratedValue。该注释提供了主键值的生成策略(仅限简单键)。通常,它与@Id注释一起使用。有不同的策略(标识、自动、顺序和表格)和一个密钥生成器。在这种情况下,该类定义了"system-uuid"(这将生成一个惟一的 36 字符 ID)。

  • @GenericGenerator。这是 Hibernate 的一部分,它允许您使用策略从前面的注释中生成一个惟一的 ID。

  • @Column。此批注指定持久属性的映射列;如果字段中没有列注释,则它是数据库中该列的默认名称。该类将创建的字段标记为仅用于插入,而不用于更新。

  • @PrePersist。这个注释是一个回调,在采取任何持久动作之前被触发。在将记录插入数据库之前,它为创建和修改的字段设置新的时间戳。

  • @PreUpdate。这个注释是在执行任何更新操作之前触发的另一个回调。它在修改的字段被更新到数据库之前为其设置新的时间戳。

最后两个注释(@PrePersist@PreUpdate)是处理日期/时间戳的非常好的方式,使开发人员更容易理解。

在我们继续之前,分析一下代码,看看与以前版本的ToDo域模型类有什么不同。

控制器:ToDoController

现在,是时候修改ToDoController类了(参见清单 5-6 )。

package com.apress.todo.controller;

import com.apress.todo.domain.ToDo;
import com.apress.todo.domain.ToDoBuilder;
import com.apress.todo.repository.ToDoRepository;
import com.apress.todo.validation.ToDoValidationError;
import com.apress.todo.validation.ToDoValidationErrorBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder

;

import javax.validation.Valid;
import java.net.URI;
import java.util.Optional;

@RestController
@RequestMapping("/api")
public class ToDoController {

    private ToDoRepository toDoRepository;

    @Autowired
    public ToDoController(ToDoRepository toDoRepository) {
        this.toDoRepository = toDoRepository;
    }

    @GetMapping("/todo")
    public ResponseEntity<Iterable<ToDo>> getToDos(){
        return ResponseEntity.ok(toDoRepository.findAll());
    }

    @GetMapping("/todo/{id}")
    public ResponseEntity<ToDo> getToDoById(@PathVariable String id){
        Optional<ToDo> toDo = toDoRepository.findById(id);
                 if(toDo.isPresent())
                     return ResponseEntity.ok(toDo.get());

                 return ResponseEntity.notFound().build();
    }

    @PatchMapping("/todo/{id}")
    public ResponseEntity<ToDo> setCompleted(@PathVariable String id){
        Optional<ToDo> toDo = toDoRepository.findById(id);
        if(!toDo.isPresent())
            return ResponseEntity.notFound().build();

        ToDo result = toDo.get();
        result.setCompleted(true);
        toDoRepository.save(result);

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .buildAndExpand(result.getId()).toUri();

        return ResponseEntity.ok().header("Location",location.toString()).build();
    }

    @RequestMapping(value="/todo", method = {RequestMethod.POST,RequestMethod.PUT})
    public ResponseEntity<?> createToDo(@Valid @RequestBody ToDo toDo, Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(ToDoValidationErrorBuilder.fromBindingErrors(errors));
        }

        ToDo result = toDoRepository.save(toDo);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
                .buildAndExpand(result.getId()).toUri();
        return ResponseEntity.created(location).build();
    }

    @DeleteMapping("/todo/{id}")
    public ResponseEntity<ToDo> deleteToDo(@PathVariable String id){
        toDoRepository.delete(ToDoBuilder.create().withId(id).build());
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping("/todo")
    public ResponseEntity<ToDo> deleteToDo(@RequestBody ToDo toDo){
        toDoRepository.delete(toDo);
        return ResponseEntity.noContent().build();
    }

    @ExceptionHandler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public ToDoValidationError handleException(Exception exception) {
        return new ToDoValidationError(exception.getMessage());
    }

}

Listing 5-6com.apress.todo.controller.ToDoController.java

清单 5-6 显示了修改后的ToDoController级。它现在直接使用ToDoRepository接口,一些方法,比如findById,返回一个 Java 8 Optional类型。

在我们继续之前,分析一下这个类,看看它与以前的版本有什么不同。大部分代码保持不变。

Spring Boot JPA 房地产公司

Spring Boot 提供了允许您在使用 Spring Data JPA 时覆盖默认值的属性。其中之一是创建 DDL(数据定义语言)的能力,默认情况下它是关闭的,但是您可以启用它从您的领域模型进行逆向工程。换句话说,该属性从您的域模型类中生成表和任何其他关系。

您还可以告诉您的 JPA 提供者创建、删除、更新或验证您现有的 DDL/data,这是一种有用的迁移机制。此外,您可以设置一个属性来显示针对数据库引擎执行的 SQL 语句。

application.properties文件添加必要的属性,如清单 5-7 所示。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2
spring.h2.console.enabled=true

Listing 5-7src/main/resources/application.properties

清单 5-7 显示了application.properties和 JPA 属性。它基于域模型类生成表,并在每次应用启动时创建表。以下是spring.jpa.hibernate.ddl-auto属性的可能值。

  • create(创建模式并销毁以前的数据)。

  • create-drop(在会话结束时创建并销毁模式)。

  • update(如有必要,更新模式)。

  • validate(验证模式,不更改数据库)。

  • none(禁用 DDL 处理)。

运行和测试:ToDo 应用

现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。如果您使用的是 Maven,请执行

./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

./gradlew bootRun

要测试 ToDo 应用,您可以运行 ToDoClient 应用。它应该没有任何问题。你也可以用 cURL 命令发送 ToDo 并查看 H2 控制台(http://localhost:8080/h2-console)。

Spring Data 架

Spring Data REST 项目构建在 Spring Data 存储库之上。它分析你的领域模型类,它使用 HATEOAS(超媒体作为应用状态的引擎,HAL +JSON)公开超媒体驱动的 HTTP 资源。以下是其中的一些功能。

  • 使用 HAL 作为媒体类型,从您的域模型类中公开一个可发现的 RESTful API。

  • 支持分页并将您的域类公开为集合。

  • 展示存储库中定义的查询方法的专用搜索资源。

  • 如果您想扩展默认设置,支持您自己的控制器的高度定制。

  • 允许通过处理 Spring ApplicationEvents来处理 REST 请求。

  • 带来了一个 HAL 浏览器来公开所有的元数据;对于开发非常有用。

  • 支持 Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data Solr、Spring Data Cassandra、Spring Data Gemfire。

Spring Data 由 Spring Boot 提供

如果你想在一个常规的 Spring MVC app 中使用 Spring Data REST,你需要通过在你的 JavaConfig 类中包含带有@Import注释的RepositoryRestMvcConfiguration类来触发它的配置(在那里你有你的@Configuration注释);但是如果你直接使用 Spring Boot,你不需要做任何事情。多亏了@EnableAutoConfiguration注释,Spring Boot 处理了这个问题。

如果您想在 Spring Boot 应用中使用 Spring Data REST,您需要包含spring-boot-starter-data-restspring-boot-starter-data-*技术依赖项,和/或 SQL 驱动程序(如果您打算使用 SQL 数据库引擎)。

带有 Spring Data JPA 和 Spring Data REST 的 ToDo 应用

您可以从头开始创建您的 ToDo 应用,或者在您的pom.xmlbuild.gradle文件中查看您需要哪些类以及必要的依赖项。

从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-rest

  • 名称:todo-rest

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, JPA, REST Repositories, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-5 )。

img/340891_2_En_5_Fig5_HTML.jpg

图 5-5

https://start . spring . io

您只能复制域模型ToDoToDoRepository类和application.properties文件;是的,只有两个类和一个属性文件。

运行:待办事项应用

现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。如果您使用的是 Maven,请执行

./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

./gradlew bootRun

运行 ToDo 应用时,要看到的一个重要内容是输出。

Mapped "{[/{repository}/search],methods=[HEAD],produces  ...
Mapped "{[/{repository}/search],methods=[GET],produces=  ...
Mapped "{[/{repository}/search],methods=[OPTIONS],produ  ...
Mapped "{[/{repository}/search/{search}],methods=[GET],  ...
Mapped "{[/{repository}/search/{search}],methods=[GET],  ...
Mapped "{[/{repository}/search/{search}],methods=[OPTIO  ...
Mapped "{[/{repository}/search/{search}],methods=[HEAD]  ...
Mapped "{[/{repository}/{id}/{property}],methods=[GET],  ...
Mapped "{[/{repository}/{id}/{property}/{propertyId}],m  ...
Mapped "{[/{repository}/{id}/{property}],methods=[DELET  ...
Mapped "{[/{repository}/{id}/{property}],methods=[GET],  ...
Mapped "{[/{repository}/{id}/{property}],methods=[PATCH  ...
Mapped "{[/{repository}/{id}/{property}/{propertyId}],m  ...
Mapped "{[/ || ],methods=[OPTIONS],produces=[applicatio  ...
Mapped "{[/ || ],methods=[HEAD],produces=[application/h  ...
Mapped "{[/ || ],methods=[GET],produces=[application/ha  ...
Mapped "{[/{repository}],methods=[OPTIONS],produces=[ap  ...
Mapped "{[/{repository}],methods=[HEAD],produces=[appli  ...
Mapped "{[/{repository}],methods=[GET],produces=[applic  ...
Mapped "{[/{repository}],methods=[GET],produces=[applic  ...
Mapped "{[/{repository}],methods=[POST],produces=[appli  ...
Mapped "{[/{repository}/{id}],methods=[OPTIONS],produce  ...
Mapped "{[/{repository}/{id}],methods=[HEAD],produces=[  ...
Mapped "{[/{repository}/{id}],methods=[GET],produces=[a  ...
Mapped "{[/{repository}/{id}],methods=[PUT],produces=[a  ...
Mapped "{[/{repository}/{id}],methods=[PATCH],produces=  ...
Mapped "{[/{repository}/{id}],methods=[DELETE],produces  ...
Mapped "{[/profile/{repository}],methods=[GET],produces  ...
Mapped "{[/profile/{repository}],methods=[OPTIONS],prod  ...
Mapped "{[/profile/{repository}],methods=[GET],produces  ...

它定义了存储库的所有映射端点(在这个应用中只有一个),以及您可以使用的所有 HTTP 方法。

测试:待办事项应用

为了测试 ToDo 应用,我们将使用 cURL 命令和浏览器。需要修改 ToDoClient app 接受媒体类型,HAL+JSON;所以在这一节,我们就不用了。首先看看你的浏览器。转到http://localhost:8080你应该会看到类似于图 5-6 的东西。

首先,看看你的浏览器。转到http://localhost:8080。你应该会看到类似于图 5-6 的东西。

img/340891_2_En_5_Fig6_HTML.jpg

图 5-6

http://localhost:8080

如果您看到相同的信息,但是是 RAW 格式,请尝试为您的浏览器安装 JSON Viewer 插件并重新加载页面。它将http://localhost:8080/toDos URL 公开为端点,这意味着您可以访问并执行这个 URL 的所有 HTTP 方法(来自日志)。

让我们用 cURL 命令添加一些 ToDo(是一行)。

curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read the Pro Spring Boot 2nd Edition Book"}' http://localhost:8080/toDos

HTTP/1.1 201
Location: http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 07 May 2018 03:43:57 GMT

{
  "description" : "Read the Pro Spring Boot 2nd Edition Book",
  "created" : "2018-05-06T21:43:57.676",
  "modified" : "2018-05-06T21:43:57.677",
  "completed" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000"
    },
    "toDo" : {
      "href" : "http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000"
    }
  }
}

您将得到一个 HAL+JSON 结果。再添加几个之后,你可以回到你的浏览器,点击http://localhost:8080/toDos链接。你会看到类似图 5-7 的东西。

img/340891_2_En_5_Fig7_HTML.jpg

图 5-7

http://localhost:8080/toDos

图 5-7 显示了访问/toDos端点时 HAL+JSON 的响应。

使用 HAL 浏览器进行测试:ToDo 应用

Spring Data REST 项目有一个工具 HAL 浏览器。它是一个 web 应用,帮助开发人员以交互方式可视化所有端点。因此,如果您不想直接使用端点和/或 cURL 命令,您可以使用 HAL 浏览器。

要使用 HAL 浏览器,请添加以下依赖项。如果您使用的是 Maven,将以下内容添加到您的pom.xml文件中。

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

Maven pom.xml

如果您使用的是 Gradle,将以下内容添加到您的build.gradle文件中。

compile 'org.springframework.data:spring-data-rest-hal-browser'

Gradle build.gradle

现在,您可以重新启动您的 ToDo 应用,并在浏览器中直接进入http://localhost:8080。你同图 5-8 所示。

img/340891_2_En_5_Fig8_HTML.jpg

图 5-8

http://localhost:8080

您可以单击 GET 和 NON-GET 列来与每个端点和 HTTP 方法进行交互。这对开发人员来说是一个很好的选择。

我向您展示的是您可以使用 Spring Data REST 实现的许多特性中的一部分。你真的不需要任何控制器了。此外,由于 Spring Boot 和 Spring Data REST 中的简单覆盖,您可以公开任何域类。

没有 SQL 数据库

NoSQL 数据库是持久化数据的另一种方式,但与关系数据库的表格关系不同。这些新兴的 NoSQL 数据库已经有了一个分类系统。你可以根据它的数据模型找到它。

  • 专栏(Cassandra、HBase 等。)

  • 文件(CouchDB、mongodb 等)。)

  • 关键值(Redis、Riak 等)。)

  • Graph (Neo4J,Virtuoso 等。)

  • 多模型(OrientDB、arangodb 等)。)

如你所见,你有很多选择。我认为最重要的一种特性是找到一个可伸缩的、可以轻松处理数百万条记录的数据库。

Spring Data MongoDB

Spring Data MongoDB 项目为您提供了与 MongoDB 文档数据库的必要交互。其中一个重要的特性是,您仍然可以使用使用@Document注释的域模型类,并声明使用CrudRepository<T,ID>的接口。这创建了 MongoDB 用于持久性的必要集合。

以下是该项目的一些特点。

  • Spring Data MongoDB 提供了一个MongoTemplate助手类(与JdbcTemplate非常相似),它处理与 MongoDB 文档数据库交互的所有样板文件。

  • 持久性和映射生命周期事件。

  • MongoTemplate助手类。它还提供了使用MongoReader / MongoWriter抽象的低级映射。

  • 基于 Java 的查询、标准和更新 DSL。

  • 地理空间和 MapReduce 集成和 GridFS 支持。

  • 对 JPA 实体的跨存储持久性支持。这意味着您可以使用标有@Entity和其他注释的类,并使用它们通过 MongoDB 文档数据库来持久化/检索数据。

Spring Boot 的 Spring Data MongoDB

要将 MongoDB 与 Spring Boot 一起使用,您需要添加spring-boot-starter-data-mongodb依赖项并访问 MongoDB 服务器实例。

Spring Boot 使用自动配置特性来设置与 MongoDB 服务器实例通信的一切。默认情况下,Spring Boot 尝试连接到本地主机并使用端口 27017(MongoDB 标准端口)。如果您有一个 MongoDB 远程服务器,您可以通过覆盖缺省值来连接它。您需要使用application.properties文件中的spring.mongodb.*属性(最简单的方法),或者您可以使用 XML 或在 JavaConfig 类中进行 bean 声明。

Spring Boot 还自动配置了MongoTemplate类(该类与JdbcTemplate非常相似),因此它可以与 MongoDB 服务器进行任何交互。另一个很棒的特性是您可以使用,这意味着您可以重用用于 JPA 的相同接口。

mongodb 安装

在开始之前,您需要确保您的计算机上安装了 MongoDB 服务器。

如果您使用带有brew命令( http://brew.sh/ )的 Mac/Linux,请执行以下命令。

brew install mongodb

您可以用这个命令运行它。

mongod

或者你可以从 https://www.mongodb.org/downloads#production 的网站下载 MongoDB 并按照说明进行安装。

MongoDB 嵌入式

使用 MongoDB 还有另一种方法,至少作为开发环境。可以使用 MongoDB Embedded。通常,您在测试环境中使用它,但是您可以在开发模式中用运行时范围轻松地运行它。

要使用 MongoDB Embedded,您需要添加以下依赖项。如果使用的是 Maven,可以添加到pom.xml文件中。

<dependency>
      <groupId>de.flapdoodle.embed</groupId>
      <artifactId>de.flapdoodle.embed.mongo</artifactId>
      <scope>runtime</scope>
</dependency>

Maven pom.xml

如果您使用的是 Gradle:

runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

Gradle build.gradle

接下来,您需要配置 Mongo 客户端来使用 MongoDB 嵌入式服务器(参见清单 5-8 )。

package com.apress.todo.config;

import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;

@Configuration
public class ToDoConfig {

    private Environment environment;
    public ToDoConfig(Environment environment){
        this.environment = environment;
    }

    @Bean
         @DependsOn("embeddedMongoServer")
    public MongoClient mongoClient() {
        int port =
            this.environment.getProperty("local.mongo.port",
                                                Integer.class);
        return new MongoClient("localhost",port);
    }
}

Listing 5-8com.apress.todo.config.ToDoConfig.java

清单 5-8 显示了MongoClient bean 的配置。MongoDB Embedded 在应用启动时使用一个随机端口,这就是为什么还需要使用Environment bean 的原因。

如果采用这种方法来使用 MongoDB 服务器,则不需要设置任何其他属性。

带有 Spring Data 的 ToDo 应用 MongoDB

您可以从头开始创建您的 ToDo 应用,或者在您的pom.xmlbuild.gradle文件中查看您需要哪些类以及必要的依赖项。

从头开始,打开你的浏览器,打开 Spring Initializr ( https://start.spring.io )。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-mongo

  • 名称:todo-mongo

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, MongoDB, Embedded MongoDB

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-9 )。

img/340891_2_En_5_Fig9_HTML.jpg

图 5-9

https://start.spring.io

您可以从 todo-jpa 项目中复制所有的类。在下一节中,您将看到哪些类需要修改。

域模型:ToDo

打开ToDo域模型类并相应地修改它(参见清单 5-9 )。

package com.apress.todo.domain;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;

@Document

@Data
public class ToDo {

    @NotNull
    @Id
    private String id;
    @NotNull
    @NotBlank
    private String description;

    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        LocalDateTime date = LocalDateTime.now()

;
        this.id = UUID.randomUUID().toString();
        this.created = date;
        this.modified = date;
    }

    public ToDo(String description){
        this();
        this.description = description;
    }
}

Listing 5-9com.apress.todo.domain.ToDo.java

清单 5-9 显示了修改后的ToDo类。该类正在使用@Document批注来标记为持久的;它还使用@Id来声明一个惟一键。

如果您有一个远程 MongoDB 服务器,您可以覆盖指向本地主机的缺省值。您可以转到您的application.properties文件并添加以下属性。

## MongoDB
spring.data.mongodb.host=my-remote-server
spring.data.mongodb.port=27017
spring.data.mongodb.username=myuser
spring.data.mongodb.password=secretpassword

接下来,你可以复习你的ToDoRepositoryToDoController类,它们应该根本不会改变。这就是使用 Spring Data 的美妙之处:您可以为交叉存储和所有以前的类重用您的模型,使开发更容易、更快。

运行和测试:ToDo 应用

现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。或者,如果您使用的是 Maven,请执行

./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

./gradlew bootRun

要测试 ToDo 应用,您可以运行您的 ToDoClient 应用—就这样。切换持久性引擎非常容易,没有太多的麻烦。也许你想知道如果你想使用 map-reduce 或者更低级的操作会发生什么。嗯,你可以通过使用MongoTemplate类。

带有 Spring Data 的 ToDo 应用 MongoDB REST

如何创建一个 MongoDB REST 应用?您需要将spring-boot-starter-data-rest依赖项添加到您的pom.xmlbuild.gradle文件中——就这样!!当然,您需要删除控制器和验证包以及ToDoBuilder类;你只需要两节课。

请记住,Spring Data REST 公开了存储库端点,并使用 HATEOAS 作为媒体类型(HAL+JSON)。

注意

你可以在 Apress 网站或 GitHub 的 https://github.com/Apress/pro-spring-boot-2 找到这一部分的解决方案。该项目的名称是 todo-mongo-rest。

Spring Data Redis(Spring Data 重定向器)

Spring Data Redis 提供了一种配置和访问 Redis 服务器的简单方法。它提供了从低级到高级的抽象来与之交互,并遵循相同的 Spring Data 标准,提供了一个RedisTemplate类和基于存储库的持久性。

以下是 Spring Data Redis 的一些特性。

  • RedisTemplate给出了所有 Redis 操作的高级抽象。

  • 通过发布/订阅发送消息。

  • Redis Sentinel 和 Redis 群集支持。

  • 跨多个驱动程序使用连接包作为底层,如 Jedis 和莴苣。

  • 储存库,通过使用@EnableRedisRepositories进行排序和分页。

  • Redis 实现了 Spring 缓存,所以您可以使用 Redis 作为您的 web 缓存机制。

与 Spring Boot 的 Spring Data Redis

如果您想使用 Spring Data Redis,您只需要添加spring-boot-starter-data-redis依赖项来访问 Redis 服务器。

Spring Boot 使用自动配置来设置使用 Redis 服务器的默认值。如果您正在使用存储库特性,它会自动使用@EnableRedisRepositories(您不需要添加它)。

默认情况下,使用本地主机和端口 6379。当然,您可以通过更改application.properties文件中的spring.redis.*属性来覆盖默认设置。

任何带有 Spring Data Redis 的应用

您可以从头开始创建您的 ToDo 应用,或者在您的pom.xmlbuild.gradle文件中查看您需要哪些类以及必要的依赖项。

从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-redis

  • 名称:todo-redis

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, Redis

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 5-10 )。

img/340891_2_En_5_Fig10_HTML.jpg

图 5-10

https://start.spring.io

您可以从todo-mongo项目中复制所有的类。在下一节中,我们将看到哪些类需要修改。

域模型:ToDo

打开ToDo域模型类并做相应修改(见清单 5-10 )。

package com.apress.todo.domain;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;

@Data

@RedisHash

public class ToDo {

    @NotNull
    @Id
    private String id;
    @NotNull
    @NotBlank
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified

;
    private boolean completed;

    public ToDo(){
        LocalDateTime date = LocalDateTime.now();
        this.id = UUID.randomUUID().toString();
        this.created = date;
        this.modified = date;
    }

    public ToDo(String description){
        this();
        this.description = description;
    }
}

Listing 5-10com.apress.todo.ToDo.java

清单 5-10 显示了您修改的唯一一个类。该类使用了将该类标记为持久的@RedisHash注释,并且还使用了@Id注释作为组合键的一部分。当插入一个 ToDo 时,有一个散列包含一个格式为class:id的键。对于这个应用,组合键类似于"com.apress.todo.domain.ToDo: bbee6e32-37f3-4d5a-8f29-e2c79a28c301"

如果有远程 Redis 服务器,可以覆盖指向本地主机的默认值。您可以转到您的application.properties文件并添加以下属性。

## Redis - Remote
spring.redis.host=my-remote-server
spring.redis.port=6379
spring.redis.password=my-secret

你可以复习一下你的ToDoRepositoryToDoController类,应该根本不会改变;和以前一样。

运行和测试:ToDo 应用

现在是运行和测试这个 ToDo 应用的时候了。您可以在您的 IDE 中运行它。或者,如果您使用的是 Maven,请执行

./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

./gradlew bootRun

要测试 ToDo 应用,您可以运行您的 ToDoClient 应用;仅此而已。如果你想使用一个不同的结构(比如集合、列表、字符串、ZSET)一个低级操作,你可以使用已经由 Spring Boot 设置和配置的RedisTemplate类。

注意

记住你可以从 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 获得这本书的源代码。

Spring Boot 的更多数据功能

还有更多用于操作数据的特性和支持的引擎,从使用带有 jOOQ(Java Object-Oriented query atwww.jooq.org)的 DSL,jOOQ 从您的数据库生成 Java 代码,并允许您通过它自己的 DSL 以类型安全的方式构建 SQL 查询。

有很多方法可以进行数据库迁移,可以使用 Flyway ( https://flywaydb.org/ )或 Liquibase ( http://www.liquibase.org/ )在启动时运行。

多个数据源

Spring Boot 的一个重要特性(我认为这是一个必须具备的特性)是操纵多个DataSource实例,不管使用的是哪种持久技术。

正如您已经知道的,Spring Boot 提供了一个基于应用类路径的默认自动配置,您可以覆盖它,没有任何问题。要使用多个DataSource实例,它们可能指向不同的数据库和/或不同的引擎,您必须覆盖缺省值。如果你还记得第四章和的话,我们创建了一个完整而简单的 web 应用,需要设置:DataSourceEntityManagerTransactionManagerJpaVendor等等。如果我们想要使用多个数据源,我们需要添加相同的配置。换句话说,我们需要添加多个EntityManagerTransactionManager等等。

我们如何将此应用到 ToDo 应用中?回顾你在第四章所做的内容。仔细看看每个配置,就可以想象需要做什么了。

你可以在书中的源代码中找到解决方法。项目名称为todo-rest-2ds。这个项目包含所有的UserToDo域类,它们将数据保存到自己的数据库中。

摘要

在本章中,您学习了不同的数据技术以及它们如何与 Spring Boot 配合使用。您还了解了 Spring Boot 使用其自动配置特性来基于类路径应用默认值。

您了解到,如果您有两个或更多的 SQL 驱动程序,其中一个是 H2、HSQL 或 Derby,如果您还没有定义一个DataSource实例,Spring Boot 将配置嵌入式数据库引擎。您看到了如何配置和覆盖一些数据默认值。您了解了 Spring Data 实现了一个模板模式来隐藏没有 Spring 框架通常会完成的所有复杂任务。

在下一章中,我们用反应式编程将 web 和数据提升一个层次,并探索 WebFlux 和反应式数据。

六、Spring Boot 的网络流量和反应数据

在这一章中,我将向你展示 Spring Framework 5 的最新补充,以及如何在 Spring Boot 中使用它。为 web 应用构建的新的反应器栈是 Spring WebFlux,它是在 Spring 框架的 5.0 版本中添加的。它是一个完全非阻塞的框架,依赖于 Project Reactor,支持 reactive streams back pressure,运行在 Netty 和 Undertow 等服务器以及 Servlet 3.1+容器上。

在我向你展示如何在 Spring Boot 中使用 WebFlux 之前,让我们先了解一下反应式系统以及 Project Reactor ( https://projectreactor.io/ )是如何实现它们的。

反应系统

在过去的十年里,我们参与了软件的变革,让软件变得更稳定、更健壮、更有弹性、更灵活,以接受更现代的需求,不仅是用户(使用桌面或网络),还有许多设备(手机、传感器等)。).接受这些新的工作负载面临许多挑战;这就是为什么一组组织共同努力,带来一份清单,以涵盖当今数据需求的许多方面。

反动宣言

《反动宣言》( https://www.reactivemanifesto.org/ )签署于 2014 年 9 月 16 日。它定义了反应系统应该如何。反应式系统是灵活的、松散耦合的和可伸缩的。这些系统更能容忍失败,当失败发生时,它们通过应用模式来处理它,以避免灾难。这些反应系统已经定义了某些特征。

反应系统是

  • 反应灵敏。如果可能的话,大多数系统都会及时响应;他们专注于提供快速一致的响应时间,并可靠地交付一致的服务质量。

  • 富有弹性。他们应用复制、包容、隔离和委托模式来提供弹性系统。系统的故障必须通过隔离来控制;故障不应影响其他系统。必须从另一个系统恢复,以确保高可用性(HA)。

  • 弹性。系统必须对任何类型的工作负载做出响应。反应式系统可以通过增加或减少分配给这些输入的资源来对输入速率的变化做出反应。不应该有任何瓶颈,这意味着系统有能力共享或复制组件。反应式系统必须支持预测算法,以确保商用硬件的成本效益弹性。

  • 消息驱动。反应式系统必须依赖异步消息传递来建立组件之间的边界,确保系统是松散耦合的、隔离的和位置透明的。它必须通过在需要时提供背压模式来支持负载管理、弹性和流量控制。通信必须是非阻塞的,以允许在活动时使用资源,从而降低系统开销。

随着 Reactive Manifesto 的出现,不同的计划开始出现并实现框架和库,帮助世界各地的许多开发人员。Reactive Streams ( www.reactive-streams.org )是一个规范,定义了四个简单的接口(Publisher<T>,一个无限数量有序元素的提供者,根据订阅者的需求发布它们;Subscriber<T>订阅出版商;Subscription表示订阅发布者的订阅者一对一的生命周期;和Processor,这是对SubscriberPublisher以及不同实现的处理阶段,如 react vex rx Java(http://reactivex.io/)、Akka Streams ( https://akka.io/ )、Ratpack ( https://ratpack.io/ )、Vert。X ( https://vertx.io/ )、Slick、Project Reactor 等等。

在 Java 9 SDK 版本中,Reactive Streams API 有自己的实现;换句话说,截至 2017 年 12 月,Reactive Streams 1 . 0 . 2 版是 JDK9 的一部分。

项目反应器

Project Reactor 3.x 是一个围绕 Reactive Streams 规范构建的库,为 JVM 带来了反应式编程范例。反应式编程是一种基于事件模型的范例,其中数据在变得可用时被推送给消费者;它处理异步事件序列。反应式编程提供了完全异步和非阻塞的模式,是在 JDK(回调、API 和未来< V > 接口)中执行异步代码的有限方式的替代方案。

Reactor 是一个完整的、非阻塞的反应式编程框架,它管理背压并集成了与 Java 8 功能 API(CompletableFutureStreamDuration)的交互。Reactor 提供了两个反应式的可组合异步 APIsFlux N,以及 Mono 0|1。Reactor 可以用于开发微服务架构,因为它提供了带有 reactor-ipc 组件的 IPC(进程间通信)和用于 HTTP(包括 WebSockets、TCP 和 UDP)的背压就绪网络引擎,并且完全支持反应式编码和解码。

Project Reactor 提供了处理器、操作器和计时器,可以在低内存占用的情况下保持每秒数千万条消息的高吞吐率。

注意

如果您想了解更多关于 Project Reactor 的信息,请访问 https://projectreactor.io/ 及其在 http://projectreactor.io/docs/core/release/reference/docs/index.html 的文档。

带有反应器的待办事项应用

让我们开始在 ToDo 应用中使用 Reactor,并尝试 Flux 和 Mono APIs。在本节中,创建一个简单的 Reactor 示例来处理 ToDo。

打开自己喜欢的浏览器,指向 Spring Initializr(https://start.spring.io);将以下值添加到字段中。

  • 组:com.apress.reactor

  • 神器:example

  • 依赖关系:Lombok

您可以选择 Maven 或 Gradle 作为项目类型。然后点击 Generate Project 按钮,下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 6-1 )。

img/340891_2_En_6_Fig1_HTML.jpg

图 6-1

spring initializehttps://start.spring.io

如您所见,这次在龙目语( https://projectlombok.org/ )中没有明显的依赖关系。我们会尽可能简单地解决这个问题。这只是 Flux 和 Mono APIs 的一点点味道。稍后,您将使用 WebFlux 框架创建一个 web 应用。

如果您正在使用 Maven,请打开您的pom.xml并添加以下部分和依赖项。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-bom</artifactId>
            <version>Bismuth-SR10</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
      <!-- ... more dependencies here ... -->

      <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
      </dependency>

</dependencies>

Reactor 对reactive-streamsjar 有一个可传递的依赖,所以通过添加物料清单(BOM), Project Reactor 提供了所有必需的 jar。

如果您使用的是 Gradle,将下面的部分和依赖项添加到您的build.gradle文件中。

dependencyManagement {
    imports {
        mavenBom "io.projectreactor:reactor-bom:Bismuth-SR10"
    }
}

dependencies {
      // ... More dependencies here ...
      compile('io.projectreactor:reactor-core')
}

接下来,让我们创建ToDo域类,但是这一次,它没有持久性(参见清单 6-1 )。

package com.apress.reactor.example.domain;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-1com.apress.reactor.example.domain.ToDo.java

清单 6-1 显示了ToDo类。这个类没有什么特别的,但是我们一直在使用持久技术;在这种情况下,您可以让它保持原样—简单。让我们从定义 Mono 和 Flux 反应 API 开始,并添加必要的代码来使用ToDo域类。

Mono ,异步[0|1]结果

Mono<T>是发射一个项目的专用Publisher<T>,它可以选择终止于onCompleteonError信号。您可以应用操作符来操作该项目(参见图 6-2 )。

img/340891_2_En_6_Fig2_HTML.jpg

图 6-2

Mono 0|1 文档)。

接下来,让我们创建一个MonoExample类,并学习如何使用 Mono API(参见清单 6-2 )。

package com.apress.reactor.example;

import com.apress.reactor.example.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.scheduler.Schedulers;

import java.time.Duration;

@Configuration
public class MonoExample {

    static private Logger LOG = LoggerFactory.getLogger(MonoExample.class);

    @Bean
    public CommandLineRunner runMonoExample(){
        return args -> {

       MonoProcessor<ToDo> promise = MonoProcessor.create();
       Mono<ToDo> result = promise
            .doOnSuccess(p -> LOG.info("MONO >> ToDo: {}", p.getDescription()))
            .doOnTerminate( () -> LOG.info("MONO >> Done"))
            .doOnError(t -> LOG.error(t.getMessage(), t))
            .subscribeOn(Schedulers.single());

            promise.onNext(
new ToDo("Buy my ticket for SpringOne Platform 2018"));
          //promise.onError(
new IllegalArgumentException("There is an error processing the ToDo..."));

            result.block(Duration.ofMillis(1000));
        };
    }
}

Listing 6-2com.apress.reactor.example.MonoExample.java

清单 6-2 显示了MonoExample类;我们来分析一下。

  • MonoProcessor。在 Reactor 中,有些处理器既是发布者,也是订阅者;这意味着您可以订阅处理器,但也可以调用方法来手动将数据注入序列或终止序列。在本例中,您使用了onNext方法来发出一个ToDo实例。

  • Mono。这是一个带有基本运算符的反应式流发布器,通过发出元素或出错来成功完成。

  • doOnSuccess。当Mono成功完成时,调用或触发该方法。

  • doOnTerminate。当Mono因成功完成或出错而终止时,调用或触发该方法。

  • doOnError。当Mono类型完成时出现错误,这个方法被调用。

  • subscribeOn。订阅Mono类型并请求对指定的Scheduler工作线程的无限制需求。

  • onNext。这个方法发出一个可以标记为@Nullable的值。

  • block。订阅Mono类型并阻塞,直到接收到下一个信号或超时。

你可以看到一个非常简单的例子,但请记住,现在我们讨论的是反应式编程,不再有来自服务器的阻塞或轮询,而是推送到消费者,直到它发回一个完成的信号。这使得我们的应用更加高效和健壮。我们可以说我们再也不能拥有迟钝的消费者了。

现在,您可以使用命令行或 IDE 来运行该应用。您应该会看到以下输出:

INFO 55588 - [single-1] c.a.r.e.MonoExample   : MONO >> ToDo: Buy my ticket for SpringOne Platform 2018
INFO 55588 - [single-1] c.a.r.e.MonoExample   : MONO >> Done

注意

这段代码是如何运行的?记得我们已经将类标记为@Configuration,并且声明了一个返回CommandLineRunner接口的@Bean。Spring Boot 在完成所有 Spring beans 的连接之后、应用启动之前执行这个 bean;所以这是在应用运行之前执行代码(比如初始化)的好方法。

Flux :一个[0|N]项的异步序列

Flux 是一个代表 0 到 N 个发射项目的异步序列的Publisher<T>,该序列可以通过使用onCompleteonError信号选择性地终止(见图 6-3 )。

img/340891_2_En_6_Fig3_HTML.jpg

图 6-3

Flux 0|N 文档)。

创建FluxExample类(参见清单 6-3 )。

package com.apress.reactor.example;

import com.apress.reactor.example.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.util.List;

@Configuration
public class FluxExample {

    static private Logger LOG = LoggerFactory.getLogger(FluxExample.class);

    @Bean
    public CommandLineRunner runFluxExample(){
        return args -> {

       EmitterProcessor<ToDo> stream =
                        EmitterProcessor.create();

       Mono<List<ToDo>> promise = stream
                     .filter( s -> s.isCompleted())
                     .doOnNext(s -> LOG.info("FLUX >>> ToDo: {}", s.getDescription()))
                   .collectList()
                   .subscribeOn(Schedulers.single());

       stream.onNext(new ToDo("Read a Book",true));
       stream.onNext(new ToDo("Listen Classical Music",true));
       stream.onNext(new ToDo("Workout in the Mornings"));
       stream.onNext(new ToDo("Organize my room", true));
       stream.onNext(new ToDo("Go to the Car Wash", true));
       stream.onNext(new ToDo("SP1 2018 is coming" , true));

            stream.onComplete();

            promise.block();

        };
    }
}

Listing 6-3com.apress.reactor.example.FluxExample.java

清单 6-3 显示了FluxExample类。我们来分析一下。

  • EmitterProcessor。请记住,处理器是一种发布者;在这种情况下,我们使用一个同步处理器,它可以通过用户操作和订阅上游发布者并同步排出数据来推送数据。这个处理器创建了一系列ToDo实例;它提供了一个由RingBuffer支持的消息传递处理器的实现,该处理器实现了带有同步漏循环的publish-subscribe。如果你想使用异步处理器,你可以使用WorkQueueProcessorTopicProcessor

  • filter。请记住,您可以将运算符应用于 Flux 和 Mono APIs 在这种情况下,使用一个谓词来应用过滤器,该谓词评估并在成功时发出一个值(即,如果完成了ToDo)。

  • doOnNext。当通量发出一个项目时触发。

  • collectList。将 flux 发出的所有元素收集到一个列表中,该列表在此序列完成时由结果单声道发出。

  • subscribeOn。基于调度器工作线程订阅此流量。

  • onNext。向通量发出新值。

  • onComplete。完成上游。

  • block。订阅Mono类型并阻塞,直到接收到下一个信号或超时。

现在,您可以运行代码了。您应该得到类似于下面的输出。

INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Read a Book
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Listen Classical Music
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Organize my room
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Go to the Car Wash
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: SP1 2018 is coming

同样,这是一个简单的 ToDo 应用示例。想象一下,你有数百万用户访问你的应用,每个账户发布 ToDos,你想跟踪他们中的每一个人,就像 Twitter feed 一样,你不想阻止任何用户。你要反应!

webflux

长期以来,Spring MVC 一直是使用 Spring 框架创建 web 应用的主要方式。现在,另一个参与者出现了——反应式堆栈,Spring WebFlux 框架!Spring WebFlux 是一个完全异步和非阻塞的框架,它依赖于 Project Reactor 的 Reactive Streams 实现。

Spring WebFlux 的一个主要特性是它提供了两种编程模型。

  • 带注释的控制器。与 Spring MVC 一致,基于来自spring-web模块的相同注释;这意味着您可以使用相同的已知注释(@*Controller@*Mapping@RequestBody等。)但是有了 Reactor 和 RxJava 的所有反应式支持。

    @RestController
    public class ToDoController {
    
          @GetMapping("/todo/{id}")
          public Mono<ToDo> getToDo(@PathVariable Long id) {
                // ...
          }
    
          @GetMapping("/todo")
          public Flux<ToDo> getToDos() {
                // ...
          }
    }
    
    
  • 功能终点。一个函数式编程模型,其中可以使用基于 lambda 的调用。您需要通过声明RouterFunctionbean 和返回带有MonoFlux类型的响应的端点处理程序来提供路由端点。

    @Configuration
    public class ToDoRoutingConfiguration {
    
          @Bean
          public RouterFunction<ServerResponse>
                      monoRouterFunction(ToDoHandler toDoHandler) {
                return
        route(GET("/todo/{id}")
    .and(accept(APPLICATION_JSON)),toDoHandler::getToDo)
          .andRoute(GET("/todo")
         .and(accept(APPLICATION_JSON)), toDoHandler::getToDos);
          }
    }
    
    @Component
    public class ToDoHandler {
    
      public Mono<ServerResponse> getToDo(ServerRequest request){
                // ...
          }
    
      public Mono<ServerResponse> getToDos(ServerRequest request){
                // ...
          }
    }
    
    

过滤器异常处理器CORS视图技术web 安全的处理方式与 Spring MVC 相同。这就是使用 Spring Framework 的美妙之处——不管新技术如何,都是一个一致的生态系统。

web 客户端

WebFlux 模块还引入了一个用于 HTTP 请求的反应式非阻塞客户机,它具有函数式 API 客户机和反应式流支持。WebClient界面有以下特点。

  • 一个利用 lambda 编程风格的函数式 API

  • 非阻塞和反应性

  • 用较少的硬件资源支持高并发性

  • 支持从服务器向上和向下流式传输

  • 支持同步和异步通信

WebClient非常容易使用。这个客户端有retrieveexchange方法来获取响应体并解码。

WebClient client = WebClient.create("http://my-to-dos.com");

// [0|1] ToDo
Mono<ToDo> result = client
      .get()

      .uri("/todo/{id}", id)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(ToDo.class);

//[0|N] ToDos
Flux<ToDo> result = client
     .get()
     .uri("/todo").accept(MediaType.TEXT_EVENT_STREAM)
     .retrieve()
     .bodyToFlux(ToDo.class);

稍后,我们将创建一个使用该客户端的小示例,但是如果您有兴趣了解更多相关信息,请查看 https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client

WebFlux 和 Spring Boot 自动配置

有了 Spring Boot,Spring WebFlux 比以往任何时候都容易,因为 Spring Boot 通过为HttpMessageReaderHttpMessageWriter实例配置必要的编解码器来提供自动配置。它支持服务静态资源,包括对 WebJars 的支持。它采用了支持 WebFlux 的最新模板引擎技术,如 FreeMarker ( https://freemarker.apache.org/ )、百里叶( www.thymeleaf.org )、Mustache ( https://mustache.github.io/ )。默认情况下,自动配置将 Netty ( https://netty.io )设置为主容器。

如果您需要覆盖默认的 WebFlux 自动配置,您可以添加自己的类型为WebFluxConfigurer@Configuration类。

重要说明

如果您想完全控制 WebFlux 的自动配置,那么您需要添加您的自定义的用@EnableWebFlux注释的@Configuration

结合 Spring Boot 使用 WebFlux

要将 WebFlux 与 Spring Boot 一起使用,需要将spring-boot-starter-webflux依赖项添加到您的pom.xmlbuild.gradle文件中。

重要说明

您可以同时使用spring-boot-starter-webspring-boot-starter-webflux,但是 Spring Boot 自动配置是 Spring MVC。如果你想使用 WebFlux 的所有功能,那么你需要使用SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)

带有 WebFlux 的 ToDo 应用

让我们从使用 WebFlux 模块创建 ToDo 应用开始。让我们使用新的反应通量和 Mono APIs。

打开你喜欢的浏览器,指向 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-webflux

  • 名称:todo-webflux

  • 包名:com.apress.todo

  • 依赖关系:Lombok, Reactive Web

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以点击生成项目按钮来下载一个 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 6-4 )。

img/340891_2_En_6_Fig4_HTML.jpg

图 6-4

spring initializehttps://start.spring.io

这一次您使用了反应式 Web 依赖项;在这种情况下,spring-boot-starter-webflux启动器。让我们创建ToDo域类,它类似于其他项目(参见清单 6-4 )。

package com.apress.todo.domain;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        this.id = UUID.randomUUID().toString();
        this.created = LocalDateTime.now();
        this.modified = LocalDateTime.now();
    }

    public ToDo(String description){
        this();
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this();
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-4com.apress.todo.domain.ToDo.java

清单 6-4 显示了ToDo类;正如您所看到的,除了默认构造函数中的初始化之外,没有什么新的东西。

接下来,让我们创建ToDoRepository类,它保存内存中的 ToDo(参见清单 6-5 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Arrays;

public class ToDoRepository {

    private Flux<ToDo> toDoFlux =
Flux.fromIterable(Arrays.asList(
            new ToDo("Do homework"),
            new ToDo("Workout in the mornings", true),
            new ToDo("Make dinner tonight"),
            new ToDo("Clean the studio", true)));

    public Mono<ToDo> findById(String id){
       return Mono
            .from(
            toDoFlux.filter( todo -> todo.getId().equals(id)));
    }

    public Flux<ToDo> findAll(){
        return toDoFlux;
    }
}

Listing 6-5com.apress.todo.repository.ToDoRepository.java

清单 6-5 显示了ToDoRepository类,其中toDoFlux实例处理一个Flux<ToDo>。看看从通量返回一个Mono<ToDo>findById方法。

使用带注释的控制器

让我们继续创建一些你已经知道的东西(类似于 Spring MVC):一个处理FluxMono类型的ToDoController类(参见清单 6-6 )。

package com.apress.todo.controller;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class ToDoController {

    private ToDoRepository repository;

    public ToDoController(ToDoRepository repository){
        this.repository = repository;
    }

    @GetMapping("/todo/{id}")
    public Mono<ToDo> getToDo(@PathVariable String id){
        return this.repository.findById(id);
    }

    @GetMapping("/todo")
    public Flux<ToDo> getToDos(){
        return this.repository.findAll();
    }

}

Listing 6-6com.apress.todo.controller.ToDoController.java

从代码中可以看出,你返回的是一个Mono<ToDo>或者一个Flux<ToDo>,和 Spring MVC 不同;请记住,我们正在进行异步和非阻塞调用。

您可以运行应用,进入浏览器并指向http://localhost:8080/todo来查看结果 ToDo 的 JSON 响应;或者,您可以在终端中执行以下命令,并看到相同的输出。

$ curl http://localhost:8080/todo
[
    {

        "completed": false,
        "created": "2018-08-14T20:46:05.542",
        "description": "Do homework",
        "id": "5520e646-47aa-4be6-802a-ef6df500d6fb",
        "modified": "2018-08-14T20:46:05.542"
    },
    {
        "completed": true,
        "created": "2018-08-14T20:46:05.542",
        "description": "Workout in the mornings",
        "id": "3fe07f8d-64b0-4a39-ab1b-658bde4815d7",
        "modified": "2018-08-14T20:46:05.542"
    },
      ...
]

在控制台日志中,请注意运行容器是 Netty 容器,这是 Spring Boot 自动配置的默认容器。

使用功能端点

记住 Spring WebFlux 使用函数式编程来创建反应式 web 应用。让我们创建声明路由函数的ToDoRouter类(参见清单 6-7 )。

package com.apress.todo.reactive;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class ToDoRouter {

    @Bean

    public RouterFunction<ServerResponse> monoRouterFunction(ToDoHandler toDoHandler) {
        return route(GET("/todo/{id}").and(accept(APPLICATION_JSON)), toDoHandler::getToDo)
                .andRoute(GET("/todo").and(accept(APPLICATION_JSON)), toDoHandler::getToDos);
    }
}

Listing 6-7com.apress.todo.reactive.ToDoRouter.java

清单 6-7 显示了使用的路由(端点)和处理程序。Spring WebFlux 提供了一个非常好的流畅的 API 来轻松构建任何路线。

接下来,让我们创建具有服务请求的逻辑的ToDoHandler类(参见清单 6-8 )。

package com.apress.todo.reactive;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

@Component
public class ToDoHandler {

    private ToDoRepository repository;

    public ToDoHandler(ToDoRepository repository){
        this.repository = repository;
    }

    public Mono<ServerResponse> getToDo(ServerRequest request) {
        String toDoId = request.pathVariable("id");
        Mono<ServerResponse> notFound =
ServerResponse.notFound().build();
        Mono<ToDo> toDo = this.repository.findById(toDoId);

        return toDo
                .flatMap(t ->
                          ServerResponse
                        .ok()
                        .contentType(APPLICATION_JSON)
                        .body(fromObject(t)))
                .switchIfEmpty(notFound);
    }

    public Mono<ServerResponse> getToDos(
                                          ServerRequest request) {
        Flux<ToDo> toDos = this.repository.findAll();
        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(toDos, ToDo.class);
    }
}

Listing 6-8com.apress.todo.reactive.ToDoHandler.java

清单 6-8 向您展示了处理程序。我们来分析一下。

  • Mono<ServerResponse>。这种响应类型用在两种方法上,它使用带有BodyBuilder fluent API 的ServerResponse接口,为响应添加一个主体。BodyBuilder接口提供了有用的方法,可以帮助您构建响应,比如用ok方法添加状态。您可以使用headers方法添加标题,等等。

在运行应用之前,从 Spring 容器中移除ToDoController类是很重要的。您可以通过注释掉@RestController注释来做到这一点。

运行应用,您会得到和以前一样的结果。我知道这是一个非常简单的例子,因为这个应用在内存中做所有的事情,对吗?好吧,那就加上反应式持久吧!

反应数据

Spring Data 团队使用 RxJava 和 Project Reactor 实现的动态 API 创建了反应式存储库。这个抽象定义了几种包装器类型。Spring Data 在后台转换反应式包装器类型,这样您就可以坚持使用自己喜欢的组合库,这对于开发人员来说更容易。

  • ReactiveCrudRepository

  • ReactiveSortingRepository

  • RxJava2CrudRepository

  • RxJava2SortingRepository

Spring Data 提供了不同的 reactive 模块:MongoDB、Redis 和 Cassandra Reactive Streams,为您提供了 Reactive Streams 计划的所有灵活性和所有好处。

MongoDB 反应流

Spring Data MongoDB 构建在 MongoDB 反应流之上,它提供了反应流的最大互操作性。它提供了使用FluxMono类型的ReactiveMongoOperations助手接口。

要使用 MongoDB Reactive Streams,有必要将spring-boot-starter-data-mongodb-reactive依赖项包含到您的pom.xmlbuild.gradle文件中。MongoDB Reactive Streams 还为存储库声明提供了一个专用接口,即ReactiveMongoRepository<T,ID>接口。遵循相同的存储库模式,您可以声明自己的名为方法的查询,该查询返回一个FluxMono类型。

带有反应数据的待办事项应用

让我们通过使用 MongoDB 添加反应性数据来完成 ToDo 应用,并使用函数式编程模式对任何请求和响应使用 WebFlux。

可以打开自己喜欢的浏览器,指向 Spring Initializr(https://start.spring.io);将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-reactive-data

  • 名称:todo-reactive-data

  • 包名:com.apress.todo

  • 依赖关系:Lombok, Reactive Web, Reactive MongoDB

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮来下载 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 6-5 )。

img/340891_2_En_6_Fig5_HTML.jpg

图 6-5

spring initializehttps://start.spring.io

如您所见,我们现在有了反应式 Web 和反应式 MongoDB 依赖项。因为我们用的是 MongoDB,所以不需要有服务器。你用的是嵌入式 Mongo 通常,它是用于测试,但我们将在这个应用中使用它。

让我们从添加嵌入式 Mongo 依赖项开始。如果您使用的是 Maven,打开您的pom.xml文件并添加以下依赖项。

<dependency>
     <groupId>de.flapdoodle.embed</groupId>
     <artifactId>de.flapdoodle.embed.mongo</artifactId>
     <scope>runtime</scope>
  </dependency>

如果您使用的是 Gradle,您可以打开build.gradle文件并添加以下依赖项。

runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

接下来,让我们创建ToDo域类(参见清单 6-9 )。

package com.apress.todo.domain;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.LocalDateTime;
import java.util.UUID;

@Document

@Data
public class ToDo {

    @Id
    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        this.id = UUID.randomUUID().toString();
        this.created = LocalDateTime.now();
        this.modified = LocalDateTime.now();
    }

    public ToDo(String description){
        this();
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this();
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-9com.apress.todo.domain.ToDo.java

这个类使用了@Document@Id注释,将它们标记为 MongoDB 的持久类。

接下来,让我们创建ToDoRepository接口(参见清单 6-10 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ToDoRepository extends
            ReactiveMongoRepository<ToDo, String> {
}

Listing 6-10com.apress.todo.repository.ToDoRepository.java

这个接口是从ReactiveMongoRepository<T,ID>扩展而来的。该接口提供了您已经知道的相同的Repository功能,但是现在它返回了FluxMono类型。记住这和前面的章节是一样的。在这里,您可以定义返回反应类型的自定义命名查询。

对于这个 ToDo 应用,我们将使用函数式编程。让我们在ToDoRouter类中创建路由器和处理器(参见清单 6-11 )。

package com.apress.todo.reactive;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class ToDoRouter {

    @Bean
    public RouterFunction<ServerResponse>
            monoRouterFunction(ToDoHandler toDoHandler) {
        return
      route(GET("/todo/{id}").and(accept(APPLICATION_JSON)), toDoHandler::getToDo)

.andRoute(GET("/todo").and(accept(APPLICATION_JSON)), toDoHandler::getToDos)

.andRoute(POST("/todo").and(accept(APPLICATION_JSON)), toDoHandler::newToDo);
    }
}

Listing 6-11com.apress.todo.reactive.ToDoRouter.java

清单 6-11 显示了端点声明。有一个新方法,一个帖子。接下来,让我们创建处理程序(参见清单 6-12 )。

package com.apress.todo.reactive;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;

@Component
public class ToDoHandler {

    private ToDoRepository repository;

    public ToDoHandler(ToDoRepository repository){
        this.repository = repository;
    }

    public Mono<ServerResponse> getToDo(ServerRequest request) {
        return findById(request.pathVariable("id"));
    }

    public Mono<ServerResponse> getToDos(ServerRequest request) {
        Flux<ToDo> toDos = this.repository.findAll();
        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(toDos, ToDo.class);
    }

    public Mono<ServerResponse> newToDo(ServerRequest request) {
        Mono<ToDo> toDo = request.bodyToMono(ToDo.class);

        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(fromPublisher(toDo.flatMap(this::save),ToDo.class));
    }

    private Mono<ServerResponse> findById(String id){
        Mono<ToDo> toDo = this.repository.findById(id);

        Mono<ServerResponse> notFound = ServerResponse.notFound().build();

        return toDo
                .flatMap(t -> ServerResponse
                        .ok()

                        .contentType(APPLICATION_JSON)
                        .body(fromObject(t)))
                .switchIfEmpty(notFound);
    }

    private Mono<ToDo> save(ToDo toDo) {
        return Mono.fromSupplier(
                () -> {
                    repository
                            .save(toDo)
                            .subscribe();
                    return toDo;
                });
    }
}

Listing 6-12com.apress.todo.reactive.ToDoHandler.java

清单 6-12 显示了处理端点响应的处理器。每个方法都返回一个Mono<ServerResponse>。看看使保存到 MongoDB 服务器成为可能的Mono操作符,以及BodyBuilder如何创建响应。

接下来,创建设置到嵌入式 MongoDB 服务器的连接的配置。创建ToDoConfig类(参见清单 6-13 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

@Configuration

@EnableReactiveMongoRepositories(basePackages = "com.apress.todo.repository")

public class ToDoConfig extends AbstractReactiveMongoConfiguration {

    private final Environment environment;

    public ToDoConfig(Environment environment){
        this.environment = environment;
    }

    @Override
    protected String getDatabaseName() {
        return "todos";
    }

    @Override
    @Bean
    @DependsOn("embeddedMongoServer")
    public MongoClient reactiveMongoClient() {
        int port = environment.getProperty("local.mongo.port", Integer.class);
        return MongoClients.create(String.format("mongodb://localhost:%d", port));
    }

    @Bean
    public CommandLineRunner insertAndView(ToDoRepository repository, ApplicationContext context){
        return args -> {

            repository.save(new ToDo("Do homework")).subscribe();
            repository.save(new ToDo("Workout in the mornings", true)).subscribe();
            repository.save(new ToDo("Make dinner tonight")).subscribe();
            repository.save(new ToDo("Clean the studio", true)).subscribe();

            repository.findAll().subscribe(System.out::println);
        };
    }
}

Listing 6-13com.apress.todo.config.ToDoConfig.java

清单 6-13 显示了使用 MongoDB 反应流嵌入式服务器所需的配置。我们来分析一下这个类。

  • @EnableReactiveMongoRepositories。为反应流 API 设置所有必要的基础设施需要这个注释。告诉这个注释存储库在哪里也很重要(如果 repos 是包的一部分,这是不必要的)。

  • AbstractReactiveMongoConfiguration。要设置嵌入式 Mongo,有必要从这个抽象类扩展并实现reactiveMongoClientgetDatabaseName方法。reactiveMongoClient创建MongoClient实例,该实例连接到嵌入式 MongoDB 端口设置的任何地方(由于环境属性local.mongo.port)。

  • @DependsOn。这个注释是在embeddedMongoServer bean 之后创建reactiveMongoClient的助手。

该类还运行代码,其中 ToDo 保存到 MongoDB。

现在,您已经准备好运行 ToDo 应用了。运行应用后,您可以进入浏览器或执行以下命令。

$ curl http://localhost:8080/todo
[
    {
        "completed": false,
        "created": "2018-08-14T20:46:05.542",
        "description": "Do homework",
        "id": "5520e646-47aa-4be6-802a-ef6df500d6fb",
        "modified": "2018-08-14T20:46:05.542"
    },
    {
        "completed": true,
        "created": "2018-08-14T20:46:05.542",
        "description": "Workout in the mornings",
        "id": "3fe07f8d-64b0-4a39-ab1b-658bde4815d7",
        "modified": "2018-08-14T20:46:05.542"
    },
      ...
]

您可以通过执行以下命令来添加新的 ToDo。

$ curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read a book"}' http://localhost:8080/todo
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 164

{
"id":"a3133b8d-1d8b-4b2e-b7d9-48a73999a104",
"description":"Read a book",
"created":"2018-08-14T22:51:19.734",
"modified":"2018-08-14T22:51:19.734",
"completed":false
}

恭喜你!现在,您知道如何使用 Spring WebFlux 和 Project Reactor 的强大功能创建一个反应式、异步和非阻塞的应用了!

注意

所有代码都可以从 Apress 网站上获得。您还可以在 https://github.com/felipeg48/pro-spring-boot-2nd 资源库获取最新信息。

摘要

本章讨论了如何使用 Spring 框架的新成员 WebFlux。您了解了 WebFlux 是一个非阻塞、异步和反应式框架,它实现了反应式流。

您还了解了 WebFlux 提供了两种使用 Spring Boot 编程的方式:带注释的控制器和功能端点,在这里您可以定义 Flux 和 Mono 响应类型。您了解到 WebFlux 还提供了一个WebClient接口来使用这些新的反应式 API。

您了解了 Spring Boot 通过使用spring-boot-starter-webflux为反应式堆栈提供自动配置,默认情况下使用 Netty 作为服务器容器。

下一章将测试这些应用,并展示 Spring Boot 如何让 TDD 的应用变得更加容易。

七、Spring Boot 测试

软件测试是执行程序或系统以发现错误或缺陷(通常称之为 bug)并确保每个程序或系统真正工作的过程。

这是你能在互联网上找到的许多定义之一。

许多公司通过创建令人惊叹的框架,尽一切努力寻找正确和简单的方法来进行测试,这种做法被称为 TDD(测试驱动开发)。

TDD 是一个基于在非常短的开发周期内重复的过程;这里,反馈在过程中起着重要的作用,因为开发人员用最少的代码编写代码来传递一个用例。由于有了反馈,代码可以被重构,直到它被最终用户接受。

Spring 测试框架

Spring 框架的主要思想之一是鼓励开发人员创建简单和松散耦合的类,编程到接口,使软件更加健壮和可扩展。Spring 框架提供了使单元和集成测试变得容易的工具(实际上,如果你真的编程接口,你不需要 Spring 来测试你的系统的功能);换句话说,您的应用应该可以使用带有对象的 JUnit 或 TestNG 测试引擎进行测试(通过使用 new 操作符进行简单的实例化),而不需要 Spring 或任何其他容器。

Spring 框架有几个测试包,帮助为应用创建单元和集成测试。它通过提供几个模拟对象(EnvironmentPropertySourceJNDIServlet)来提供单元测试;反应式:ServerHttpRequestServerHttpResponse测试实用程序)帮助测试你的代码。

Spring 框架最常用的测试特性之一是集成测试。其主要目标是

  • 管理测试执行之间的 Spring IoC 容器缓存

  • 事务管理

  • 测试夹具实例的依赖注入

  • 特定于 Spring 的基类

Spring 框架通过在测试中集成ApplicationContext提供了一种简单的测试方法。Spring 测试模块提供了几种使用ApplicationContext的方法,通过编程和注释:

  • BootstrapWith。配置 Spring TestContext框架如何引导的类级注释。

  • @ContextConfiguration。定义类级别的元数据,以确定如何为集成测试加载和配置一个ApplicationContext。这是您的类的必备注释,因为那是ApplicationContext加载所有 bean 定义的地方。

  • @WebAppConfiguration。一个类级注释,声明集成测试的ApplicationContext负载应该是一个WebApplicationContext

  • @ActiveProfile。一个类级注释,用于声明在加载集成测试的ApplicationContext时,哪些 bean 定义概要文件应该是活动的。

  • @TestPropertySource。一个类级注释,用于配置属性文件和内联属性的位置,这些属性将被添加到为集成测试加载的ApplicationContextEnvironment中的PropertySources集合中。

  • @DirtiesContext。表示底层 Spring ApplicationContext在测试执行过程中已经被污染(例如,通过改变单例 bean 的状态而被修改或破坏),应该被关闭。

还有更多的(@TestExecutionListeners@Commit@Rollback@BeforeTransaction@AfterTransaction@Sql@SqlGroup@SqlConfig@Timed@Repeat@IfProfileValue等等。

如您所见,当您使用 Spring 框架进行测试时,有很多选择。通常,您总是使用@RunWith注释来连接所有的测试框架。举个例子,

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class ToDoRepositoryTests {

      @Test
      public void ToDoPersistenceTest(){
            //...
      }
}

现在,让我们看看如何使用 Spring 测试框架以及 Spring Boot 提供的所有特性。

Spring Boot 测试框架

Spring Boot 通过增强和添加新的注释和特性来使用 Spring 测试框架的强大功能,使得开发人员的测试更加容易。

如果你想开始使用 Spring Boot 的所有测试特性,你只需要在你的应用中添加对scope testspring-boot-starter-test依赖。这种依赖性已经存在于 Spring Initializr 服务中。

spring-boot-starter-test依赖项提供了几个测试框架,可以很好地配合所有 Spring Boot 测试特性:JUnit、AssertJ、Hamcrest、Mockito、JSONassert 和 JsonPath。当然,还有其他测试框架可以很好地使用 Spring Boot 测试模块;您只需要手动包含这些依赖项。

Spring Boot 提供了@SpringBootTest注解,简化了测试 Spring 应用的方式。通常,在 Spring 测试中,你需要添加几个注释来测试你的应用的特定特性或功能,但在 Spring Boot 中不是这样——尽管你仍然需要使用@RunWith(SpringRunner.class)注释来做测试;否则,任何新的 Spring Boot 测试注释都将被忽略。在测试 web 应用时,@SpringBootTest有一些有用的参数,比如定义一个RANDOM_PORTDEFINED_PORT

下面的代码片段是 Spring Boot 测试的框架。

@RunWith(SpringRunner.class)

@SpringBootTest

public class MyTests {

      @Test
      public void exampleTest() {
            ...
      }
}

测试 Web 端点

Spring Boot 提供了一种测试端点的方法:一个叫做MockMvc类的模拟环境。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcToDoTests {

      @Autowired
      private MockMvc mvc;

      @Test
      public void toDoTest() throws Exception {
          this.mvc
          .perform(get("/todo"))
          .andExpect(status().isOk())
          .andExpect(content()
             .contentType(MediaType.APPLICATION_JSON_UTF8));
      }

}

也可以使用TestRestTemplate类。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ToDoSimpleRestTemplateTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void toDoTest() {
        String body = this.restTemplate.getForObject("/todo", String.class);
        assertThat(body).contains("Read a Book");
    }
}

这段代码展示了一个测试,它运行一个完整的服务器,并使用TestRestTemplate实例调用/todo端点。在这里,我们假设一个String的回报。(这不是测试 JSON 返回的最佳方式。不要担心,稍后您会看到使用TestRestTemplate类的正确方法。

嘲讽豆

Spring Boot 测试模块提供了一个@MockBean注释,为ApplicationContext中的 bean 定义了一个 Mockito mock。换句话说,您可以通过添加这个注释来模仿一个新的 Spring bean 或者替换一个现有的定义。记住,这发生在ApplicationContext内部。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ToDoSimpleMockBeanTests {

    @MockBean
    private ToDoRepository repository;

    @Test
    public void toDoTest() {
        given(this.repository.findById("my-id"))
            .Return(new ToDo("Read a Book"));
        assertThat(
            this.repository.findById("my-id").getDescription())
            .isEqualTo("Read a Book");
    }
}

Spring Boot 测试切片

Spring Boot 提供的最重要的特性之一是不需要特定的基础设施就可以执行测试。Spring Boot 测试模块包括来测试应用的特定部分,而不需要服务器或数据库引擎。

@JsonTest

Spring Boot 测试模块提供了@JsonTest注释,这有助于对象 JSON 序列化和反序列化,并验证一切都按预期工作。@JsonTest根据类路径中的库:Jackson、GSON 或 JSONB,自动配置支持的 JSON 映射器。

@RunWith(SpringRunner.class)

@JsonTest

public class ToDoJsonTests {

    @Autowired
    private JacksonTester<ToDo> json;

    @Test
    public void toDoSerializeTest() throws Exception {
        ToDo toDo = new ToDo("Read a Book");

        assertThat(this.json.write(toDo))
        .isEqualToJson("todo.json");

        assertThat(this.json.write(toDo))
        .hasJsonPathStringValue("@.description");

        assertThat(this.json.write(toDo))
        .extractingJsonPathStringValue("@.description")
        .isEqualTo("Read a Book");
    }

    @Test
    public void toDoDeserializeTest() throws Exception {
        String content = "{\"description\":\"Read a Book\",\"completed\": true }";
        assertThat(this.json.parse(content))
                .isEqualTo(new ToDo("Read a Book", true));
         assertThat(
           this.json.parseObject(content).getDescription())
         .isEqualTo("Read a Book");
    }
}

@WebMvcTest

如果您需要在不使用完整服务器的情况下测试您的控制器,Spring Boot 提供了@WebMvcTest注释,该注释自动配置 Spring MVC 基础设施并将扫描的 beans 限制为@Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterWebMvcConfigurerHandlerMethodArgumentResolver;这样你就知道你的控制器是否按预期工作。

重要的是要知道,当使用这个注释时,标记为@Component的 beans 不会被扫描,但是如果需要的话,您仍然可以使用@MockBean

@RunWith(SpringRunner.class)

@WebMvcTest(ToDoController.class)

public class ToDoWebMvcTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private ToDoRepository toDoRepository;

    @Test
    public void toDoControllerTest() throws Exception {
        given(this.toDoRepository.findById("my-id"))
                .Return(new ToDo("Do Homework", true));

        this.mvc.perform(get("/todo/my-id").accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk()).andExpect(content().string("{\"id\":\"my-id\",\"description\":\"Do Homework\",\"completed\":true}"));
    }

}

@WebFluxTest

Spring Boot 为任何反应式控制器提供了@WebFluxTest注释。这个注释自动配置 Spring WebFlux 模块基础设施,并且只扫描@Controller@ControllerAdvice@JsonComponentConverterGenericConverterWebFluxConfigurer

重要的是要知道,当使用这个注释时,标记为@Component的 beans 不会被扫描,但是如果需要的话,您仍然可以使用@MockBean

@RunWith(SpringRunner.class)

@WebFluxTest(ToDoFluxController.class)

public class ToDoWebFluxTest {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private ToDoRepository toDoRepository;

    @Test
    public void testExample() throws Exception {
        given(this.toDoRepository.findAll())
                .Return(Arrays.asList(new ToDo("Read a Book"), new ToDo("Buy Milk")));
        this.webClient.get().uri("/todo-flux").accept(MediaType.APPLICATION_JSON_UTF8)
                .exchange()
                .expectStatus().isOk()
                .expectBody(List.class);
    }
}

@DataJpaTest

如果您需要测试您的 JPA 应用,Spring Boot 测试模块提供了@DataJpaTest,它自动配置内存中的嵌入式数据库。它扫描@Entity。这个注释不会加载任何@Component bean。它还提供了与 JPA EntityManager类非常相似的TestEntityManager助手类,专门用于测试。

@RunWith(SpringRunner.class)

@DataJpaTest

public class TodoDataJpaTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private ToDoRepository repository;

    @Test
    public void toDoDataTest() throws Exception {
        this.entityManager.persist(new ToDo("Read a Book"));
        Iterable<ToDo> toDos = this.repository.findByDescriptionContains("Read a Book");
        assertThat(toDos.iterator().next()).toString().contains("Read a Book");
    }

}

请记住,使用@DataJpaTest使用了嵌入式内存数据库引擎(H2、Derby、HSQL),但是如果您想使用真实的数据库进行测试,您需要添加下面的@AutoConfigureTestDatabase(replace=Replace.NONE)注释作为测试类的标记。

@RunWith(SpringRunner.class)

@DataJpaTest

@AutoConfigureTestDatabase(replace=Replace.NONE)

public class TodoDataJpaTests {
      //...
}

@JdbcTest

这个注释和@DataJpaTest很像;唯一的区别是,它只做与 JDBC 相关的测试。它自动配置内存中的嵌入式数据库引擎,并配置JdbcTemplate类。它省略了所有标记为@Component的类。

@RunWith(SpringRunner.class)

@JdbcTest

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class TodoJdbcTests {

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    private CommonRepository<ToDo> repository;

      @Test
      public void toDoJdbcTest() {
          ToDo toDo = new ToDo("Read a Book");

        this.repository = new ToDoRepository(jdbcTemplate);
        this.repository.save(toDo);

        ToDo result = this.repository.findById(toDo.getId());
        assertThat(result.getId()).isEqualTo(toDo.getId());
      }

}

@DataMongoTest

Spring Boot 测试模块提供了@DataMongoTest注释来测试 Mongo 应用。它自动配置内存中的嵌入式 Mongo 服务器(如果可用),如果没有,您需要添加正确的spring.data.mongodb.*属性。它配置MongoTemplate类并扫描@Document注释。@Component豆子不会被扫描。

@RunWith(SpringRunner.class)

@DataMongoTest

public class TodoMongoTests {

    @Autowired
    private MongoTemplate mongoTemplate;

      @Test
      public void toDoMongoTest() {
        ToDo toDo = new ToDo("Read a Book");
        this.mongoTemplate.save(toDo);

        ToDo result = this.mongoTemplate.findById(toDo.getId(),ToDo.class);
        assertThat(result.getId()).isEqualTo(toDo.getId());
      }

}

如果您需要一个外部 MongoDB 服务器(不是内嵌在内存中的),请将excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class参数添加到@DataMongoTest注释中。

@RunWith(SpringRunner.class)

@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)

public class ToDoMongoTests {
      // ...
}

@RestClientTest

另一个重要的注释是@RestClientTest,它测试您的 REST 客户端。这个注释自动配置 Jackson、GSON 和 JSONB 支持。它配置了RestTemplateBuilder并增加了对MockRestServiceServer的支持。

@RunWith(SpringRunner.class)

@RestClientTest(ToDoService.class)

public class ToDoRestClientTests {

    @Autowired
    private ToDoService service;

    @Autowired
    private MockRestServiceServer server;

    @Test
    public void toDoRestClientTest()
            throws Exception {
        String content = "{\"description\":\"Read a Book\",\"completed\": true }";
        this.server.expect(requestTo("/todo/my-id"))
                .andRespond(withSuccess(content,MediaType.APPLICATION_JSON_UTF8));
        ToDo result = this.service.findById("my-id");
        assertThat(result).isNotNull();
        assertThat(result.getDescription()).contains("Read a Book");
    }

}

还有很多其他的切片可以看看。需要注意的重要一点是,您不需要有一个完整的基础设施或者运行服务器来进行测试。切片有助于 Spring Boot 应用的更多测试。

摘要

在本章中,您学习了用 Spring Boot 测试应用的不同方法。尽管这一章很短,我还是向您展示了一些重要的特性,比如切片。

在下一章中,我们将讨论安全性,并了解 Spring Boot 如何保护我们的应用。

八、Spring Boot 的安全

本章向您展示了如何在您的 Spring Boot 应用中使用安全性来保护您的 web 应用。从使用基本安全性到使用 OAuth,您可以学到很多东西。在过去的十年中,安全性已经成为桌面、web 和移动应用的首要和重要因素。但是安全性有点难以实现,因为您需要考虑所有的事情——跨站点脚本、授权和认证、安全会话、身份识别、加密等等。在应用中实现简单的安全性还有很多工作要做。

Spring security 团队努力让开发人员更容易为他们的应用带来安全性,从保护服务方法到整个 web 应用。春安围绕AuthenticationProviderAuthenticationManager,专业化UserDetailsService;它还提供了与身份提供者系统的集成,如 LDAP、Active Directory、Kerberos、PAM、OAuth 等。你将在本章的例子中回顾其中的一些。

Spring Security

Spring Security 是高度可定制的强大框架,有助于身份验证和授权(或访问控制);它是保护 Spring 应用的默认模块。以下是一些重要的功能。

  • Servlet API 集成

  • 与 Spring Web MVC 和 WebFlux 的集成

  • 防范诸如会话固定、点击劫持、CSRF(跨站点请求伪造)、CORS(跨源资源共享)等攻击

  • 对认证和授权的可扩展和全面的支持

  • 与这些技术的集成:HTTP Basic、HTTP Digest、X.509、LDAP、基于表单、OpenID、CAS、RMI、Kerberos、JAAS、Java EE 等等

  • 与第三方技术的集成:AppFuse、DWR、Grails、Tapestry、JOSSO、AndroMDA、Roller 等等

Spring Security 已经成为在许多 Java 和 Spring 项目上使用安全性的事实上的方法,因为它以最小的努力集成和定制,创建健壮和安全的应用。

与 Spring Boot 的安全

Spring Boot 使用 Spring Security 框架的力量来保护应用。要使用 Spring Security 性,有必要添加spring-boot-starter-security依赖项。这个依赖关系提供了所有的spring-security核心 jar,它自动配置策略来决定是使用httpBasic还是formLogin认证机制。它默认为单用户的UserDetailService。这个用户名是user,当应用启动时,密码被打印(随机字符串)为一个带有 INFO 级别的日志。

换句话说,通过添加spring-boot-starter-security依赖项,您的应用已经安全了。

具有基本安全性的 ToDo 应用

先说 ToDo app。这里,您使用与 JPA REST 项目相同的代码;但是我会再复习一遍。让我们开始吧。从头开始,打开你的浏览器,打开 Spring Initializr ( https://start.spring.io )。将以下值添加到字段中:

  • 组:com.apress.todo

  • 神器:todo-simple-security

  • 名称:todo-simple-security

  • 包名:com.apress.todo

  • 依赖关系:Web, Security, Lombok, JPA, REST Repositories, H2, MySQL, Mustache

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以按下生成项目按钮;这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 8-1 )。

img/340891_2_En_8_Fig1_HTML.jpg

图 8-1

Spring 初始化 zr

这个项目现在有了安全模块和模板引擎 Mustache。很快你就会知道如何使用它。

让我们从 ToDo 域类开始(参见清单 8-1 )。

package com.apress.todo.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-1com.apress.todo.domain.ToDo.java

清单 8-1 显示了ToDo域类。你已经知道了。它标有@Entity,并且使用@Id作为主键。这个类来自于 todo-rest 项目。

接下来,让我们回顾一下ToDoRepository接口(见清单 8-2 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> {

}

Listing 8-2com.apress.todo.repository.ToDoRepository.java

清单 8-2 显示的是ToDoRepository,当然,你已经知道了。定义从CrudRepository<T,ID>扩展的接口,该接口不仅有 CRUD 方法,还有 Spring Data REST,创建所有必要的 REST APIs 来支持域类。

接下来,让我们回顾一下application.properties,看看有什么新内容(参见清单 8-3 )。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2-Console: http://localhost:8080/h2-console
# jdbc:h2:mem:testdb
spring.h2.console.enabled=true

# REST API

spring.data.rest.base-path=/api

Listing 8-3src/main/resources/application.properties

清单 8-3 显示了application.properties文件。除了最后一个,你已经看过一些房产了,对吗?spring.data.rest.base-path告诉 REST controller(Spring Data REST 配置的)使用/api作为根来公开所有的 REST API 端点。因此,如果我们想获得 ToDo,我们需要在http://localhost:8080/api/toDos访问端点。

在运行应用之前,让我们以脚本的形式添加端点。用下面的 SQL 语句创建src/main/resources/data.sql文件。

insert into to_do (id,description,created,modified,completed)
values ('8a8080a365481fb00165481fbca90000', 'Read a Book','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137',true);

insert into to_do (id,description,created,modified,completed)
values ('ebcf1850563c4de3b56813a52a95e930', 'Buy Movie Tickets','2018-08-17 09:50:10.126','2018-08-17 09:50:10.126',false);

insert into to_do (id,description,created,modified,completed)
values ('78269087206d472c894f3075031d8d6b', 'Clean my Room','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137',false);

现在,如果您运行您的应用,您应该在日志中看到以下输出:

Using generated security password: 2a569843-122a-4559-a245-60f5ab2b6c51

这是你的密码。您现在可以进入浏览器并打开https://localhost:8080/api/toDos。当你按下回车键访问那个 URL 时,你会得到类似于图 8-2 的东西。

img/340891_2_En_8_Fig2_HTML.jpg

图 8-2

所有应用:http://localhost:8080/log in 页

图 8-2 显示了一个登录页面,这是添加spring-boot-starter-security依赖项时的默认行为。默认情况下,安全是开启的—如此简单!!那么,用户和密码是什么呢?嗯,我之前提到过,用户是user,密码是随机打印在日志中的(在本例中是2a569843-122a-4559-a245-60f5ab2b6c51)。所以,继续输入用户名和密码;然后你应该得到待办事项列表(见图 8-3 )。

img/340891_2_En_8_Fig3_HTML.jpg

图 8-3

http://localhost:80808/API/全部

如果您想尝试使用命令行,您可以在终端窗口中执行以下命令。

$ curl localhost:8080/api/toDos
{"timestamp":"2018-08-19T21:25:47.224+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/api/toDos"}

正如您在输出中看到的,您无权进入该端点。需要认证吧?您可以执行以下命令。

$ curl localhost:8080/api/toDos -u user:2a569843-122a-4559-a245-60f5ab2b6c51
{
  "_embedded" : {
    "toDos" :  {
      "description" : "Read a Book",
      "created" : "2018-08-17T07:42:44.136",
      "modified" : "2018-08-17T07:42:44.137",
      "completed" : true,
...
}

正如您现在看到的,您正在传递用户名和随机密码,并且您正在得到带有待办事项列表的响应。

可能你已经知道,每次你重启这个应用,安全自动配置生成另一个随机密码,这不是最佳的;也许只是为了发展。

覆盖简单安全性

随机密码在生产环境中不起作用。Spring Boot 安全性允许您以多种方式覆盖默认值。最简单的方法是通过添加下面的spring.security.*属性,用application.properties文件覆盖它。

spring.security.user.name=apress
spring.security.user.password=springboot2
spring.security.user.roles=ADMIN,USER

如果再次运行该应用,用户名为apress,密码为springboot2(与命令行中相同)。还要注意,在日志中,随机密码不再打印。

另一种方法是以编程方式提供身份验证。创建一个从WebSecurityConfigureAdapter扩展而来的ToDoSecurityConfig类。看看清单 [8-4 。

package com.apress.todo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(
      AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                      .passwordEncoder(passwordEncoder())
                      .withUser("apress")
                      .password(passwordEncoder().encode("springboot2"))
                      .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Listing 8-4com.apress.todo.config.ToDoSecurityConfig.java

清单 8-4 显示了以编程方式构建安全性的必要配置,在这种情况下,只有一个用户(当然,您可以添加更多用户)。我们来分析一下代码。

  • WebSecurityConfigurerAdapter。扩展该类是重写安全性的一种方式,因为它允许您重写您真正需要的方法。在这种情况下,代码覆盖了configure(AuthenticationManagerBuilder)签名。

  • AuthenticationManagerBuilder。这个类创建了一个AuthenticationManager,允许您轻松构建内存、LDAP、JDBC 认证、UserDetailsService并添加AutheticationProvider。在本例中,您正在构建一个内存认证。有必要添加一个PasswordEncoder和一个新的更安全的方法来使用和加密/解密密码。

  • BCryptPasswordEncoder。在这段代码中,您使用了使用 BCrypt 强散列函数的BCryptPasswordEncoder(返回一个PasswordEncoder实现)。您也可以使用Pbkdf2PasswordEncoder(使用 PBKDF2,具有可配置的迭代次数和一个随机的 8 字节随机 salt 值),或者SCryptPasswordEncoder(使用 SCrypt 散列函数)。更好的是使用DelegatingPasswordEncoder,支持密码升级。

在运行应用之前,注释掉添加到application.properties文件中的spring.security.*属性。如果你运行这个应用,它应该像预期的那样工作。您需要提供用户名apress和密码springboot2

覆盖默认登录页面

Spring Security 允许您以几种方式覆盖默认登录页面。一种方法是配置HttpSecurityHttpSecurity类允许您为特定的 HTTP 请求配置基于 web 的安全性。默认情况下,它适用于所有请求,但是可以使用requestMatcher(RequestMatcher)或类似的方法进行限制。

让我们来看看对ToDoSecurityConfig类的修改(参见清单 8-5 )。

package com.apress.todo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
                .withUser("apress")
                .password(passwordEncoder().encode("springboot2"))
                .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().fullyAuthenticated()
                .and()
                .httpBasic();

    }
}

Listing 8-5com.apress.todo.config.ToDoSecurityConfig.java – v2

清单 8-5 显示了ToDoSecurityConfig类的版本 2。如果你运行应用并进入浏览器(http://localhost:8080/api/toDos),你现在会得到一个基本认证的弹出窗口(见图 8-4 )。

img/340891_2_En_8_Fig4_HTML.jpg

图 8-4

Http://localhost:8080/API/toDos—Http 基本认证

您可以使用您已经知道的用户名和密码,并且您应该得到 ToDo 列表。命令行也是如此。你需要认证

$ curl localhost:8080/api/toDos -u apress:springboot2

自定义登录页面

通常在应用中,你不会看到这样的页面;通常情况下,有一个非常漂亮和设计良好的登录页面,对不对?Spring Security 允许您创建和定制您的登录页面。

让我们准备带有登录页面的 ToDo 应用。首先,我们将添加一些 CSS 和众所周知的 jQuery 库。如今,在 Spring Boot 的应用中,我们可以使用 WebJars 依赖。这种新方式避免了手动下载文件;相反,你可以把它们作为资源。Spring Boot web 自动配置为他们创建了必要的访问权限。

如果您使用的是 Maven,打开pom.xml并添加以下依赖项。

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.3.7</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.2.1</version>
</dependency>

如果您正在使用 Gradle,打开您的build.gradle文件并添加以下依赖项。

compile ('org.webjars:bootstrap:3.3.7')
compile ('org.webjars:jquery:3.2.1')

接下来,让我们创建登录页面,它具有.mustache扩展名(login.mustache)。必须在src/main/resources/templates文件夹中创建它(参见清单 8-6 )。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDo's API Login Page</title>
    <link href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="css/signin.css" rel="stylesheet">
  </head>

  <body>

    <div class="container">
      <form class="form-signin" action="/login" method="POST">
        <h2 class="form-signin-heading">Please sign in</h2>

        <label for="username" class="sr-only">Username</label>
        <input type="text"     name="username" class="form-control" placeholder="Username" required autofocus>

        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" name="password" class="form-control" placeholder="Password" required>

        <button class="btn btn-lg btn-primary btn-block" id="login" type="submit">Sign in</button>
        <input type="hidden" name="_csrf" value="{{_csrf.token}}" />
      </form>
    </div>
  </body>
</html>

Listing 8-6src/main/resources/templates/login.mustache

清单 8-6 显示了 HTML 登录页面。本页面使用 CSS 从 Bootstrap ( https://getbootstrap.com )通过 WebJars ( www.webjars.org )依赖。这些文件作为文件资源从这些 jar 中获取。HTML-FORM 正在使用用户名密码作为名称(这是 Spring Security 必须的)。我们需要包含 CSRF 令牌以避免任何攻击。小胡子引擎为此提供了{{_csrf.token}}值。Spring Security 使用同步器令牌模式来避免请求中的任何攻击。稍后,我们将看到如何获得这个值。

接下来,让我们创建一个索引页面,让您可以看到主页并注销。在src/main/resources/templates文件夹中创建index.mustache页面(参见清单 8-7 )。

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDo's API</title>
    <link href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="webjars/jquery/3.2.1/jquery.min.js"></script>

</head>

<body>
<div class="container">
    <div class="header clearfix">
        <nav>
            <a href="#" id="logoutLink">Logout</a>
        </nav>
    </div>

    <div class="jumbotron">
        <h1>ToDo's Rest API</h1>
        <p class="lead">Welcome to the ToDo App. A Spring Boot application!</p>
    </div>
</div>

<form id="logout" action="/logout" method="POST">

    <input type="hidden" name="_csrf" value="{{_csrf.token}}" />

</form>

<script>
    $(function(){
        $('#logoutLink').click(function(){
            $('#logout').submit();
        });
    });
</script>
</body>
</html>

Listing 8-7src/main/resources/templates/index.mustache

清单 8-7 显示了索引页面。我们仍然使用 Bootstrap 和 jQuery 资源,以及最重要的部分{{_csrf.token}},用于注销。

接下来,我们先从配置说起。首先,需要修改ToDoSecurityConfig类(参见清单 8-8 )。

package com.apress.todo.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import 

org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
                .withUser("apress")
                .password(passwordEncoder().encode("springboot2"))
                .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers(
                        PathRequest
                              .toStaticResources()
                              .atCommonLocations()).permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                    .logoutRequestMatcher(
                        new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/login");
    }

}

Listing 8-8com.apress.todo.config.ToDoSecurityConfig.java – v3

清单 8-8 显示了ToDoSecurityConfig类的版本 3。新的修改显示了如何配置HttpSecurity。首先,它添加了指向公共位置的requestMatchers,比如静态资源(static/*)。这是 CSS、JS 或任何其他简单 HTML 可以存在的地方,并且不需要任何安全性。那么它用的是anyRequest,应该是fullyAuthenticated。这意味着/api/*将会。然后,它使用formLoginloginPage("/login")指定它是查找登录页面的端点。接下来,声明注销及其端点("/logout");如果注销成功,它将重定向到"/login"端点/页面。

现在需要告诉 Spring MVC 如何定位登录页面。创建ToDoWebConfig类(参见清单 8-9 )。

package com.apress.todo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ToDoWebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}

Listing 8-9com.apress.todo.config.ToDoWebConfig.java

清单 8-9 展示了在 Spring MVC 中配置 web 控制器的不同方式。您仍然可以使用用@Controller注释的类,并为登录页面创建映射;但这是 JavaConfig 的方式。

这里,该类实现了WebMvcConfigure接口。它实现了addViewControllers方法,并通过告诉控制器视图的位置来注册/login端点。这将定位到templates/login.mustache页面。

最后,有必要通过添加以下属性来更新application.properties文件。

spring.mustache.expose-request-attributes=true

还记得{{_csrf.token}}吗?这就是它获取值的方式——通过添加spring.mustache.expose-request-attributes属性。

现在,您可以运行应用了。如果你去http://localhost:8080,你会得到类似于图 8-5 的东西。

img/340891_2_En_8_Fig5_HTML.jpg

图 8-5

http://localhost:8080/log in

您将获得自定义登录页面。完美!!现在你可以输入凭证,它返回索引页面(见图 8-6 )。

img/340891_2_En_8_Fig6_HTML.jpg

图 8-6

登录后 http://localhost:8080

一旦你有了主页,你就可以访问http://localhost:8080/api/toDos。您应该完全通过了身份验证,并且可以返回到待办事项列表。您可以返回到主页并按下 Logout 链接,它会再次将您重定向到/login端点。

现在,如果您尝试在终端窗口中执行以下命令行,会发生什么情况?

$ curl localhost:8080/api/toDos -u apress:springboot2

它不会返回任何东西。这是一个空行。如果您使用-i标志,它会告诉您正在被重定向到http://localhost:8080/login。但是没有办法从命令行交互,对吧?那么我们能做些什么来解决这个问题呢?事实上,有些客户从来不使用 web 界面。大多数客户端都是应用,并且在编程上需要使用 REST API,但是使用这个解决方案,没有办法进行身份验证来与表单交互。

打开ToDoSecurityConfig类并修改configure(HttpSecurity)方法。它应该类似于下面的代码片段。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                    .logoutRequestMatcher(
                          new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/login")
                .and()
                            .httpBasic();
    }

该方法的最后两行添加了httpBasic调用,它允许客户端(比如 cURL)使用基本的认证机制。您可以重新运行 ToDo 应用,并查看现在执行命令行的工作。

对 JDBC 使用安全性

想象一下,您的公司已经有了一个员工数据库,您希望重用它来对 ToDo 应用进行身份验证和授权。集成这样的东西很好,对吗?

Spring Security 允许您将 AuthenticationManager 与内存、LDAP 和 JDBC 机制结合使用。在本节中,我们将修改 ToDo 应用,使其与 JDBC 一起运行。

JDBC 安全的目录应用

在本节中,您将创建一个新的应用——一个包含所有人员的目录应用。目录应用与 ToDo 应用集成在一起,以进行身份验证和授权。因此,如果客户机需要添加一个新的 ToDo,它需要用一个USER角色进行认证。

让我们开始吧。从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.directory

  • 神器:directory

  • 名称:directory

  • 包名:com.apress.directory

  • 依赖关系:WebSecurityLombokJPAREST RepositoriesH2MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 8-7 )。

img/340891_2_En_8_Fig7_HTML.jpg

图 8-7

Spring 初始化 zr

如您所见,依赖项与其他项目非常相似。我们将利用 Spring Data、安全性和 REST 的力量。让我们从添加一个保存个人信息的新类开始。创建Person类(参见清单 8-10 )。

package com.apress.directory.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Data
@Entity
public class Person {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @Column(unique = true)
    private String email;
    private String name;
    private String password;
    private String role = "USER";
    private boolean enabled = true;
    private LocalDate birthday;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;

    public Person() {
    }

    public Person(String email, String name, String password, String birthday) {
        this.email = email;
        this.name = name;
        this.password = password;
        this.birthday = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyy-MM-dd"));

    }

    public Person(String email, String name, String password, LocalDate birthday) {
        this.email = email;
        this.name = name;
        this.password = password;
        this.birthday = birthday;
    }

    public Person(String email, String name, String password, String birthday, String role, boolean enabled) {
        this(email, name, password, birthday);
        this.role = role;
        this.enabled = enabled;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-10com.apress.directory.domain.Person.java

清单 8-10 显示的是Person类;很简单。它保存了足够的关于一个人的信息。接下来,让我们创建存储库——PersonRepository接口(参见清单 8-11 )。

package com.apress.directory.repository;

import com.apress.directory.domain.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface PersonRepository extends CrudRepository<Person,String> {
    public Person findByEmailIgnoreCase(@Param("email") String email);
}

Listing 8-11com.apress.directory.repository.PersonRepository.java

清单 8-11 显示PersonRepository界面;但是和其他的有什么不同呢?它声明了一个以电子邮件为参数的查询方法 findByEmailIgnoreCase(由@Param注释)。该语法告诉 Spring Data REST 它需要实现这些方法并相应地创建 SQL 语句(这是基于名称和域类中的字段,在本例中是email字段)。

注意

如果您想了解更多关于如何定义自己的查询方法的信息,请查看位于 https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods 的 Spring Data JPA 参考。

接下来,创建从WebSecurityConfigurerAdapter类扩展而来的DirectorySecurityConfig类。记住,通过扩展这个类,我们可以定制为这个应用设置 Spring Security 性的方式(参见清单 8-12 )。

package com.apress.directory.config;

import com.apress.directory.repository.PersonRepository;
import com.apress.directory.security.DirectoryUserDetailsService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class DirectorySecurityConfig extends WebSecurityConfigurerAdapter {

    private PersonRepository personRepository;

    public DirectorySecurityConfig(PersonRepository personRepository){
        this.personRepository = personRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").hasRole("ADMIN")
                .and()
                .httpBasic();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(
            new DirectoryUserDetailsService(this.personRepository));
    }

}

Listing 8-12com.apress.directory.config.DirectorySecurityConfig.java

清单 8-12 显示了DirectorySecurityConfig类。该类通过只允许具有ADMIN角色的用户使用基本身份验证访问任何端点(/**)来配置HttpSecurity

与其他安全配置还有什么不同?你是对的!AuthenticationManager正在配置一个UserDetailsService实现。这是使用任何其他第三方安全应用并将它们与 Spring Security 集成的关键。

如您所见,userDetailsService方法使用了DirectoryUserDetailsService类。让我们创建它(见清单 8-13 )。

package com.apress.directory.security;

import com.apress.directory.domain.Person;
import com.apress.directory.repository.PersonRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

public class DirectoryUserDetailsService implements UserDetailsService {

    private PersonRepository repo;

    public DirectoryUserDetailsService(PersonRepository repo) {
        this.repo = repo;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            final Person person = this.repo.findByEmailIgnoreCase(username);

            if (person != null) {
                PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
                String password = encoder.encode(person.getPassword());

                return User.withUsername(person.getEmail()).accountLocked(!person.isEnabled()).password(password).roles(person.getRole()).build();
            }
        }catch(Exception ex){
            ex.printStackTrace();
        }

        throw new UsernameNotFoundException(username);
    }
}

Listing 8-13com.apress.directory.security.DirectoryUserDetailsService.java

清单 8-13 显示了DirectoryUserDetailsService类。这个类实现了UserDetailsService接口,需要实现loadUserByUserName并返回一个UserDetails实例。在这个实现中,代码显示了如何使用PersonRepository。在这种情况下,它使用findByEmailIgnoreCase;因此,如果在用户想要访问/**(任何端点)时发现一个人有提供的电子邮件,它会通过创建一个UserDetails实例,将电子邮件与提供的密码、角色以及帐户是否锁定进行比较。

太神奇了!这个应用使用 JDBC 作为认证机制。同样,您可以插入任何其他可以实现UserDetailService的安全系统/应用,并返回一个UserDetails实例;就这样。

接下来,让我们快速查看一下application.properties文件,看看它的属性。

# Server
server.port=${port:8181}

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2
spring.h2.console.enabled=true

唯一不同的是它有server.port属性,上面写着:如果你提供变量端口(要么命令行,要么环境)我就用它;如果没有,我就用端口 8181 。就是这个:。这是 SpEL (Spring 表达式语言)的一部分。

在运行目录应用之前,让我们添加一些数据。在src/main/resources文件夹中创建data.sql文件。

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('dc952d19ccfc4164b5eb0338d14a6619','Mark','mark@example.com','secret','USER',true,'1960-03-29','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('02288a3b194e49ceb1803f27be5df457','Matt','matt@example.com','secret','USER',true,'1980-07-03','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('4fe22e358d0e4e38b680eab91787f041','Mike','mike@example.com','secret','ADMIN',true,'19820-08-05','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('84e6c4776dcc42369510c2692f129644','Dan','dan@example.com','secret','ADMIN',false,'1976-10-11','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('03a0c396acee4f6cb52e3964c0274495','Administrator','admin@example.com','admin','ADMIN',true,'1978-12-22','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

现在,我们准备将这个应用用作身份验证和授权机制。运行目录应用。此应用从 8181 端口启动。您可以使用浏览器和/或cURL命令对其进行测试。

$ curl localhost:8181/persons/search/findByEmailIgnoreCase?email=mark@example.com  -u admin@example.com:admin
{
  "email" : "mark@example.com",
  "name" : "Mark",
  "password" : "secret",
  "role" : "USER",
  "enabled" : true,
  "birthday" : "1960-03-29",
  "created" : "2018-08-17T07:42:44.136",
  "modified" : "2018-08-17T07:42:44.137",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8181/persons/dc952d19ccfc4164b5eb0338d14a6619"
    },
    "person" : {
      "href" : "http://localhost:8181/persons/dc952d19ccfc4164b5eb0338d14a6619"
    }
  }
}

在这个命令中,您通过提供一个具有ADMIN角色的人的用户名/密码来获得用户 Mark 在这种情况下,使用-uadmin@example.com:admin参数。

太好了。您正在使用 JDBC 通过使用 Spring Data REST 和 Spring Security 来查找用户!你可以让这个项目运行。

在待办事项应用中使用目录应用

是时候把这个目录 app 和 ToDo app 整合起来了。这很容易。

打开您的 ToDo 应用,让我们创建一个Person类。是的,我们需要一个Person类来保存足够的信息用于认证和授权。不需要有出生日期或任何其他信息(见清单 8-14 )。

package com.apress.todo.directory;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {

    private String email;
    private String password;
    private String role;
    private boolean enabled;
}

Listing 8-14com.apress.todo.directory.Person.java

清单 8-14 显示了Person类。这个类只有身份验证和授权过程所必需的字段。值得一提的是,调用 Directory app 会返回一个更完整的 JSON 对象。它必须匹配才能进行反序列化(使用 Jackson 库从 JSON 到 object),但是因为不需要额外的信息,所以这个类使用了@JasonIgnoreProperties(ignoreUnknown=true)注释来帮助匹配所需的字段。我认为这是一个很好的解耦类的方法。

注意

Java 中的一些序列化工具要求在同一个包中有相同的类并实现java.io.Serializable,这使得开发人员和客户更难管理和扩展。

接下来,创建保存目录应用信息的ToDoProperties类,如Uri(什么是地址和基 Uri)、UsernamePassword拥有ADMIN角色并有权访问 REST API 的人(参见清单 8-15 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "todo.authentication")
public class ToDoProperties {

    private String findByEmailUri;
    private String username;
    private String password;

}

Listing 8-15com.apress.todo.config.ToDoProperties.java

清单 8-15 显示了ToDoProperties类;注意前缀是todo.authentication.*。接下来,修改ToDoSecurityConfig类。你可以注释整个类并复制清单 8-16 中的代码。

package com.apress.todo.config;

import com.apress.todo.directory.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@EnableConfigurationProperties(ToDoProperties.class)
@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    private final Logger log = LoggerFactory.getLogger(ToDoSecurityConfig.class);

    //Use this to connect to the Directory App
    private RestTemplate restTemplate;
    private ToDoProperties toDoProperties;
    private UriComponentsBuilder builder;

    public ToDoSecurityConfig(RestTemplateBuilder restTemplateBuilder, ToDoProperties toDoProperties){
        this.toDoProperties = toDoProperties;
        this.restTemplate = restTemplateBuilder.basicAuthorization(toDoProperties.getUsername(),toDoProperties.getPassword()).build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new UserDetailsService(){

            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

                try {
                    builder = UriComponentsBuilder
                  .fromUriString(toDoProperties.getFindByEmailUri())
                            .queryParam("email", username);

                    log.info("Querying: " + builder.toUriString());

                    ResponseEntity<Resource<Person>> responseEntity =
                    restTemplate.exchange(
                                  RequestEntity.get(URI.create(builder.toUriString()))
                                          .accept(MediaTypes.HAL_JSON)
                                          .build()
                            , new ParameterizedTypeReference<Resource<Person>>() {
                            });

                    if (responseEntity.getStatusCode() == HttpStatus.OK) {

                        Resource<Person> resource = responseEntity.getBody();
                        Person person = resource.getContent();

                        PasswordEncoder encoder =
                 PasswordEncoderFactories.createDelegatingPasswordEncoder();
                        String password = encoder.encode(person.getPassword());

                        return User
                  .withUsername(person.getEmail())
                  .password(password)
                  .accountLocked(!person.isEnabled())
                  .roles(person.getRole()).build();
                    }

                }catch(Exception ex) {
                    ex.printStackTrace();
                }
                throw new UsernameNotFoundException(username);
            }
        });

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .antMatchers("/","/api/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .and()

                .httpBasic();
    }

}

Listing 8-16com.apress.todo.config.ToDoSecurityConfig.java

清单 8-16 展示了新的ToDoSecurityConfig类。我们来分析一下。

  • WebSecurityConfigurerAdapter。此类覆盖了我们为应用自定义安全性所需的内容;但你已经知道了,对吧?

  • RestTemplate。这个助手类对目录应用端点,特别是/persons/search/findByEmailIgnoreCase Uri,进行 REST 调用。

  • UriComponentsBuilder。记住/persons/search/findByEmailIgnoreCase端点需要一个参数(email);那是由loadUserByUsername方法(username)提供的。

  • AuthenticationBuilder。认证提供了userDetailsService。在这段代码中,有一个UserDetailsService的匿名实现和loadUserByUsername方法的实现。这就是使用RestTemplate调用目录应用和端点的地方。

  • ResponseEntity。因为目录 app 响应是HAL+JSON,所以需要使用一个ResponseEntity来管理来自协议的所有资源。如果有HttpStatus.OK,很容易获得内容作为Person实例,并用它创建UserDetails

  • antMatchers。这个类像以前一样配置HttpSecurity,但是这一次它包含了一个antMatchers方法,该方法公开了由具有USER角色的有效人员访问的端点。

我们重用了目录应用中的相同技术。AuthenticationManager被配置为通过使用RestTemplate调用目录服务来提供UserDetails实例。目录应用用一个HAL+JSON协议响应,这就是为什么需要使用ResponseEntity来获得作为资源的人。

接下来,在application.properties文件中添加下面的todo.authentication.*属性。

# ToDo - Directory integration
todo.authentication.find-by-email-uri=http://localhost:8181/persons/search/findByEmailIgnoreCase
todo.authentication.username=admin@example.com
todo.authentication.password=admin

有必要指定搜索电子邮件端点的完整 Uri,以及具有ADMIN角色的人。

现在你已经准备好使用 ToDo 应用了。您可以使用浏览器或命令行。确定目录应用已启动并正在运行。运行端口 8080 中运行的 ToDo 应用。

您可以在终端窗口中执行以下命令。

$ curl localhost:8080/api/toDos -u mark@example.com:secret
{
  "_embedded" : {
    "toDos" :  {
      "description" : "Read a Book",
      "created" : "2018-08-17T07:42:44.136",
      "modified" : "2018-08-17T07:42:44.137",
      "completed" : true,

...
...

"profile" : {
      "href" : "http://localhost:8080/api/profile/toDos"
    }
  }
}

现在,您使用 Mark 进行身份验证和授权,他拥有USER角色。恭喜你!!您将自己的 JDBC 服务与 ToDo 应用集成在一起。

WebFlux 安全性

为了给 WebFlux 应用增加安全性,什么都不会改变。您需要添加spring-boot-starter-security依赖项,Spring Boot 会通过它的自动配置来完成剩下的工作。如果你想像我们之前那样定制,你唯一需要做的就是用ReactiveUserDetailsService(而不是UserDetailsService)或者用ReactiveAuthenticationManager(而不是AuthenticationManager)。请记住,现在您正在处理单一和通量反应流类型。

带有 OAuth2 的待办事项应用

有了 Spring Boot 和 Spring Security,OAuth2 比以往任何时候都容易。在本章的这一节,我们将使用 OAuth2 直接进入 ToDo 应用。我假设你知道 OAuth2,以及使用它作为与第三方提供商——如谷歌、脸书和 GitHub——直接进入你的应用的认证机制的所有好处。

让我们开始吧。从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-oauth2

  • 名称:todo-oauth2

  • 包名:com.apress.todo

  • 依赖关系:WebSecurityLombokJPAREST RepositoriesH2MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以按下生成项目按钮;这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 [8-8 )。

img/340891_2_En_8_Fig8_HTML.jpg

图 8-8

Spring 初始化 zr

如果您使用的是 Maven,将以下依赖项添加到您的pom.xml文件中。

<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

如果您正在使用 Gradle,请将以下依赖项添加到您的build.gradle中:

compile('org.springframework.security:spring-security-oauth2-client')
compile('org.springframework.security:spring-security-oauth2-jose')

可以想象,当 Spring Boot 看到spring-security-oauth2-client时,它会自动配置所有必要的 beans 来使用 OAuth2 安全性。重要的是要提到对包含 Spring Security 对 JOSE (JavaScript 对象签名和加密)框架支持的spring-security-oauth2-jose的需求。JOSE 框架旨在提供一种在各方之间安全转移索赔的方法。它是由一系列规范构建而成的:JSON Web Token (JWT)、JSON Web Signature (JWS)、JSON Web Encryption (JWE)和 JSON Web Key (JWK)。

接下来,您可以重用ToDo类和ToDoRepository接口(参见清单 8-17 和 8-18 )。

package com.apress.todo.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-17com.apress.todo.domain.ToDo.java

如你所见,一切都没变。它保持不变。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> { }

Listing 8-18com.apress.todo.repository.ToDoRepository.java

这个接口也是一样——没什么变化。我们来回顾一下application.properties

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2-Console: http://localhost:8080/h2-console
# jdbc:h2:mem:testdb
spring.h2.console.enabled=true

什么都没变。嗯,我们将很快添加更多的属性。

现在重要的部分来了。您将使用 GitHub 对 ToDo 应用进行 OAuth2 身份验证。

在 GitHub 中创建 ToDo 应用

我假设您可能已经有了一个 GitHub 帐户;如果没有,您可以在 https://github.com 非常轻松地打开一个新的。您可以登录您的帐户,然后打开 https://github.com/settings/applications/new 。这是创建应用的地方。您可以使用下列值。

这对授权回调 URL 很重要,因为这是 Spring Security 的OAuth2LoginAuthenticationFilter期望与这个端点模式/login/oauth2/code/*一起工作的方式;当然,可以使用redirect-uri-template属性进行定制(见图 8-9 )。

img/340891_2_En_8_Fig9_HTML.jpg

图 8-9

GitHub 新 app: https://github.com/settings/applications/new

您可以单击“注册应用”按钮。之后,GitHub 会在您的应用中创建您需要的密钥(参见图 8-10 )。

img/340891_2_En_8_Fig10_HTML.jpg

图 8-10

客户端 ID 和客户端密钥

一旦你有了这个,复制客户端 id 和客户端密钥,并把它们和spring.security.oauth2.client.registration.*密钥一起添加到application.properties中。

# OAuth2
spring.security.oauth2.client.registration.todo.client-id=ac5b347117eb11705b70
spring.security.oauth2.client.registration.todo.client-secret=44abe272a15834a5390423e53b58f57c35647a98
spring.security.oauth2.client.registration.todo.client-name=ToDo App with GitHub Authentication
spring.security.oauth2.client.registration.todo.provider=github
spring.security.oauth2.client.registration.todo.scope=user
spring.security.oauth2.client.registration.todo.redirect-uri-template=http://localhost:8080/login/oauth2/code/github

spring.security.oauth2.client.registration接受包含必要键的地图,如client-idclient-secret

就是这样!!你不需要别的东西。您现在可以运行您的应用了。打开浏览器,指向 http://localhost:8080。你会得到一个重定向到 GitHub 的链接(见图 8-11 )。

img/340891_2_En_8_Fig11_HTML.jpg

图 8-11

http://localhost:8080

您可以点击链接,这将引导您完成登录过程,但使用 GitHub 认证机制(参见图 8-12 )。

img/340891_2_En_8_Fig12_HTML.jpg

图 8-12

GitHub 认证

您现在可以使用您的凭据登录。接下来,你会被重定向到另一个页面,在那里你需要授予 todo-app 使用联系信息的权限(参见图 8-13 )。

img/340891_2_En_8_Fig13_HTML.jpg

图 8-13

GitHub 授权流程

然后,您可以单击授权按钮,使用 ToDo REST API 返回到您的应用(参见图 8-14 )。

img/340891_2_En_8_Fig14_HTML.jpg

图 8-14

GitHub 授权流程后

恭喜你!!现在您知道使用 Spring Boot 和 Spring Security 将 OAuth2 与不同的提供商集成是多么容易了。

注意

你可以在 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 或者我的个人资源库 https://github.com/felipeg48/pro-spring-boot-2nd 找到这部分的解决方法。

摘要

在这一章中,您学习了使用 Spring Boot 进行安全保护的不同方法。您了解了通过添加spring-boot-security-starter依赖项来保护应用是多么容易。

您还了解了自定义和覆盖 Spring Boot 为您提供的 Spring Security 默认设置是很容易的。您可以使用spring.security.*属性,也可以使用WebSecurityConfigurerAdapter类对其进行定制。

您了解了如何使用 JDBC 并连接两个应用,其中一个作为身份验证和授权的安全机构。

最后,您了解了将 OAuth2 与第三方认证和授权提供商(如脸书、谷歌、GitHub 等)一起使用是多么容易。

在下一章中,我们开始与消息传递代理一起工作。

九、Spring Boot 的通信

这一章是关于信息传递的。它通过示例解释了如何使用 ActiveMQ 实现 JMS (Java 消息服务),使用 RabbitMQ 实现 AMQP(高级消息队列协议),使用 Redis 实现发布/订阅,使用 WebSockets 实现 STOMP(简单或面向流文本的消息协议)和 Spring Boot。

什么是消息传递?

消息传递是一种在一个或多个实体之间进行通信的方式,它无处不在。

自从计算机发明以来,各种形式的计算机信息就一直存在。它被定义为硬件和/或软件组件或应用之间的一种通信方法。总是有一个发送者和一个或多个接收者。消息传递可以是同步和异步的、发布/订阅和对等的、RPC 的、基于企业的、消息代理、ESB(企业服务总线)、MOM(面向消息的中间件)等等。

消息传递支持必须松散耦合的分布式通信,这意味着无论发送方如何发布消息或发布什么消息,接收方都会在不通知发送方的情况下使用消息。

当然,关于消息传递,我们可以说很多——从旧的技巧和技术到新的协议和消息传递模式,但本章的目的是用例子来说明 Spring Boot 是如何进行消息传递的。

记住这一点,让我们开始使用现有的一些技术和消息代理来创建示例。

与 Spring Boot 的 JMS

让我们从使用 JMS 开始。这是一项老技术,仍被有遗留应用的公司使用。JMS 是由 Sun Microsystems 创建的,它提供了一种同步和异步发送消息的方法;它定义了需要由消息代理实现的接口,比如 WebLogic、IBM MQ、ActiveMQ、HornetQ 等等。

JMS 是一种只支持 Java 的技术,因此有人试图创建消息桥来将 JMS 与其他编程语言结合起来;尽管如此,混合不同的技术还是很困难或者非常昂贵。我知道您认为这是不正确的,因为您可以使用 Spring integration、Google Protobuffers、Apache Thrift 和其他技术来集成 JMS,但是这仍然需要大量的工作,因为您需要了解和维护所有这些技术的代码。

带有 JMS 的待办事项应用

让我们从使用 JMS 和 Spring Boot 创建 ToDo 应用开始。想法是将 ToDo 发送到 JMS 代理,并接收和保存它们。

Spring Boot 团队有几个可用的 JMS 初学者 poms 在这种情况下,您使用 ActiveMQ,它是 Apache 基金会的一个开源异步消息传递代理( http://activemq.apache.org )。其中一个主要优势是,您可以使用内存中的代理或远程代理。(如果喜欢可以下载安装;本节中的代码使用内存中的代理,但是我将告诉您如何配置远程代理。

可以打开自己喜欢的浏览器,指向已知的 Spring Initializr(https://start.spring.io);将下列值添加到下列字段中。

  • 组:com.apress.todo

  • 神器:todo-jms

  • 名称:todo-jms

  • 包名:com.apress.todo

  • 依赖关系:JMS (ActiveMQ), Web, Lombok, JPA, REST Repositories, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以点击生成项目按钮来下载一个 ZIP 文件。将其解压缩并在您最喜欢的 IDE 中导入项目(参见图 9-1 )。

img/340891_2_En_9_Fig1_HTML.jpg

图 9-1

Spring 初始化 zr

从依赖关系中可以看出,您重用了前面章节中的 JPA 和 REST Repositories 代码。代替使用文本消息(一种测试消息传递的常用方法),您使用一个ToDo实例,它被转换为 JSON 格式。为此,您需要手动将下一个依赖项添加到您的pom.xmlbuild.gradle中。

如果您使用的是 Maven,将下面的依赖项添加到您的pom.xml文件中。

<dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
</dependency>

如果您使用的是 Gradle,将下面的依赖项添加到您的build.gradle文件中。

compile("com.fasterxml.jackson.core:jackson-databind")

这个依赖项提供了使用 JSON 序列化ToDo实体所需的所有 Jackson jars。

在接下来的部分中,我将向您展示重要的文件,以及 JMS 是如何在 ToDo 应用中使用的。该示例使用简单的点对点模式,其中有一个生产者、一个队列和一个消费者。稍后我将展示如何配置它来使用一个带有一个生产者、一个主题和多个消费者发布者-订阅者模式。

ToDo 生产者

让我们从介绍向 ActiveMQ 代理发送 ToDo 的生产者开始。这个生产者可以在自己的项目上;可以脱离 app 但是出于演示的目的,在 ToDo 应用中,您将生成器放在相同的代码库中。

创建ToDoProducer类。这个类将一个 ToDo 发送到一个 JMS 队列中(参见清单 9-1 )。

package com.apress.todo.jms;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;

@Component

public class ToDoProducer {

    private static final Logger log = LoggerFactory.getLogger(ToDoProducer.class);

    private JmsTemplate jmsTemplate;

    public ToDoProducer(JmsTemplate jmsTemplate){
        this.jmsTemplate = jmsTemplate;
    }

    public void sendTo(String destination, ToDo toDo) {
        this.jmsTemplate.convertAndSend(destination, toDo);
        log.info("Producer> Message Sent");
    }
}

Listing 9-1com.apress.todo.jms.ToDoProducer.java

清单 9-1 显示了生产者类。这个类使用@Component进行标记,因此它在 Spring 应用上下文中被注册为一个 Spring bean。使用了JmsTemplate类,它非常类似于其他的*Template类,这些类包装了所有正在使用的技术的样板文件。通过类构造函数注入JmsTemplate实例,并使用convertAndSend方法发送消息。您正在发送一个 ToDo 对象(JSON 字符串)。该模板具有将其序列化并发送到 ActiveMQ 队列的机制。

ToDo 消费者

接下来,让我们创建消费者类,它监听来自 ActiveMQ 队列的任何传入消息(参见清单 9-2 )。

package com.apress.todo.jms;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.validation.Valid;

@Component
public class ToDoConsumer {

    private Logger log = LoggerFactory.getLogger(ToDoConsumer.class);

    private ToDoRepository repository;

    public ToDoConsumer(ToDoRepository repository){
        this.repository = repository;
    }

    @JmsListener(destination = "${todo.jms.destination}",containerFactory = "jmsFactory")
    public void processToDo(@Valid ToDo todo){
        log.info("Consumer> " + todo);
        log.info("ToDo created> " + this.repository.save(todo));
    }
}

Listing 9-2com.apress.todo.jms.ToDoConsumer.java

清单 9-2 显示了消费者。在这个类中,您使用的是ToDoRepository,它在这里监听来自 ActiveMQ 队列的任何消息。确保您使用的是使该方法处理来自队列的任何传入消息的@JmsListener注释;在这种情况下,一个有效的 ToDo(@Valid注释可以用来验证域模型的任何字段)。@JmsListener注释有两个属性。destination属性强调要连接的队列/主题的名称(目的地属性评估todo.jms.destination属性,该属性将在下一节中创建/使用)。属性是作为配置的一部分创建的。

配置待办事项应用

现在,是时候配置 ToDo 应用来发送和接收 ToDo 了。清单 9-1 和清单 9-2 分别显示了生产者和消费者类。在这两个类中都使用了一个ToDo实例,这意味着有必要进行序列化。大多数使用序列化的 Java 框架要求您的类从java.io.Serializable开始实现。将这些类转换成字节是一种简单的方法,但是这种方法已经争论了很多年,因为实现Serializable降低了在发布使用后修改类实现的灵活性。

Spring 框架提供了另一种不需要从Serializable开始实现序列化的方法——通过一个MessageConverter接口。这个接口提供了toMessagefromMessage方法,您可以在其中插入任何适合对象转换的技术。

让我们为生产者和消费者创建一个使用ToDo实例的配置(参见清单 9-3 )。

package com.apress.todo.config;

import com.apress.todo.error.ToDoErrorHandler;
import com.apress.todo.validator.ToDoValidator;
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.JmsListenerConfigurer;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerEndpointRegistrar;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;

import javax.jms.ConnectionFactory;

@Configuration

public class ToDoConfig {

    @Bean
    public MessageConverter jacksonJmsMessageConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("_class_");
        return converter;
    }

    @Bean
    public JmsListenerContainerFactory<?> jmsFactory(ConnectionFactory connectionFactory,
                                                     DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setErrorHandler(new ToDoErrorHandler());
        configurer.configure(factory, connectionFactory);
        return factory;
    }

    @Configuration
    static class MethodListenerConfig implements JmsListenerConfigurer{

        @Override
        public void configureJmsListeners (JmsListenerEndpointRegistrar jmsListenerEndpointRegistrar){
            jmsListenerEndpointRegistrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
        }

        @Bean
        public DefaultMessageHandlerMethodFactory myHandlerMethodFactory () {
            DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
            factory.setValidator(new ToDoValidator());
            return factory;
        }
    }

}

Listing 9-3com.apress.todo.config.ToDoConfig.java

清单 9-3 显示了应用使用的ToDoConfig类。我们来分析一下。

  • @Configuration。这是一个已知的注释,它标记了用于配置 SpringApplication 上下文的类。

  • MessageConverter。方法jacksonJmsMessageConverter返回 MessageConverter 接口。这个接口促进了toMessagefromMessage方法的实现,这有助于插入您想要使用的任何序列化/转换。在这种情况下,您通过使用MappingJackson2MessageConverter类实现来使用 JSON 转换器。这个类是 Spring 框架中的默认实现之一。它使用 Jackson 库,这些库使用映射器在 JSON 和对象之间进行转换。因为您使用的是ToDo实例,所以有必要指定一个目标类型(setTargetType),这意味着 JSON 对象被作为文本和一个 type-id 属性名(setTypeIdPropertyName)来处理,该属性名标识了生产者和消费者之间的属性。type-id 属性名必须始终与生产者和消费者相匹配。它可以是你需要的任何值(最好是你能识别的值,因为它用于设置要与 JSON 相互转换的类的名称(包括包));换句话说,com.apress.todo.domain.Todo类必须在生产者和消费者之间共享,以便映射器知道从哪里获取该类。

  • JmsListenerContainerFactoryjmsFactory方法返回JmsListenerContainerFactory。这个 bean 需要ConnectionFactoryDefaultJmsListenerContainerFactoryConfigurer(都是由 Spring 注入的),它创建了DefaultJmsListenerContainerFactory,后者设置了一个错误处理程序。通过设置containerFactory属性,这个 bean 被用在@JmsListener注释中。

  • JmsListenerConfigurer。在本课中,您将创建一个静态配置。MethodListenerConfig类实现了JmsListenerConfigurer接口。该接口要求您注册一个具有验证器配置的 bean(ToDoValidator类);在这种情况下,DefaultMessageHandlerMethodFactory比恩。

如果还不想验证,可以从jmsFactory bean 声明中移除MethodListenerConfig类和setErrorHandler调用;但是如果你想试验验证,那么你需要创建ToDoValidator类(参见清单 9-4 )。

package com.apress.todo.validator;

import com.apress.todo.domain.ToDo;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class ToDoValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(ToDo.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ToDo toDo = (ToDo)target;

        if (toDo == null) {
            errors.reject(null, "ToDo cannot be null");
        }else {
            if (toDo.getDescription() == null || toDo.getDescription().isEmpty())
                errors.rejectValue("description",null,"description cannot be null or empty");
        }
    }
}

Listing 9-4com.apress.todo.validator.ToDoValidator.java

清单 9-4 显示了为每个消息调用的验证器类,并验证description字段不为空。这个类实现了验证器接口,并实现了supportsvalidate方法。

这是ToDoErrorHandler代码。

package com.apress.todo.error;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ErrorHandler;

public class ToDoErrorHandler implements ErrorHandler {
    private static Logger log = LoggerFactory.getLogger(ToDoErrorHandler.class);

    @Override
    public void handleError(Throwable t) {
        log.warn("ToDo error...");
        log.error(t.getCause().getMessage());
    }
}

如您所见,这个类实现了ErrorHandler接口。

现在,让我们创建保存todo.jms.destination属性的ToDoProperties类,该属性指示要连接到哪个队列/主题(参见清单 9-5 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "todo.jms")
public class ToDoProperties {

    private String destination;

}

Listing 9-5com.apress.todo.config.ToDoProperties.java

清单 9-5 显示了ToDoProperties类。还记得在清单 9-2(ToDoConsumer类)中,processToDo方法被标注了@JmsListener注释,这暴露了destination属性。该属性通过评估您在该类中定义的SpEL(Spring Expression Language)${todo.jms.destination}表达式来获取其值。

您可以在application.properties文件中设置该属性。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop

# ToDo JMS

todo.jms.destination=toDoDestination

src/main/resources/application.properties

运行待办事项应用

接下来,让我们创建一个 config 类,它使用生产者向队列发送消息(参见清单 9-6 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.jms.ToDoProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ToDoSender {

    @Bean
    public CommandLineRunner sendToDos(@Value("${todo.jms.destination}") String destination, ToDoProducer producer){
        return args -> {
            producer.sendTo(destination,new ToDo("workout tomorrow morning!"));
        };
    }

}

Listing 9-6com.apress.todo.config.ToDoSender.java

清单 9-6 显示了使用ToDoProducer实例和目的地(来自todo.jms.destination属性)发送消息的配置类。

要运行这个应用,您可以使用您的 IDE(如果您导入了它)或者您可以使用 Maven 包装器。

./mvnw spring-boot:run

或者是格拉德包装。

./gradlew bootRun

您应该从日志中获得以下文本。

Producer> Message Sent
Consumer> ToDo(id=null, description=workout tomorrow morning!, created=null, modified=null, completed=false)
ToDo created> ToDo(id=8a808087645bd67001645bd6785b0000, description=workout tomorrow morning!, created=2018-07-02T10:32:19.546, modified=2018-07-02T10:32:19.547, completed=false)

您可以看一看http://localhost:8080/toDos并查看创建的 ToDo。

使用 JMS 发布/订阅

如果你想使用发布/订阅模式,你想让多个消费者接收一条消息(通过使用主题订阅),我将解释你需要在你的应用中做什么。

因为我们使用 Spring Boot,这使得配置发布/订阅模式更容易。如果您使用默认监听器(一个@JmsListener(destination)默认监听器容器),那么您可以使用application.properties文件中的spring.jms.pub-sub-domain=true属性。

但是,如果您使用自定义侦听器容器,那么您可以通过编程方式设置它。

@Bean
public DefaultMessageListenerContainer jmsListenerContainerFactory() {
    DefaultMessageListenerContainer dmlc = new DefaultMessageListenerContainer();
    dmlc.setPubSubDomain(true);
    // Other configuration here ...
    return dmlc;
}

远程 ActiveMQ

ToDo 应用正在使用内存代理(spring.activemq.in-memory=true)。这对于演示或测试来说可能是好的,但实际上,您使用的是远程 ActiveMQ 服务器。如果您需要一个远程服务器,将下面的键添加到您的application.properties文件中(相应地修改它)。

spring.activemq.broker-url=tcp://my-awesome-server.com:61616
spring.activemq.user=admin
spring.activemq.password=admin

src/main/resources/application.properties

对于 ActiveMQ 代理,您可以使用更多的属性。去 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.htmlspring.activemq.*键。

Spring Boot 的兔子

自从 Sun、Oracle 和 IBM 以及微软和 MSMQ 等公司首次尝试 JMS 以来,所使用的协议都是专有的。JMS 定义了一个接口 API,但是试图混合技术或编程语言是一件麻烦的事情。多亏了摩根大通的一个团队,AMQP(高级消息队列协议)诞生了。它是面向 MOM 的开放标准应用层。换句话说,AMQP 是一个有线级协议,这意味着您可以使用任何技术或编程语言来实现这个协议。

消息传递代理相互竞争,以证明它们是健壮的、可靠的和可伸缩的,但最重要的问题是它们有多快。我与许多经纪人合作过,到目前为止,最容易使用和扩展,也是最快的是 RabbitMQ,它实现了 AMQP 协议。

描述 RabbitMQ 的每个部分和所有相关概念需要一整本书,但是我将基于本节的例子来解释其中的一些概念。

安装 RabbitMQ

在我说 RabbitMQ 之前,先安装一下。如果你使用的是 Mac OS X/Linux,你可以使用brew命令。

$ brew upgrade
$ brew install rabbitmq

如果你使用的是 UNIX 或者 Windows 系统,你可以去 RabbitMQ 网站使用安装程序( www.rabbitmq.com/download.html )。RabbitMQ 是用 Erlang 编写的,所以它的主要依赖是在您的系统中安装 Erlang 运行时。现在,所有的 RabbitMQ 安装程序都附带了所有的 Erlang 依赖项。确保可执行文件在您的PATH变量中(对于 Windows 和 Linux,取决于您使用的操作系统)。如果你正在使用brew,你不需要担心设置PATH变量。

RabbitMQ/AMQP:交换、绑定和队列

AMQP 定义了三个概念,这三个概念与 JMS 世界略有不同,但是非常容易理解。AMQP 定义了交换,这是发送消息的实体。每个交换机接收一条消息,并将其路由到零个或更多的队列。这种路由涉及一种基于交换类型和规则的算法,称为绑定

AMPQ 协议定义了五种交换类型:直接扇出主题、报头。图 9-2 显示了这些不同的交换类型。

img/340891_2_En_9_Fig2_HTML.jpg

图 9-2

AMQP 交易所/绑定/队列

图 9-2 显示了可能的交换类型。因此,主要思想是向一个交换发送一个消息,包括一个路由关键字,然后交换根据它的类型将消息传递给队列(或者如果路由关键字不匹配,它就不传递)。

默认交换自动绑定到每个创建的队列。直接交换通过路由关键字绑定到队列;您可以将这种交换类型视为一对一绑定。话题交流类似于直接交流;唯一的区别是,在它的绑定中,您可以在其路由关键字中添加一个通配符。标题交换类似于主题交换;唯一的区别是绑定是基于消息头的(这是一个非常强大的交换,您可以对它的消息头执行 allany 表达式)。扇出交换机将消息复制到所有绑定队列;你可以把这种交流看作是一种信息广播。

你可以在 www.rabbitmq.com/tutorials/amqp-concepts.html 获得更多关于这些话题的信息。

本节中的示例使用默认的交换类型,这意味着路由关键字等于队列的名称。每次创建队列时,RabbitMQ 都会使用队列的名称创建一个从默认交换(实际名称是一个空字符串)到队列的绑定。

任何有 rabbitmq 的应用

让我们重新使用 ToDo 应用,并添加一条 AMQP 消息。与之前的应用一样,您可以使用 ToDo 实例。您发送和接收一个 JSON 消息,并将其转换为 object。

先打开你最喜欢的浏览器,指向已知的 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-rabbitmq

  • 名称:todo-rabbitmq

  • 包名:com.apress.todo

  • 依赖关系:RabbitMQ, Web, Lombok, JPA, REST Repositories, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 9-3 )。

img/340891_2_En_9_Fig3_HTML.jpg

图 9-3

spring initializehttps://start.spring.io

您可以从前面的章节中复制/粘贴 JPA/REST 项目的代码。

ToDo 生产者

让我们从创建一个向 Exchange 发送消息的生产者类开始(默认 Exchange-direct)(参见清单 9-7 )。

package com.apress.todo.rmq;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component

public class ToDoProducer {

    private static final Logger log = LoggerFactory.getLogger(ToDoProducer.class);
    private RabbitTemplate template;

    public ToDoProducer(RabbitTemplate template){
        this.template = template;
    }

    public void sendTo(String queue, ToDo toDo){
        this.template.convertAndSend(queue,toDo);
        log.info("Producer> Message Sent");
    }
}

Listing 9-7com.apress.todo.rmq.ToDoProducer.java

清单 9-7 显示了ToDoProducer.java类。让我们检查一下。

  • @Component。这个注释标记了 Spring 容器要拾取的类。

  • RabbitTemplateRabbitTemplate是一个助手类,它简化了对 RabbitMQ 的同步/异步访问,以便发送和/或接收消息。这与你之前看到的JmsTemplate非常相似。

  • sendTo(routingKey,message)。该方法将路由关键字和消息作为参数。在这种情况下,路由关键字是队列的名称。这个方法使用rabbitTemplate实例来调用接受路由键和消息的convertAndSend方法。请记住,消息被发送到交换(默认交换),交换将消息路由到正确的队列。这个路由关键字恰好是队列的名称。还要记住,默认情况下,RabbitMQ 总是将默认交换(直接交换)绑定到队列,路由关键字是队列的名称。

ToDo 消费者

接下来,是时候创建监听指定队列的消费者类了(参见清单 9-8 )。

package com.apress.todo.rmq;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component

public class ToDoConsumer {

    private Logger log = LoggerFactory.getLogger(ToDoConsumer.class);
    private ToDoRepository repository;

    public ToDoConsumer(ToDoRepository repository){
        this.repository = repository;
    }

    @RabbitListener(queues = "${todo.amqp.queue}")
    public void processToDo(ToDo todo){
        log.info("Consumer> " + todo);
        log.info("ToDo created> " + this.repository.save(todo));
    }
}

Listing 9-8com.apress.todo.rmq.ToDoConsumer.java

清单 9-8 显示了ToDoConsumer.java类。让我们检查一下。

  • @Component。你已经知道这个注释了。它标记了 Spring 容器要拾取的类。

  • @RabbitListener。该注释标记了为任何传入消息创建处理程序的方法(因为您也可以在类中使用该注释),这意味着它创建了一个连接到 RabbitMQ 队列的侦听器,并将该消息传递给该方法。在幕后,监听器通过使用正确的消息转换器(一个org.springframework.amqp.support.converter.MessageConverter接口的实现)尽最大努力将消息转换成适当的类型。这个接口属于spring-amqp项目);在这种情况下,它从 JSON 转换成一个ToDo实例。

ToDoProducerToDoConsumer可以看出,代码非常简单。如果您只使用 RabbitMQ Java 客户端( www.rabbitmq.com/java-client.html )来创建它,至少您需要更多的代码行来创建连接、通道和消息并发送消息,或者如果您正在编写一个消费者,那么您需要打开一个连接、创建一个通道、创建一个基本消费者,并进入一个循环来处理每个传入的消息。这对简单的生产者或消费者来说是很多的。这就是为什么 Spring AMQP 团队创造了这种简单的方法,用几行代码完成一项繁重的任务。

配置待办事项应用

接下来让我们配置应用。请记住,您发送的是 ToDo 实例,所以实际上,这与我们使用 JMS 时的配置是一样的。我们需要设置转换器和监听器容器(参见清单 9-9 )。

package com.apress.todo.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ToDoConfig {

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(new Jackson2JsonMessageConverter());
        return template;
    }

    @Bean
    public Queue queueCreation(@Value("${todo.amqp.queue}") String queue){
        return new Queue(queue,true,false,false);
    }
}

Listing 9-9com.apress.todo.config.ToDoConfig.java

清单 9-9 向您展示了配置。它有几个 bean 定义;让我们检查一下。

  • SimpleRabbitListenerContainerFactory。当使用@RabbitListener注释进行自定义设置时,该工厂是必需的,因为您正在使用ToDo实例;有必要设置消息转换器。

  • Jackson2JsonMessageConverter。该转换器用于生产(带RabbitTemplate)和消耗(@RabbitListener);它使用 Jackson 库进行映射和转换。

  • RabbitTemplate。这是一个可以发送和接收消息的助手类。在这种情况下,有必要使用 Jackson 转换器对其进行定制以生成 JSON 对象。

  • Queue。您可以手动创建队列,但在这种情况下,您是以编程方式创建的。如果队列是持久的或排他的,那么您可以传递队列的名称,并自动删除。

请记住,在 AMQP 协议中,您需要一个绑定到队列的交换,所以这个特定的示例在运行时创建了一个名为spring-boot的队列,默认情况下,所有队列都绑定到一个默认的交换。这就是为什么你没有提供任何关于交换的信息。因此,当生产者发送消息时,它首先被发送到默认交换,然后被路由到队列(spring-boot)。

运行待办事项应用

让我们创建发送 ToDo 消息的 sender 类(参见清单 9-10 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.rmq.ToDoProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ToDoSender {

    @Bean
    public CommandLineRunner sendToDos(@Value("${todo.amqp.queue}") String destination, ToDoProducer producer){
        return args -> {
            producer.sendTo(destination,new ToDo("workout tomorrow morning!"));
        };
    }
}

Listing 9-10com.apress.todo.config.ToDoSender.java

将以下键(声明发送/消费队列)添加到您的application.properties文件中。

todo.amqp.queue=spring-boot

在运行您的示例之前,请确保您的 RabbitMQ 服务器已经启动并正在运行。您可以通过打开终端并执行以下命令来启动它。

$ rabbitmq-server

使用来宾/来宾凭证访问http://localhost:15672/,确保您可以访问 RabbitMQ web 控制台。如果您在访问 web 控制台时遇到问题,请确保通过运行以下命令启用了管理插件。

$ rabbitmq-plugins list

如果整个列表中的复选框未被选中,则管理插件尚未启用(通常在全新安装时发生)。要启用这个插件,您可以执行以下命令。

$ rabbitmq-plugins enable rabbitmq_management --online

现在,你可以再试一次。然后,您应该会看到一个类似于图 9-4 的 web 控制台。

img/340891_2_En_9_Fig4_HTML.jpg

图 9-4

rabbitmq web 控制台管理

图 9-4 显示了 RabbitMQ web 控制台。现在,您可以使用 IDE 像往常一样运行项目了。如果您使用的是 Maven,请执行

$ ./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

$./gradlew bootRun

在您执行这个命令之后,您应该得到类似于下面的输出。

Producer> Message Sent
Consumer> ToDo(id=null, description=workout tomorrow morning!, created=null, modified=null, completed=false)
ToDo created> ToDo(id=8a808087645bd67001645bd6785b0000, description=workout tomorrow morning!, created=2018-07-02T10:32:19.546, modified=2018-07-02T10:32:19.547, completed=false)

如果您查看 RabbitMQ web 控制台的 Queues 选项卡,您应该已经定义了spring-boot队列(参见图 9-5 )。

img/340891_2_En_9_Fig5_HTML.jpg

图 9-5

rabbitmq web 控制台队列选项卡

图 9-5 显示了 RabbitMQ web 控制台的 Queues 选项卡。你发的信息马上就送到了。如果你想多玩一点,看看一部分吞吐量,可以修改清单 9-11 所示的ToDoSender类,但是不要忘记停止你的 app。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.rmq.ToDoProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.text.SimpleDateFormat;
import java.util.Date;

@EnableScheduling

@Configuration
public class ToDoSender {

    @Autowired
    private ToDoProducer producer;
    @Value("${todo.amqp.queue}")
    private String destination;
    private SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 500L)
    private void sendToDos(){
        producer.sendTo(destination,new ToDo("Thinking on Spring Boot at " + dateFormat.format(new Date())));
    }

}

Listing 9-11Version 2 of com.apress.todo.config.ToDoSender.java

清单 9-11 显示了ToDoSender类的修改版本。让我们检查这个新版本。

  • @EnableScheduling。这个注释告诉(通过自动配置)Spring 容器需要创建org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor类。它根据@Scheduled注释中的fixedRatefixedDelaycron表达式,注册所有用@Scheduled注释的方法,供org.springframework.scheduling.TaskScheduler接口实现调用。

  • @Scheduled(fixedDelay = 500L)。这个注释告诉TaskScheduler接口实现以 500 毫秒的固定延迟执行sendToDos方法。这意味着每半秒钟就向队列发送一条消息。

你已经知道的应用的另一部分。因此,如果您再次执行该项目,您应该会看到无休止的消息。在运行时,查看 RabbitMQ 控制台并查看输出。你可以放一个for循环来在半秒钟内发送更多的消息。

远程兔子 MQ

如果您想要访问一个远程 RabbitMQ,您可以将以下属性添加到application.properties文件中。

spring.rabbitmq.host=mydomain.com
spring.rabbitmq.username=rabbituser
spring.rabbitmq.password=thisissecured
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/production

你可以在 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 的 Spring Boot 参考中读到 RabbitMQ 的所有属性。

现在你知道在 Spring Boot 上使用 RabbitMQ 有多简单了。如果您想了解 RabbitMQ 和 Spring AMQP 技术的更多信息,您可以在主项目网站 http://projects.spring.io/spring-amqp/ 获得更多信息。

您可以通过按 Ctrl+C 来停止 RabbitMQ,这是您启动代理的地方。关于如何使用 RabbitMQ,有更多的选择,比如创建一个集群或具有高可用性。您可以在 www.rabbitmq.com 了解更多相关信息。

使用 Spring Boot 重定向消息

现在轮到雷迪斯了。Redis(远程字典服务器)是一个 NoSQL 键值存储数据库。它是用 C 语言编写的,尽管它的内核很小,但它非常可靠、可伸缩、功能强大、速度超快。它的主要功能是存储数据结构,如列表、散列、字符串、集合和排序集合。一个主要的特性是提供一个发布/订阅消息系统,这就是为什么您将使用 Redis 作为消息代理的原因。

正在安装 Redis

安装 Redis 非常简单。如果您使用的是 Mac OS X/Linux,您可以使用brew并执行以下命令。

$ brew update && brew install redis

如果您使用的是不同版本的 UNIX 或 Windows,您可以访问 Redis 网站,在 http://redis.io/download 下载 Redis 安装程序。或者,如果您想根据您的系统来编译它,也可以通过下载源代码来完成。

任何带有 Redis 的应用

使用 Redis 进行发布/订阅消息传递非常简单,与其他技术非常相似。您使用 Redis 的发布/订阅消息模式发送和接收 ToDo。

我们先打开你最喜欢的浏览器,指向 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-redis

  • 名称:todo-redis

  • 包名:com.apress.todo

  • 依赖关系:Redis, Web, Lombok, JPA, REST Repositories, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 9-6 )。

img/340891_2_En_9_Fig6_HTML.jpg

图 9-6

Spring 初始化 zr

您使用前面章节中的 ToDo 域和 repo。

ToDo 生产者

让我们创建将 Todo 实例发送到特定主题的 Producer 类(参见清单 9-12 )。

package com.apress.todo.redis;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class ToDoProducer {

    private static final Logger log = LoggerFactory.getLogger(ToDoProducer.class);
    private RedisTemplate redisTemplate;

    public ToDoProducer(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    public void sendTo(String topic, ToDo toDo){
        log.info("Producer> ToDo sent");
        this.redisTemplate.convertAndSend(topic, toDo);
    }
}

Listing 9-12com.apress.todo.redis.ToDoProducer.java

清单 9-12 显示了生产者类。它与以前的技术非常相似。它使用了一个*Template模式类;在这种情况下,发送ToDo实例到特定主题的RedisTemplate

ToDo 消费者

接下来,创建订阅主题的消费者(参见清单 9-13 )。

package com.apress.todo.redis;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ToDoConsumer {

    private static final Logger log = LoggerFactory.getLogger(ToDoConsumer.class);
    private ToDoRepository repository;

    public ToDoConsumer(ToDoRepository repository){
        this.repository = repository;
    }

    public void handleMessage(ToDo toDo) {
        log.info("Consumer> " + toDo);
        log.info("ToDo created> " + this.repository.save(toDo));
    }

}

Listing 9-13com.apress.todo.redis.ToDoConsumer.java

清单 9-13 显示了订阅任何传入ToDo消息主题的消费者。重要的是要知道,必须有一个handleMessage方法名来使用监听器(这是创建MessageListenerAdapter时的一个约束)。

配置待办事项应用

接下来,让我们为 ToDo 应用创建配置(参见清单 9-14 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.redis.ToDoConsumer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

@Configuration
public class ToDoConfig {

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter toDoListenerAdapter, @Value("${todo.redis.topic}") String topic) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(toDoListenerAdapter, new PatternTopic(topic));
        return container;
    }

    @Bean
    MessageListenerAdapter toDoListenerAdapter(ToDoConsumer consumer) {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(consumer);
        messageListenerAdapter.setSerializer(new Jackson2JsonRedisSerializer<>(ToDo.class));
        return messageListenerAdapter;
    }

    @Bean
    RedisTemplate<String, ToDo> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<String,ToDo> redisTemplate = new RedisTemplate<String,ToDo>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(ToDo.class));
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

Listing 9-14com.apress.todo.config.ToDoConfig.java

清单 9-14 显示了 ToDo 应用所需的配置。这个类声明了下面的 Spring beans。

  • RedisMessageListenerContainer。这个类负责连接到 Redis 主题。

  • MessageListenerAdapter。这个适配器接受一个 POJO (Plain Old Java Object)类来处理消息。作为一个要求,方法必须被命名为handleMessage;这个方法接收来自主题的消息作为一个ToDo实例,这就是为什么它也需要一个序列化器。

  • Jackson2JsonRedisSerializer。这个序列化程序从/到ToDo实例进行转换。

  • RedisTemplate。这个类实现了Template模式,与其他消息传递技术非常相似。这个类需要一个序列化器来处理 JSON 和来往于ToDo实例。

使用 JSON 格式并在 ToDo 实例之间进行正确的转换需要这种定制;但是您可以避免一切,使用缺省配置,该配置需要一个可序列化的对象(比如一个字符串)来发送,并使用StringRedisTemplate来代替。

application.properties文件中,添加以下内容。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop

# ToDo Redis
todo.redis.topic=todos

运行待办事项应用

在运行 ToDo 应用之前,请确保您已经启动并运行了 Redis 服务器。要启动它,请在终端中执行以下命令。

$ redis-server
89887:C 11 Feb 20:17:55.320 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
89887:M 11 Feb 20:17:55.321 * Increased maximum number of open files to 10032 (it was originally set to 256).
                _._
           _.-``__ “-._
      _.-``    `.  `_.  “-._         Redis 4.0.10 64 bit
  .-`` .-```.  ```java\/    _.,_ “-._
 (    '      ,       .-`  | `,    )   Standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|   Port: 6379
 |    `-._   `._    /     _.-'    |   PID: 89887
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |   http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

89887:M 11 Feb 20:17:55.323 # Server started, Redis version 3.0.7
89887:M 11 Feb 20:17:55.323 * The server is now ready to accept connections on port 6379

这个输出表明 Redis 已经准备好,正在监听端口 6379。您可以打开一个新的终端窗口并执行以下命令。

$ redis-cli

这是一个连接到 Redis 服务器的 shell 客户端。您可以通过执行以下命令订阅“todos”主题。

127.0.0.1:6379> SUBSCRIBE todos
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "todos"
3) (integer) 1

现在,您可以像往常一样运行项目(通过在您的 ide 中运行或者使用 Maven 或 Gradle)。如果您使用的是 Maven,请执行

$ ./mvnw spring-boot:run

执行该命令后,您的日志中应该会有类似于以下输出的内容。

...
Producer> Message Sent
Consumer> ToDo(id=null, description=workout tomorrow morning!, created=null, modified=null, completed=false)
ToDo created> ToDo(id=8a808087645bd67001645bd6785b0000, description=workout tomorrow morning!, created=2018-07-02T10:32:19.546, modified=2018-07-02T10:32:19.547, completed=false)
...

如果您看一看 Redis shell,您应该会看到如下所示的内容。

1) "message"
2) "todos"
3) "{\"id\":null,\"description\":\"workout tomorrow morning!\",\"created\":null,\"modified\":null,\"completed\":false}"

当然,你可以在浏览器的http://localhost:8080/toDos查看新的待办事项。

干得好!您已经使用 Redis 创建了一个 Spring Bot 消息应用。您可以通过按 Ctrl+C 关闭 Redis。

远程重定向

如果想要远程访问 Redis,需要向application.properties文件添加以下属性。

spring.redis.database=0
spring.redis.host=localhost
spring.redis.password=mysecurepassword
spring.redis.port=6379

你可以在 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 的《Spring Boot 参考》中读到关于 Redis 的所有性质。

您看到您需要使用 Redis 作为消息传递代理,但是如果您想了解更多关于 Spring 的键值存储,您可以在 http://projects.spring.io/spring-data-redis/ 查看 Spring Data Redis 项目。

带 Spring Boot 的 WebSockets

关于 WebSockets 的主题应该放在 web 一章中,这似乎是合乎逻辑的,但我认为 WebSockets 与消息传递更相关,这就是为什么这一节在本章中的原因。

WebSockets 是一种新的通信方式,取代了客户机/服务器 web 技术。它允许客户端和服务器之间长期保持单个 TCP 套接字连接。它也被称为技术,这是服务器可以向 web 发送数据,而无需客户端进行长时间轮询来请求新的更改。

本节向您展示了一个示例,其中您通过 REST 端点(Producer)发送消息,并使用网页和 JavaScript 库接收消息(Consumer)。

带有 WebSockets 的 ToDo 应用

创建使用 JPA REST 存储库的 ToDo 应用。每次有新的待办事项时,它都会被发布到网页上。从网页到 ToDo 应用的连接使用使用 STOMP 协议的 WebSockets。

我们先打开你最喜欢的浏览器,指向 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-websocket

  • 名称:todo-websocket

  • 包名:com.apress.todo

  • 依赖关系:Websocket, Web, Lombok, JPA, REST Repositories, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 9-7 )。

img/340891_2_En_9_Fig7_HTML.jpg

图 9-7

Spring 初始化 zr

您可以重用和复制/粘贴ToDoToDoRepository类。您还需要添加以下依赖项;如果您使用的是 Maven,将以下内容添加到pom.xml文件中。

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>sockjs-client</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>stomp-websocket</artifactId>
    <version>2.3.3</version>
</dependency>

<!--  jQuery  -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.1.1</version>
</dependency>

<!-- Bootstrap -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.3.5</version>
</dependency>

如果您使用的是 Gradle,将以下依赖项添加到build.gradle文件中。

compile('org.webjars:sockjs-client:1.1.2')
compile('org.webjars:stomp-websocket:2.3.3')
compile('org.webjars:jquery:3.1.1')
compile('org.webjars:bootstrap:3.3.5')

这些依赖项创建了您需要连接到消息传递代理的 web 客户端。WebJars 是将外部资源作为包包含进来的一种非常方便的方式,而不用担心一个接一个地下载。

ToDo 生产者

当使用 HTTP POST 方法发布新的 ToDo 时,生成器向主题发送 STOMP 消息。要做到这一点,有必要捕捉当域类被持久化到数据库时 Spring Data REST 发出的事件。

Spring Data REST 框架有几个事件,允许在持久化操作之前、期间和之后进行控制。创建一个监听after-create事件的ToDoEventHandler类(参见清单 9-15 )。

package com.apress.todo.event;

import com.apress.todo.config.ToDoProperties;
import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.rest.core.annotation.HandleAfterCreate;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component

@RepositoryEventHandler(ToDo.class)

public class ToDoEventHandler {

    private Logger log = LoggerFactory.getLogger(ToDoEventHandler.class);
    private SimpMessagingTemplate simpMessagingTemplate;
    private ToDoProperties toDoProperties;

    public ToDoEventHandler(SimpMessagingTemplate simpMessagingTemplate,ToDoProperties toDoProperties){
        this.simpMessagingTemplate = simpMessagingTemplate;
        this.toDoProperties = toDoProperties;
    }

    @HandleAfterCreate
    public void handleToDoSave(ToDo toDo){
        this.simpMessagingTemplate.convertAndSend(this.toDoProperties.getBroker() + "/new",toDo);
        log.info(">> Sending Message to WS: ws://todo/new - " + toDo);
    }
}

Listing 9-15com.apress.todo.event.ToDoEventHandler.java

清单 9-15 向您展示了接收after-create事件的事件处理程序。我们来分析一下。

  • @RepositoryEventHandler。这个注释告诉BeanPostProcessor这个类需要检查处理程序方法。

  • SimpMessagingTemplate。这个类是Template模式的另一个实现,用于使用 STOMP 协议发送消息。它的行为方式与前面章节中的其他*模板类相同。

  • ToDoProperties。此类是自定义属性处理程序。它描述了 WebSockets 的代理(todo.ws.broker)、端点(todo.ws.endpoint)和应用端点。

  • @HandleAfterCreate。该注释标记了获取域类保存到数据库后发生的任何事件的方法。如您所见,它使用了保存到数据库中的ToDo实例。在这个方法中,您使用SimpMessagingTemplate/todo/new端点发送一个ToDo实例。该端点的任何订阅者都会获得 JSON 格式的 ToDo(STOMP)。

接下来,让我们创建保存端点信息的ToDoProperties类(参见清单 9-16 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data

@ConfigurationProperties(prefix = "todo.ws")

public class ToDoProperties {

    private String app = "/todo-api-ws";
    private String broker = "/todo";
    private String endpoint = "/stomp";

}

Listing 9-16com.apress.todo.config.ToDoProperties.java

ToDoProperties类是一个助手,用来保存关于代理(/stomp和 web 客户端连接到哪里的信息(主题- /todo/new)。

配置待办事项应用

这一次,ToDo 应用创建了一个消息传递代理,它接受 WebSocket 通信并使用 STOMP 协议进行消息交换。

创建配置类(参见清单 9-17 )。

package com.apress.todo.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration

@EnableWebSocketMessageBroker

@EnableConfigurationProperties(ToDoProperties.class)
public class ToDoConfig implements WebSocketMessageBrokerConfigurer {

    private ToDoProperties props;

    public ToDoConfig(ToDoProperties props){
        this.props = props;
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(props.getEndpoint()).setAllowedOrigins("*").withSockJS();

    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker(props.getBroker());
        config.setApplicationDestinationPrefixes(props.getApp());
    }

}

Listing 9-17com.apress.todo.config.ToDoConfig.java

清单 9-17 显示了ToDoConfig类。让我们检查一下。

  • @Configuration。您知道这将类标记为 Spring 容器的配置。

  • @EnableWebSocketMessageBroker。该注释使用自动配置来创建所有必要的构件,以便使用一个非常高级的消息传递子协议通过 WebSockets 实现代理支持的消息传递。如果您需要定制端点,您需要覆盖来自WebSocketMessageBrokerConfigurer接口的方法。

  • WebSocketMessageBrokerConfigurer。它重写方法以自定义协议和端点。

  • registerStompEndpoints(StompEndpointRegistry registry)。此方法注册 STOMP 协议;在这种情况下,它注册了/stomp端点,并使用 JavaScript 库 SockJS ( https://github.com/sockjs )。

  • configureMessageBroker(MessageBrokerRegistry config)。此方法配置消息代理选项。在这种情况下,它启用了/todo端点中的代理。这意味着想要使用 WebSockets 代理的客户端需要使用/todo来连接。

接下来,让我们向application.properties文件添加信息。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop

# Rest Repositories

spring.data.rest.base-path=/api

# WebSocket
todo.ws.endpoint=/stomp
todo.ws.broker=/todo
todo.ws.app=/todo-api-ws

src/main/resources/application.properties

因为客户端是一个 HTML 页面并且是默认的 index.html,application.properties文件声明了一个新的 REST base-path端点(/api);这意味着 REST 存储库位于/api/*端点,而不是应用的根目录。

所有 Web 客户端

web 客户机连接到消息传递代理,subscribe(使用 STOMP 协议)接收发布的任何新 ToDo。这个客户端可以是处理 WebSockets 并知道 STOMP 协议的任何类型。

让我们创建一个连接到代理的简单 index.html 页面(参见清单 9-18 )。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ToDo WebSockets</title>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.5/css/bootstrap-theme.min.css">
</head>
<body>
<div class="container theme-showcase" role="main">
    <div class="jumbotron">
        <h1>What ToDo?</h1>
        <p>An easy way to find out what your are going to do NEXT!</p>
    </div>

    <div class="page-header">
        <h1>Everybody ToDo's</h1>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">ToDo:</h3>
                </div>
                <div class="panel-body">
                    <div id="output">
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="/webjars/jquery/3.1.1/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>

<script>

    $(function(){
        var stompClient = null;
        var socket = new SockJS('http://localhost:8080/stomp');
        stompClient = Stomp.over(socket);

        stompClient.connect({}, function (frame) {
            console.log('Connected: ' + frame);

            stompClient.subscribe('/todo/new', function (data) {
                console.log('>>>>> ' + data);
                var json = JSON.parse(data.body);
                var result = "<span><strong>[" + json.created + "]</strong>&nbsp" + json.description + "</span><br/>";
                $("#output").append(result);
            });

        });

    });
</script>
</body>
</html>

Listing 9-18src/main/resources/static/index.html

清单 9-18 显示了使用SockJS类连接到/stomp端点的客户端 index.html。它订阅了/todo/new主题,并一直等到 get a new ToDo 被添加到列表中。对 JavaScript 库和 CSS 的引用是 WebJars 类资源。

运行待办事项应用

现在,您已经准备好启动您的待办事项应用。您可以像往常一样运行应用,既可以使用 IDE,也可以在命令行中运行。如果您使用的是 Maven,请执行

$ ./mvnw spring-boot:run

如果您使用的是 Gradle,请执行

$ ./gradlew bootRun

打开浏览器,进入http://localhost:8080。您应该会看到一个空的待办事项框。接下来,打开终端并执行以下命令。

$ curl -XPOST -d '{"description":"Learn to play Guitar"}' -H "Content-Type: application/json" http://localhost:8080/api/toDos
$ curl -XPOST -d '{"description":"read Spring Boot Messaging book from Apress"}' -H "Content-Type: application/json" http://localhost:8080/api/toDos

喜欢的话可以再加。在您的浏览器中,您会看到待办事项(参见图 9-8 )。

img/340891_2_En_9_Fig8_HTML.jpg

图 9-8

SockJS 和 Stomp 消息:待办事项列表

图 9-8 显示了通过 WebSockets 发布消息的结果。现在,想象一下需要实时通知的新应用的可能性(例如创建实时聊天室、为您的客户即时更新股票,或者更新您的网站而无需预览或重启)。有了 Spring Boot 和 WebSockets,你就被覆盖了。

注意

所有的代码都可以从网站上获得。也可以在 https://github.com/felipeg48/pro-spring-boot-2nd 获取最新。

摘要

本章讨论了用于消息传递的所有技术,包括 JMS 和 Artemis。还讨论了如何通过在application.properties文件中提供服务器名称和端口来连接到远程服务器。

您了解了 AMQP 和 RabbitMQ,以及如何使用 Spring Boot 收发信息。您还了解了 Redis 以及如何使用它的发布/订阅消息传递。最后,您了解了 WebSockets 以及用 Spring Boot 实现它是多么容易。

如果你对消息传递感兴趣,我写了 Spring Boot 消息传递 (Apress,2017) ( www.apress.com/us/book/9781484212257 ),其中详细讨论了它,并揭示了更多的消息传递模式,从简单的应用事件到使用 Spring Cloud Stream 及其传输抽象的云解决方案。

下一章将讨论 Spring Boot 执行器以及如何监控您的 Spring Boot 应用。

十、Spring Boot 执行器

本章讨论了 Spring Boot 执行器模块,并解释了如何使用其所有功能来监控您的 Spring Boot 应用。

每个开发人员在开发期间和之后的一个共同任务是开始检查日志。开发人员检查业务逻辑是否如预期的那样工作,或者检查服务的处理时间,等等。即使他们应该有他们的单元、集成和回归测试,他们也不能避免外部故障,包括网络(连接、速度等。)、磁盘(空间、权限等。),还有更多。

当您部署到生产环境时,这一点甚至更加重要。你必须关注你的应用,有时还要关注整个系统。当您开始依赖非功能性需求时,例如检查不同应用健康状况的监控系统,或者当您的应用达到某个阈值时发出警报,或者更糟的是,当您的应用崩溃时,您需要尽快采取行动。

开发人员依赖许多第三方技术来完成他们的工作,我并不是说这不好,但这意味着所有的重担都由 DevOps 团队承担。他们必须监控每一个应用和整个系统。

Spring Boot 执行器

Spring Boot 包括一个致动器模块,它将生产就绪的非功能性要求引入到您的应用中。Spring Boot 执行器模块提供了开箱即用的监控、指标和审计功能。

使执行器模块更有吸引力的是,您可以通过不同的技术公开数据,比如 HTTP(端点)和 JMX。Spring Boot 致动器指标监控可以使用 Micrometer 框架( http://micrometer.io/ )来完成,它允许您编写一次指标代码,并在任何厂商中立的引擎中使用,如 Prometheus、Atlas、CloudWatch、Datadog 等等。

带执行器的待办事项应用

让我们开始使用 ToDo 应用中的 Spring Boot 执行器模块来看看执行器是如何工作的。您可以从头开始,也可以跟随下一部分来了解您需要做什么。如果您是从零开始,那么您可以转到 Spring Initializr ( https://start.spring.io )并将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-actuator

  • 名称:todo-actuator

  • 包名:com.apress.todo

  • 依赖关系:Web, Lombok, JPA, REST Repositories, Actuator, H2, MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 10-1 )。

img/340891_2_En_10_Fig1_HTML.jpg

图 10-1

Spring 初始化 zr

目前没有来自其他项目的任何内容;唯一的新依赖是执行器模块。您可以复制/重用ToDo域类和ToDoRepository接口(参见清单 10-1 和 10-2 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> { }

Listing 10-2com.apress.todo.repository.ToDoRepository.java

package com.apress.todo.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now())

;
    }
}

Listing 10-1com.apress.todo.domain.ToDo.java

在运行 ToDo 应用之前,看看你的pom.xml(如果你使用 Maven)或build.gradle(如果你使用 Gradle)中是否有spring-boot-starter-actuator依赖项。

您可以运行 ToDo 应用,需要注意的重要事情是日志输出。你应该有类似的东西。

INFO 41925 --- [main] s... : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"  ...
INFO 41925 --- [main] s... : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"  ...
INFO 41925 --- [main] s... : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"  ...

默认情况下,执行器模块公开了您可以访问的三个端点。

  • /actuator/health。此端点提供基本的应用健康信息。如果从浏览器或命令行进行访问,您会得到以下响应:

    {
        "status": "UP"
    }
    
    
  • /actuator/info。此端点显示任意应用信息。如果您访问这个端点,您将得到一个空响应;但是如果您将以下内容添加到您的application.properties文件中:

    spring.application.name=todo-actuator
    
    info.application-name=${spring.application.name}
    info.developer.name=Awesome Developer
    info.developer.email=awesome@example.com
    
    

    您会得到以下内容:

    {
        "application-name": "todo-actuator",
        "developer": {
            "name": "Awesome Developer",
            "email": "awesome@example.com"
        }
    }
    
    
  • /actuator。该端点是所有执行器端点的前缀。如果您通过浏览器或命令行访问该端点,您会看到:

    {
      "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        "info": {
            "href": "http://localhost:8080/actuator/info",
            "templated": false
        }
      }
    }
    
    

默认情况下,除了/actuator/shutdown端点之外,所有端点(还有更多)都是启用的;但是为什么只暴露两个端点(健康和信息)?实际上,所有这些都是通过 JMX 暴露的,这是因为其中一些包含敏感信息;所以,知道通过网络暴露什么信息是很重要的。

如果您想在 web 上公开它们,有两个属性:management.endpoints.web.exposure.includemanagement.endpoints.web.exposure.exclude。您可以用逗号将它们分开列出,也可以使用*将它们全部列出。

这同样适用于通过具有属性的 JMX 公开端点。

management.endpoints.jmx.exposure.includemanagement.endpoints.jmx.exposure.exclude。请记住,默认情况下,所有端点都通过 JMX 公开。

正如我之前提到的,您不仅有公开端点的方法,还有启用它们的方法。可以用下面的语义:management.endpoint.<ENDPOINT-NAME>.enabled。所以,如果你想启用/actuator/shutdown(默认情况下是禁用的),你需要在application.properties中这样做。

management.endpoint.shutdown.enabled=true

您可以将以下属性添加到您的application.properties文件中,以公开所有的 web actuator 端点。

management.endpoints.web.exposure.include=*

如果你看一下输出,你会得到更多的执行器端点,比如/actuator/beans/actuator/conditions等等。让我们更详细地回顾一下其中的一些。

/执行器

/actuator端点是所有端点的前缀,但是如果您访问它,它会为所有其他端点提供一个基于超媒体的发现页面。所以,如果你去http://localhost:8080/actuator,你应该看到类似于图 10-2 的东西。

img/340891_2_En_10_Fig2_HTML.jpg

图 10-2

http://localhost:8080/actuator

/致动器/条件

此端点显示自动配置报告。它给你两组:positiveMatchesnegativeMatches。请记住,Spring Boot 的主要特性是它通过查看类路径和依赖项来自动配置您的应用。这与您添加到pom.xml文件中的起始 POM 和额外的依赖项有很大关系。如果你去http://localhost:8080/actuator/conditions,你应该会看到类似于图 10-3 的东西。

img/340891_2_En_10_Fig3_HTML.jpg

图 10-3

http://localhost:8080/actuator/conditions

/执行器/bean

该端点显示应用中使用的所有 Spring beans。请记住,尽管您添加了几行代码来创建一个简单的 web 应用,但在幕后,Spring 开始创建运行您的应用所需的所有 beans。如果你去http://localhost:8080/actuator/beans,你应该会看到类似于图 10-4 的东西。

img/340891_2_En_10_Fig4_HTML.jpg

图 10-4

http://localhost:8080/actuator/beans

/执行器/配置

这个端点列出了由@ConfigurationPropertiesbean 定义的所有配置属性,这是我在前面的章节中向您展示过的。记住,你可以添加你自己的配置属性前缀,它们可以在application.properties或 YAML 文件中定义和访问。图 10-5 显示了该端点的一个例子。

img/340891_2_En_10_Fig5_HTML.jpg

图 10-5

http://localhost:8080/actuator/configprops

/执行器/线程转储

该端点执行应用的线程转储。它显示了所有正在运行的线程以及运行您的应用的 JVM 的堆栈跟踪。前往http://localhost:8080/actuator/threaddump终点(见图 10-6 )。

img/340891_2_En_10_Fig6_HTML.jpg

图 10-6

http://localhost:8080/actuator/threaddump

/执行器/环境

这个端点公开了 Spring 的ConfigurableEnvironment接口的所有属性。这将显示所有活动的概要文件和系统环境变量以及所有应用属性,包括 Spring Boot 属性。转到http://localhost:8080/actuator/env(见图 10-7 )。

img/340891_2_En_10_Fig7_HTML.jpg

图 10-7

http://localhost:8080/actuator/env

/执行器/健康

此端点显示应用的健康状况。默认情况下,它向您显示整体系统健康状况。

{
   "status": "UP"
}

如果您想查看关于其他系统的更多信息,您需要在application.properties文件中使用以下属性。

management.endpoint.health.show-details=always

修改application.properties并重新运行 ToDo 应用。如果您有一个数据库应用(我们有),您会看到数据库状态,默认情况下,您还会看到来自您系统的diskSpace。如果你正在运行你的 app,你可以去http://localhost:8080/actuator/health(见图 10-8 )。

img/340891_2_En_10_Fig8_HTML.jpg

图 10-8

http://localhost:8080/actuator/health -详细信息

/执行器/信息

此端点显示公共应用信息。这意味着您需要将这些信息添加到application.properties中。如果您有多个 Spring Boot 应用,建议您添加它。

/执行器/记录器

此端点显示应用中可用的所有记录器。图 10-9 显示了特定包装的水平。

img/340891_2_En_10_Fig9_HTML.jpg

图 10-9

http://localhost:8080/actuator/loggers

/执行器/记录器/

通过这个端点,您可以查找特定的包及其日志级别。因此,如果您配置了logging. level.com .apress.todo=DEBUG,并且您到达了http://localhost:8080/actuator/loggers/com.apress.todo端点,您将得到以下结果。

{
  "configuredLevel": DEBUG,
  "effectiveLevel": "DEBUG"
}

/执行器/指标

这个端点显示当前应用的指标信息,在这里您可以确定它使用了多少内存、有多少内存可用、应用的正常运行时间、使用的堆的大小、使用的线程数量等等(参见图 10-10 和图 10-11 )。

img/340891_2_En_10_Fig10_HTML.jpg

图 10-10

http://localhost:8080/actuator/metrics

您可以通过在端点末尾添加名称来访问每个指标;所以如果你想了解更多关于jvm.memory.max的信息,你需要到达http://localhost:8080/actuator/metrics/jvm.memory.max(见图 10-11 )。

img/340891_2_En_10_Fig11_HTML.jpg

图 10-11

http://localhost:8080//actuator/metrics/jvm.memory.max

如果你看一下图 10-11 ,在可用标签部分,你可以通过追加tag=KEY:VALUE获得更多信息。您可以使用http://localhost:8080/actuator/metrics/jvm.memory.max?tag=area:heap并获得关于堆的信息。

/执行器/映射

这个端点显示了应用中声明的所有@RequestMapping路径的所有列表。如果您想更多地了解声明了哪些映射,这非常有用。如果您的应用正在运行,您可以转到http://localhost:8080/actuator/mappings端点(参见图 10-12 )。

img/340891_2_En_10_Fig12_HTML.jpg

图 10-12

http://localhost:8080/actuator/mappings

/致动器/关闭

默认情况下,此端点未启用。它允许应用正常关闭。这个端点是敏感的,这意味着它可以安全地使用,也应该如此。如果您的应用正在运行,您现在可以停止它。如果您想要启用/actuator/shutdown端点,您需要向application.properties添加以下内容。

management.endpoint.shutdown.enabled=true

保护这个端点是明智的。您需要将spring-boot-starter-security依赖项添加到您的pom.xml(如果您使用的是 Maven)。

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

如果您使用的是 Gradle,您可以将下面的依赖项添加到您的build.gradle中。

complie ('org.springframework.boot: spring-boot-starter-security')

请记住,通过添加安全性依赖项,您将默认启用安全性。用户名为user,密码打印在日志中。此外,您可以通过使用内存、数据库或 LDAP 用户来建立更好的安全性;有关更多信息,请参见 Spring Boot 安全性章节。

现在,让我们添加management.endpoint.shutdown.enabled=truespring-boot-starter-security依赖项并重新运行应用。运行应用后,查看日志并保存打印的密码,以便可以在/actuator/shutdown端点上使用。

...
Using default security password: 2875411a-e609-4890-9aa0-22f90b4e0a11
...

现在,如果您打开一个终端,您可以执行以下命令。

$ curl -i -X POST http://localhost:8080/shutdown -u user:2875411a-e609-4890-9aa0-22f90b4e0a11
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Application-Context: application
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 17 Feb 2018 04:22:58 GMT

{"message":"Shutting down, bye..."}

正如您从这个输出中看到的,您正在使用一个POST方法来访问/actuator/shutdown端点,并且您正在传递之前打印的用户和密码。结果是Shutting down, bye..的消息。当然,你的申请被终止了。同样,知道这个特定的端点必须在任何时候都受到保护是很重要的。

/actuator/httptrace

这个端点显示跟踪信息,通常是最后几个 HTTP 请求。这个端点对于查看所有请求信息和返回的信息以在 HTTP 级别调试应用非常有用。您可以运行您的应用并转到http://localhost:8080/actuator/httptrace。你应该会看到类似于图 10-13 的东西。

img/340891_2_En_10_Fig13_HTML.jpg

图 10-13

http://localhost:8080/actuator/httptrace

更改端点 ID

您可以配置端点 ID,这会更改名称。假设你不喜欢/actuator/beans端点,它指的是 Spring beans,那么如果你把这个端点改成/actuator/spring呢。

您以management.endpoints.web.path-mapping.<endpoint-name>=<new-name>的形式在application.properties文件中进行这种更改;例如,management . endpoints . web . path-mapping。 =

如果您重新运行您的应用(停止并重新启动以应用更改),您可以使用/actuator/spring端点来访问/actuator/beans端点。

致动器 CORS 支架

使用 Spring Boot 执行器模块,您可以配置 CORS(跨源资源共享),这允许您指定哪些跨域被授权使用执行器的端点。通常,这允许应用间连接到您的端点,由于安全原因,只有授权的域能够执行这些端点。

您可以在application.properties文件中进行配置。

management.endpoints.web.cors.allowed-origins=http://mydomain.com
management.endpoints.web.cors.allowed-methods=GET, POST

如果您的应用正在运行,请停止并重新运行它。

通常在management.endpoints.web.cors.allowed-origins中,你应该输入一个类似于 http://mydomain.com 或者http://localhost:9090(不是*)的域名,这样就可以访问你的端点以避免任何黑客入侵你的网站。这与在任何控制器中使用@CrossOrigin(origins = "http://localhost:9000")注释非常相似。

更改管理端点路径

默认情况下,Spring Boot 执行器将它在/actuator中的管理作为根,这意味着执行器的所有端点都可以从/actuator访问;例如,/actuator/beans/actuator/health等等。在继续之前,请停止您的应用。您可以通过向application.properties文件添加以下属性来更改其管理上下文路径。

management.endpoints.web.base-path=/monitor

如果您重新运行您的应用,您会看到EndpointHandlerMapping正在通过添加/monitor/<endpoint-name>上下文路径来映射所有端点。您现在可以通过http://localhost:8080/monitor/httptrace访问/httptrace端点。

您还可以使用management.server.*属性更改服务器地址、添加 SSL、使用特定的 IP 或更改端点的端口。

management.server.servlet.context-path=/admin
management.server.port=8081
management.server.address=127.0.0.1

该配置的端点为context-path /admin/actuator/<endpoint-name>。端口是 8081(这意味着您有两个监听端口:8080 用于您的应用,8081 用于您的管理端点)。端点或管理绑定到 127.0.0.1 地址。

如果您想要禁用端点(出于安全原因),您有两个选择,您可以使用management.endpoints.enabled-by-default=false或者您可以使用management.server.port=-1属性。

保护端点

您可以通过包含spring-boot-starter-security和配置WebSecurityConfigurerAdapter来保护您的致动器端点;这是通过HttpSecurityRequestMatcher配置实现的。

@Configuration
public class ToDoActuatorSecurity extends WebSecurityConfigurerAdapter {

      @Override
      protected void configure(HttpSecurity http) throws Exception {
            http
            .requestMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeRequests()
            .anyRequest().hasRole("ENDPOINT_ADMIN")
            .and()
            .httpBasic();
      }

}

为了安全起见,访问端点需要ENDPOINT_ADMIN角色,知道这一点很重要。

配置端点

默认情况下,您会看到执行器端点缓存响应不接受任何参数的读取操作;所以如果需要改变这种行为,可以使用management.endpoint.<endpoint-name>.cache.time-to-live属性。作为另一个例子,如果您需要更改/actuator/beans缓存,您可以将以下内容添加到application.properties文件中。

management.endpoint.beans.cache.time-to-live=10s

实现自定义执行器端点

您可以扩展或创建自定义执行器端点。你需要用@Endpoint标记你的类,也要用@ReadOperation@WriteOperation@DeleteOperation标记你的方法;默认情况下,您的端点通过 JMX 和基于 HTTP 的 web 公开。

您可以更具体地决定是否只想向 JMX 公开您的端点,然后将您的类标记为@JmxEndpoint。如果你只是在网络上需要它,那么你可以用@WebEndpoint来标记你的类。

当创建方法时,您可以接受参数,这些参数被转换成ApplicationConversionService实例所需的正确类型。这些类型使用了application/vnd.spring-boot.actuator.v2+jsonapplication/json内容类型。

您可以在任何方法签名中返回任何类型(甚至是voidVoid)。通常,返回的内容类型因类型而异。如果是org.springframework.core.io.Resource类型,则返回一个application/octet-stream内容类型;对于所有其他类型,它返回一个application/vnd.spring-boot.actuator.v2+json, application/json内容类型。

当在 web 上使用您的定制执行器端点时,这些操作定义了它们自己的 HTTP 方法:@ReadOperation ( Http.GET)、@WriteOperation ( Http.POST)和@DeleteOperation ( Http.DELETE)。

带有自定义执行器端点的 ToDo 应用

让我们创建一个定制端点(/todo-stats)来显示数据库中 ToDo 的计数以及完成的数量。此外,我们可以创建一个写操作来完成一个待办事项,甚至创建一个删除待办事项的操作。

让我们创建ToDoStatsEndpoint来保存定制端点的所有逻辑(参见清单 10-3 )。

package com.apress.todo.actuator;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.boot.actuate.endpoint.annotation.*;
import org.springframework.stereotype.Component;

@Component

@Endpoint(id="todo-stats")

public class ToDoStatsEndpoint {

    private ToDoRepository toDoRepository;

    ToDoStatsEndpoint(ToDoRepository toDoRepository){
        this.toDoRepository = toDoRepository;
    }

    @ReadOperation
    public Stats stats() {
        return new Stats(this.toDoRepository.count(),this.toDoRepository.countByCompleted(true));
    }

    @ReadOperation
    public ToDo getToDo(@Selector String id) {
        return this.toDoRepository.findById(id).orElse(null);
    }

    @WriteOperation
    public Operation completeToDo(@Selector String id) {
        ToDo toDo = this.toDoRepository.findById(id).orElse(null);
        if(null != toDo){
            toDo.setCompleted(true);
            this.toDoRepository.save(toDo);
            return new Operation("COMPLETED",true);
        }

        return new Operation("COMPLETED",false);
    }

    @DeleteOperation
    public Operation removeToDo(@Selector String id) {
        try {
            this.toDoRepository.deleteById(id);
            return new Operation("DELETED",true);
        }catch(Exception ex){
            return new Operation("DELETED",false);
        }
    }

    @AllArgsConstructor
    @Data
    public class Stats {
        private long count;
        private long completed;
    }

    @AllArgsConstructor
    @Data
    public class Operation{
        private String name;
        private boolean successful; 

    }
}

Listing 10-3com.apress.todo.actuator.ToDoStatsEndpoint.java

清单 10-3 显示了执行诸如显示统计数据(ToDo 的总数和已完成的总数)等操作的定制端点。它获取一个 ToDo 对象,移除它,并将其设置为已完成。我们来复习一下。

  • @Endpoint。将类型标识为提供有关正在运行的应用的信息的执行器终结点。端点可以通过多种技术公开,包括 JMX 和 HTTP。这是ToDoStatsEndpoint类,它是执行器的端点。

  • @ReadOperation。将端点上的方法标识为读取操作(看到该类通过 ID 返回 ToDo)。

  • @Selector。可以在端点方法的参数上使用选择器,以指示该参数选择端点数据的子集。这是一种修改值的方法,在这种情况下,用于将 ToDo 更新为已完成。

  • @WriteOperation。将端点上的方法标识为写操作。这与 POST 事件非常相似。

  • @DeleteOperation。将端点上的方法标识为删除操作。

清单 10-3 非常类似于 REST API,但是在这种情况下,所有这些方法都通过 JMX 协议公开。另外,stats方法使用toDoRepository来调用countByCompleted方法。让我们将其添加到ToDoRepository接口中(参见清单 10-4 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> {
    public long countByCompleted(boolean completed);
}

Listing 10-4com.apress.todo.repository.ToDoRepository.java – v2

清单 10-4 显示了ToDoRepository接口的版本 2。这个接口现在有了一个新的方法声明,countByCompleted;请记住,这是一个名为 query 的方法,Spring Data 模块负责创建适当的 SQL 语句来计算已经完成的 ToDo 的数量。

如您所见,这对于创建自定义端点来说非常简单。现在,如果您运行应用,并转到http://localhost:8080/actuator,您应该会看到列出的todo-stats端点(参见图 10-14 )。

img/340891_2_En_10_Fig14_HTML.jpg

图 10-14

http://localhost:8080/actuator - todo-stats 自定义端点

如果你点击第一个todo-stats链接,你会看到如图 10-15 所示的内容。

img/340891_2_En_10_Fig15_HTML.jpg

图 10-15

http://localhost:8080/actuator/todo-stats

很简单,对吧?但是其他的行动呢。让我们试用一下。为此,我们将 JMX 与 JConsole 一起使用(它随 JDK 安装一起提供)。您可以打开一个终端窗口并执行jconsole命令。

  1. Select from the com.apress.todo.ToDoActuatorApplication list and click Connect.

    img/340891_2_En_10_Fig16_HTML.jpg

  2. Right now there is no secured connection, but it’s OK to click the Insecure Connection button.

    img/340891_2_En_10_Fig17_HTML.jpg

  3. From the main screen, select the MBeans tab. Expand the org.springframework.boot package and the Endpoint folder. You see the Todo-stats. You can expand it and see all the operations.

    img/340891_2_En_10_Fig18_HTML.jpg

  4. Click the stats item to see the MBeans operation stats.

    img/340891_2_En_10_Fig19_HTML.jpg

  5. You can click the stats button (that actually is the call to the stats method), and you will get.

    img/340891_2_En_10_Fig20_HTML.jpg

如你所见,这和浏览网页是一样的。您可以尝试使用completeToDo操作。

  1. Click the completeToDo operation. On the right, fill out the ID field with ebcf1850563c4de3b56813a52a95e930, which is the Buy Movie Tickets ToDo that is not completed.

    img/340891_2_En_10_Fig21_HTML.jpg

  2. Click completeToDo to get the confirmation (an Operation object).

    img/340891_2_En_10_Fig22_HTML.jpg

  3. If you redo the stats operation, you should now see that two are completed.

    img/340891_2_En_10_Fig23_HTML.jpg

如您所见,通过 JConsole 工具使用 JMX 非常容易。现在,您知道了如何为您需要的数据创建自定义端点。

Spring Boot 执行器健康

如今,我们在系统中寻找可见性,这意味着我们需要密切监控它们并对任何事件做出反应。我记得很久以前,监控服务器的方法是简单的 ping 但现在这还不够。我们不仅监控服务器,还监控系统及其洞察力。我们仍然需要查看我们的系统是否启动了,如果没有,我们需要获得关于该问题的更多信息。

Spring Boot 致动器health端点营救!/actuator/health端点提供正在运行的应用的状态或健康检查。它提供了一个特殊的属性,management.endpoint.health.show-details,您可以使用它来显示关于整个系统的更多信息。以下是可能的值。

  • never。细节从不显示;这是默认值。

  • when-authorized。仅向授权用户显示详细信息;您可以通过设置management.endpoint.health.roles属性来配置角色。

  • always。所有细节都显示给所有用户。

Spring Boot 执行器提供了收集系统所有信息的HealthIndicator接口;它返回一个包含所有这些信息的Health实例。执行器运行状况有几个现成的运行状况指示器,这些指示器通过运行状况聚合器自动配置,以确定系统的最终状态。它非常类似于日志级别。你可以开始工作了。别担心。我将用一个例子来说明这一点。

以下是一些自动配置的运行状况指示器。

  • CassandraHealthIndicator。检查 Cassandra 数据库是否已启动并正在运行。

  • DiskSpaceHealthIndicator。检查磁盘空间是否不足。

  • RabbitHealthIndicator。检查兔子服务器是否启动并运行。

  • RedisHealthIndicator。检查 Redis 服务器是否已启动并正在运行。

  • DataSourceHealthIndicator。检查来自数据源的数据库连接。

  • MongoHealthIndicator。检查 MongoDB 是否启动并运行。

  • MailHealthIndicator。检查邮件服务器是否启动。

  • SolrHealthIndicator。检查 Solr 服务器是否启动。

  • JmsHealthIndicator。检查 JMS 代理是否启动并运行。

  • ElasticsearchHealthIndicator。检查 ElasticSearch 集群是否启动。

  • Neo4jHealthIndicator。检查 Neo4j 服务器是否已启动并正在运行。

  • InfluxDBHealthIndicator。检查 InfluxDB 服务器是否已启动。

还有很多。如果依赖项在您的类路径中,所有这些都是自动配置的;换句话说,您不需要担心配置或使用它们。

让我们测试一下健康指标。

img/340891_2_En_10_Fig24_HTML.jpg

  1. 确保application.properties文件中有management.endpoints.web.exposure.include=*(在 ToDo 应用中)。

  2. management.endpoint.health.show-details=always属性添加到application.properties文件。

  3. 如果您运行 ToDo 应用,并访问http://localhost:8080/actuator/health,您应该会得到以下内容。

H2 数据库DataSourceHealthIndicatorDiskSpaceHealthIndicator正在被自动配置。

  1. Add the following dependency to your pom.xml file (if you are using Maven).

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

    如果您使用的是 Gradle,将下面的依赖项添加到您的build.gradle文件中。

    compile('org.springframework.boot:spring-boot-starter-amqp')
    
    
  2. You guessed right. We are adding AMQP dependencies. Re-run the app and take a look at the /actuator/health endpoint .

    img/340891_2_En_10_Fig25_HTML.jpg

因为您添加了spring-boot-starter-amqp依赖项,所以它是关于 RabbitMQ 代理的,并且您有执行器,RabbitHealthIndicator被自动配置为到达本地主机(或具有spring.rabbitmq.*属性设置的特定代理)。如果它是活的,那么它报告它。在这种情况下,您会在日志中看到一些连接失败,在health端点中,您会看到系统关闭。如果您有一个 RabbitMQ 代理(来自前一章),您可以运行它(用rabbitmq-server命令)并刷新health端点。你看,一切都准备好了!

img/340891_2_En_10_Fig26_HTML.jpg

就这样。这就是你如何使用所有开箱即用的健康指标。添加所需的依赖项——这样就完成了!

带有自定义健康指示器的待办事项应用

现在轮到 ToDo 的应用拥有自己的自定义健康指标了。实现一个非常容易。您需要实现HealthIndicator接口并用Health实例返回期望的状态。

创建ToDoHealthCheck来访问一个FileSystem路径,并检查它是否可用、可读和可写(参见清单 10-5 )。

package com.apress.todo.actuator;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.health.Health;
import org.springframework.stereotype.Component;

import java.io.File;

@Component
public class ToDoHealthCheck implements HealthIndicator {

    private String path;

    public ToDoHealthCheck(@Value("${todo.path:/tmp}")String path){
        this.path = path;
    }

    @Override
    public Health health() {

        try {

            File file = new File(path);
            if(file.exists()){

                if(file.canWrite())
                    return Health.up().build();

                return Health.down().build();

            }else{
                return Health.outOfService().build();
            }
        }catch(Exception ex) {
            return Health.down(ex).build();

        }
    }
}

Listing 10-5com.apress.todo.actuator.ToDoHealthCheck.java

清单 10-5 显示了标记为@ComponentToDoHealthCheck类,它是HealthIndicator接口的实现。有必要实现health方法(参见Health类有一个流畅的 API 来帮助创建健康状态。分析代码,看看 path 变量必须设置成什么(环境中的一个属性,命令行中的一个参数,或者application.properties);否则,默认使用/tmp。如果这个路径存在,那么它检查你是否能写它;如果是,它公开UP状态,如果不是,它报告一个DOWN状态。如果路径不存在,它报告一个OUT_OF_SERVICE状态。如果有任何异常,它会显示一个DOWN状态。

在前面的代码中,需要一个todo.path属性。让我们创建一个保存该属性信息的ToDoProperties类。你已经知道了(见清单 10-6 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data

@ConfigurationProperties(prefix = "todo")

public class ToDoProperties {
    private String path;
}

Listing 10-6com.apress.todo.config.ToDoProperties.java

如你所见,这很简单。如果你还记得,要使用一个@ConfigurationProperties标记的类,就要用@EnableConfigurationProperties来调用它。让我们创建ToDoConfig类来支持它(参见清单 10-7 )。

package com.apress.todo.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties(ToDoProperties.class)

@Configuration
public class ToDoConfig {

}

Listing 10-7com.apress.todo.config.ToDoConfig.java

这个班没什么特别的。在新属性中添加application.properties文件。

todo.path=/tmp/todo

如果您使用的是 Windows,您可以尝试一些类似

todo.path=C:\\tmp\\todo

查看文档中的正确字符。那么,你都准备好了。如果您重新运行您的 ToDo 应用,并查看/actuator/health的响应,您应该会得到以下内容。

img/340891_2_En_10_Fig27_HTML.jpg

JSON 响应中有toDoHealthCheck键;并且与逻辑设置相匹配。接下来,通过创建一个可写的/tmp/todo目录来解决这个问题。

img/340891_2_En_10_Fig28_HTML.jpg

您可以通过使用application.properties文件中的以下属性来配置状态/严重性顺序(例如,日志记录级别)。

management.health.status.order=FATAL, DOWN, OUT_OF_SERVICE, UNKNOWN, UP

如果使用 HTTP 上的health端点,每个状态/严重性都有自己的 HTTP 代码或映射可用。

  • 向下–503

  • 停止服务–503

  • 向上–200

  • 下降-200

您可以通过以下方式使用自己的代码

management.health.status.http-mapping.FATAL=503

此外,您可以使用Health.status("IN_BAD_CONDITION").build();创建自己的状态,如IN_BAD_CONDITION

使用 Spring Boot 执行器创建自定义健康指示器非常简单!

Spring Boot 执行器指标

如今,每个系统都需要被监控。有必要通过观察每个应用中发生的事情来保持可见性,无论是单独还是整体。Spring Boot 致动器提供基本的度量和集成,并自动配置千分尺( http://micrometer.io )。

Micrometer 为许多流行的监控系统提供了一个简单的仪表客户端界面;换句话说,您可以编写一次监控代码,并使用任何其他第三方系统,如 Prometheus、网飞 Atlas、CloudWatch、Datadog、Graphite、Ganglia、JMX、InfluxDB/Telegraf、New Relic、StatsD、SignalFX 和 WaveFront(以及更多即将推出的系统)。

记住 Spring Boot 执行器有/actuator/metrics。如果您运行 ToDo 应用,您将获得基本指标;您在前面的章节中已经了解了这一点。我没有向您展示的是如何使用千分尺创建您的自定义指标。这个想法是编写一次代码,并使用任何其他第三方监测工具。Spring Boot 致动器和测微计将这些指标暴露给所选的监控工具。

让我们直接跳到实现测微计代码,并使用 Prometheus 和 Grafana 来看看它有多容易使用。

带测微计的 ToDo 应用:普罗米修斯和格拉夫纳

让我们实现测微计代码,并使用普罗米修斯和格拉夫纳。

到目前为止,我们已经看到,一旦 Spring Data REST 模块看到所有扩展了Repository<T,ID>接口的接口,它就会代表我们创建 web MVC 控制器(REST 端点)。想象一下,我们需要拦截这些 web 请求,并开始创建一个聚合;这个指标告诉我们一个特定的 REST 端点和 HTTP 方法被请求了多少次。这有助于我们确定哪个端点适合微服务。这个拦截器还获取所有请求,包括/actuator请求。

为此,Spring MVC 提供了一个我们可以使用的HandlerInterceptor接口。它有三个默认方法,但我们只需要其中一个。让我们从创建ToDoMetricInterceptor类开始(参见清单 10-8 )。

package com.apress.todo.interceptor;

import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ToDoMetricInterceptor implements HandlerInterceptor {

    private static Logger log = LoggerFactory.getLogger(ToDoMetricInterceptor.class);

    private MeterRegistry registry;
    private String URI, pathKey, METHOD;

    public ToDoMetricInterceptor(MeterRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        URI = request.getRequestURI();
        METHOD = request.getMethod();
        if (!URI.contains("prometheus")){
            log.info(" >> PATH: {}",URI);
            log.info(" >> METHOD: {}", METHOD);

            pathKey = "api_".concat(METHOD.toLowerCase()).concat(URI.replaceAll("/","_").toLowerCase());
            this.registry.counter(pathKey).increment();
        }
    }
}

Listing 10-8com.apress.todo.interceptor.ToDoMetricInterceptor.java

清单 10-8 显示了ToDoMetricInterceptor类(它实现了HandlerInterceptor接口)。该接口有三种默认方法:preHandlepostHandleafterCompletion。这个类只实现了afterCompletion方法。这个方法有HttpServletRequest,有助于发现请求了哪个端点和 HTTP 方法。

您的类使用的是MeterRegistry实例,它是 Micrometer 框架的一部分。实现从请求实例中获取路径和方法,并使用counter方法来递增。pathKey很简单;如果有对/toDos端点的GET请求,则pathKeyapi_get_todos。如果对/toDos端点有一个POST请求,那么pathKey就是api_post_todos,依此类推。因此,如果有几个对/toDos的请求,registry递增(使用那个pathKey)并聚合到现有值。

接下来,让我们确保ToDoMetricInterceptor正在被 Spring MVC 拾取和配置。打开ToDoConfig类并添加一个MappedInterceptor bean(参见清单 10-9 的版本 2)。

package com.apress.todo.config;

import com.apress.todo.interceptor.ToDoMetricInterceptor;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.MappedInterceptor;

@EnableConfigurationProperties(ToDoProperties.class)
@Configuration
public class ToDoConfig {

    @Bean
    public MappedInterceptor metricInterceptor(MeterRegistry registry) {
       return new MappedInterceptor(new String[]{"/**"},
                        new ToDoMetricInterceptor(registry));
    }
}

Listing 10-9com.apress.todo.config.ToDoConfig.java v2

清单 10-9 显示了新的ToDoConfig类,它有MappedInterceptor。它通过使用"/**"匹配器为每个请求使用ToDoMetricInterceptor

接下来,让我们添加两个将数据导出到 JMX 和普罗米修斯的依赖项。如果您有 Maven,您可以向pom.xml文件添加以下依赖项。

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-jmx</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

如果您使用的是 Gradle,您可以将以下依赖项添加到build.gradle

compile('io.micrometer:micrometer-registry-jmx')
compile('io.micrometer:micrometer-registry-prometheus')

Spring Boot 执行器自动配置和注册每个千分尺注册表,在这种情况下,JMX 和普罗米修斯。对于 Prometheus,致动器配置/actuator/prometheus端点。

先决条件:使用 Docker

在使用指标测试 ToDo 应用之前,有必要安装 Docker(尝试安装最新版本)。

我为什么选择 Docker?嗯,这是一种安装我们需要的东西的简单方法。我们将在接下来的章节中再次用到它。Docker Compose 通过使用 Docker 允许我们使用 DNS 名称的内部网络来方便安装 Prometheus 和 Grafana。

坞站-组合. yml

这是用来启动普罗米修斯和格拉夫纳的docker-compose.yml文件。

version: '3.1'

networks:
  micrometer:

services:

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus/:/etc/prometheus/
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    ports:
      - 9090:9090
    networks:
      - micrometer
    restart: always

  grafana:
    image: grafana/grafana
    user: "104"
    depends_on:
      - prometheus
    volumes:
      - ./grafana/:/etc/grafana/
    ports:
      - 3000:3000
    networks:
      - micrometer
    restart: always

您可以用任何编辑器创建这个文件。记住,这是一个 YML 文件,没有缩进的制表符。你需要创建两个文件夹:prometheusgrafana。在每个文件夹中,都有一个文件。

prometheus文件夹中,有一个prometheus.yml文件,内容如下。

global:
  scrape_interval:     5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'todo-app'

    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:8080']

关于这个文件最重要的是metrics_path和 targets 键。当发现micrometer-registry-prometheus时,Spring Boot 执行器自动配置/actuator/prometheus端点。该值是metrics_path所必需的。另一个非常重要的值是targets键。普罗米修斯每 5 秒刮一次/actuator/prometheus终点。它需要知道自己的位置(它使用的是host.docker.internal域名)。这是 Docker 寻找其主机的部分(正在运行的localhost:8080/todo-actuator应用)。

grafana文件夹包含一个空的grafana.ini文件。为了确保 Grafana 采用缺省值,您显示了以下目录结构。

img/340891_2_En_10_Fig29_HTML.jpg

运行待办事项应用指标

现在,是时候开始测试和配置 Grafana 来查看有用的指标了。运行您的待办事项应用。检查日志并确保/actuator/prometheus端点在那里。

打开一个终端,转到有docker-compose.yml文件的地方,执行下面的命令。

$ docker-compose up

这个命令行启动 docker-compose 引擎,它下载图像并运行它们。让我们通过打开浏览器并点击http://localhost:9090/targets来确保 Prometheus 应用正常工作。

img/340891_2_En_10_Fig30_HTML.jpg

这意味着prometheus.yml配置被成功采用。换句话说,普罗米修斯是在刮http://localhost:8080/actuator/prometheus端点。

接下来,我们来配置 Grafana。

img/340891_2_En_10_Fig31_HTML.jpg

  1. 打开另一个浏览器标签,点击http://localhost:3000

可以用admin/admin作为凭证。

  1. You can press the Skip button the following screen.

    img/340891_2_En_10_Fig32_HTML.jpg

  2. Click the Add Data Source icon.

    img/340891_2_En_10_Fig33_HTML.jpg

  3. 填写所有必填字段。重要的是

    • 名称:todo-app

    • 类型:Prometheus

    • URL: http://prometheus:9090

    • 刮擦间隔:3s

  4. Click the Save button.

    img/340891_2_En_10_Fig34_HTML.jpg

    URL 值http://prometheus:9090指的是 docker - compose 服务,是 docker-compose 提供的内部 DNS,不需要做本地主机。您可以保留默认的其他值,并点击保存&测试。如果一切按预期运行,您会在页面底部看到一个绿色横幅,上面写着,数据源正在运行

  5. You can go home by going back or pointing the browser to http://localhost:3000. You can click the New Dashboard button on the homepage.

    img/340891_2_En_10_Fig35_HTML.jpg

  6. You can click the Graph icon, a panel appears. Click Panel Title and then click Edit.

    img/340891_2_En_10_Fig36_HTML.jpg

  7. Configure two queries, the api_get_total and api_post_todos_total, which were generated as metrics by the Micrometer and Spring Boot Actuator for Prometheus engine.

    img/340891_2_En_10_Fig37_HTML.jpg

  8. Perform requests to the /toDos (several times) and post to the /toDos endpoints. You see something like the next figure.

    img/340891_2_En_10_Fig38_HTML.jpg

恭喜你!您已经使用 Micrometer、Prometheus 和 Grafana 创建了自定义指标。

Spring Boot 和格拉夫纳的一般统计

我为 Grafana 找到了一个非常有用的配置,它允许你利用 Spring Boot 执行器提供的每一个指标。这个配置可以导入到 Grafana 中。

https://grafana.com/dashboards/6756 下载此配置。文件名为spring-boot-statistics_rev2.json。你接下来需要它。

在 Grafana 主页的左上角(http://localhost:3000,点击 Grafana 图标,打开侧边栏。点击+符号并选择导入。

img/340891_2_En_10_Fig39_HTML.jpg

设置默认值,但是在 Prometheus 字段中,选择 todo-app(您之前配置的数据源)。

img/340891_2_En_10_Fig40_HTML.jpg

点击导入—然后!您拥有一个包含所有 Spring Boot 执行器指标和监控的完整仪表板!

img/340891_2_En_10_Fig41_HTML.jpg

花点时间回顾每一张图。所有数据都来自于/actuator/prometheus端点。

您可以通过在另一个终端窗口中执行以下命令来关闭 docker-compose。

$ docker-compose down

注意

你可以在 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 或者我的个人知识库 https://github.com/felipeg48/pro-spring-boot-2nd 找到这部分的解决方法。

摘要

本章讨论了 Spring Boot 执行器,包括它的端点和它的可定制性。使用执行器模块,您可以监控您的 Spring Boot 应用,从使用/health端点到使用/httptrace进行更细粒度的调试。

您了解了可以使用千分尺并插入任何第三方工具来使用 Spring Boot 执行器指标。

在下一章中,您将进一步了解如何使用 Spring Integration 和 Spring Cloud Stream。

十一、Spring Boot、Spring Integration 和 Spring Cloud Stream

在这一章中,我将向您展示 Java 社区的最佳集成框架之一:Spring Integration project,它基于 Spring 框架。我也呈现一下 Spring Cloud Stream,它是基于 Spring Integration 的。它创建了连接到共享消息传递系统的强大且可扩展的事件驱动微服务——所有这些都是通过 Spring Boot 完成的。

如果我们看看软件开发和业务需求,作为一名开发人员或架构师,我们总是在考虑如何集成组件和系统,无论是我们架构的内部还是外部,并探索什么是功能完整、高度可用、易于维护和增强的。

以下是开发人员或架构师通常面临的主要用例。

  • 创建一个可靠的文件传输或文件分析系统。大多数应用需要从文件中读取信息,然后对其进行处理,因此我们需要创建健壮的文件系统来保存和读取数据,同时还要共享和处理文件的大小。

  • 在共享环境中使用数据的能力,在这种环境中,多个客户端(系统或用户)需要访问同一个数据库或同一个表,执行操作并处理不一致、重复等问题。

  • 远程访问不同的系统,从执行远程程序到发送大量信息。我们总是希望以实时和异步的方式实现这一点。尽可能快地获得响应的能力,而不忘记远程系统总是需要可到达的;换句话说,具有所需的容错和高可用性。

  • 消息传递—从基本的内部呼叫到每秒向远程代理发送数十亿条消息。通常,我们以异步方式进行消息传递,因此我们需要处理并发性、多线程、速度(网络延迟)、高可用性、容错等。

我们如何解决或实现所有这些用例?大约 15 年前,软件工程师 Gregor Hohpe 和 Bobby Woolf 写了企业集成模式:设计、构建和部署消息传递解决方案 (Addison-Wesley,2003)。这本书揭示了解决我提到的用例所需的所有消息传递模式。它让您更好地理解系统如何互连和工作,以及如何使用应用架构、面向对象设计和面向消息创建健壮的集成系统。

在接下来的小节中,我将使用 Spring 框架中的 Spring Integration 项目向您展示其中的一些模式。

Spring Integration 底漆

Spring Integration 是实现企业集成解决方案的简单模型。它促进了 Spring Boot 应用中的异步和消息驱动。它实现了所有的企业集成模式,用于创建企业的、健壮的、可移植的集成解决方案。

Spring Integration 项目提供了一种让组件松散耦合以实现模块化和可测试性的方法。它有助于加强业务和集成逻辑之间的关注点分离。

Spring Integration 公开了以下主要组件。

  • 消息。这是任何 Java 对象的通用包装器。它由报头和有效载荷组成。标头通常包含重要的信息,如 ID、时间戳、相关 ID 和返回地址;当然,你也可以添加你自己的。有效负载可以是任何类型的数据,从字节数组到自定义对象。您可以在org.springframework.messaging包的 spring-messaging 模块中找到它的定义。

    public interface Message<T> {
          T getPayload();
          MessageHeaders getHeaders();
    }
    
    

    如你所见,定义中没有什么花哨的东西。

img/340891_2_En_11_Fig1_HTML.jpg

图 11-1

信息通道

  • 消息通道。管道和过滤器架构,非常类似于您在 UNIX 系统中使用的命令。要使用它,你需要有生产者和消费者;生产者将消息发送到消息通道,消费者接收消息(参见图 11-1 )。

这个消息通道遵循消息传递模式,例如点对点和发布/订阅模型。Spring Integration 提供了几个消息通道,比如可轮询通道(允许在队列中缓冲消息)或用户可订阅通道。

  • 消息端点。将应用代码连接到消息传递框架的过滤器。大多数端点都是企业集成模式实现的一部分。
    • 滤镜。消息过滤器确定消息何时应该传递到输出通道。

    • 变压器。消息转换器修改消息的内容或结构,并将其传递到输出通道。

    • 路由器。消息路由器根据规则决定做什么以及将消息发送到哪里。这些规则可以在报头中,也可以在相同的有效载荷中。这个消息路由器有许多可以应用的模式。我至少会给你看其中一个。

    • 分割器。消息拆分器接受一条消息(输入通道),并拆分和返回新的多条消息(输出通道)。

    • 服务激活器。这是一个端点,通过接收消息(输入通道)并处理它来充当服务。它可以结束集成流程,也可以返回相同的消息或全新的消息(输出通道)。

    • 聚合器。此消息端点接收到多条消息(输入通道);它将它们组合成一个新的单一消息(基于发布策略)并将其发送出去(输出通道)。

    • 通道适配器。这是将消息通道连接到其他系统或传输的特定端点。Spring Integration 提供入站或出站适配器。如果需要响应,它会提供一个网关适配器。你看这些是最常用的。为什么呢?如果您的解决方案希望连接到 RabbitMQ、JMS、FTP、文件系统、HTTP 或任何其他技术,Spring Integration 有适配器可以连接到它,而无需您编写任何客户端。

关于 Spring Integration 和消息模式、消息通道、适配器等等,可能需要写一整本书,但是如果你对这项技术感兴趣,我推荐雷颂德博士写的Pro Spring Integration(a press,2011)。

在下一节中,我将向您展示一些组件和模式,它们足以让您入门。

编程 Spring Integration

使用 Spring Integration,有几种方法可以配置所有组件(消息、消息通道和消息端点):XML、JavaConfig 类、注释和新的集成 DSL。

与 Spring Integration 的 ToDo 应用

先说大家熟知的 ToDo App,马上用 Spring Integration。您可以从头开始,也可以跟随下一部分来了解您需要做什么。如果您是从零开始,那么您可以转到 Spring Initializr ( https://start.spring.io )并将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-integration

  • 名称:todo-integration

  • 包名:com.apress.todo

  • 依赖关系:Spring Integration, Lombok

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 11-2 )。

img/340891_2_En_11_Fig2_HTML.jpg

图 11-2

Spring 初始化 zr

从依赖关系中可以看出,我们现在使用的是 Spring Integration。您可以重用或复制ToDo类(参见清单 11-1 )。

package com.apress.todo.domain;

import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        this.id = UUID.randomUUID().toString();
        this.created = LocalDateTime.now();
        this.modified = LocalDateTime.now();
    }

    public ToDo(String description){
        this();
        this.description = description;
    }

    public ToDo(String description,boolean completed){
        this(description);
        this.completed = completed;

    }

}

Listing 11-1com.apress.todo.domain.ToDo.java

清单 11-1 向您展示众所周知的ToDo级。这没什么新鲜的。接下来,让我们使用 DSL 创建一个具有第一个 Spring Integration 流的ToDoIntegration类(参见清单 11-2 )。

package com.apress.todo.integration;

import com.apress.todo.domain.ToDo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.channel.MessageChannels;

@EnableIntegration

@Configuration
public class ToDoIntegration {

    @Bean
    public DirectChannel input(){
        return MessageChannels.direct().get();
    }

    @Bean
    public IntegrationFlow simpleFlow(){
        return IntegrationFlows
              .from(input())
              .filter(ToDo.class, ToDo::isCompleted)
              .transform(ToDo.class,
                  toDo -> toDo.getDescription().toUpperCase())
              .handle(System.out::println)
                   .get();
     }
}

Listing 11-2com.apress.todo.integration.ToDoIntegration.java

清单 11-2 显示了一个基本的例子。此示例从输入通道(ToDo 实例)接收消息,如果只有 ToDo 完成,则过滤此消息,然后通过大写描述转换消息,并通过在控制台上打印来处理它。所有这些被称为一个集成流程。但是让我们更深入地看看里面。

  • IntegrationFlow。将 DSL 公开为 bean(需要有一个@Bean注释)。这个类是IntegrationFlowBuilder的工厂,定义了集成的流程。它注册所有组件,如消息通道、端点等。

  • IntegrationFlows。这个类公开了一个帮助构建集成流程的 fluent API。很容易合并端点,如转换、过滤、处理、分割、聚合、路由、桥接。有了这些端点,就可以使用任何 Java 8(及更高版本)lambda 表达式作为参数。

  • from。这是一个重载方法,通常传递消息源;在这种情况下,我们调用通过MessageChannels fluent API 返回一个DirectChannel实例的input方法。

  • filter。这个重载的方法填充了MessageFilterMessageFilter委托给MessageSelector,如果选择器接受消息,?? 将消息发送到过滤器的输出通道。

  • transform。这个方法可以接收一个 lambda 表达式,但实际接收的是GenericTransformer<S,T>,其中S是源,T是它要转换成的类型。这里我们可以使用开箱即用的变压器,如ObjectToJsonTransformerFileToStringTransformer等等。在这个例子中,我们是类类型(ToDo),执行了一个 lambda 在这种情况下,获取 ToDo 的描述并将其转换为大写。

  • handle。这是一个填充ServiceActivatingHandler的重载方法。通常,我们可以使用一个 POJO,它允许您接收消息并返回新消息或触发另一个呼叫。这是一个有用的端点,我们将在本章和下一章中看到它是一个服务激活器端点。

  • @EnableIntegration。这里我们使用了一个新的注释,它设置了我们的流所需的所有 Spring Integration beans。这个注释注册了不同的 beans,比如errorChannelLoggingHandlertaskScheduler等等。这些 beans 在集成解决方案中补充了我们的流程。在 Spring Boot 应用中使用 Java 配置、注释和 DSL 时,这个注释是必需的。

如果这看起来与您过去使用集成解决方案所做的不同,不要太担心。您将会对我接下来向您展示的所有示例更加熟悉,并且会变得更加容易。

接下来,让我们创建一个ToDoConfig类,在其中通过输入通道发送一个 ToDo(参见清单 11-3 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

@Configuration
public class ToDoConfig {

    @Bean
    public ApplicationRunner runner(MessageChannel input){
        return args -> {
            input.send(
            MessageBuilder
                  .withPayload(new ToDo("buy milk today",true))
                  .build());
        };
    }
}

Listing 11-3com.apress.todo.config.ToDoConfig.java

清单 11-3 显示了ApplicationRunner bean,当应用启动时它被执行(看到MessageChannel被注入了——在ToDoIntegration类中声明的那个)。这个方法使用了一个MessageBuilder类,它提供了一个创建消息的流畅 API。在这种情况下,这个类使用了withPayload方法来创建一个新的ToDo实例,标记为完成。

现在是时候运行我们的应用了。如果运行它,您应该会看到类似下面的输出。

...
INFO 39319 - [main] o.s.i.e.EventDrivenConsumer: started simpleFlow.org.springframework.integration.config.ConsumerEndpointFactoryBean#2
INFO 39319 - [main] c.a.todo.TodoIntegrationApplication      : Started TodoIntegrationApplication in 0.998 seconds (JVM running for 1.422)

GenericMessage [payload=BUY MILK TODAY, headers={id=c245b7a3-3191-641b-7ad8-1f6eb950f62e, timestamp=1535772540543}]

...

请记住,消息是关于有效载荷的,这就是为什么我们得到了具有有效载荷的GenericMessage类,其中最后的消息是“今天买牛奶”,头包括 ID 和时间戳。这是应用过滤器并转换消息的结果。

使用 XML

接下来,让我们修改类以使用另一种类型的配置,XML,并看看如何配置您的集成流。创建src/main/resources/META-INF/spring/integration/todo-context.xml文件(见清单 11-4 )。

<?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:int="http://www.springframework.org/schema/integration"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">

    <int:channel id="input" />
    <int:filter input-channel="input"
                expression="payload.isCompleted()"

                output-channel="filter" />
    <int:channel id="filter" />
    <int:transformer input-channel="filter"
                     expression="payload.getDescription().toUpperCase()"
                     output-channel="log" />
    <int:channel id="log" />
    <int:logging-channel-adapter channel="log" />

</beans>

Listing 11-4src/main/resources/META-INF/spring/integration/todo-context.xml

清单 11-4 显示了配置 ToDo 集成流的 XML 版本。我认为这很简单。如果你正在使用 STS IDE,你可以使用 Spring Integration 流的拖放面板(集成图),或者如果你正在使用 IDEA IntelliJ,生成图(参见图 11-3 )。

img/340891_2_En_11_Fig3_HTML.jpg

图 11-3

STS 中的 spring integration-图形面板

图 11-3 显示了 integration-graph 面板,在这里您可以通过使用拖放组件来图形化地创建您的流程。此功能仅适用于 STS IDE。IDEA IntelliJ 基于 XML 生成一个图表(右键单击)。

正如你在图 11-X 中看到的,有通道、路由、转换、端点等等。图 11-3 实际上是 XML 的翻译;换句话说,您可以从使用 XML 开始,如果您切换到集成图,它会显示到目前为止您所拥有的,反之亦然。您可以使用这个特性并切换到源代码来获得 XML。这是一种非常酷的创造流量的方式,你不觉得吗?

要运行这个例子,有必要注释掉来自ToDoIntegration类的所有 bean 声明。然后,您需要使用@ImportResource注释来指示您创建的 XML 所在的位置。它应该类似于清单 11-5 中所示的代码片段。

package com.apress.todo.integration;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.integration.config.EnableIntegration;

@ImportResource("META-INF/spring/integration/todo-context.xml")

@EnableIntegration
@Configuration
public class ToDoIntegration {

}

Listing 11-5com.apress.todo.integration.ToDoIntregration.java – v2

清单 11-5 展示了ToDoIntegration类的新版本(实际上没有代码)。我们添加了@ImportResource注释。这告诉 Spring Boot 有一个配置文件需要处理。如果您运行它,您应该有以下输出。

...
INFO 43402 - [main] o.s.i.channel.PublishSubscribeChannel    : Channel 'application.errorChannel' has 1 subscriber(s).
2018-09-01 07:23:20.668  INFO 43402 --- [           main] o.s.i.endpoint.EventDrivenConsumer       : started _org.springframework.integration.errorLogger
INFO 43402 - [main] c.a.todo.TodoIntegrationApplication      : Started TodoIntegrationApplication in 1.218 seconds (JVM running for 1.653)

INFO 43402 - [main] o.s.integration.handler.LoggingHandler   : BUY MILK TODAY

...

使用注释

Spring Integration 具有集成注释,可以帮助您使用 POJO (Plain Old Java Object)类,因此您可以向您的流添加更多的业务逻辑,并有更多的控制。

让我们修改ToDoIntegration类,看起来像清单 11-6 。

package com.apress.todo.integration;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.Filter;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.annotation.Transformer;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.channel.MessageChannels;
import org.springframework.messaging.MessageChannel;

@EnableIntegration
@Configuration
public class ToDoIntegration {

    @Bean
    public MessageChannel input(){
        return new DirectChannel();
    }

    @Bean
    public MessageChannel toTransform(){
        return new DirectChannel();
    }

    @Bean
    public MessageChannel toLog(){
        return new DirectChannel();
    }

    @MessageEndpoint
    class SimpleFilter {
        @Filter(inputChannel="input"
                        ,outputChannel="toTransform")
        public boolean process(ToDo message){
            return message.isCompleted();
        }
    }

    @MessageEndpoint
    class SimpleTransformer{
        @Transformer(inputChannel="toTransform"
                        ,outputChannel="toLog")
        public String process(ToDo message){
            return message.getDescription().toUpperCase();
        }
    }

    @MessageEndpoint
    class SimpleServiceActivator{
        Logger log = LoggerFactory
                  .getLogger(SimpleServiceActivator.class);
        @ServiceActivator(inputChannel="toLog")
        public void process(String message){
            log.info(message);
        }
    }
}

Listing 11-6com.apress.todo.integration.ToDoIntegration.java – v3

清单 11-6 向您展示了与之前相同的流程,现在我们使用集成注释。还可以看看内部类来简化这个例子。我们来详细看看这段代码。

  • MessageChannel。这是一个定义发送消息的方法的接口。

  • DirectChannel。这是一个消息通道,为发送的每条消息调用一个订户。它通常在不需要任何消息队列时使用。

  • @MessageEndpoint。这是一个有用的注释,它将类标记为端点。

  • @Filter。这个注释标记了一个实现消息过滤器功能的方法。通常,您需要返回一个布尔值。

  • @Transformer。这个注释标记了一个方法来完成转换消息、消息头和/或有效负载的功能。

  • @ServiceActivator。这个注释标记了一个能够处理消息的方法。

要运行这个例子,注释掉@ImportResource注释。就这样。您应该有类似于以下输出的日志。

 ...
INFO 43940 - [main] c.a.todo.TodoIntegrationApplication      : Started TodoIntegrationApplication in 1.002 seconds (JVM running for 1.625)

INFO 43940 - [main] i.ToDoIntegration$SimpleServiceActivator : BUY MILK TODAY

...

使用 JavaConfig

JavaConfig 与我们刚刚做的非常相似。我们接下来要做的是改变流程的最后一部分。因此,注释掉SimpleServiceActivator内部类消息端点,并用下面的代码替换它。

@Bean
@ServiceActivator(inputChannel = "toLog")
public LoggingHandler logging() {
    LoggingHandler adapter = new
                  LoggingHandler(LoggingHandler.Level.INFO);
    adapter.setLoggerName("SIMPLE_LOGGER");
    adapter.setLogExpressionString
("headers.id + ': ' + payload");
    return adapter;
}

这段代码创建了一个LoggingHandler对象,它实际上是 XML 从logging-channel-adapter标签生成的同一个对象。它记录了带有报头 ID 和有效载荷的SIMPLE_LOGGER消息,在本例中是“今天买牛奶”消息。

同样,我知道这只是一个微不足道的例子,但至少它让您了解了 Spring Integration 是如何工作的,以及如何配置它。客户经常问我是否有可能混合配置。绝对的!我们很快就会看到这一点。

文件集成待办事项

接下来,我们来看看如何整合文件读取。集成系统是一项非常常见的任务。这是最常用的用例之一。让我们首先创建一个ToDoProperties类来帮助外部属性读取文件的路径和名称(参见清单 11-7 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix="todo")
public class ToDoProperties {

    private String directory;
    private String filePattern;

}

Listing 11-7com.apress.todo.config.ToDoProperties.java

正如你在清单 11-7 中看到的,没有什么新的东西。因为这个应用从文件中读取,所以需要创建一个转换器来读取一个字符串条目,解析它,并返回一个新的ToDo实例。创建ToDoConverter类(参见清单 11-8 )。

package com.apress.todo.integration;

import com.apress.todo.domain.ToDo;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
public class ToDoConverter implements Converter<String, ToDo> {
    @Override
    public ToDo convert(String s) {
        List<String> fields = Stream.of(s.split(",")).map(String::trim).collect(Collectors.toList());
        return new ToDo(fields.get(0),Boolean.parseBoolean(fields.get(1)));
    }
}

Listing 11-8com.apress.todo.integration.ToDoConverter.java

清单 11-8 没有什么特别的。这里唯一的要求是实现通用的Converter接口。我会在下一节讲。另一个必要的类是处理ToDo实例的处理器。创建ToDoMessageHandler类(参见清单 11-9 )。

package com.apress.todo.integration;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ToDoMessageHandler {
    private Logger log = LoggerFactory.getLogger(ToDoMessageHandler.class);

    public void process(ToDo todo){
        log.info(">>> {}", todo);
         // More process...
    }
}

Listing 11-9com.apress.todo.integration.ToDoMessageHandler.java

清单 11-9 是一个简单的 POJO 类;接收ToDo实例的方法。

接下来,让我们创建主流程。创建ToDoFileIntegration类(参见清单 11-10 )。

package com.apress.todo.integration;

import com.apress.todo.config.ToDoProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.file.dsl.Files;
import org.springframework.integration.file.splitter.FileSplitter;

import java.io.File;

@EnableConfigurationProperties(ToDoProperties.class)
@Configuration
public class ToDoFileIntegration {

    private ToDoProperties props;
    private ToDoConverter converter;

    public ToDoFileIntegration(ToDoProperties props,
ToDoConverter converter){
        this.props = props;
        this.converter = converter;
    }

    @Bean
         public IntegrationFlow fileFlow(){
              return IntegrationFlows
                .from(
            Files.inboundAdapter(
                        new File(this.props.getDirectory()))
                 .preventDuplicates(true)
                  .patternFilter(this.props.getFilePattern())
                        , e ->
                              e.poller(Pollers.fixedDelay(5000L))
                )
                .split(Files.splitter().markers())
                .filter(
                  p -> !(p instanceof FileSplitter.FileMarker))
                .transform(Transformers.converter(converter))

                .handle("toDoMessageHandler","process")
                .get();

    }
}

Listing 11-10com.apress.todo.integration.ToDoFileIntegration.java

清单 11-10 显示了主要的集成流程,它读取文件的内容(从文件系统中),将内容转换成对象(在本例中,使用ToDoConverter类转换成ToDo对象),并处理任何额外逻辑的消息。我们来详细分析一下这个。

  • from。这是一个重载的方法,通常传递MessageSource;在这种情况下,我们传递两个值:Files.inboundAdapter(我将在下面解释)和一个接收SourcePollingChannelAdapterSpec的消费者;在本例中,我们使用一个 lambda 表达式,通过使用Pollers类,每 5 秒钟在文件系统中轮询一次新文件。

  • Files. This is a protocol adapter that works out of the box; you just need to configure it. This adapter is used to pick up files from the file system. The Files class belongs to the Spring Integration Java DSL and provides several useful methods:

    • inboundAdapter。这个适配器带来了一个返回FileInboundChannelAdapterSpec的流畅 API,该 API 有如下方法

    • preventDuplicates。这意味着您可以通过将此设置为 true 来避免多次读取同一个文件。

    • patternFilter。这将查找命名模式的文件。

    在这个例子中,我们从目录(从todo.directory属性值)和基于模式的名称(从todo.file-pattern属性值)中读取,两者都来自ToDoProperties类。

  • split。该方法调用表明所提供的参数(可能是 bean、服务、处理程序等。)可以拆分单个消息或消息有效载荷,并产生多个消息或有效载荷;在这种情况下,我们使用的是FileMarker,当有顺序文件进程时,它对文件数据进行定界。

  • filter。因为我们使用标记来查看每个消息的开始和结束,我们接收文件的内容作为FileMarker开始,然后是实际内容,最后是FileMarker结束,所以这就是为什么我们在这里说,“传递给我有效载荷或内容,而不是标记。”

  • transform。这里我们使用了一个带有几个实现的Transformers类来转换消息和转换器(一个定制的转换器,ToDoConverter类,参见清单 11-8 )。

  • handle。这里,我们使用一个类来处理消息,将 bean 的名称(toDoMessageHandler)和处理消息过程的方法作为第一个参数传递(查看ToDoMessageHandler类中的代码,参见清单 11-9 )。ToDoMessageHandler类是一个使用@Component注释标记的 POJO。

注意

Spring Integration Java DSL 支持(暂时)以下协议适配器类:AMQP、JMS、文件、SFTP、FTP、HTTP、Kafka、邮件、脚本和 Feed。这些类在 org . spring framework . integration . DSL . *包中。

application.properties中,添加以下内容。

todo.directory=/tmp/todo
todo.file-pattern=list.txt

当然,您可以添加任何目录和/或文件模式。list.txt可以是你想要的任何东西。如果您查看了ToDoConverter,它只需要两个值:描述和布尔值。所以,list.txt的文件是这样的:

buy milk today, true
read a book, false
go to the movies, true
workout today, false
buy some bananas, false

要运行代码,注释掉ToDoIntegration类中的所有代码。一旦您运行它,您应该得到类似于下面的输出。

INFO 47953 - [           main] c.a.todo.TodoIntegrationApplication      : Started TodoIntegrationApplication in 1.06 seconds (JVM running for 1.633)
INFO 47953 - [ask-scheduler-1] c.a.todo.integration.ToDoMessageHandler  : >>> ToDo(id=3037a45b-285a-4631-9cfa-f89251e1a634, description=buy milk today, created=2018-09-01T19:29:38.309, modified=2018-09-01T19:29:38.310, completed=true)
INFO 47953 - [ask-scheduler-1] c.a.todo.integration.ToDoMessageHandler  : >>> ToDo(id=7eb0ae30-294d-49d5-92e2-d05f88a7befd, description=read a book, created=2018-09-01T19:29:38.320, modified=2018-09-01T19:29:38.320, completed=false)
INFO 47953 - [ask-scheduler-1] c.a.todo.integration.ToDoMessageHandler  : >>> ToDo(id=5380decb-5a6f-4463-b4b6-1567361c37a7, description=go to the movies, created=2018-09-01T19:29:38.320, modified=2018-09-01T19:29:38.320, completed=true)
INFO 47953 - [ask-scheduler-1] c.a.todo.integration.ToDoMessageHandler  : >>> ToDo(id=ac34426f-83fc-40ae-b3a3-0a816689a99a, description=workout today, created=2018-09-01T19:29:38.320, modified=2018-09-01T19:29:38.320, completed=false)
INFO 47953 - [ask-scheduler-1] c.a.todo.integration.ToDoMessageHandler  : >>> ToDo(id=4d44b9a8-92a1-41b8-947c-8c872142694c, description=buy some bananas, created=2018-09-01T19:29:38.320, modified=2018-09-01T19:29:38.320, completed=false)

正如您所看到的,这是一种非常简单的方式来集成文件、读取其内容以及使用数据进行任何业务逻辑。

还记得之前我告诉过你,你可以混合配置 Spring Integration 的方式吗?那么,如果您想使用实际的注释来处理消息,您需要做什么呢?您可以使用@ServiceActivator注释作为配置的一部分。

@ServiceActivator(inputChannel="input")
public void process(ToDo message){

}

要使用这种服务激活器方法,您需要更改流程。替换此行:

handle("toDoMessageHandler","process")

有了这个:

.channel("input")

如果您重新运行该示例,您会得到相同的结果。您是否意识到没有定义输入通道?最棒的是,Spring Integration 发现您需要这个通道,并在幕后为您创建了一个通道。

它需要一本完整的书来解释所有 Spring Integration 的好处;当然,这本书是一本入门书——对集成多个系统的能力的基本介绍。

SpringCloudStream

到目前为止,您已经看到了所有可用的消息传递技术,使用 Spring 框架和 Spring Boot 可以让开发人员和架构师轻松创建非常健壮的消息传递解决方案。在这一节中,我们向前迈出了新的一步;我们进入云原生应用开发,这是下一章的介绍。

在下一节中,我将讨论 Spring Cloud Stream 以及这项新技术如何帮助我们编写消息驱动的微服务应用。

SpringCloud

在我开始说 Spring Cloud Stream 内部和用法之前,先说一下它的保护伞项目 Spring Cloud。

Spring Cloud 是一组工具,允许开发人员创建使用分布式系统中所有常见模式的应用:配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、分布式会话、服务对服务调用、分布式消息传递等等。

基于 Spring Cloud 有几个项目,包括 Spring Cloud Config、Spring Cloud 网飞、Spring Cloud Bus、Spring Cloud for Cloud Foundry、Spring Cloud Cluster、Spring Cloud Stream、Spring Cloud Stream App Starters。

如果您想从这些技术开始,如果您使用的是 Maven,那么您可以将以下部分和依赖项添加到您的pom.xml文件中。

  • 添加带有 GA 版本的<dependencyManagement/>标签;例如

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    
  • <dependencies/>标签中添加您想要使用的技术;例如,

    <dependencies>
     <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
     </dependency>
    
     <!—MORE Technologies here -->
    
    </dependencies>
    
    

如果您使用的是 Gradle,您可以将以下内容添加到您的build.gradle文件中。

ext {
      springCloudVersion = 'Finchley.SR1'
}
dependencyManagement {
         imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
}

dependencies {
      // ...
      compile ('org.springframework.cloud:spring-cloud-starter-stream-rabbit')
      // ...
}

如果深入研究 Spring Cloud 注释的pom.xml文件,您会发现命名约定现在是spring-cloud-starter-<technology to use>。还要注意,我们添加了一个依赖管理标签,允许您处理传递性依赖和库版本管理。

SpringCloudStream

该说 SpringCloudStream 了。为什么我没有介绍其他技术?它有什么特别之处?嗯,Spring Cloud Stream 是一个轻量级的消息驱动的微服务框架,基于 Spring Integration 和 Spring Boot(提供自以为是的运行时,便于配置)。您可以轻松创建企业就绪的消息传递和集成解决方案应用。它为使用 RabbitMQ 或 Apache Kafka 发送和接收消息提供了一个简单的声明性模型。

我认为 Spring Cloud Stream 最重要的特性之一是通过创建可以开箱即用的绑定来分离生产者和消费者之间的消息传递。换句话说,您不需要向您的应用添加任何特定于代理的代码来生成或使用消息。向您的应用添加所需的绑定(我将在后面解释)依赖项,Spring Cloud Stream 负责消息传递连接和通信。

所以,让我们来看看 Spring Cloud Stream 的主要组件,并学习如何使用这个框架。

SpringCloudStream 概念

以下是 SpringCloudStream 的主要组成部分。

  • 应用模型。应用模型是一个中间件中立的核心,这意味着应用通过 binder 实现使用输入和输出通道与外部代理通信(作为一种传输消息的方式)。

  • 活页夹抽象。Spring Cloud Stream 提供了 Kafka 和 RabbitMQ 绑定器实现。这种抽象使得 Spring Cloud Stream 应用连接到中间件成为可能。但是,这个抽象是如何知道目的地的呢?它可以在运行时根据频道动态选择目的地。通常,我们需要通过application.properties文件将它作为spring.cloud.stream.bindings.[input|ouput].destination属性提供。当我们看例子的时候,我会讨论这个问题。

  • 持续发布/订阅。应用通信是通过众所周知的发布/订阅模型进行的。如果使用 Kafka,它遵循自己的主题/订阅者模型,如果使用 RabbitMQ,它为每个队列创建一个主题交换和必要的绑定。这个模型降低了生产者和消费者的复杂性。

  • 消费群体。你发现你的消费者可能需要在某个时候能够扩大规模。这就是为什么可伸缩性是使用消费者组的概念来实现的(这类似于 Kafka 消费者组特性),其中您可以在一个组中有多个消费者来实现负载平衡场景,这使得可伸缩性非常容易设置。

  • 分区支持。Spring Cloud Stream 支持数据分区,允许多个生产者向多个消费者发送数据,并确保公共数据由相同的消费者实例处理。这有利于数据的性能和一致性。

  • 绑定 API 。Spring Cloud Stream 提供了一个 API 接口——Binder SPI(服务提供者接口),您可以通过修改原始代码来扩展核心,因此很容易实现特定的 Binder,如 JMS、WebSockets 等。

在本节中,我们将更多地讨论编程模型和绑定器。如果你想了解更多关于其他概念的知识,可以看看 Spring Cloud Stream 参考。这个想法是向您展示如何开始使用 Spring Cloud Stream 创建事件驱动的微服务。为了向您展示我们将要涵盖的内容,请看图 11-4 。

img/340891_2_En_11_Fig4_HTML.jpg

图 11-4

SpringCloudStream 式应用

SpringCloudStream 编程

看图 11-4 创建一个 SpringCloudStream app 需要什么?

  • <dependencyManagement/>。您需要使用最新的 Spring Cloud 库依赖项添加这个标签。

  • 装订器。你需要选择你需要哪种活页夹。

    • 卡夫卡。如果您选择 Kafka 作为您的绑定器,那么如果您使用 Maven,您需要在您的pom.xml中添加以下依赖项。

      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-kafka</artifactId>
      </dependency>
      
      

      如果你使用的是 Gradle,那么你需要在build.gradle文件中添加以下依赖项。

      compile('org.springframework.cloud:spring-cloud-starter-stream-kafka')
      
      
    • RabbitMQ 。如果您选择 RabbitMQ 作为绑定器,那么如果您使用 Maven,您需要在您的pom.xml中添加以下依赖项。

      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
      </dependency>
      
      

      如果你使用的是 Gradle,那么你需要在build.gradle文件中添加以下依赖项。

      compile('org.springframework.cloud:spring-cloud-starter-stream-rabbit')
      
      

启动并运行 Kafka 或 RabbitMQ。你能同时使用两者吗?是的,你可以。您可以在application.properties文件中配置它们。

  • @EnableBinding。这是一个 Spring Boot 应用,所以添加@EnableBinding足以将应用转换为 Spring Cloud 流。

在接下来的小节中,我们使用 RabbitMQ 作为传输层,在不知道任何关于代理 API 的细节或者如何配置生产者或消费者消息的情况下,从一个应用向另一个应用发送和接收消息。

Spring Cloud Stream 使用通道(输入/输出)作为发送和接收消息的机制。Spring Cloud Stream 应用可以有任意数量的通道,因此它定义了两个注释,@Input@Output,用于标识消费者和生产者。通常情况下,SubscribableChannel类用@Input标注来监听传入的消息,MessageChannel类用@Output标注来发送消息。

还记得我跟你说过,SpringCloudStream 是基于春集成的吗?

如果您不想直接处理这些通道和注释,Spring Cloud Stream 通过添加三个接口来简化事情,这三个接口涵盖了最常见的消息传递用例:SourceProcessorSink。在幕后,这些接口拥有您的应用需要的通道(输入/输出)。

  • SourceSource用于从外部系统获取数据的应用中(通过监听队列、REST 调用、文件系统、数据库查询等)。)并通过输出通道发送它。这是来自 Spring Cloud Stream 的实际界面:

    public interface Source {
    
      String OUTPUT = "output";
    
      @Output(Source.OUTPUT)
      MessageChannel output();
    
    }
    
    
  • Processor。当您想要开始监听来自输入通道的新的输入消息,对接收的消息进行处理(增强、转换等)时,您可以在应用中使用Processor。),并向输出通道发送新消息。这是 Spring Cloud Stream 中的实际界面:

    public interface Processor extends Source, Sink {
    
    }
    
    
  • Sink。当您想要开始监听来自输入通道的新消息、进行处理和结束流程(保存数据、启动任务、登录控制台等)时,可以使用Sink应用。).这是 Spring Cloud Stream 中的实际界面:

    public interface Sink {
    
      String INPUT = "input";
    
      @Input(Sink.INPUT)
      SubscribableChannel input();
    
    }
    
    

图 11-5 和 11-6 是我们使用的模型。

img/340891_2_En_11_Fig6_HTML.jpg

图 11-6

源➤处理器➤接收器

img/340891_2_En_11_Fig5_HTML.jpg

图 11-5

源➤汇

带 SpringCloudStream 的 ToDo App

这个项目的目的是展示如何创建一个Source接口,并通过它的输出通道发送消息;一个Processor接口以及如何分别从输入和输出通道接收和发送消息;和一个Sink接口以及如何从输入通道接收消息。我展示的是图 11-6 描述的内容,但每次只展示一个流式应用。

目前,这些应用之间的通信是手动的,这意味着我们需要在它们之间执行一些步骤,因为我希望您了解每个应用是如何工作的。在下一节中,我们将看到整个流程是如何工作的。

您可以从头开始,也可以跟随下一部分来了解您需要做什么。如果您是从零开始,那么您可以转到 Spring Initializr 并向字段中添加以下值。

  • 组:com.apress.todo

  • 神器:todo-cloud

  • 名称:todo-cloud

  • 包名:com.apress.todo

  • 依赖关系:Cloud Stream, Lombok

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 11-7 )。

img/340891_2_En_11_Fig7_HTML.jpg

图 11-7

Spring 初始化 zr

您可以使用之前的todo-integration项目中的ToDo域类(参见清单 11-1 )。

来源

我们将从定义一个Source开始。记住这个组件有一个输出通道。创建ToDoSource类。它应该看起来像清单 11-11 。

package com.apress.todo.cloud;

import com.apress.todo.domain.ToDo;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.support.MessageBuilder;

@EnableBinding(Source.class)

public class ToDoSource {

    @Bean
    @InboundChannelAdapter(channel=Source.OUTPUT)
    public MessageSource<ToDo> simpleToDo(){
        return () -> MessageBuilder
                .withPayload(new ToDo("Test Spring Cloud Stream"))
                .build();
    }

}

Listing 11-11com.apress.todo.cloud.ToDoSource.java

清单 11-11 展示了你可以拥有的最简单的Source流式应用。让我们来看看。

  • @EnableBinding。该注释将该类作为 Spring Cloud Stream 应用,并通过提供的绑定器对发送或接收消息进行必要的配置。

  • Source。该接口将 Spring Cloud Stream app 标记为Source流。它创造了必要的渠道;在这种情况下,输出通道向绑定器提供发送消息。

  • @InboundChannelAdapter。这个注释是 Spring Integration 框架的一部分。它每秒轮询一次simpleToDo方法,这意味着每秒发送一条新消息。您实际上可以通过添加轮询器和修改默认设置来更改消息的频率和数量;例如,

    @InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "5000", maxMessagesPerPoll = "2"))
    
    

    这个声明中的重要部分是通道,这里指向Source.OUTPUT意味着它使用输出通道(MessageChannel output())。

  • MessageSource。这是一个发回Message<T>的接口,它是一个包含有效载荷和报头的包装器。

  • MessageBuilder。你已经熟悉这个类了,它发送一个MessageSource类型;在这种情况下,我们发送一个ToDo实例消息。

在运行这个例子之前,请记住有必要拥有 binder 依赖项,因为我们将使用 RabbitMQ,所以如果您使用 Maven,有必要将它添加到您的pom.xml文件中。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

如果您使用的是 Gradle,将下面的依赖项添加到您的build.gradle文件中。

compile('org.springframework.cloud:spring-cloud-starter-stream-rabbit')

确保您已经启动并运行了 RabbitMQ。接下来,运行示例。你可能看不到多少,但它确实在做一些事情。现在,我们进入 RabbitMQ。

  1. 在浏览器中打开 RabbitMQ Web 管理。转到http://localhost:15672。用户名是guest,密码是guest

  2. Go to the Exchanges tab (see Figure 11-8).

    img/340891_2_En_11_Fig8_HTML.jpg

    图 11-8

    RabbitMQ 交换选项卡

    请注意,创建了一个输出(主题交换),消息速率为 1.0/s。

  3. Next, let’s bind this exchange to a queue; but first, let’s create a queue. Go to the Queues tab and create a new queue named my-queue (see Figure 11-9).

    img/340891_2_En_11_Fig9_HTML.jpg

    图 11-9

    创建队列:我的队列

  4. Once the queue is created, it appears in the list. Click my-queue. Go to the Bindings section and add the binding. See Figure 11-10 for the values.

    img/340891_2_En_11_Fig10_HTML.jpg

    图 11-10

    绑定器

  5. 在 From Exchange 字段中填入值 output (这是交换的名称)。路由关键字字段的值为 # ,允许任何消息进入我的队列。

  6. After you bind the output exchange to my-queue, you start seeing several messages. Open the Overview panel (see Figure 11-11).

    img/340891_2_En_11_Fig11_HTML.jpg

    图 11-11

    概观

  7. Let’s review a message by opening the Get Messages panel. You can get any number of messages and see its contents (see Figure 11-12).

    img/340891_2_En_11_Fig12_HTML.jpg

    图 11-12

    获取消息

如果您选择了几条消息,请查看有效负载。你每秒钟都有一条信息。(注意,默认格式是 JSON 有效负载。还要注意消息有属性,比如带有contentType: application/jsondelivery_mode: 2的头,这意味着消息正在被持久化。Spring Cloud Stream 和它的 binder 就是这样连接 RabbitMQ 发布消息的。

如果您看一下消息,您会看到日期和所有细节都暴露出来了。

{"id":"68d4100a-e706-4a51-a254-d88545ffe7ef","description":"Test Spring Cloud Stream","created":{"year":2018,"month":"SEPTEMBER","hour":21,"minute":9,"second":5,"nano":451000000,"monthValue":9,"dayOfMonth":2,"dayOfWeek":"SUNDAY","dayOfYear":245,"chronology":{"id":"ISO","calendarType":"iso8601"}},"modified":{"year":2018,"month":"SEPTEMBER","hour":21,"minute":9,"second":5,"nano":452000000,"monthValue":9,"dayOfMonth":2,"dayOfWeek":"SUNDAY","dayOfYear":245,"chronology":{"id":"ISO","calendarType":"iso8601"}},"completed":false}

您可以看到非常冗长的日期序列化,但是如果您使用 Maven,可以通过在pom.xml文件中添加以下依赖项来改变这种情况。

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

如果您正在使用 Gradle,将下面的依赖项添加到build.gradle文件中。

compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')

重新运行应用。现在,您应该会看到以下消息。

{"id":"37be2854-91b7-4007-bf3a-d75c805d3a0a","description":"Test Spring Cloud Stream","created":"2018-09-02T21:12:12.415","modified":"2018-09-02T21:12:12.416","completed":false}

处理器

该部分使用一个Listener作为通道输入(所有新的输入消息都到达这里)。它得到一个ToDo消息。它转换为大写描述,将 ToDo 标记为已完成,然后将其发送到输出通道。

创建ToDoProcessor类。它应该看起来像清单 11-12 。

package com.apress.todo.cloud;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.messaging.handler.annotation.SendTo;

import java.time.LocalDateTime;

@EnableBinding(Processor.class)

public class ToDoProcessor {

    private Logger log = LoggerFactory.getLogger(ToDoProcessor.class);

    @StreamListener(Processor.INPUT)
         @SendTo(Processor.OUTPUT)
    public ToDo transformToUpperCase(ToDo message) {
        log.info("Processing >>> {}", message);
        ToDo result = message;
        result.setDescription(message.getDescription().toUpperCase());
        result.setCompleted(true);
        result.setModified(LocalDateTime.now());
        log.info("Message Processed >>> {}", result);
        return result;
    }
}

Listing 11-12com.apress.todo.cloud.ToDoProcessor.java

清单 11-12 显示了一个简单的Processor流。我们来复习一下。

  • @EnableBinding。该注释使该类成为 Spring Cloud Stream 应用。它支持通过提供的绑定器发送或接收消息的必要配置。

  • Processor。该接口将 Spring Cloud Stream app 标记为Processor流。它创造了必要的渠道;在这种情况下,输入通道(用于监听新的传入消息)和输出通道(用于将消息发送到提供的绑定器)。

  • @StreamListener。这个注释是 Spring Cloud Stream 框架的一部分,非常类似于@RabbitListener或者@JmsListener。它在Processor.INPUT信道(SubscribableChannel input())中监听新的输入消息。

  • @SendTo。您已经知道这个注释;它与前一章中使用的是同一个。它的任务是一样的;你可以看做是回复,也可以看做是制作人。它向Processor.OUTPUT通道(MessageChannel output())发送一条消息。

我认为这是一个关于你可以用一个Processor流做什么的平凡但很好的例子。所以在运行它之前,请确保从ToDoSource类中注释掉@EnableBinding注释,并删除输出交换和my-queue队列。

运行示例。还是那句话,应用没有做太多,但是让我们去 RabbitMQ web management。

  1. 进入你的浏览器,点击http://localhost:15672(用户名:guest,密码:guest)。

  2. Click the Exchanges tab, and you see the same output exchange and a new input exchange being created. Remember that the Processor stream uses input and output channels (see Figure 11-13).

    img/340891_2_En_11_Fig13_HTML.jpg

    图 11-13

    交换

    请注意,现在在任何新的交换中都没有消息速率。

  3. Next, go to the Queues tab. You notice a new queue named input.anonymous and random text has been created (see Figure 11-14).

    img/340891_2_En_11_Fig14_HTML.jpg

    图 11-14

    行列

    就这样。ToDoProcessor 流创建输出交换和input.anonymous.*队列,这意味着流连接到绑定器,在本例中是 RabbitMQ。现在的问题是如何传递信息,对吗?有不同的方法可以做到这一点:使用 RabbitMQ 模拟消息,或者以编程方式实现。我们将双管齐下。

    我们将创建一个名为my-queue的队列,并将其绑定到输出,这与我们在Source流部分中所做的非常相似。

  4. 转到 Queues 选项卡,创建一个名为my-queue的队列,并用路由关键字#将其绑定到输出交换。这类似于Source流中的步骤 2 和 3。还要注意的是,input.anonymous.*队列绑定到输入交换。

  5. 现在,我们将使用输入交换发送一条消息。转到交换选项卡。单击输入交换并选择发布消息面板。

  6. Enter the following in the Payload field.

    {"id":"37be2854-91b7-4007-bf3a-d75c805d3a0a","description":"Test Spring Cloud Stream","created":"2018-09-02T21:12:12.415","modified":"2018-09-02T21:12:12.416","completed":false}
    
    

    Enter content-type=application/json in the Properties field (see Figure 11-15).

    img/340891_2_En_11_Fig15_HTML.jpg

    图 11-15

    发布消息

    然后单击“发布消息”按钮。它应该显示为“消息已发布”的消息

img/340891_2_En_11_Fig16_HTML.jpg

图 11-16

获取消息

  1. Next, take a look at the app’s logs. You should have something similar to the following output.

    ...
    Processing >>> ToDo(id=37be2854-91b7-4007-bf3a-d75c805d3a0a, description=Test Spring Cloud Stream, created=2018-09-02T21:12:12.415, modified=2018-09-02T21:12:12.416, completed=false)
    Message Processed >>> ToDo(id=37be2854-91b7-4007-bf3a-d75c805d3a0a, description=TEST SPRING CLOUD STREAM, created=2018-09-02T21:12:12.415, modified=2018-09-02T21:54:55.048, completed=true)
    ...
    
    

    如果你看一看my-queue队列,并得到消息,你应该看到几乎相同的结果(见图 11-16 )。

这很简单,但不是正确的方法。您永远不会使用 RabbitMQ 控制台发送消息,除了一个小测试。

我提到过我们能够以编程方式发送消息。创建ToDoSender类(参见清单 11-13 )。

package com.apress.todo.sender;

import com.apress.todo.domain.ToDo;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

@Configuration
public class ToDoSender {

    @Bean
    public ApplicationRunner send(MessageChannel input){
        return args -> {
            input
                 .send(MessageBuilder
                 .withPayload(new ToDo("Read a Book"))
                 .build());

        };
    }
}

Listing 11-13com.apress.todo.sender.ToDoSender.java

如果您运行应用,现在您有一个用大写字母描述的 ToDo,并在日志和my-queue队列中设置为完成。如你所见,我们使用了一个你从 Spring Integration 中了解到的类,并使用了MessageChannel接口。有意思的是,Spring 知道该从哪个渠道注入。记住,@Processor注释公开了输入通道。

水槽

Sink流创建一个输入通道来监听新的输入消息。让我们创建ToDoSink类(参见清单 11-14 )。

package com.apress.todo.cloud;

import com.apress.todo.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;

@EnableBinding(Sink.class)

public class ToDoSink {

    private Logger log = LoggerFactory.getLogger(ToDoSink.class);

    @StreamListener(Sink.INPUT)
    public void process(ToDo message){
        log.info("SINK - Message Received >>> {}",message);
    }

}

Listing 11-14com.apress.todo.cloud.ToDoSink.java

清单 11-14 显示了一个Sink流,您已经知道了注释。@EnableBinding将这个类转换成一个Source流,并通过@StreamListenerSink.INPUT通道监听新的输入消息。Sink.INPUT创建一个输入通道(SubscribableChannel input())。

如果您使用清单 11-13 注释掉ToDoProcessor类中的@EnableBinding并运行应用,看看 RabbitMQ 管理,您会看到输入交换和input.anonymous.*被创建并相互绑定。对于Sink日志,您应该得到相同的 ToDo。

请记住,接收流对收到的消息做了额外的工作,但它结束了流。

到目前为止,我所解释的并没有太多概念上的探索,实际上这是我的意图,因为我想让你们了解内部是如何工作的。现在,让我们使用一个真实的场景,其中我们实际上创建了一个完整的流,并查看这些流如何在不进入 RabbitMQ 管理的情况下相互通信。

微服务

我想谈谈这种使用微服务创建可扩展和高可用性应用的新方法。本节最重要的部分是使用消息传递在流之间进行通信的能力。最后,您应该将每个流(源、处理器和接收器)视为一个微服务。

待办事项:一个完整的流程

让我们列出这个新的 ToDo 应用的一些要求。

  • 创建一个Source来读取从文件中声明的任何 ToDo,并过滤那些已完成的 ToDo 实例。

  • 创建一个接受 ToDo 并创建文本消息的Processor

  • 创建一个接收文本并向收件人发送电子邮件的Sink

你认为你能做到吗?参见图 11-17 。

img/340891_2_En_11_Fig17_HTML.jpg

图 11-17

所有的流量

图 11-17 显示真实流程(注意每个部分都是独立的 app)。换句话说,你创建了todo-sourcetodo-processortodo-sink

看看第十一章的源代码,找到每个项目。这是你的家庭作业。让它们发挥作用。根据您的设置更改属性,在本例中,是在todo-sink项目中。

SpringCloudStream 式应用首发

如果我告诉你,我们可以避免创建前面的例子,而使用 Spring Cloud Stream 应用启动器,会怎么样?

Spring Cloud Stream 提供了开箱即用的应用启动器。Spring Cloud 团队已经实现了大约 52 个应用,您可以下载、配置和执行它们。这些应用启动器按SourceProcessorSink型号划分。

  • Source:文件、ftp、gemfire、gemfire-cq、http、jdbc、jms、负载生成器、日志聚合器、邮件、mongodb、rabbit、s3、sftp、syslog、tcp、tcp-client、时间、触发器、triggertask、twitterstream

  • Processor:桥、过滤器、groovy-过滤器、groovy-转换、httpclient、pmml、脚本化转换、分离器、TCP-客户端、转换等等

  • Sink : aggregate-counter、cassandra、counter、field-value-counter、file、ftp、gemfire、gpfdist、hdfs、hdfs-dataset、jdbc、log、rabbit、redis-pubsub、router、s3、sftp、task-launcher-local、task-launcher-yarn、tcp、throughput、websocket 等等

注意

如果您需要获得最新版本的应用启动器,您可以从 http://repo.spring.io/libs-release/org/springframework/cloud/stream/app/ 获得。

如果你想使用其他的 Spring Cloud Stream 应用启动器,看看它们的配置,看看 http://docs.spring.io/spring-cloud-stream-app-starters/docs/current/reference/html/ 作为参考。

摘要

在本章中,您学习了如何在 Spring Boot 中使用 Spring Integration 和 Spring Cloud Stream。

您了解了 Spring Integration 如何帮助您创建可以与其他系统集成的健壮且可伸缩的应用。

您了解了 Spring Cloud Stream 如何提供轻松创建微服务的工具。你学会了如何使用这个框架和任何你想要的传输方法。它是一个不可知的传输协议框架,隐藏了所有的消息细节;换句话说,你不需要学习 RabbitMQ 或者 Kafka 来使用这个框架。

在下一章,你将看到 Spring Boot 如何生活在云中。

十二、云中的 Spring Boot

云计算是 IT 行业最重要的概念之一。希望走在最新技术前沿的公司希望通过提高服务速度来实现快速发展。他们希望在客户不知情的情况下,尽可能快地从错误或失误中恢复,从而确保安全。他们希望通过水平扩展(通常是指向外扩展基础架构容量,例如产生更多服务器来分担负载)而不是垂直扩展(指增加可用资源(CPU、内存、磁盘空间等)的能力)来实现可扩展性。)对于像服务器这样的现有实体)。但是什么样的技术可以提供所有这些概念呢?

术语云原生架构开始出现。它允许开发人员轻松地遵循提供速度、安全性和可伸缩性的模式。在本章中,我将向您展示如何通过遵循其中的一些模式来创建和部署云的 Spring Boot 应用。

云和云原生架构

我想你听说过这些公司:Pivotal、亚马逊、谷歌、Heroku、网飞和优步。他们正在应用我提到的所有概念。但是,这些公司如何同时做到快速、安全和可扩展呢?

云计算的第一批先驱之一是亚马逊,它开始使用虚拟化作为主要工具来创建资源弹性;这意味着任何部署的应用都可以通过增加虚拟机器、内存、处理器等的数量来获得更强的计算能力,而不需要任何 IT 人员的参与。所有这些扩展应用的新方法都是满足不断增长的用户需求的结果。

网飞如何满足他们所有的用户需求?我们谈论的是每天都在播放流媒体内容的数百万用户。

所有这些公司都拥有云时代所需的 IT 基础设施,但是您不认为任何想要成为云的一部分的应用都需要适应这种新技术吗?您需要开始考虑扩展资源会如何影响应用。您需要开始更多地考虑分布式系统,对吗?在这种环境中,应用如何与遗留系统进行通信,或者如何在它们之间进行通信。如果您的一个系统出现故障,会发生什么情况?怎么恢复?用户(如果数百万)如何利用云?

新的云原生架构回答了所有这些问题。请记住,您的应用需要快速、安全和可伸缩。

首先,您需要在这个新的云环境中具有可见性,这意味着您需要有一种更好的方法来监控您的应用—设置警报、拥有仪表板等等。您需要故障隔离和容错,这意味着应用是上下文相关的,并且应用之间不应该有任何依赖关系。如果您的一个应用宕机,其他应用应该会继续运行。如果您正在持续部署一个应用,它不应该影响整个系统。这意味着您需要考虑自动恢复,即整个系统能够识别故障并进行恢复。

十二因素应用

Heroku 的工程师们确定了许多模式,这些模式成为了十二因素应用指南( https://12factor.net )。本指南展示了一个应用(一个单独的单元)如何需要关注声明性配置、无状态和独立于部署。您的应用需要快速、安全和可伸缩。

以下是十二要素应用指南的总结。

  • 代码库。在 VCS 追踪到一个代码库/多个部署。一个应用有一个被版本控制系统(VCS)跟踪的单一代码库,比如 Git、Subversion、Mercurial 等等。您可以为开发、测试、试运行和生产环境进行许多部署(从相同的代码库)。

  • 依赖关系。显式声明并隔离依赖关系。有时环境没有互联网连接(如果是私有系统),所以您需要考虑打包您的依赖项(jar、gem、共享库等)。).如果您有一个库的内部存储库,那么您可以声明一个清单,比如 poms、gemfile、bundles 等等。永远不要依赖你最终环境中的一切。

  • 配置。在环境中存储配置。你不应该硬编码任何变化的东西。使用环境变量或配置服务器。

  • 后台服务。将后台服务视为附属资源。通过 URL 或配置连接到服务。

  • 构建,发布,运行。严格分离构建和运行阶段。与 CI/CD 相关(持续集成,持续交付)。

  • 流程。将应用作为一个或多个无状态进程执行。进程不应该存储内部状态。不分享任何东西。任何必要的状态都应被视为后备服务。

  • 端口绑定。通过端口绑定导出服务。您的应用是独立的,这些应用通过端口绑定公开。一个应用可以成为另一个应用的服务。

  • 并发。通过流程模型向外扩展。通过添加更多应用实例进行扩展。单个进程可以自由多线程化。

  • 一次性。快速启动和平稳关闭,最大限度地提高稳定性。进程应该是可处置的(记住,它们是无状态的)。容错。

  • 环境平价。让开发、试运行和生产环境尽可能相似。这是高质量的结果,并确保连续交货。

  • 日志。将日志视为事件流。你的应用应该写到标准输出。日志是聚合的、按时间顺序排列的事件流。

  • 管理进程。作为一次性流程运行管理任务。在平台上运行管理流程:数据库迁移、一次性脚本等等。

微服务

术语微服务是一种创建应用的新方法。您需要将微服务视为一种将整体应用分解为不同且独立的组件的方式,这些组件遵循十二因素应用指南。展开时,它们工作(见图 12-1 )。

img/340891_2_En_12_Fig1_HTML.jpg

图 12-1

整体服务与微服务

我认为自从 UNIX 发明以来微服务就存在了,因为你可以使用命令行工具,例如 grep,它是一个很好地完成工作的单一单元。如果你将其中的几个命令(例如find . -name microservices.txt | grep -i spring-boot)组合起来,你可以创建一个更好的应用或系统。但是这些命令是相互独立的,通过 UNIX 管道(|)进行通信。在您的应用中也是如此。

微服务帮你加速开发。为什么呢?因为您可以指定一个小团队,使用遵循十二因素应用指南的有限上下文,只处理应用的一个特性。

关于微服务以及如何将现有架构迁移到微服务的指南有很多要说的,但这里的想法是探索 Spring Boot 并学习如何将其部署到云环境中。

将 ToDo 应用准备为微服务

要将 Spring Boot ToDo 应用转化为微服务,你需要做些什么?其实没什么!是的,没什么,因为 Spring Boot 是一种轻松创建微服务的方式。因此,您将使用相同的 ToDo 应用部署到云平台。哪个站台?Pivotal 云代工厂!

您可以从前面的章节中选择 todo-rest 项目。如果您修改了它,请检查它,并确保您可以运行它。

确保您拥有这些依赖关系是很重要的;如果您使用的是 Maven,那么在您的pom.xml文件中应该有以下依赖项。

...
<dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
</dependency>
<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
</dependency>
...

如果你使用的是 Gradle,看看你的build.gradle文件中是否有这些依赖项。

...
runtime('com.h2database:h2')
runtime('mysql:mysql-connector-java')
...

为什么这些依赖关系很重要?您将在接下来的章节中了解这一点。接下来,转到您的application.properties文件,并确保它像下面的内容。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

这里改变的是ddl-auto属性;在使用create-drop之前,它在会话结束时创建和销毁模式。并且您正在将这个属性更改为update,这意味着它会在必要时更新模式。这是有道理的,但请在接下来的部分中看到它的实际应用。

让我们通过执行下面的命令来准备应用,源代码在。(您也可以在 ide 中执行 Maven 目标或 Gradle 任务;查看文档了解如何操作。)如果你用的是 Maven,可以执行

$ ./mvnw clean package

如果你使用的是 Gradle,你可以执行

$ ./gradlew clean build

这些命令生成了即将部署的 JAR。所以,保管好它;我们要回去了。

注意

如果 ToDo 域类的 Java 构造函数有问题,您使用的是 Lombok 的旧版本(因为域类中有@NoArgsConstructor 注释)。Spring Boot 团队还没有更新这个库,所以使用 Lombok 版本 1.18.2 或更高版本。

Pivotal 云铸造厂

Cloud Foundry 从 2008 年就有了;它最初是 VMWare 的一个开源项目,然后在 2013 年转移到 Pivotal。此后,Cloud Foundry 一直是使用最多的开源 PaaS。Cloud Foundry 作为开源解决方案,拥有最大的社区支持。它得到了几家大型 It 公司的支持,包括 IBM(拥有 Bluemix)、微软、英特尔、SAP,当然还有 Pivotal(拥有 Pivotal Cloud Foundry——PAS 和 PKS)和 VMware。

Cloud Foundry 是唯一一个你可以毫无问题地下载和运行的开源解决方案。你可以找到两个版本的 Cloud Foundry:在 www.cloudfoundry.org 开源和在 http://pivotal.io/platform 的 Pivotal Cloud Foundry PAS 和 PKS(商业版)。如果你有兴趣下载商业版,你可以在 https://network.pivotal.io/products/pivotal-cf 下载,不需要任何试用,也没有时间限制。实际上,这是一个免费版本,但是如果您想要获得关于如何安装的支持或帮助,这时您需要联系 Pivotal 销售代表。

2018 年初,Pivotal 发布了平台 2.0 版本,为最终用户提供了更多选择。它将 Pivotal 应用服务(PAS)和 Pivotal 容器服务(PKS,基于 Kubernetes)解决方案推向市场(见图 12-2 )。

img/340891_2_En_12_Fig2_HTML.jpg

图 12-2

Pivotal 云铸造厂 2.x

在接下来的部分中,我将只介绍 PAS 和开始云原生开发的简单方法,因为您只需要关心您的应用、数据,其他什么都不需要关心!

PAS: Pivotal 应用服务

Pivotal 应用服务(PAS)构建于开放架构之上,它提供了以下功能。

  • 路由器。将传入流量路由到适当的组件,通常是云控制器或 DEA 节点上正在运行的应用。

  • 认证。OAuth2 服务器和登录服务器共同提供身份管理。

  • 云控制器。云控制器负责管理应用的生命周期。

  • 监控。监控、确定和协调应用,以确定它们的状态、版本和实例数量,并重定向到云控制器以采取措施纠正任何差异。

  • 花园/迭戈牢房。管理应用实例,跟踪已启动的实例,并广播状态消息。

  • Blob store 。资源、应用代码、构建包和 droplets。

  • 服务经纪人。当开发人员向应用提供和绑定服务时,service broker 负责提供服务实例。

  • 消息总线。Cloud Foundry 使用 NATS(不同于网络 NAT),这是一个轻量级的发布-订阅和分布式排队消息传递系统,用于组件之间的内部通信。

  • 记录和统计。指标收集器从组件收集指标。运营商可以使用这些信息来监控 Cloud Foundry 的实例。

PAS 功能

由 Cloud Foundry(开源)提供支持的 PAS,通过领先的应用和数据服务,在多个基础设施上提供交钥匙 PaaS 体验。

  • 基于 Cloud Foundry 开源的商业支持版本。

  • vSphere、vCloud Air、AWS、Microsoft Azure、Google Cloud 或 OpenStack 上的全自动部署、更新和一键式水平和垂直扩展,生产停机时间最短。

  • 即时横向应用层扩展。

  • 用于资源管理以及应用和服务管理的 Web 控制台。

  • 应用受益于内置服务,如负载平衡和 DNS、自动化健康管理、日志记录和审计。

  • 通过提供的 Java buildpack 支持 Java Spring。

  • Spring 框架的优化开发者体验。

  • 用于快速开发和测试的 MySQL 服务。

  • Pivotal 服务的自动应用绑定和服务供应,如 Pivotal RabbitMQ、Pivotal Redis、Pivotal Cloud Cache(基于 GemFire)和用于 Pivotal Cloud Foundry 的 MySQL。

开源版和商业版有什么区别?所有列出的特征。在开源版本中,您需要主要使用命令行手动完成所有事情(安装、配置、升级等)。),但在商业版中,您可以使用 web 控制台来管理您的基础架构和运行您的应用。要知道你可以在亚马逊 AWS、OpenStack、谷歌云、微软 Azure、vSphere 上安装 Cloud Foundry,这一点很重要。Pivotal Cloud Foundry (PAS 和 PKS)是 IaaS 不可知的!

使用 PWS /PAS

要使用 PWS/PAS,您需要在 Pivotal Web Services 的 https://run.pivotal.io 处开立一个账户。你可以得到一个试用账户(见图 12-3 )。

img/340891_2_En_12_Fig3_HTML.jpg

图 12-3

Pivotal Web 服务- run。枢轴。io

注册时,系统会提示您输入电话号码,您会收到一个开始试用的代码。它还要求您提供一个组织,可以是您的名字加一个-org;比如我的是fg-org。默认情况下,它会创建您将工作的空间(名为development)(见图 12-4 )。

img/340891_2_En_12_Fig4_HTML.jpg

图 12-4

Pivotal Web 服务

现在,您已经准备好部署应用了。默认情况下,因为它是一个试用帐户,您只有 2GB 的内存,但这足以部署 ToDo 应用。您可以浏览左侧的选项卡。

“工具”选项卡显示下载 CLI 工具(在下一节中安装)的链接,以及如何登录到 PWS 实例。

注意

在接下来的部分中,我不太清楚地使用了 PWS/PAS,但它指的是云计算。

Cloud Foundry CLI:命令行界面

在开始使用 PAS 之前,您必须安装一个命令行工具,该工具对于部署和执行许多其他任务非常有用。如果您使用的是 Windows 操作系统,您可以从 https://github.com/cloudfoundry/cli#downloads 获取最新版本,也可以使用工具选项卡(上一节)并根据您的操作系统进行安装。

如果你用的是 Mac OS/Linux,可以用 brew。

$ brew update
$ brew tap cloudfoundry/tap
$ brew install cf-cli

安装之后,您可以通过运行

$ cf --version
cf version 6.39.0+607d4f8be.2018-09-11

现在您已经准备好使用 Cloud Foundry 了。不用太担心。我将向您展示部署和运行 ToDo 应用的基本命令。

使用 CLI 工具登录 PWS/PAS

要登录到 PWS 和您的帐户,您可以执行以下命令。

 $ cf login -a api.run.pivotal.io
API endpoint: api.run.pivotal.io

Email> your-email@example.org

Password>
Authenticating...
OK

Targeted org your-org
Targeted space development

API endpoint:   https://api.run.pivotal.io (API version: 2.121.0)
User:           your-email@example.org
Org:            your-org
Space:          development

默认情况下,您被置于发展空间。您已经准备好对 PWS(一个 PAS 实例)执行创建、部署、扩展等命令。

将待办事项应用部署到 PAS 中

是时候在 PAS 里部署 ToDo App 了。知道您部署的应用必须有一个唯一的子域是很重要的。我以后会谈到它。

找到您的 JAR 文件(todo-rest-0.0.1-SNAPSHOT.jar)。如果用 Maven,应该在target目录下。如果你使用 Gradle,它应该在build/libs目录中。

要推送应用,您需要使用以下命令。

$ cf push <name-of-the-app> [options]

因此,要部署 ToDo 应用,您可以执行以下命令。

$ cf push todo-app -p todo-rest-0.0.1-SNAPSHOT.jar -n todo-app-fg

Pushing app todo-app to org your-org / space development as your-email@example.org...
Getting app info...
Creating app with these attributes...
+ name:       todo-app
  path:       /Users/Books/pro-spring-boot-2nd/ch12/todo-rest/target/todo-rest-0.0.1-SNAPSHOT.jar
  routes:
+   todo-app-fg.cfapps.io

Creating app todo-app...
Mapping routes...
Comparing local files to remote cache...
Packaging files to upload...
...
...
     state     since       cpu     memory         disk
#0   running   T01:25:10Z  33.0%   292.7M of 1G   158.3M of 1G

cf命令提供了几个选项。

  • -p。告诉cf命令它上传一个文件或者一个特定目录的所有内容。

  • -n。创建一个必须唯一的子域。默认情况下,每个应用都有<sub-domain>.cfapps.io URI,它必须是唯一的。你可以省略-n 选项,但是cf取 app 的名字,它可以和其他名字冲突。在这个例子中,我使用了todo-app-[my-initials] ( todo-app-fg)。我建议你这样做。

在后台,ToDo 应用在一个容器(不是 Docker 容器)中运行。这个容器是由 RunC ( https://github.com/opencontainers/runc )创建的,它使用主机的资源,在不损害安全性的情况下被隔离。现在,你可以进入你的浏览器,使用给定的 URI;在这个例子中, https://todo-app-fg.cfapps.io/toDos

查看 PWS 以查看您的应用(参见图 12-5 )。

img/340891_2_En_12_Fig5_HTML.jpg

图 12-5

PWS 所有应用

如果你将鼠标悬停在 todo-app 的名称上,你会看到如图 12-6 所示的内容。

img/340891_2_En_12_Fig6_HTML.jpg

图 12-6

PWS 所有应用详细信息

你可以检查每个环节。您可以通过单击日志选项卡来查看日志。您可以通过单击查看 PCF 指标链接来获取指标,以了解有关应用内存、每分钟请求数、CPU 使用率、磁盘等的更多信息。

查看日志的另一种方法是执行以下命令。

$ cf logs todo-app

该命令跟踪日志。您可以刷新或向应用发送请求来查看日志。这是调试应用的一种有用方式。

创建服务

您可以通过执行如下命令将 ToDo 添加到应用中。

$ curl -XPOST -d '{"description":"Learn to play Guitar"}' -H "Content-Type: application/json" https://todo-app-fg.cfapps.io/toDos

{
  "description" : "Learn to play Guitar",
  "created" : "2018-09-18T01:58:34.211",
  "modified" : "2018-09-18T01:58:34.211",
  "completed" : false,
  "_links" : {
    "self" : {
      "href" : "https://todo-app-fg.cfapps.io/toDos/8a70ee1f65ea47de0165ea668de30000"
    },
    "toDo" : {
      "href" : "https://todo-app-fg.cfapps.io/toDos/8a70ee1f65ea47de0165ea668de30000"
    }
  }
}

那么,之前的待办事项保存到哪里了呢?记住这个应用有两个驱动:一个是 H2(内存中),另一个是 MySQL,对吗?将此应用部署到 PWS 使用与本地相同的 H2 驱动程序。为什么呢?因为我们还没有指定任何外部 MySQL 服务。

PAS 提供了一种创建服务的方法。如果你回顾十二要素原则的部分,你会看到有一项是关于使用服务作为附加资源的。PAS 通过提供服务来帮助实现这一点,因此您无需担心安装、强化或管理服务。PAS 称之为托管服务

让我们看看 PWS 有多少服务。您可以执行以下命令。

$ cf marketplace

该命令打印出 PAS 安装和配置的所有可用的托管服务。在这种情况下,我们将使用具有 MySQL 服务的 ClearDB 服务。

要告诉 PAS 我们将创建一个cleardb实例服务,您需要执行命令。

$ cf create-service <provider> <plan> <service-name>

因此,要使用 MySQL 服务,请执行以下命令。

$ cf create-service cleardb spark mysql
Creating service instance mysql in org your-org / space development as your-email@example.org...
OK

您选择的计划是spark计划,这是一个免费计划。如果你选择不同的东西,你需要添加你的信用卡,并预计每月收费。

您可以使用以下命令查看服务。

$ cf services
Getting services in org your-org / space development as your -email@example.org...

name    service   plan    bound apps   last operation
mysql   cleardb   spark                create succeeded

从前面的命令中可以看到,绑定应用列是空的。这里我们需要告诉 ToDo app 使用这个服务(mysql)。要将应用与服务绑定,您可以执行以下命令。

$ cf bind-service todo-app mysql
Binding service mysql to app todo-app in org your-org / space development as your-email@example.org...
OK
TIP: Use 'cf restage todo-app' to ensure your env variable changes take effect

在后台,运行 ToDo 应用的容器创建了一个包含所有凭证的环境变量VCAP_SERVICES;,因此 ToDo 应用可以很容易地连接到mysql服务。为了让 ToDo 应用识别此环境变量,需要重新启动应用。您可以执行以下命令。

$ cf restart todo-app

应用重新启动后,看看它是否工作。转到浏览器并添加待办事项。让我们来看看VCAP_SERVICES环境变量。执行以下命令。

$ cf env todo-app
Getting env variables for app todo-app in org your-org / space development as your-email@example.org...
OK

System-Provided:
{
 "VCAP_SERVICES": {
  "cleardb": 
   {
    "binding_name": null,
    "credentials": {
     "hostname": "us-cdbr-iron-east-01.cleardb.net",
     "jdbcUrl": "jdbc:mysql://us-cdbr-iron-east-01.cleardb.net/ad_9a533ebf2e8e79a?user=b2c041b9ef8f25\u0026password=30e7a38b",
     "name": "ad_9a533ebf2e8e79a",
     "password": "30e7a38b",
     "port": "3306",
     "uri": "mysql://b2c041b9ef8f25:30e7a38b@us-cdbr-iron-east-01.cleardb.net:3306/ad_9a533ebf2e8e79a?reconnect=true",
     "username": "b2c041b9ef8f25"
    },
    "instance_name": "mysql",
    "label": "cleardb",
    "name": "mysql",
    "plan": "spark",...
...
....

看到VCAP_SERVICES变量有hostnameusernamepasswordjdbcUrl属性。事实上,你可以连接到它。您可以使用任何 MySQL 客户端并使用这些属性。例如,如果您有mysql客户端命令行,您可以执行

$ mysql -h us-cdbr-iron-east-01.cleardb.net -ub2c041b9ef8f25 -p30e7a38b ad_9a533ebf2e8e79a
...
...
mysql> show tables;
+------------------------------+
| Tables_in_ad_9a533ebf2e8e79a |
+------------------------------+
| to_do                        |
+------------------------------+
1 row in set (0.07 sec)

mysql> select * from to_do \G
*************************** 1\. row ***************************
         id: 8a72072165ea86ef0165ea887cd10000
  completed:
    created: 2018-09-18 02:35:38
description: Learn to play Guitar
   modified: 2018-09-18 02:35:38
1 row in set (0.07 sec)

mysql>

如你所见,现在 ToDo 应用正在使用 MySQL 服务。但是怎么做呢?Cloud Foundry 使用构建包来检查你的应用,并知道你正试图运行哪种编程语言。Cloud Foundry 与编程语言无关;因此,它识别出你正在使用一个 Spring Boot 应用。它还看到您有一个有界服务(mysql),所以它检查您是否有正确的驱动程序(在这种情况下,它是嵌入在 fat JAR 中的 MySQL 驱动程序),这样它就可以连接到它。但是最好的部分是什么?嗯,你甚至不需要改变你的代码中的任何东西!!Cloud Foundry 和 Java buildpack 会为您自动配置数据源。就这么简单!

Cloud Foundry 帮助您只关注应用,而无需担心基础架构、服务等。

您为创建服务所做的一切都可以使用 web 控制台来完成。您可以在 PWS 页面的 Marketplace 选项卡中进行客户端操作,并选择您需要的服务(参见图 [12-7 )。

img/340891_2_En_12_Fig7_HTML.jpg

图 12-7

PWS 市场

您可以单击 ClearDB MySQL 数据库图标并选择 Spark 计划来配置它。

恭喜你!现在您知道如何将 Spring Boot 应用部署到云中了。你能部署其他的例子吗?todo-mongotodo-redistodo-rabbitmq项目怎么样?

清理

清理您的服务和应用非常重要。这有助于你在需要的时候使用更多的积分。让我们从解除服务与应用的绑定开始。执行以下命令。

$ cf unbind-service todo-app mysql
Unbinding app todo-app from service mysql in org your-org / space development as your-email@example.org...
OK

然后,让我们用下面的命令删除这个服务。

$ cf delete-service mysql

Really delete the service mysql?> y
Deleting service mysql in org your-org / space development as your-email@exmaple.org...
OK

如您所见,系统会提示您确认是否要删除该服务。您可以使用-f标志来避免这种情况。

最后,让我们删除应用。执行以下命令。

$ cf delete -f todo-app
Deleting app todo-app in org your-org / space development as your-email@example.org...
OK

你可以执行

$ cf apps
Getting apps in org your-org / space development as your-email@example.org...
OK

No apps found

查看您当前的应用是否正在运行。

注意

记住你可以从 Apress 网站或者 GitHub 上的 https://github.com/Apress/pro-spring-boot-2 获得这本书的源代码。

摘要

在本章中,您了解了更多关于微服务和云的知识。您了解了更多关于帮助您创建云原生应用的十二要素原则。

您还了解了 Cloud Foundry 及其提供的服务,包括 Pivotal 应用服务和 Pivotal 容器服务。您了解了 Cloud Foundry 是与编程语言无关的,buildpacks 会检查您的应用并自动配置它。

在下一章,你将学习如何扩展和创造你自己的spring-boot-start技术。

十三、扩展 Spring Boot

开发人员和软件架构师经常在寻找要应用的设计模式、要实现的新算法、易于使用和维护的可重用组件,以及改进开发的新方法。找到一个独特或完美的解决方案并不总是容易的,有必要使用不同的技术和方法来实现让应用运行且永不失败的目标。

本章解释了斯普林和 Spring Boot 团队是如何为易于使用和实现的可重用组件创建模式的。实际上,你已经在整本书中学习了这个模式,尤其是在 Spring Boot 配置一章。

本章详细介绍了自动配置,包括如何扩展和创建可重用的新 Spring Boot 模块。我们开始吧。

创建 Spring 启动程序

在这一节中,我将向您展示如何创建一个定制的spring-boot-starter,但是让我们先讨论一些需求。因为您正在 to do 应用中工作,所以这个自定义启动器是一个客户端,您可以使用它来执行 ToDo 的任何操作,例如createfindfindAll。此客户端需要一个连接到 ToDo REST API 服务的主机。

让我们从设置项目开始。到目前为止,还没有为定制的spring-boot-starter设置基线的模板,所以,我们需要手动完成这项工作。创建以下结构。

todo-client-starter/
├── todo-client-spring-boot-autoconfigure
└── todo-client-spring-boot-starter

您需要创建一个名为todo-client-starter的文件夹,其中创建了两个子文件夹:todo-client-spring-boot-autoconfiguretodo-client-spring-boot-starter。是的,这里有一个命名约定。Spring Boot 团队建议任何定制启动器都遵循这个命名约定:<name-of-starter>-spring-boot-starter<name-of-starter>-spring-boot-autoconfigure。自动配置模块拥有启动程序所需的所有代码和必要的依赖项;别担心,我会给你所需要的信息。

首先,让我们创建一个主pom.xml文件,它有两个模块:自动配置和启动程序。在todo-client-starter文件夹中创建一个pom.xml文件。您的结构应该如下所示:

todo-client-starter/
├── pom.xml
├── todo-client-spring-boot-autoconfigure
└── todo-client-spring-boot-starter

pom.xml文件看起来列出了 13-1 。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.apress.todo</groupId>
      <artifactId>todo-client</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>pom</packaging>
      <name>todo-client</name>

      <modules>
            <module>todo-client-spring-boot-autoconfigure</module>
            <module>todo-client-spring-boot-starter</module>
      </modules>

      <dependencyManagement>
            <dependencies>
                  <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-dependencies</artifactId>
                        <version>2.0.5.RELEASE</version>
                        <type>pom</type>
                        <scope>import</scope>
                  </dependency>
            </dependencies>
      </dependencyManagement>
</project>

Listing 13-1todo-client-starter/pom.xml

清单 13-1 显示了有两个模块的主pom.xml。需要提及的一件重要事情是,<packaging/>标签是一个pom,因为最后需要将这些 jar 安装到本地 repo 中以备后用。同样重要的是,这个pom声明了一个<dependencyManagement/>标签,允许我们使用 Spring Boot 罐子及其所有依赖项。最后,我们不需要声明版本。

todo-client-spring-boot-starter

接下来,让我们在todo-client-spring-boot-starter文件夹中创建另一个pom.xml文件。您应该具有以下结构。

todo-client-starter/
├── pom.xml
├── todo-client-spring-boot-autoconfigure
└── todo-client-spring-boot-starter
    └── pom.xml

见清单 13-2 。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.apress.todo</groupId>
    <artifactId>todo-client-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>todo-client-spring-boot-starter</name>
    <description>Todo Client Spring Boot Starter</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

 <parent>
        <groupId>com.apress.todo</groupId>
        <artifactId>todo-client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>..</relativePath>
    </parent>

 <dependencies>
        <dependency>
            <groupId>com.apress.todo</groupId>
            <artifactId>todo-client-spring-boot-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

Listing 13-2todo-client-starter/todo-client-spring-boot-starter/pom.xml

如你所见,列出 13-2 并不是什么新鲜事。它声明了一个与之前的pom.xml文件相关的<parent/>标签,并且声明了autoconfigure模块。

todo-client-spring-boot-starter到此为止;没别的了。您可以将此视为一个标记,在此您可以声明执行繁重工作的模块。

todo-client-spring-boot-自动配置

接下来,让我们在todo-client-spring-boot-autoconfigure文件夹中创建结构。你应该有下面的最终结构。

todo-client-starter/
├── pom.xml
├── todo-client-spring-boot-autoconfigure
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           └── resources
└── todo-client-spring-boot-starter
    └── pom.xml

您的todo-client-spring-boot-autoconfigure文件夹应该是这样的:

todo-client-spring-boot-autoconfigure/
├── pom.xml
└── src
    └── main
        ├── java
        └── resources

一个基本的 Java 项目结构。让我们从pom.xml文件开始(参见清单 13-3 )。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.apress.todo</groupId>
    <artifactId>todo-client-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>

                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <name>todo-client-spring-boot-autoconfigure</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>com.apress.todo</groupId>
        <artifactId>todo-client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>..</relativePath>
    </parent>

    <dependencies>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>

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

        <!-- JSON / Traverson -->
        <dependency>

            <groupId>org.springframework.plugin</groupId>
            <artifactId>spring-plugin-core</artifactId>
        </dependency>

        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

Listing 13-3todo-client-starter/todo-client-spring-boot-autoconfigure

在这种情况下,autoconfigure 项目依赖于 web、Lombok、security、Hateoas 和 JsonPath。

Spring.工厂

如果你还记得第一章,我告诉过你 Spring Boot 的方法,基于类路径自动配置一切;这是 Spring Boot 背后真正的魔力。我提到过,当应用启动时,Spring Boot 自动配置从META-INF/spring.factories文件中加载所有的类来执行每个自动配置类,这为应用提供了运行所需的默认设置。记住,对于 Spring 应用,Spring Boot 是一个固执己见的运行时 ??。

让我们创建spring.factories文件,该文件包含进行自动配置和设置一些默认值的类(参见清单 13-4 )。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.apress.todo.configuration.ToDoClientAutoConfiguration

Listing 13-4src/main/resources/META-INF/spring.factories

注意,spring.factories文件声明了ToDoClientAutoConfiguration类。

自动配置

让我们从创建ToDoClientAutoConfiguration类开始(参见清单 13-5 )。

package com.apress.todo.configuration;

import com.apress.todo.client.ToDoClient;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.Resource;
import org.springframework.web.client.RestTemplate;

@RequiredArgsConstructor

@Configuration

@ConditionalOnClass({Resource.class, RestTemplateBuilder.class})

@EnableConfigurationProperties(ToDoClientProperties.class)
public class ToDoClientAutoConfiguration {

    private final Logger log = LoggerFactory.getLogger(ToDoClientAutoConfiguration.class);
    private final ToDoClientProperties toDoClientProperties;

    @Bean
    public ToDoClient client(){
        log.info(">>> Creating a ToDo Client...");
        return new ToDoClient(new RestTemplate(),this.toDoClientProperties);
    }

}

Listing 13-5com.apress.todo.configuration.ToDoClientAutoConfiguration.java

清单 13-5 显示了执行的自动配置。它使用了@ConditionalOnClass注释,这表示如果它在类路径中找到了,Resource.classRestTemplateBuilder.class将继续配置。当然,因为依赖项之一是spring-boot-starter-web,所以它有那些类。但是当有人排除这些资源时会发生什么呢?这是这个类完成任务的时候。

这个类声明了一个使用了RestTemplateToDoClientProperties实例的TodoClient bean。

就这样。非常简单的自动配置。如果它在您的项目中找到使用这个定制启动器的那些资源类,它将设置默认的ToDoClient bean。

助手类

接下来,让我们创建助手类。创建 ToDoClientProperties 和 ToDoClient 类(参见清单 13-6 和 13-7 )。

package com.apress.todo.configuration;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix="todo.client")
public class ToDoClientProperties {

    private String host = "http://localhost:8080";
    private String path = "/toDos";

}

Listing 13-6com.apress.todo.configuration.ToDoClientProperties.java

正如您所看到的,没有什么新的东西——只有两个字段保存主机和路径的默认值。这意味着您可以在application.properties文件中覆盖它们。

package com.apress.todo.client;

import com.apress.todo.configuration.ToDoClientProperties;
import com.apress.todo.domain.ToDo;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.client.Traverson;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.Collection;

@AllArgsConstructor
@Data
public class ToDoClient {

    private RestTemplate restTemplate;
    private ToDoClientProperties props;

    public ToDo add(ToDo toDo){
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .uri(URI.create(this.props.getHost())).path(this.props.getPath()).build();

        ResponseEntity<ToDo> response =
                this.restTemplate.exchange(
                        RequestEntity.post(uriComponents.toUri())
                                .body(toDo)
                        ,new ParameterizedTypeReference<ToDo>() {});

        return response.getBody();
    }

    public ToDo findById(String id){
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .uri(URI.create(this.props.getHost())).pathSegment(this.props.getPath(), "/{id}")
                .buildAndExpand(id);

        ResponseEntity<ToDo> response =
                this.restTemplate.exchange(
                        RequestEntity.get(uriComponents.toUri()).accept(MediaTypes.HAL_JSON).build()
                        ,new ParameterizedTypeReference<ToDo>() {});

        return response.getBody();
    }

    public Collection<ToDo> findAll() {
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .uri(URI.create(this.props.getHost())).build();

        Traverson traverson = new Traverson(uriComponents.toUri(), MediaTypes.HAL_JSON, MediaType.APPLICATION_JSON_UTF8);
        Traverson.TraversalBuilder tb = traverson.follow(this.props.getPath().substring(1));
        ParameterizedTypeReference<Resources<ToDo>> typeRefDevices = new ParameterizedTypeReference<Resources<ToDo>>() {};

        Resources<ToDo> toDos = tb.toObject(typeRefDevices);

        return toDos.getContent();
    }

}

Listing 13-7com.apress.todo.client.ToDoClient.java

ToDoClient类是一个非常简单的实现。这个类在所有方法中都使用了RestTemplate;即使findAll方法正在使用一个Traverson(JavaScript Traverson 库( https://github.com/traverson/traverson )的 Java 实现,这是一种操纵所有 HATEOAS 链接的方法)实例;它在幕后使用的是RestTemplate

花几分钟时间分析代码。请记住,这是一个请求并发送到 ToDo 的 REST API 服务器的客户端。

要使用这个客户端,有必要创建 ToDo 域类(参见清单 13-8 )。

package com.apress.todo.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@JsonIgnoreProperties(ignoreUnknown = true)
@NoArgsConstructor
@Data
public class ToDo {

    private String id;
    private String description;

    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime created;

    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime modified;

    private boolean completed;

    public ToDo(String description){
        this.description = description;
    }
}

Listing 13-8com.apress.todo.domain.ToDo.java

这里我们介绍@Json*注解。一个忽略任何链接(由HAL+JSON协议提供),一个序列化LocalDateTime实例。

我们差不多完成了。让我们添加一个安全实用程序来帮助加密/解密 ToDo 描述。

创建@Enable*功能

Spring 和 Spring Boot 技术的一个很酷的特性是它们公开了几个@Enable*特性,这些特性隐藏了所有的样板配置并为我们做了大量的工作。

所以,让我们创建一个定制的@EnableToDoSecurity特性。让我们首先创建由 Spring Boot 自动配置获得的注释(参见清单 13-9 )。

package com.apress.todo.annotation;

import com.apress.todo.security.ToDoSecurityConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(ToDoSecurityConfiguration.class)
public @interface EnableToDoSecurity {
    Algorithm algorithm() default Algorithm.BCRYPT;
}

Listing 13-9com.apress.todo.annotation.EnableToDoSecurity.java

此批注使用了算法枚举;让我们创建它(参见清单 13-10 )。

package com.apress.todo.annotation;

public enum Algorithm {
    BCRYPT, PBKDF2
}

Listing 13-10com.apress.todo.annotation.Algorithm.java

这意味着我们可以将一些参数传递给@EnableToDoSecurity。我们选择BCRYPT或者PBKDF2,如果没有参数,默认为 BCRYPT。

接下来,创建一个ToDoSecurityConfiguration类,如果声明了@EnableToDoSecurity,它将触发任何配置(参见清单 13-11 )。

package com.apress.todo.security;

import com.apress.todo.annotation.Algorithm;
import com.apress.todo.annotation.EnableToDoSecurity;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

public class ToDoSecurityConfiguration implements ImportSelector {
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes =
                AnnotationAttributes.fromMap(
                        annotationMetadata.getAnnotationAttributes(EnableToDoSecurity.class.getName(), false));
        Algorithm algorithm = attributes.getEnum("algorithm");
        switch(algorithm){
            case PBKDF2:
                return new String[] {"com.apress.todo.security.Pbkdf2Encoder"};
            case BCRYPT:
            default:
                return new String[] {"com.apress.todo.security.BCryptEncoder"};
        }
    }
}

Listing 13-11com.apress.todo.security.ToDoSecurityConfiguration.java

清单 13-11 向您展示了只有在声明了@EnableToDoSecurity注释的情况下才会执行自动配置。Spring Boot 还跟踪每个实现了ImportSelector接口的类,该接口隐藏了所有的样板处理注释。

因此,如果找到了@EnableToDoSecurity注释,那么通过调用selectImports方法来执行这个类,该方法返回一个字符串数组,这些字符串是必须配置的类;在这种情况下,要么是com.apress.todo.security.Pbkdf2Encoder类(如果您将PBKDF2算法设置为参数),要么是com.apress.todo.security.BCryptEncoder类(如果您将BCRYPT算法设置为参数)。

这些课程有什么特别之处?让我们创建BCryptEncoderPbkdf2Encoder类(参见清单 13-12 和清单 13-13 )。

package com.apress.todo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

@Configuration
public class Pbkdf2Encoder {

    @Bean
    public ToDoSecurity utils(){
        return new ToDoSecurity(new Pbkdf2PasswordEncoder());
    }
}

Listing 13-13com.apress.todo.security.Pbkdf2Encoder.java

package com.apress.todo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class BCryptEncoder {

    @Bean
    public ToDoSecurity utils(){
        return new ToDoSecurity(new BCryptPasswordEncoder(16));
    }
}

Listing 13-12com.apress.todo.security.BCryptEncoder.java

两个类都声明了ToDoSecurity bean。所以,如果您选择了PBKDF2算法,那么ToDoSecurity bean 就用Pbkdf2PasswordEncoder实例创建了;如果您选择了BCRYPT算法,那么ToDoSecurity bean 将由BCryptPasswordEncoder(16)实例创建。

清单 13-14 显示了ToDoSecurity类。

package com.apress.todo.security;

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.crypto.password.PasswordEncoder;

@AllArgsConstructor
@Data
public class ToDoSecurity {

    private PasswordEncoder encoder;
}

Listing 13-14com.apress.todo.security.ToDoSecurity.java

如你所见,这门课没什么特别的。

ToDo REST API 服务

让我们准备 ToDo REST API 服务。你可以重用使用了data-jpadata-resttodo-rest项目,你在其他章节中也是这么做的。让我们回顾一下,看看我们需要做什么(参见清单 13-15 、 13-16 和 13-17 )。

package com.apress.todo.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
@NoArgsConstructor
public class ToDo {

    @NotNull
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime created;

    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime modified;

    private boolean completed;

    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 13-15com.apress.todo.domain.ToDo.java

这个ToDo类并不是什么新东西;您已经了解了这里使用的每个注释。唯一的区别是它只对特定格式的日期使用了@Json*注释。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> {

}

Listing 13-16com.apress.todo.repository.ToDoRepository.java

和以前一样;关于这个界面,没有什么是你不知道的。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class ToDoRestConfig extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(ToDo.class);
    }
}

Listing 13-17com.apress.todo.config.ToDoRestConfig.java

清单 13-17 向您展示了一个新的类,即从RespositoryRestConfigurerAdapter扩展而来的ToDoRestConfig;这个类可以帮助从 JPA 存储库自动配置默认配置的所有东西中配置部分RestController实现。它通过公开域类的 id 来覆盖configureRepositoryRestConfiguration。当我们在其他章节中使用 REST 时,id 不会根据请求显示;但有了这种超越,我们就能实现它。我们需要这个特性,因为我们想获得 ToDo 实例的 ID。

application.properties中,你应该有以下内容。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

还是那句话,没什么新鲜的。

安装和测试

让我们为在新的定制启动器上运行做好一切准备。让我们从安装todo-client-spring-boot-starter开始。打开一个终端,进入你的todo-client-starter文件夹,执行下面的命令。

$ mvn clean install

这个命令将您的 jar 安装在本地的.m2目录中,这个目录可以被另一个使用它的项目获取。

任务项目

既然已经安装了todo-client-spring-boot-starter,是时候测试一下了。您将创建一个新项目。您可以像往常一样创建项目。转到 Spring Initializr ( https://start.spring.io )并用以下值设置字段。

  • 组:com.apress.task

  • 神器:task

  • 名称:task

  • 包名:com.apress.task

你可以选择 Maven 或者 Gradle。然后单击“生成项目”按钮。这将生成并下载一个 ZIP 文件。你可以将其解压缩,然后导入到你喜欢的 IDE 中(见图 13-1 )。

img/340891_2_En_13_Fig1_HTML.jpg

图 13-1

Spring 初始化 zr

接下来需要添加todo-client-spring-boot-starter。如果您使用的是 Maven,请转到您的pom.xml文件并添加依赖项。

        <dependency>
            <groupId>com.apress.todo</groupId>
            <artifactId>todo-client-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

如果您使用的是 Gradle,将依赖项添加到您的build.gradle文件中。

compile('com.apress.todo:todo-client-spring-boot-starter:0.0.1-SNAPSHOT')

就这样。现在打开主类,其中有清单 13-18 所示的代码。

package com.apress.task;

import com.apress.todo.annotation.EnableToDoSecurity;
import com.apress.todo.client.ToDoClient;
import com.apress.todo.domain.ToDo;
import com.apress.todo.security.ToDoSecurity;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@EnableToDoSecurity

@SpringBootApplication
public class TaskApplication {

      public static void main(String[] args) {
        SpringApplication app = new SpringApplication(TaskApplication.class);
        app.setWebApplicationType(WebApplicationType.NONE);
        app.run(args);
      }

      @Bean
    ApplicationRunner createToDos(ToDoClient client){
          return args -> {
            ToDo toDo = client.add(new ToDo("Read a Book"));
            ToDo review = client.findById(toDo.getId());
            System.out.println(review);
            System.out.println(client.findAll());
        };
    }

    @Bean
    ApplicationRunner secure(ToDoSecurity utils){
        return args -> {
            String text = "This text will be encrypted";
            String hash = utils.getEncoder().encode(text);
            System.out.println(">>> ENCRYPT: " + hash);
            System.out.println(">>> Verify: " + utils.getEncoder().matches(text,hash));
        };
    }
}

Listing 13-18com.apress.task.TaskApplication.java

有两个 ApplicationRunner beans 每个都有一个参数。createToDos使用ToDoClient bean 实例(如果没有RestTemplateBuilderResource,将会失败)。就是用你知道的方法(addfindByIdfindAll)。

secure 方法使用的是ToDoSecurity bean 实例,这多亏了@EnableToDoSecurity才成为可能。如果您删除它或注释掉它,它会告诉您它找不到ToDoSecurity bean。

花几分钟时间分析代码,看看发生了什么。

运行任务应用

要运行应用,首先确保todo-rest应用已启动并正在运行。它应该在端口 8080 上运行。记住你已经用mvn clean install命令安装了todo-client-spring-boot-starter

因此,如果您正在运行它,您会看到一些响应,并且 ToDo 保存在 ToDo REST 服务中。它还向您显示加密文本。

如果您在不同的 IP、主机、端口或路径中运行 ToDo REST API,您可以通过使用application.properties文件中的todo.client.*属性来更改默认值。

# ToDo Rest API
todo.client.host=http://some-new-server.com:9091
todo.client.path=/api/toDos

记住如果不覆盖,默认为http://localhost:8080/toDos。运行任务应用后,您应该会看到类似于以下输出的内容。

INFO - [ main] c.a.t.c.ToDoClientAutoConfiguration      : >>> Creating a ToDo Client...

INFO - [ main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
INFO - [ main] com.apress.task.TaskApplication          : Started TaskApplication in 1.047 seconds (JVM running for 1.853)

ToDo(id=8a8080a365f427c00165f42adee50000, description=Read a Book, created=2018-09-19T17:29:34, modified=2018-09-19T17:29:34, completed=false)

[ToDo(id=8a8080a365f427c00165f42adee50000, description=Read a Book, created=2018-09-19T17:29:34, modified=2018-09-19T17:29:34, completed=false)]

>>> ENCRYPT: $2a$16$pVOI../twnLwN3GFiChdR.zRFfyCIZMEbwEXbAtRoIHqxeLB3gmUG

>>> Verify: true

恭喜你!您刚刚创建了您的第一个自定义 Spring Boot 启动器和@Enable 功能!

注意

记住你可以从 Apress 网站或者 GitHub 上的 https://github.com/Apress/pro-spring-boot-2 获得这本书的源代码。

摘要

本章向您展示了如何使用自动配置模式为 Spring Boot 创建一个模块。我向您展示了如何创建您的定制健康监控器。正如你所看到的,扩展 Spring Boot 应用非常简单,所以请随意修改代码并进行实验。

我们没有做太多的单元或集成测试。对你来说,练习我给你看的所有细节将是很好的功课。我想它会帮助你更好地理解 Spring Boot 是如何运作的。重复,你就会掌握!

posted @   绝不原创的飞龙  阅读(91)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
历史上的今天:
2020-10-02 《线性代数》(同济版)——教科书中的耻辱柱
点击右上角即可分享
微信分享提示