SpringBoot2 高级教程(五)

随笔2周前发布 皇帝
39 0 0

原文:Pro Spring Boot 2

协议:CC BY-NC-SA 4.0

十一、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();
}


12345

如你所见,定义中没有什么花哨的东西。

SpringBoot2 高级教程(五)

图 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 )。

SpringBoot2 高级教程(五)

图 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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233343536

清单 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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526272829303132333435

清单 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



SpringBoot2 高级教程(五)12345678910111213141516171819202122232425

清单 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}]

...


12345678

请记住,消息是关于有效载荷的,这就是为什么我们得到了具有有效载荷的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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223

清单 11-4 显示了配置 ToDo 集成流的 XML 版本。我认为这很简单。如果你正在使用 STS IDE,你可以使用 Spring Integration 流的拖放面板(集成图),或者如果你正在使用 IDEA IntelliJ,生成图(参见图 11-3 )。

SpringBoot2 高级教程(五)

图 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



SpringBoot2 高级教程(五)12345678910111213141516

清单 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

...


123456789

使用注释

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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

清单 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

...


1234567

使用 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;
}


1234567891011

这段代码创建了一个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



SpringBoot2 高级教程(五)12345678910111213141516

正如你在清单 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



SpringBoot2 高级教程(五)123456789101112131415161718192021

清单 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



SpringBoot2 高级教程(五)12345678910111213141516171819

清单 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



SpringBoot2 高级教程(五)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152

清单 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


123

当然,您可以添加任何目录和/或文件模式。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


123456

要运行代码,注释掉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)


1234567

正如您所看到的,这是一种非常简单的方式来集成文件、读取其内容以及使用数据进行任何业务逻辑。

还记得之前我告诉过你,你可以混合配置 Spring Integration 的方式吗?那么,如果您想使用实际的注释来处理消息,您需要做什么呢?您可以使用@ServiceActivator注释作为配置的一部分。

@ServiceActivator(inputChannel="input")
public void process(ToDo message){

}


12345

要使用这种服务激活器方法,您需要更改流程。替换此行:

handle("toDoMessageHandler","process")


12

有了这个:

.channel("input")


12

如果您重新运行该示例,您会得到相同的结果。您是否意识到没有定义输入通道?最棒的是,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>


123456789101112

<dependencies/>标签中添加您想要使用的技术;例如,

<dependencies>
 <dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
 </dependency>

 <!—MORE Technologies here -->

</dependencies>


12345678910

如果您使用的是 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')
      // ...
}


123456789101112131415

如果深入研究 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 。

SpringBoot2 高级教程(五)

图 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>


12345

如果你使用的是 Gradle,那么你需要在build.gradle文件中添加以下依赖项。

compile('org.springframework.cloud:spring-cloud-starter-stream-kafka')


12

RabbitMQ 。如果您选择 RabbitMQ 作为绑定器,那么如果您使用 Maven,您需要在您的pom.xml中添加以下依赖项。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>


12345

如果你使用的是 Gradle,那么你需要在build.gradle文件中添加以下依赖项。

compile('org.springframework.cloud:spring-cloud-starter-stream-rabbit')


12

启动并运行 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();

}


123456789

Processor。当您想要开始监听来自输入通道的新的输入消息,对接收的消息进行处理(增强、转换等)时,您可以在应用中使用Processor。),并向输出通道发送新消息。这是 Spring Cloud Stream 中的实际界面:

public interface Processor extends Source, Sink {

}


1234

Sink。当您想要开始监听来自输入通道的新消息、进行处理和结束流程(保存数据、启动任务、登录控制台等)时,可以使用Sink应用。).这是 Spring Cloud Stream 中的实际界面:

public interface Sink {

  String INPUT = "input";

  @Input(Sink.INPUT)
  SubscribableChannel input();

}


123456789

图 11-5 和 11-6 是我们使用的模型。

SpringBoot2 高级教程(五)

图 11-6

源➤处理器➤接收器

SpringBoot2 高级教程(五)

图 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 )。

SpringBoot2 高级教程(五)

图 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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526

清单 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"))


12

这个声明中的重要部分是通道,这里指向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>


12345

如果您使用的是 Gradle,将下面的依赖项添加到您的build.gradle文件中。

compile('org.springframework.cloud:spring-cloud-starter-stream-rabbit')


12

确保您已经启动并运行了 RabbitMQ。接下来,运行示例。你可能看不到多少,但它确实在做一些事情。现在,我们进入 RabbitMQ。

在浏览器中打开 RabbitMQ Web 管理。转到http://localhost:15672。用户名是guest,密码是guest

Go to the Exchanges tab (see Figure 11-8).

SpringBoot2 高级教程(五)

图 11-8

RabbitMQ 交换选项卡

请注意,创建了一个输出(主题交换),消息速率为 1.0/s。

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).

SpringBoot2 高级教程(五)

图 11-9

创建队列:我的队列

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.

SpringBoot2 高级教程(五)

图 11-10

绑定器

在 From Exchange 字段中填入值 output (这是交换的名称)。路由关键字字段的值为 # ,允许任何消息进入我的队列。

After you bind the output exchange to my-queue, you start seeing several messages. Open the Overview panel (see Figure 11-11).

SpringBoot2 高级教程(五)

图 11-11

概观

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).

SpringBoot2 高级教程(五)

图 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}


12

您可以看到非常冗长的日期序列化,但是如果您使用 Maven,可以通过在pom.xml文件中添加以下依赖项来改变这种情况。

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>


12345

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

compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')


12

重新运行应用。现在,您应该会看到以下消息。

{"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}


12
处理器

该部分使用一个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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233

清单 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。

进入你的浏览器,点击http://localhost:15672(用户名:guest,密码:guest)。

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).

SpringBoot2 高级教程(五)

图 11-13

交换

请注意,现在在任何新的交换中都没有消息速率。

Next, go to the Queues tab. You notice a new queue named input.anonymous and random text has been created (see Figure 11-14).

SpringBoot2 高级教程(五)

图 11-14

行列

就这样。ToDoProcessor 流创建输出交换和input.anonymous.*队列,这意味着流连接到绑定器,在本例中是 RabbitMQ。现在的问题是如何传递信息,对吗?有不同的方法可以做到这一点:使用 RabbitMQ 模拟消息,或者以编程方式实现。我们将双管齐下。

我们将创建一个名为my-queue的队列,并将其绑定到输出,这与我们在Source流部分中所做的非常相似。

转到 Queues 选项卡,创建一个名为my-queue的队列,并用路由关键字#将其绑定到输出交换。这类似于Source流中的步骤 2 和 3。还要注意的是,input.anonymous.*队列绑定到输入交换。

现在,我们将使用输入交换发送一条消息。转到交换选项卡。单击输入交换并选择发布消息面板。

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}


12

Enter content-type=application/json in the Properties field (see Figure 11-15).

SpringBoot2 高级教程(五)

图 11-15

发布消息

然后单击“发布消息”按钮。它应该显示为“消息已发布”的消息

SpringBoot2 高级教程(五)

图 11-16

获取消息

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)
...


12345

如果你看一看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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526

如果您运行应用,现在您有一个用大写字母描述的 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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324

清单 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 。

SpringBoot2 高级教程(五)

图 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 )。

SpringBoot2 高级教程(五)

图 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>
...


12345678910111213

如果你使用的是 Gradle,看看你的build.gradle文件中是否有这些依赖项。

...
runtime('com.h2database:h2')
runtime('mysql:mysql-connector-java')
...


12345

为什么这些依赖关系很重要?您将在接下来的章节中了解这一点。接下来,转到您的application.properties文件,并确保它像下面的内容。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true


12345

这里改变的是ddl-auto属性;在使用create-drop之前,它在会话结束时创建和销毁模式。并且您正在将这个属性更改为update,这意味着它会在必要时更新模式。这是有道理的,但请在接下来的部分中看到它的实际应用。

让我们通过执行下面的命令来准备应用,源代码在。(您也可以在 ide 中执行 Maven 目标或 Gradle 任务;查看文档了解如何操作。)如果你用的是 Maven,可以执行

$ ./mvnw clean package


12

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

$ ./gradlew clean build


12

这些命令生成了即将部署的 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 )。

SpringBoot2 高级教程(五)

图 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 )。

SpringBoot2 高级教程(五)

图 12-3

Pivotal Web 服务- run。枢轴。io

注册时,系统会提示您输入电话号码,您会收到一个开始试用的代码。它还要求您提供一个组织,可以是您的名字加一个-org;比如我的是fg-org。默认情况下,它会创建您将工作的空间(名为development)(见图 12-4 )。

SpringBoot2 高级教程(五)

图 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


1234

安装之后,您可以通过运行

$ cf --version
cf version 6.39.0+607d4f8be.2018-09-11


123

现在您已经准备好使用 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



SpringBoot2 高级教程(五)1234567891011121314151617

默认情况下,您被置于发展空间。您已经准备好对 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]


12

因此,要部署 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



SpringBoot2 高级教程(五)12345678910111213141516171819

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 )。

SpringBoot2 高级教程(五)

图 12-5

PWS 所有应用

如果你将鼠标悬停在 todo-app 的名称上,你会看到如图 12-6 所示的内容。

SpringBoot2 高级教程(五)

图 12-6

PWS 所有应用详细信息

你可以检查每个环节。您可以通过单击日志选项卡来查看日志。您可以通过单击查看 PCF 指标链接来获取指标,以了解有关应用内存、每分钟请求数、CPU 使用率、磁盘等的更多信息。

查看日志的另一种方法是执行以下命令。

$ cf logs todo-app


12

该命令跟踪日志。您可以刷新或向应用发送请求来查看日志。这是调试应用的一种有用方式。

创建服务

您可以通过执行如下命令将 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"
    }
  }
}



SpringBoot2 高级教程(五)1234567891011121314151617

那么,之前的待办事项保存到哪里了呢?记住这个应用有两个驱动:一个是 H2(内存中),另一个是 MySQL,对吗?将此应用部署到 PWS 使用与本地相同的 H2 驱动程序。为什么呢?因为我们还没有指定任何外部 MySQL 服务。

PAS 提供了一种创建服务的方法。如果你回顾十二要素原则的部分,你会看到有一项是关于使用服务作为附加资源的。PAS 通过提供服务来帮助实现这一点,因此您无需担心安装、强化或管理服务。PAS 称之为托管服务

让我们看看 PWS 有多少服务。您可以执行以下命令。

$ cf marketplace


12

该命令打印出 PAS 安装和配置的所有可用的托管服务。在这种情况下,我们将使用具有 MySQL 服务的 ClearDB 服务。

要告诉 PAS 我们将创建一个cleardb实例服务,您需要执行命令。

$ cf create-service <provider> <plan> <service-name>


12

因此,要使用 MySQL 服务,请执行以下命令。

$ cf create-service cleardb spark mysql
Creating service instance mysql in org your-org / space development as your-email@example.org...
OK


1234

您选择的计划是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


123456

从前面的命令中可以看到,绑定应用列是空的。这里我们需要告诉 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


12345

在后台,运行 ToDo 应用的容器创建了一个包含所有凭证的环境变量VCAP_SERVICES;,因此 ToDo 应用可以很容易地连接到mysql服务。为了让 ToDo 应用识别此环境变量,需要重新启动应用。您可以执行以下命令。

$ cf restart todo-app


12

应用重新启动后,看看它是否工作。转到浏览器并添加待办事项。让我们来看看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=b2c041b9ef8f25u0026password=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",...
...
....



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526

看到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>



SpringBoot2 高级教程(五)12345678910111213141516171819202122

如你所见,现在 ToDo 应用正在使用 MySQL 服务。但是怎么做呢?Cloud Foundry 使用构建包来检查你的应用,并知道你正试图运行哪种编程语言。Cloud Foundry 与编程语言无关;因此,它识别出你正在使用一个 Spring Boot 应用。它还看到您有一个有界服务(mysql),所以它检查您是否有正确的驱动程序(在这种情况下,它是嵌入在 fat JAR 中的 MySQL 驱动程序),这样它就可以连接到它。但是最好的部分是什么?嗯,你甚至不需要改变你的代码中的任何东西!!Cloud Foundry 和 Java buildpack 会为您自动配置数据源。就这么简单!

Cloud Foundry 帮助您只关注应用,而无需担心基础架构、服务等。

您为创建服务所做的一切都可以使用 web 控制台来完成。您可以在 PWS 页面的 Marketplace 选项卡中进行客户端操作,并选择您需要的服务(参见图 [12-7 )。

SpringBoot2 高级教程(五)

图 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


1234

然后,让我们用下面的命令删除这个服务。

$ 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


123456

如您所见,系统会提示您确认是否要删除该服务。您可以使用-f标志来避免这种情况。

最后,让我们删除应用。执行以下命令。

$ cf delete -f todo-app
Deleting app todo-app in org your-org / space development as your-email@example.org...
OK


1234

你可以执行

$ cf apps
Getting apps in org your-org / space development as your-email@example.org...
OK

No apps found


123456

查看您当前的应用是否正在运行。

注意

记住你可以从 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


1234

您需要创建一个名为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


12345

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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930

清单 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


123456

见清单 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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233343536

如你所见,列出 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


1234567891011

您的todo-client-spring-boot-autoconfigure文件夹应该是这样的:

todo-client-spring-boot-autoconfigure/
├── pom.xml
└── src
    └── main
        ├── java
        └── resources


1234567

一个基本的 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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495

在这种情况下,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


1234

注意,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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233343536

清单 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



SpringBoot2 高级教程(五)12345678910111213141516

正如您所看到的,没有什么新的东西——只有两个字段保存主机和路径的默认值。这意味着您可以在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



SpringBoot2 高级教程(五)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

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



SpringBoot2 高级教程(五)123456789101112131415161718192021222324252627282930313233343536

这里我们介绍@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



SpringBoot2 高级教程(五)12345678910111213141516171819

此批注使用了算法枚举;让我们创建它(参见清单 13-10 )。

package com.apress.todo.annotation;

public enum Algorithm {
    BCRYPT, PBKDF2
}

Listing 13-10com.apress.todo.annotation.Algorithm.java


12345678

这意味着我们可以将一些参数传递给@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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526

清单 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



SpringBoot2 高级教程(五)1234567891011121314151617
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



SpringBoot2 高级教程(五)1234567891011121314151617

两个类都声明了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


123456789101112131415

如你所见,这门课没什么特别的。

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



SpringBoot2 高级教程(五)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

这个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


1234567891011

和以前一样;关于这个界面,没有什么是你不知道的。

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



SpringBoot2 高级教程(五)123456789101112131415161718

清单 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


12345

还是那句话,没什么新鲜的。

安装和测试

让我们为在新的定制启动器上运行做好一切准备。让我们从安装todo-client-spring-boot-starter开始。打开一个终端,进入你的todo-client-starter文件夹,执行下面的命令。

$ mvn clean install


12

这个命令将您的 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 )。

SpringBoot2 高级教程(五)

图 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>


123456

如果您使用的是 Gradle,将依赖项添加到您的build.gradle文件中。

compile('com.apress.todo:todo-client-spring-boot-starter:0.0.1-SNAPSHOT')


12

就这样。现在打开主类,其中有清单 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



SpringBoot2 高级教程(五)12345678910111213141516171819202122232425262728293031323334353637383940414243444546

有两个 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


1234

记住如果不覆盖,默认为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


12345678910111213

恭喜你!您刚刚创建了您的第一个自定义 Spring Boot 启动器和@Enable 功能!

注意

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

摘要

本章向您展示了如何使用自动配置模式为 Spring Boot 创建一个模块。我向您展示了如何创建您的定制健康监控器。正如你所看到的,扩展 Spring Boot 应用非常简单,所以请随意修改代码并进行实验。

我们没有做太多的单元或集成测试。对你来说,练习我给你看的所有细节将是很好的功课。我想它会帮助你更好地理解 Spring Boot 是如何运作的。重复,你就会掌握!

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...