如何在 Spring 中使用事件

【注】本文译自:Spring Events | Baeldung

1. 概述

在本教程中,我们将讨论如何在 Spring 中使用事件

事件是框架中最容易被忽视的功能之一,但也是更有用的功能之一。和 Spring 中的许多其他东西一样,事件发布是 ApplicationContext 提供的功能之一。

有一些简单的指导方针可以遵循:

  • 如果我们使用 Spring Framework 4.2 之前的版本,事件类应该扩展 ApplicationEvent。从 4.2 版本开始,事件类不再需要扩展 ApplicationEvent 类。
  • 发布者应该注入一个 ApplicationEventPublisher 对象。
  • 监听器应实现 ApplicationListener 接口。

    2.自定义事件

    Spring 允许我们创建和发布默认同步的自定义事件。这有一些优点,例如监听器能够参与发布者的事务上下文。

2.1.一个简单的应用程序事件

让我们创建一个简单的事件类——只是一个用于存储事件数据的占位符。

在这种情况下,事件类包含一个 String 消息:

public class CustomSpringEvent extends ApplicationEvent {
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

2.2. 发布者

现在让我们创建该事件的发布者。发布者构造事件对象并将其发布给正在收听的任何人。

要发布事件,发布者可以简单地注入 ApplicationEventPublisher 并使用 publishEvent() API:

@Component
public class CustomSpringEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        System.out.println("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

或者,发布者类可以实现
ApplicationEventPublisherAware 接口,这也会在应用程序启动时注入事件发布者。通常,将 @Autowire 注入发布者会更简单。

从 Spring Framework 4.2 开始,ApplicationEventPublisher 接口为 publishEvent(Object event) 方法提供了一个新的重载,该方法接受任何对象作为事件。因此,Spring 事件不再需要扩展 ApplicationEvent

2.3. 监听器

最后,让我们创建监听器。

监听器的唯一要求是是一个 bean 并实现 ApplicationListener 接口:

@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

请注意我们的自定义监听器如何使用自定义事件的通用类型进行参数化,这使得 onApplicationEvent() 方法类型安全。这也避免了必须检查对象是否是特定事件类的实例并对其进行转换。

而且,正如已经讨论过的(默认情况下,Spring 事件是同步的), doStuffAndPublishAnEvent() 方法会阻塞,直到所有监听器完成对事件的处理。

3.创建异步事件

在某些情况下,同步发布事件并不是我们真正想要的——我们可能需要异步处理我们的事件

我们可以通过创建一个带有执行程序的
ApplicationEventMulticaster bean 在配置中打开它。

对于我们来说 SimpleAsyncTaskExecutor 很好地实现了这个目的:

@Configuration
public class AsynchronousSpringEventsConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

事件、发布者和监听器实现与以前相同,但现在监听器将在单独的线程中异步处理事件

4.现有框架事件

Spring 本身发布了各种开箱即用的事件。例如,ApplicationContext 将触发各种框架事件:ContextRefreshedEventContextStartedEventRequestHandledEvent 等。

这些事件为应用程序开发人员提供了一个选项,可以连接到应用程序的生命周期和上下文,并在需要的地方添加他们自己的自定义逻辑。

这是监听上下文刷新的监听器的快速示例:

public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent cse) {
        System.out.println("Handling context re-freshed event. ");
    }
}

要了解有关现有框架事件的更多信息,请在此处查看我们的下一个教程

5.注解驱动的事件监听器

从 Spring 4.2 开始,事件监听器不需要是实现 ApplicationListener 接口的 bean——它可以通过 @EventListener 注解在托管 bean 的任何 public 方法上注册:

@Component
public class AnnotationDrivenEventListener {
    @EventListener
    public void handleContextStart(ContextStartedEvent cse) {
        System.out.println("Handling context started event.");
    }
}

和以前一样,方法签名声明了它使用的事件类型。

默认情况下,监听器是同步调用的。但是,我们可以通过添加 @Async 注释轻松地使其异步。我们只需要记住在应用程序中启用异步支持

6.泛型支持

也可以使用事件类型中的泛型信息来调度事件。

6.1. 泛型应用程序事件

让我们创建一个泛型事件类型。

在我们的示例中,事件类包含任何内容和 success 状态指示器:

public class GenericSpringEvent<T> {
    private T what;
    protected boolean success;

    public GenericSpringEvent(T what, boolean success) {
        this.what = what;
        this.success = success;
    }
    // ... standard getters
}

请注意 GenericSpringEventCustomSpringEvent 之间的区别。我们现在可以灵活地发布任意事件,并且不再需要从 ApplicationEvent 扩展。

6.2.监听器

现在让我们创建该事件的监听器。

我们可以像以前一样通过实现 ApplicationListener 接口来定义监听器:

@Component
public class GenericSpringEventListener implements ApplicationListener<GenericSpringEvent<String>> {
    @Override
    public void onApplicationEvent(@NonNull GenericSpringEvent<String> event) {
        System.out.println("Received spring generic event - " + event.getWhat());
    }
}

但不幸的是,这个定义要求我们从 ApplicationEvent 类继承 GenericSpringEvent。因此,对于本教程,让我们使用之前讨论过的注释驱动事件监听器。

通过在 @EventListener 注释上定义布尔 SpEL 表达式,也可以使事件监听器有条件。

在这种情况下,只有成功调用 GenericSpringEvent 的 String 对象时才会调用事件处理程序:

@Component
public class GenericSpringEventListener implements ApplicationListener<GenericSpringEvent<String>> {
    @Override
    public void onApplicationEvent(@NonNull GenericSpringEvent<String> event) {
        System.out.println("Received spring generic event - " + event.getWhat());
    }

}
Spring 表达式语言 (SpEL) 是一种强大的表达式语言,在另一篇教程中有详细介绍。

6.3. 发布者

事件发布者与上述类似。但是由于类型擦除,我们需要发布一个事件来解析我们将过滤的泛型参数,例如,类 GenericStringSpringEvent extends GenericSpringEvent

此外,还有一种发布事件的替代方法。如果我们从使用 @EventListener 注解的方法返回一个非空值作为结果,Spring Framework 会将该结果作为新事件发送给我们。此外,通过将多个新事件作为事件处理的结果返回到一个集合中,我们可以发布多个新事件。

7.事务绑定事件

本节是关于使用 @
TransactionalEventListener
注解的。要了解有关事务管理的更多信息,请查看使用 Spring 和 JPA 事务

从 Spring 4.2 开始,框架提供了一个新的 @
TransactionalEventListener
注解,它是 @EventListener 的扩展,它允许将事件的监听器绑定到事务的某个阶段。

可以绑定到以下事务阶段:

  • AFTER_COMMIT(默认)- 用于在事务成功完成时触发事件。
  • AFTER_ROLLBACK – 如果事务已回滚
  • AFTER_COMPLETION – 如果事务已完成AFTER_COMMITAFTER_ROLLBACK 的别名)
  • BEFORE_COMMIT – 用于在事务提交之前触发事件。

这是一个事务性事件监听器的快速示例:

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleCustom(CustomSpringEvent event) {
    System.out.println("Handling event inside a transaction BEFORE COMMIT.");
}

仅当存在事件生产者正在运行且即将提交的事务时,才会调用此监听器。

如果没有事务在运行,则根本不会发送事件,除非我们通过将 fallbackExecution 属性设置为 true 来覆盖它。

8. 结论

在这篇简短的文章中,我们介绍了在 Spring 中处理事件的基础知识,包括创建一个简单的自定义事件、发布它,然后在监听器中处理它。

我们还简要了解了如何在配置中启用事件的异步处理。

然后我们了解了 Spring 4.2 中引入的改进,例如注解驱动的监听器、更好的泛型支持和事件绑定到事务阶段。

与往常一样,本文中提供的代码可在 GitHub 上获得。这是一个基于 Maven 的项目,因此它应该很容易导入和运行。

使用 Spring Boot 和 @SpringBootTest 进行测试

【注】本文译自: Testing with Spring Boot and @SpringBootTest – Reflectoring

使用@SpringBootTest 注解,Spring Boot 提供了一种方便的方法来启动要在测试中使用的应用程序上下文。在本教程中,我们将讨论何时使用 @SpringBootTest 以及何时更好地使用其他工具进行测试。我们还将研究自定义应用程序上下文的不同方法以及如何减少测试运行时间。

 代码示例

本文附有 GitHub 上的工作代码示例。

“使用 Spring Boot 进行测试”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 进行单元测试
  2. 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
  4. 使用 Spring Boot 和 @SpringBootTest 进行测试

集成测试与单元测试

在开始使用 Spring Boot 进行集成测试之前,让我们定义集成测试与单元测试的区别。
单元测试涵盖单个“单元”,其中一个单元通常是单个类,但也可以是组合测试的一组内聚类。
集成测试可以是以下任何一项:

  • 涵盖多个“单元”的测试。它测试两个或多个内聚类集群之间的交互。
  • 覆盖多个层的测试。这实际上是第一种情况的特化,例如可能涵盖业务服务和持久层之间的交互。
  • 涵盖整个应用程序路径的测试。在这些测试中,我们向应用程序发送请求并检查它是否正确响应并根据我们的预期更改了数据库状态。

Spring Boot 提供了 @SpringBootTest 注解,我们可以使用它来创建一个应用程序上下文,其中包含我们对上述所有测试类型所需的所有对象。但是请注意,过度使用 @SpringBootTest 可能会导致测试套件运行时间非常长
因此,对于涵盖多个单元的简单测试,我们应该创建简单的测试,与单元测试非常相似,在单元测试中,我们手动创建测试所需的对象图并模拟其余部分。这样,Spring 不会在每次测试开始时启动整个应用程序上下文。

测试切片

我们可以将我们的 Spring Boot 应用程序作为一个整体来测试、一个单元一个单元地测试、也可以一层一层地测试。使用 Spring Boot 的测试切片注解,我们可以分别测试每一层。
在我们详细研究 @SpringBootTest 注解之前,让我们探索一下测试切片注解,以检查 @SpringBootTest 是否真的是您想要的。
@SpringBootTest 注解加载完整的 Spring 应用程序上下文。相比之下,测试切片注释仅加载测试特定层所需的 bean。正因为如此,我们可以避免不必要的模拟和副作用。

@WebMvcTest

我们的 Web 控制器承担许多职责,例如侦听 HTTP 请求、验证输入、调用业务逻辑、序列化输出以及将异常转换为正确的响应。我们应该编写测试来验证所有这些功能。
@WebMvcTest 测试切片注释将使用刚好足够的组件和配置来设置我们的应用程序上下文,以测试我们的 Web 控制器层。例如,它将设置我们的@Controller@ControllerAdvice、一个 MockMvc bean 和其他一些自动配置
要阅读有关 @WebMvcTest 的更多信息并了解我们如何验证每个职责,请阅读我关于使用 Spring Boot 和 @WebMvcTest 测试 MVC Web 控制器的文章

@WebFluxTest

@WebFluxTest 用于测试 WebFlux 控制器。 @WebFluxTest 的工作方式类似于 @WebMvcTest 注释,不同之处在于它不是 Web MVC 组件和配置,而是启动 WebFlux 组件和配置。其中一个 bean 是 WebTestClient,我们可以使用它来测试我们的 WebFlux 端点。

@DataJpaTest

就像 @WebMvcTest 允许我们测试我们的 web 层一样,@DataJpaTest 用于测试持久层。
它配置我们的实体、存储库并设置嵌入式数据库。现在,这一切都很好,但是,测试我们的持久层意味着什么? 我们究竟在测试什么? 如果查询,那么什么样的查询?要找出所有这些问题的答案,请阅读我关于使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询的文章

@DataJdbcTest

Spring Data JDBC 是 Spring Data 系列的另一个成员。 如果我们正在使用这个项目并且想要测试持久层,那么我们可以使用 @DataJdbcTest 注解 。@DataJdbcTest 会自动为我们配置在我们的项目中定义的嵌入式测试数据库和 JDBC 存储库。
另一个类似的项目是 Spring JDBC,它为我们提供了 JdbcTemplate 对象来执行直接查询。@JdbcTest 注解自动配置测试我们的 JDBC 查询所需的 DataSource 对象。依赖
本文中的代码示例只需要依赖 Spring Boot 的 test starter 和 JUnit Jupiter:

dependencies {
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
}

使用 @SpringBootTest 创建 ApplicationContext

@SpringBootTest 在默认情况下开始在测试类的当前包中搜索,然后在包结构中向上搜索,寻找用 @SpringBootConfiguration 注解的类,然后从中读取配置以创建应用程序上下文。这个类通常是我们的主要应用程序类,因为 @SpringBootApplication 注解包括 @SpringBootConfiguration 注解。然后,它会创建一个与在生产环境中启动的应用程序上下文非常相似的应用程序上下文。
我们可以通过许多不同的方式自定义此应用程序上下文,如下一节所述。
因为我们有一个完整的应用程序上下文,包括 web 控制器、Spring 数据存储库和数据源,@SpringBootTest 对于贯穿应用程序所有层的集成测试非常方便:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Autowired
  private UserRepository userRepository;

  @Test
  void registrationWorksThroughAllLayers() throws Exception {
    UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");

    mockMvc.perform(post("/forums/{forumId}/register", 42L)
            .contentType("application/json")
            .param("sendWelcomeMail", "true")
            .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isOk());

    UserEntity userEntity = userRepository.findByName("Zaphod");
    assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
  }
}

@ExtendWith
本教程中的代码示例使用 @ExtendWith 注解告诉 JUnit 5 启用 Spring 支持。从 Spring Boot 2.1 开始,我们不再需要加载 SpringExtension,因为它作为元注释包含在 Spring Boot 测试注释中,例如 @DataJpaTest@WebMvcTest@SpringBootTest

在这里,我们另外使用 @AutoConfigureMockMvc 将 MockMvc 实例添加到应用程序上下文中。
我们使用这个 MockMvc 对象向我们的应用程序执行 POST 请求并验证它是否按预期响应。
然后,我们使用应用程序上下文中的 UserRepository 来验证请求是否导致数据库状态发生预期的变化。

自定义应用程序上下文

我们可以有很多种方法来自定义 @SpringBootTest 创建的应用程序上下文。让我们看看我们有哪些选择。

自定义应用上下文时的注意事项
应用程序上下文的每个自定义都是使其与在生产设置中启动的“真实”应用程序上下文不同的另一件事。因此,为了使我们的测试尽可能接近生产,我们应该只定制让测试运行真正需要的东西!

添加自动配置

在上面,我们已经看到了自动配置的作用:

@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
  ...
}

还有很多其他可用的自动配置,每个都可以将其他 bean 添加到应用程序上下文中。以下是文档中其他一些有用的内容:

  • @AutoConfigureWebTestClient:将 WebTestClient 添加到测试应用程序上下文。它允许我们测试服务器端点。
  • @AutoConfigureTestDatabase:这允许我们针对真实数据库而不是嵌入式数据库运行测试。
  • @RestClientTest:当我们想要测试我们的 RestTemplate 时它会派上用场。 它自动配置所需的组件以及一个 MockRestServiceServer 对象,该对象帮助我们模拟来自 RestTemplate 调用的请求的响应。
  • @JsonTest:自动配置 JSON 映射器和类,例如 JacksonTesterGsonTester。使用这些我们可以验证我们的 JSON 序列化/反序列化是否正常工作。

设置自定义配置属性

通常,在测试中需要将一些配置属性设置为与生产设置中的值不同的值:

@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

如果属性 foo 存在于默认设置中,它将被此测试的值 bar 覆盖。

使用 @ActiveProfiles 外部化属性

如果我们的许多测试需要相同的属性集,我们可以创建一个配置文件 application-<profile>.propertieapplication-<profile>.yml 并通过激活某个配置文件从该文件加载属性:

# application-test.yml
foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

使用 @TestPropertySource 设置自定义属性

另一种定制整个属性集的方法是使用 @TestPropertySource 注释:

# src/test/resources/foo.properties
foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

foo.properties 文件中的所有属性都加载到应用程序上下文中。@TestPropertySource 还可以 配置更多。

使用 @MockBean 注入模拟

如果我们只想测试应用程序的某个部分而不是从传入请求到数据库的整个路径,我们可以使用 @MockBean 替换应用程序上下文中的某些 bean:

@SpringBootTest
class MockBeanTest {

  @MockBean
  private UserRepository userRepository;

  @Autowired
  private RegisterUseCase registerUseCase;

  @Test
  void testRegister(){
    // given
    User user = new User("Zaphod", "zaphod@galaxy.net");
    boolean sendWelcomeMail = true;
    given(userRepository.save(any(UserEntity.class))).willReturn(userEntity(1L));

    // when
    Long userId = registerUseCase.registerUser(user, sendWelcomeMail);

    // then
    assertThat(userId).isEqualTo(1L);
  }

}

在这种情况下,我们用模拟替换了 UserRepository bean。使用 Mockitogiven 方法,我们指定了此模拟的预期行为,以测试使用此存储库的类。
您可以在我关于模拟的文章中阅读有关 @MockBean 注解的更多信息。

使用 @Import 添加 Bean

如果某些 bean 未包含在默认应用程序上下文中,但我们在测试中需要它们,我们可以使用 @Import 注解导入它们:

package other.namespace;

@Component
public class Foo {
}

@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {

  @Autowired
  Foo foo;

  @Test
  void test() {
    assertThat(foo).isNotNull();
  }
}

默认情况下,Spring Boot 应用程序包含它在其包和子包中找到的所有组件,因此通常只有在我们想要包含其他包中的 bean 时才需要这样做。

使用 @TestConfiguration 覆盖 Bean

使用 @TestConfiguration,我们不仅可以包含测试所需的其他 bean,还可以覆盖应用程序中已经定义的 bean。在我们关于使用 @TestConfiguration 进行测试的文章中阅读更多相关信息。

创建自定义 @SpringBootApplication

我们甚至可以创建一个完整的自定义 Spring Boot 应用程序来启动测试。如果这个应用程序类与真正的应用程序类在同一个包中,但是在测试源而不是生产源中,@SpringBootTest 会在实际应用程序类之前找到它,并从这个应用程序加载应用程序上下文。
或者,我们可以告诉 Spring Boot 使用哪个应用程序类来创建应用程序上下文:

@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}

但是,在执行此操作时,我们正在测试可能与生产环境完全不同的应用程序上下文,因此仅当无法在测试环境中启动生产应用程序时,这才应该是最后的手段。但是,通常有更好的方法,例如使真实的应用程序上下文可配置以排除不会在测试环境中启动的 bean。让我们看一个例子。
假设我们在应用程序类上使用 @EnableScheduling 注解。每次启动应用程序上下文时(即使在测试中),所有 @Scheduled 作业都将启动,并且可能与我们的测试冲突。 我们通常不希望作业在测试中运行,因此我们可以创建第二个没有 @EnabledScheduling 注释的应用程序类,并在测试中使用它。但是,更好的解决方案是创建一个可以使用属性切换的配置类:

@Configuration
@EnableScheduling
@ConditionalOnProperty(
        name = "io.reflectoring.scheduling.enabled",
        havingValue = "true",
        matchIfMissing = true)
public class SchedulingConfiguration {
}

我们已将 @EnableScheduling 注解从我们的应用程序类移到这个特殊的配置类。将属性 io.reflectoring.scheduling.enabled 设置为 false 将导致此类不会作为应用程序上下文的一部分加载:

@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {

  @Autowired(required = false)
  private SchedulingConfiguration schedulingConfiguration;

  @Test
  void test() {
    assertThat(schedulingConfiguration).isNull();
  }
}

我们现在已经成功地停用了测试中的预定作业。属性 io.reflectoring.scheduling.enabled 可以通过上述任何方式指定。

为什么我的集成测试这么慢?

包含大量 @SpringBootTest 注释测试的代码库可能需要相当长的时间才能运行。Spring 的测试支持 足够智能,只创建一次应用上下文并在后续测试中重复使用,但是如果不同的测试需要不同的应用上下文,它仍然会为每个测试创建一个单独的上下文,这需要一些时间来完成每个测试。
上面描述的所有自定义选项都会导致 Spring 创建一个新的应用程序上下文。因此,我们可能希望创建一个配置并将其用于所有测试,以便可以重用应用程序上下文。
如果您对测试花费在设置和 Spring 应用程序上下文上的时间感兴趣,您可能需要查看 JUnit Insights,它可以包含在 Gradle 或 Maven 构建中,以生成关于 JUnit 5 如何花费时间的很好的报告。

结论

@SpringBootTest 是一种为测试设置应用程序上下文的非常方便的方法,它非常接近我们将在生产中使用的上下文。有很多选项可以自定义此应用程序上下文,但应谨慎使用它们,因为我们希望我们的测试尽可能接近生产运行。
如果我们想在整个应用程序中进行测试,@SpringBootTest 会带来最大的价值。为了仅测试应用程序的某些切片或层,我们还有其他选项可用。
本文中使用的示例代码可在 github 上找到。

使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询

【注】本文译自: Testing JPA Queries with Spring Boot and @DataJpaTest – Reflectoring

除了单元测试,集成测试在生产高质量的软件中起着至关重要的作用。一种特殊的集成测试处理我们的代码和数据库之间的集成。
通过 @DataJpaTest 注释,Spring Boot 提供了一种便捷的方法来设置一个具有嵌入式数据库的环境,以测试我们的数据库查询。
在本教程中,我们将首先讨论哪些类型的查询值得测试,然后讨论创建用于测试的数据库模式和数据库状态的不同方法。

 代码示例

本文附有 GitHub 上的工作代码示例

“使用 Spring Boot 进行测试”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 进行单元测试
  2. 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
  4. 使用 Spring Boot 和 @SpringBootTest 进行测试

依赖

在本教程中,除了通常的 Spring Boot 依赖项之外,我们使用 JUnit Jupiter 作为我们的测试框架,使用 H2 作为内存数据库。

dependencies {
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-web')
  runtime('com.h2database:h2')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}

测试什么?

首先要回答我们自己的问题是我们需要测试什么。 让我们考虑一个负责 UserEntity 对象的 Spring Data 存储库:

interface UserRepository extends CrudRepository<UserEntity, Long> {
    // query methods
}

我们有不同的选项来创建查询。让我们详细看看其中的一些,以确定我们是否应该用测试来覆盖它们。

推断查询

第一个选项是创建一个推断查询:

UserEntity findByName(String name);

我们不需要告诉 Spring Data 要做什么,因为它会自动从方法名称的名称推断 SQL 查询。
这个特性的好处是 Spring Data 还会在启动时自动检查查询是否有效。如果我们将方法重命名为 findByFoo() 并且 UserEntity 没有属性 foo ,Spring Data 会向我们抛出一个异常来指出这一点:

org.springframework.data.mapping.PropertyReferenceException:
  No property foo found for type UserEntity!

因此,只要我们至少有一个测试尝试在我们的代码库中启动 Spring 应用程序上下文,我们就不需要为我们的推断查询编写额外的测试。

请注意,对于从 findByNameAndRegistrationDateBeforeAndEmailIsNotNull() 等长方法名称推断出的查询,情况并非如此。这个方法名很难掌握,也很容易出错,所以我们应该测试它是否真的符合我们的预期。

话虽如此,将此类方法重命名为更短、更有意义的名称并添加 @Query 注释以提供自定义 JPQL 查询是一种很好的做法。

使用 @Query 自定义 JPQL 查询

如果查询变得更复杂,提供自定义 JPQL 查询是有意义的:

@Query("select u from UserEntity u where u.name = :name")
UserEntity findByNameCustomQuery(@Param("name") String name);

与推断查询类似,我们可以免费对这些 JPQL 查询进行有效性检查。使用 Hibernate 作为我们的 JPA 提供者,如果发现无效查询,我们将在启动时得到一个 QuerySyntaxException

org.hibernate.hql.internal.ast.QuerySyntaxException:
unexpected token: foo near line 1, column 64 [select u from ...]

但是,自定义查询比通过单个属性查找条目要复杂得多。例如,它们可能包括与其他表的连接或返回复杂的 DTO 而不是实体。
那么,我们应该为自定义查询编写测试吗?令人不满意的答案是,我们必须自己决定查询是否复杂到需要测试。

使用 @Query 的本地查询

另一种方法是使用本地查询

@Query(
  value = "select * from user as u where u.name = :name",
  nativeQuery = true)
UserEntity findByNameNativeQuery(@Param("name") String name);

我们没有指定 JPQL 查询(它是对 SQL 的抽象),而是直接指定一个 SQL 查询。此查询可能使用特定数据库的 SQL 方言。
需要注意的是,Hibernate 和 Spring Data 都不会在启动时验证本地查询。由于查询可能包含特定于数据库的 SQL,因此 Spring Data 或 Hibernate 无法知道要检查什么。
因此,本地查询是集成测试的主要候选者。但是,如果他们真的使用特定数据库的 SQL,那么这些测试可能不适用于嵌入式内存数据库,因此我们必须在后台提供一个真实的数据库(比如,在持续集成管道中按需设置的 docker 容器中)。

@DataJpaTest 简介

为了测试 Spring Data JPA 存储库或任何其他与 JPA 相关的组件,Spring Boot 提供了 @DataJpaTest 注解。我们可以将它添加到单元测试中,它将设置一个 Spring 应用程序上下文:

@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserEntityRepositoryTest {

  @Autowired private DataSource dataSource;
  @Autowired private JdbcTemplate jdbcTemplate;
  @Autowired private EntityManager entityManager;
  @Autowired private UserRepository userRepository;

  @Test
  void injectedComponentsAreNotNull(){
    assertThat(dataSource).isNotNull();
    assertThat(jdbcTemplate).isNotNull();
    assertThat(entityManager).isNotNull();
    assertThat(userRepository).isNotNull();
  }
}

@ExtendWith
本教程中的代码示例使用 @ExtendWith 注解告诉 JUnit 5 启用 Spring 支持。从 Spring Boot 2.1 开始,我们不再需要加载 SpringExtension,因为它作为元注解包含在 Spring Boot 测试注解中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest。本教程中的代码示例使用 @ExtendWith 注解告诉 JUnit 5 启用 Spring 支持。从 Spring Boot 2.1 开始,我们不再需要加载 SpringExtension,因为它作为元注解包含在 Spring Boot 测试注解中,例如 @DataJpaTest@WebMvcTest@SpringBootTest

这样创建的应用程序上下文将不包含我们的 Spring Boot 应用程序所需的整个上下文,而只是它的一个“切片”,其中包含初始化任何 JPA 相关组件(如我们的 Spring Data 存储库)所需的组件。
例如,如果需要,我们可以将 DataSource@JdbcTemplate@EntityManage 注入我们的测试类。此外,我们可以从我们的应用程序中注入任何 Spring Data 存储库。上述所有组件将自动配置为指向嵌入式内存数据库,而不是我们可能在 application.propertiesapplication.yml 文件中配置的“真实”数据库。
请注意,默认情况下,包含所有这些组件(包括内存数据库)的应用程序上下文在所有 @DataJpaTest 注解的测试类中的所有测试方法之间共享。
这就是为什么在默认情况下每个测试方法都在自己的事务中运行的原因,该事务在方法执行后回滚。这样,数据库状态在测试之间保持原始状态,并且测试保持相互独立。

创建数据库模式

在我们可以测试对数据库的任何查询之前,我们需要创建一个 SQL 模式来使用。让我们看看一些不同的方法来做到这一点。

使用 Hibernate ddl-auto

默认情况下,@DataJpaTest 会配置 Hibernate 为我们自动创建数据库模式。对此负责的属性是 spring.jpa.hibernate.ddl-auto,Spring Boot 默认将其设置为 create-drop,这意味着模式在运行测试之前创建并在测试执行后删除。
因此,如果我们对 Hibernate 为我们创建模式感到满意,我们就不必做任何事情。

使用 schema.sql

Spring Boot 支持在应用程序启动时执行自定义 schema.sql 文件。
如果 Spring 在类路径中找到 schema.sql 文件,则将针对数据源执行该文件。 这会覆盖上面讨论的 Hibernate 的 ddl-auto 配置。
我们可以使用属性 spring.datasource.initialization-mode 控制是否应该执行 schema.sql。默认值是嵌入的,这意味着它只会对嵌入的数据库执行(即在我们的测试中)。如果我们将其设置为 always,它将始终执行。
以下日志输出确认文件已被执行:

Executing SQL script from URL [file:.../out/production/resources/schema.sql]

设置 Hibernate 的 ddl-auto 配置以在使用脚本初始化架构时进行验证是有意义的,以便 Hibernate 在启动时检查创建的模式是否与实体类匹配:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.hibernate.ddl-auto=validate"
})
class SchemaSqlTest {
  ...
}

使用 Flyway

Flyway 是一种数据库迁移工具,允许指定多个 SQL 脚本来创建数据库模式。它会跟踪目标数据库上已经执行了这些脚本中的哪些脚本,以便只执行之前没有执行过的脚本。
要激活 Flyway,我们只需要将依赖项放入我们的 build.gradle 文件中(如果我们使用 Maven,则类似):

compile('org.flywaydb:flyway-core')

如果我们没有专门配置 Hibernate 的 ddl-auto 配置,它会自动退出,因此 Flyway 具有优先权,并且默认情况下会针对我们的内存数据库测试执行它在文件夹 src/main/resources/db/migration 中找到的所有 SQL 脚本。
同样,将 ddl-auto 设置为 validate 是有意义的,让 Hibernate 检查 Flyway 生成的模式是否符合我们的 Hibernate 实体的期望:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.hibernate.ddl-auto=validate"
})
class FlywayTest {
  ...
}

在测试中使用 Flyway 的价值

如果我们在生产中使用 Flyway,也能在上面描述的那样在 JPA 测试中使用它,那就太好了。只有这样我们才能在测试时知道 flyway 脚本按预期工作。
但是,这仅适用于脚本包含在生产数据库和测试中使用的内存数据库(我们的示例中为 H2 数据库)上都有效的 SQL。如果不是这种情况,我们必须在我们的测试中禁用 Flyway,方法是将 spring.flyway.enabled 属性设置为 false,并将 spring.jpa.hibernate.ddl-auto 属性设置为 create-drop 以让 Hibernate 生成模式。
无论如何,让我们确保将 ddl-auto 属性在生产配置文件中设置为 validate!这是我们抵御 Flyway 脚本错误的最后一道防线!无论如何,让我们确保将 ddl-auto 属性在生产配置文件中设置为 validate!这是我们抵御 Flyway 脚本错误的最后一道防线!

使用 Liquibase

Liquibase 是另一种数据库迁移工具,其工作方式类似于 Flyway,但支持除 SQL 之外的其他输入格式。例如,我们可以提供定义数据库架构的 YAML 或 XML 文件。
我们只需添加依赖项即可激活它:

compile('org.liquibase:liquibase-core')

默认情况下,Liquibase 将自动创建在 src/main/resources/db/changelog/db.changelog-master.yaml 中定义的模式。
同样,设置 ddl-autovalidate 是有意义的:

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.hibernate.ddl-auto=validate"
})
class LiquibaseTest {
  ...
}

在测试中使用 Liquibase 的价值

由于 Liquibase 允许多种输入格式充当 SQL 上的抽象层,因此即使它们的 SQL 方言不同,也可以跨多个数据库使用相同的脚本。这使得在我们的测试和生产中使用相同的 Liquibase 脚本成为可能。
不过,YAML 格式非常敏感,而且我最近在维护大型 YAML 文件集合时遇到了麻烦。这一点,以及尽管我们实际上必须为不同的数据库编辑这些文件的抽象,最终导致转向 Flyway。

填充数据库

现在我们已经为我们的测试创建了一个数据库模式,我们终于可以开始实际的测试了。在数据库查询测试中,我们通常会向数据库添加一些数据,然后验证我们的查询是否返回正确的结果。
同样,有多种方法可以将数据添加到我们的内存数据库中,所以让我们逐一讨论。

使用 data.sql

schema.sql 类似,我们可以使用包含插入语句的 data.sql 文件来填充我们的数据库。上述规则同样适用。

可维护性

data.sql 文件迫使我们将所有 insert 语句放在一个地方。每一个测试都将依赖于这个脚本来设置数据库状态。这个脚本很快就会变得非常大并且难以维护。如果有需要冲突数据库状态的测试怎么办?
因此,应谨慎考虑这种方法。

手动插入实体

为每个测试创建特定数据库状态的最简单方法是在运行被测查询之前在测试中保存一些实体:

@Test
void whenSaved_thenFindsByName() {
  userRepository.save(new UserEntity(
          "Zaphod Beeblebrox",
          "zaphod@galaxy.net"));
  assertThat(userRepository.findByName("Zaphod Beeblebrox")).isNotNull();
}

这对于上面示例中的简单实体来说很容易。但在实际项目中,这些实体的构建和与其他实体的关系通常要复杂得多。此外,如果我们想测试比 findByName 更复杂的查询,很可能我们需要创建比单个实体更多的数据。这很快变得非常令人厌烦。
控制这种复杂性的一种方法是创建工厂方法,可能结合 Objectmother 和 Builder 模式。
在 Java 代码中“手动”对数据库进行编程的方法比其他方法有很大的优势,因为它是重构安全的。代码库中的更改会导致我们的测试代码中出现编译错误。在所有其他方法中,我们必须运行测试才能收到有关重构导致的潜在错误的通知。使用

Spring DBUnit

DBUnit 是一个支持将数据库设置为某种状态的库。Spring DBUnit 将 DBUnit 与 Spring 集成在一起,因此它可以自动与 Spring 的事务等一起工作。
要使用它,我们需要向 Spring DBUnit 和 DBUnit 添加依赖项:

compile('com.github.springtestdbunit:spring-test-dbunit:1.3.0')
compile('org.dbunit:dbunit:2.6.0')

然后,对于每个测试,我们可以创建一个包含所需数据库状态的自定义 XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <user
        id="1"
        name="Zaphod Beeblebrox"
        email="zaphod@galaxy.net"
    />
</dataset>

默认情况下,XML 文件(我们将其命名为 createUser.xml)位于测试类旁边的类路径中。
在测试类中,我们需要添加两个 TestExecutionListeners 来启用 DBUnit 支持。要设置某个数据库状态,我们可以在测试方法上使用 @DatabaseSetup

@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        TransactionDbUnitTestExecutionListener.class
})
class SpringDbUnitTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  @DatabaseSetup("createUser.xml")
  void whenInitializedByDbUnit_thenFindsByName() {
    UserEntity user = userRepository.findByName("Zaphod Beeblebrox");
    assertThat(user).isNotNull();
  }
}

对于更改数据库状态的测试查询,我们甚至可以使用 @ExpectedDatabase 来定义数据库在测试预期处于的状态。
但是请注意,自 2016 年以来,Spring DBUnit 没有再维护

@DatabaseSetup 不起作用?

在我的测试中,我遇到了 @DatabaseSetup 注释被默默忽略的问题。原来有一个 ClassNotFoundException 因为找不到某些 DBUnit 类。不过,这个异常被吞了。
原因是我忘记包含对 DBUnit 的依赖,因为我认为 Spring Test DBUnit 可递进地含它。因此,如果您遇到相同的问题,请检查您是否包含了这两个依赖项。

使用 @Sql

一个非常相似的方法是使用 Spring 的 @Sql 注解。我们没有使用 XML 来描述数据库状态,而是直接使用 SQL:

INSERT INTO USER
            (id,
             NAME,
             email)
VALUES      (1,
             'Zaphod Beeblebrox',
             'zaphod@galaxy.net');

在我们的测试中,我们可以简单地使用 @Sql 注解来引用 SQL 文件来填充数据库:

@ExtendWith(SpringExtension.class)
@DataJpaTest
class SqlTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  @Sql("createUser.sql")
  void whenInitializedByDbUnit_thenFindsByName() {
    UserEntity user = userRepository.findByName("Zaphod Beeblebrox");
    assertThat(user).isNotNull();
  }

}

如果我们需要多个脚本,我们可以使用 @SqlGroup 来组合它们。

结论

为了测试数据库查询,我们需要创建模式并用一些数据填充它的方法。由于测试应该相互独立,因此最好对每个测试分别执行此操作。
对于简单的测试和简单的数据库实体,通过创建和保存 JPA 实体手动创建状态就足够了。对于更复杂的场景,@DatabaseSetup@Sql 提供了一种在 XML 或 SQL 文件中外部化数据库状态的方法。

使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller

【注】本文译自: Testing MVC Web Controllers with Spring Boot and @WebMvcTest – Reflectoring

在有关使用 Spring Boot 进行测试的系列的第二部分中,我们将了解 Web 控制器。首先,我们将探索 Web 控制器的实际作用,这样我们就可以构建涵盖其所有职责的测试。
然后,我们将找出如何在测试中涵盖这些职责。只有涵盖了这些职责,我们才能确保我们的控制器在生产环境中按预期运行。

 代码示例

本文附有 GitHub 上的工作代码示例。

“使用 Spring Boot 进行测试”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 进行单元测试
  2. 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
  4. 使用 Spring Boot 和 @SpringBootTest 进行测试

依赖

我们将使用 JUnit Jupiter (JUnit 5) 作为测试框架,使用 Mockito 进行模拟,使用 AssertJ 来创建断言,使用 Lombok 来减少样板代码:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
    testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

AssertJ 和 Mockito 跟随 spring-boot-starter-test 依赖自动获得。

Web 控制器的职责

让我们从一个典型的 REST 控制器开始:

@RestController
@RequiredArgsConstructor
class RegisterRestController {
    private final RegisterUseCase registerUseCase;

    @PostMapping("/forums/{forumId}/register")
    UserResource register(@PathVariable("forumId") Long forumId, @Valid @RequestBody UserResource userResource,
            @RequestParam("sendWelcomeMail") boolean sendWelcomeMail) {

        User user = new User(userResource.getName(), userResource.getEmail());
        Long userId = registerUseCase.registerUser(user, sendWelcomeMail);

        return new UserResource(userId, user.getName(), user.getEmail());
    }

}

控制器方法用 @PostMapping 注解来定义它应该侦听的 URL、HTTP 方法和内容类型。
它通过用 @PathVariable@RequestBody@RequestParam 注解的参数获取输入,这些参数会从传入的 HTTP 请求中自动填充。
参数可以使用 @Valid 进行注解,以指示 Spring 应该对它们 bean 验证
然后控制器使用这些参数,调用业务逻辑返回一个普通的 Java 对象,默认情况下该对象会自动映射到 JSON 并写入 HTTP 响应体。
这里有很多 spring 魔法。总之,对于每个请求,控制器通常会执行以下步骤:

# 职责 描述
1. 监听 HTTP 请求 控制器应该响应某些 URL、HTTP 方法和内容类型。
2. 反序列化输入 控制器应该解析传入的 HTTP 请求并根据 URL、HTTP 请求参数和请求正文中的变量创建 Java 对象,以便我们可以在代码中使用它们。
3. 验证输入 控制器是防止错误输入的第一道防线,因此它是我们可以验证输入的地方。
4. 调用业务逻辑 解析输入后,控制器必须将输入转换为业务逻辑期望的模型并将其传递给业务逻辑。
5. 序列化输出 控制器获取业务逻辑的输出并将其序列化为 HTTP 响应。
6. 转换异常 如果在某个地方发生异常,控制器应将其转换为对用户有意义的错误消息和 HTTP 状态。

控制器显然有很多工作要做!
我们应该注意不要添加更多的职责,比如执行业务逻辑。否则,我们的控制器测试将变得臃肿且无法维护。
我们将如何编写有意义的测试,涵盖所有这些职责?

单元测试还是集成测试?

我们写单元测试吗?还是集成测试?到底有什么区别?让我们讨论这两种方法并决定其中一种。
在单元测试中,我们将单独测试控制器。这意味着我们将实例化一个控制器对象,模拟业务逻辑,然后调用控制器的方法并验证响应。
这对我们有用吗?让我们检查一下可以单独的单元测试中涵盖上面确定的 6 个职责中的哪一个:

# 职责 可以在单元测试中涵盖吗
1. 监听 HTTP 请求 ❌ 不,因为单元测试不会评估 @PostMapping 注解和指定 HTTP 请求属性的类似注解。
2. 反序列化输入 ❌ 不,因为像@RequestParam 和 @PathVariable 这样的注释不会被评估。相反,我们将输入作为 Java 对象提供,从而有效地跳过 HTTP 请求的反序列化。
3. 验证输入 ❌ 不依赖于 bean 验证,因为不会评估 @Valid 注释。
4. 调用业务逻辑 ✔ 是的,因为我们可以验证是否使用预期的参数调用了模拟的业务逻辑。
5. 序列化输出 ❌ 不能,因为我们只能验证输出的 Java 版本,而不能验证将生成的 HTTP 响应。
6. 转换异常 ❌ 不可以。我们可以检查是否引发了某个异常,但不能检查它是否被转换为某个 JSON 响应或 HTTP 状态代码。

与 Spring 的集成测试会启动一个包含我们需要的所有 bean 的 Spring 应用程序上下文。这包括负责侦听某些 URL、与 JSON 之间进行序列化和反序列化以及将异常转换为 HTTP 的框架 bean。这些 bean 将评估简单单元测试会忽略的注释。总之,简单的单元测试不会覆盖 HTTP 层。所以,我们需要在我们的测试中引入 Spring 来为我们做 HTTP 魔法。因此,我们正在构建一个集成测试来测试我们的控制器代码和 Spring 为 HTTP 支持提供的组件之间的集成。
那么,我们该怎么做呢?

使用 @WebMvcTest 验证控制器职责

Spring Boot 提供了 @WebMvcTest 注释来启动一个应用程序上下文,该上下文只包含测试 Web 控制器所需的 bean:

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = RegisterRestController.class)
class RegisterRestControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private RegisterUseCase registerUseCase;

  @Test
  void whenValidInput_thenReturns200() throws Exception {
    mockMvc.perform(...);
  }
}

@ExtendWith
本教程中的代码示例使用 @ExtendWith 批注告诉 JUnit 5 启用 Spring 支持。从 Spring Boot 2.1 开始,我们不再需要加载 SpringExtension,因为它作为元注释包含在 Spring Boot 测试注解中,例如 @DataJpaTest@WebMvcTest@SpringBootTest

我们现在可以 @Autowire 从应用程序上下文中获取我们需要的所有 bean。Spring Boot 自动提供了像 ObjectMapper 这样的 bean 来映射到 JSON 和一个 MockMvc 实例来模拟 HTTP 请求。
我们使用 @MockBean 来模拟业务逻辑,因为我们不想测试控制器和业务逻辑之间的集成,而是控制器和 HTTP 层之间的集成。@MockBean 自动用 Mockito 模拟替换应用程序上下文中相同类型的 bean。
您可以在我关于模拟的文章中阅读有关 @MockBean 注解的更多信息。

使用带或不带 controllers 参数的 @WebMvcTest
通过在上面的示例中将 controllers 参数设置为 RegisterRestController.class,我们告诉 Spring Boot 将为此测试创建的应用程序上下文限制为给定的控制器 bean 和 Spring Web MVC 所需的一些框架 bean。我们可能需要的所有其他 bean 必须单独包含或使用 @MockBean 模拟。
如果我们不使用 controllers 参数,Spring Boot 将在应用程序上下文中包含所有控制器。因此,我们需要包含或模拟掉任何控制器所依赖的所有 bean。这使得测试设置更加复杂,具有更多的依赖项,但节省了运行时间,因为所有控制器测试都将重用相同的应用程序上下文。
我倾向于将控制器测试限制在最窄的应用程序上下文中,以使测试独立于我在测试中甚至不需要的 bean,即使 Spring Boot 必须为每个单独的测试创建一个新的应用程序上下文。

让我们来回顾一下每个职责,看看我们如何使用 MockMvc 来验证每一个职责,以便构建我们力所能及的最好的集成测试。

1. 验证 HTTP 请求匹配

验证控制器是否侦听某个 HTTP 请求非常简单。我们只需调用 MockMvcperform() 方法并提供我们要测试的 URL:

mockMvc.perform(post("/forums/42/register")
    .contentType("application/json"))
    .andExpect(status().isOk());

除了验证控制器对特定 URL 的响应之外,此测试还验证正确的 HTTP 方法(在我们的示例中为 POST)和正确的请求内容类型。我们上面看到的控制器会拒绝任何具有不同 HTTP 方法或内容类型的请求。
请注意,此测试仍然会失败,因为我们的控制器需要一些输入参数。
更多匹配 HTTP 请求的选项可以在 MockHttpServletRequestBuilder 的 Javadoc 中找到。

2. 验证输入序列化

为了验证输入是否成功序列化为 Java 对象,我们必须在测试请求中提供它。输入可以是请求正文的 JSON 内容 (@RequestBody)、URL 路径中的变量 (@PathVariable) 或 HTTP 请求参数 (@RequestParam):

@Test
void whenValidInput_thenReturns200() throws Exception {
  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");

   mockMvc.perform(post("/forums/{forumId}/register", 42L)
        .contentType("application/json")
        .param("sendWelcomeMail", "true")
        .content(objectMapper.writeValueAsString(user)))
        .andExpect(status().isOk());
}

我们现在提供路径变量 forumId、请求参数 sendWelcomeMail 和控制器期望的请求正文。请求正文是使用 Spring Boot 提供的 ObjectMapper 生成的,将 UserResource 对象序列化为 JSON 字符串。
如果测试结果为绿色,我们现在知道控制器的 register() 方法已将这些参数作为 Java 对象接收,并且它们已从 HTTP 请求中成功解析。

3. 验证输入验证

假设 UserResource 使用 @NotNull 注释来拒绝 null 值:

@Value
public class UserResource {

    @NotNull
    private final String name;

    @NotNull
    private final String email;

}

当我们@Valid 注解添加到方法参数时,Bean 验证会自动触发,就像我们在控制器中使用 userResource 参数所做的那样。因此,对于快乐路径(即验证成功时),我们在上一节中创建的测试就足够了。
如果我们想测试验证是否按预期失败,我们需要添加一个测试用例,在该用例中我们将无效的 UserResource JSON 对象发送到控制器。然后我们期望控制器返回 HTTP 状态 400(错误请求):

@Test
void whenNullValue_thenReturns400() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");

  mockMvc.perform(post("/forums/{forumId}/register", 42L)
      ...
      .content(objectMapper.writeValueAsString(user)))
      .andExpect(status().isBadRequest());
}

根据验证对应用程序的重要性,我们可能会为每个可能的无效值添加这样的测试用例。但是,这会很快增加很多测试用例,因此您应该与您的团队讨论您希望如何处理项目中的验证测试。

4. 验证业务逻辑调用

接下来,我们要验证业务逻辑是否按预期调用。在我们的例子中,业务逻辑由 RegisterUseCase 接口提供,并需要一个 User 对象和一个 boolean 值作为输入:

interface RegisterUseCase {
    Long registerUser(User user, boolean sendWelcomeMail);
}

我们希望控制器将传入的 UserResource 对象转换为 User 并将此对象传递给 registerUser() 方法。
为了验证这一点,我们可以要求 RegisterUseCase 模拟,它已使用 @MockBean 注解注入到应用程序上下文中:

@Test
void whenValidInput_thenMapsToBusinessModel() throws Exception {
  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
  mockMvc.perform(...);

  ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
  verify(registerUseCase, times(1)).registerUser(userCaptor.capture(), eq(true));
  assertThat(userCaptor.getValue().getName()).isEqualTo("Zaphod");
  assertThat(userCaptor.getValue().getEmail()).isEqualTo("zaphod@galaxy.net");
}

在执行了对控制器的调用之后,我们使用 ArgumentCaptor 来捕获传递给 RegisterUseCase.registerUser()User 对象并断言它包含预期值。
调用 verify 检查 registerUser() 是否被调用过一次。
请注意,如果我们对 User 对象进行大量断言,我们可以 创建自己的自定义 Mockito 断言方法 以获得更好的可读性。

5. 验证输出序列化

调用业务逻辑后,我们希望控制器将结果映射到 JSON 字符串并将其包含在 HTTP 响应中。在我们的例子中,我们希望 HTTP 响应正文包含一个有效的 JSON 格式的 UserResource 对象:

@Test
void whenValidInput_thenReturnsUserResource() throws Exception {
  MvcResult mvcResult = mockMvc.perform(...)
      ...
      .andReturn();

  UserResource expectedResponseBody = ...;
  String actualResponseBody = mvcResult.getResponse().getContentAsString();

  assertThat(actualResponseBody).isEqualToIgnoringWhitespace(
              objectMapper.writeValueAsString(expectedResponseBody));
}

要对响应主体进行断言,我们需要使用 andReturn() 方法将 HTTP 交互的结果存储在 MvcResult 类型的变量中。
然后我们可以从响应正文中读取 JSON 字符串,并使用 isEqualToIgnoringWhitespace() 将其与预期的字符串进行比较。我们可以使用 Spring Boot 提供的 ObjectMapper 从 Java 对象构建预期的 JSON 字符串。
请注意,我们可以通过使用自定义的 ResultMatcher 使其更具可读性,稍后对此加以描述

6. 验证异常处理

通常,如果发生异常,控制器应该返回某个 HTTP 状态。400 — 如果请求有问题,500 — 如果出现异常,等等。
默认情况下,Spring 会处理大多数这些情况。但是,如果我们有自定义异常处理,我们想测试它。假设我们想要返回一个结构化的 JSON 错误响应,其中包含请求中每个无效字段的字段名称和错误消息。我们会像这样创建一个 @ControllerAdvice

@ControllerAdvice
class ControllerExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        ErrorResult errorResult = new ErrorResult();
        for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
            errorResult.getFieldErrors()
                    .add(new FieldValidationError(fieldError.getField(), fieldError.getDefaultMessage()));
        }
        return errorResult;
    }

    @Getter
    @NoArgsConstructor
    static class ErrorResult {
        private final List<FieldValidationError> fieldErrors = new ArrayList<>();

        ErrorResult(String field, String message) {
            this.fieldErrors.add(new FieldValidationError(field, message));
        }
    }

    @Getter
    @AllArgsConstructor
    static class FieldValidationError {
        private String field;
        private String message;
    }
}

如果 bean 验证失败,Spring 将抛出 MethodArgumentNotValidException。我们通过将 Spring 的 FieldError 对象映射到我们自己的 ErrorResult 数据结构来处理这个异常。在这种情况下,异常处理程序会导致所有控制器返回 HTTP 状态 400,并将 ErrorResult 对象作为 JSON 字符串放入响应正文中。
为了验证这确实发生了,我们扩展了我们之前对失败验证的测试:

@Test
void whenNullValue_thenReturns400AndErrorResult() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");

  MvcResult mvcResult = mockMvc.perform(...)
          .contentType("application/json")
          .param("sendWelcomeMail", "true")
          .content(objectMapper.writeValueAsString(user)))
          .andExpect(status().isBadRequest())
          .andReturn();

  ErrorResult expectedErrorResponse = new ErrorResult("name", "must not be null");
  String actualResponseBody =
      mvcResult.getResponse().getContentAsString();
  String expectedResponseBody =
      objectMapper.writeValueAsString(expectedErrorResponse);
  assertThat(actualResponseBody)
      .isEqualToIgnoringWhitespace(expectedResponseBody);
}

同样,我们从响应正文中读取 JSON 字符串,并将其与预期的 JSON 字符串进行比较。此外,我们检查响应状态是否为 400。
这也可以以可读性更强的方式实现,我们接下来将要学习。创建自定义 ResultMatcher
某些断言很难写,更重要的是,很难阅读。特别是当我们想要将来自 HTTP 响应的 JSON 字符串与预期值进行比较时,它需要大量代码,正如我们在最后两个示例中看到的那样。
幸运的是,我们可以创建自定义的 ResultMatcher,我们可以在 MockMvc 的流畅 API 中使用它们。让我们看看如何做到这一点。匹配 JSON 输出
使用以下代码来验证 HTTP 响应正文是否包含某个 Java 对象的 JSON 表示不是很好吗?

@Test
void whenValidInput_thenReturnsUserResource_withFluentApi() throws Exception {
  UserResource user = ...;
  UserResource expected = ...;

  mockMvc.perform(...)
      ...
      .andExpect(responseBody().containsObjectAsJson(expected, UserResource.class));
}

不再需要手动比较 JSON 字符串。它的可读性要好得多。事实上,代码是如此的一目了然,这里我无需解释。
为了能够使用上面的代码,我们创建了一个自定义的 ResultMatcher

public class ResponseBodyMatchers {
    private ObjectMapper objectMapper = new ObjectMapper();

    public <T> ResultMatcher containsObjectAsJson(Object expectedObject, Class<T> targetClass) {
        return mvcResult -> {
            String json = mvcResult.getResponse().getContentAsString();
            T actualObject = objectMapper.readValue(json, targetClass);
            assertThat(actualObject).isEqualToComparingFieldByField(expectedObject);
        };
    }

    static ResponseBodyMatchers responseBody() {
        return new ResponseBodyMatchers();
    }

}

静态方法 responseBody() 用作我们流畅的 API 的入口点。它返回实际的 ResultMatcher,它从 HTTP 响应正文解析 JSON,并将其与传入的预期对象逐个字段进行比较。匹配预期的验证错误
我们甚至可以更进一步简化我们的异常处理测试。我们用了 4 行代码来验证 JSON 响应是否包含某个错误消息。我们可以改为一行:

@Test
void whenNullValue_thenReturns400AndErrorResult_withFluentApi() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");

  mockMvc.perform(...)
      ...
      .content(objectMapper.writeValueAsString(user)))
      .andExpect(status().isBadRequest())
      .andExpect(responseBody().containsError("name", "must not be null"));
}

同样,代码是自解释的。
为了启用这个流畅的 API,我们必须从上面添加方法 containsErrorMessageForField() 到我们的 ResponseBodyMatchers 类:

public class ResponseBodyMatchers {
    private ObjectMapper objectMapper = new ObjectMapper();

    public ResultMatcher containsError(String expectedFieldName, String expectedMessage) {
        return mvcResult -> {
            String json = mvcResult.getResponse().getContentAsString();
            ErrorResult errorResult = objectMapper.readValue(json, ErrorResult.class);
            List<FieldValidationError> fieldErrors = errorResult.getFieldErrors().stream()
                    .filter(fieldError -> fieldError.getField().equals(expectedFieldName))
                    .filter(fieldError -> fieldError.getMessage().equals(expectedMessage)).collect(Collectors.toList());

            assertThat(fieldErrors).hasSize(1).withFailMessage(
                    "expecting exactly 1 error message" + "with field name '%s' and message '%s'", expectedFieldName,
                    expectedMessage);
        };
    }

    static ResponseBodyMatchers responseBody() {
        return new ResponseBodyMatchers();
    }
}

所有丑陋的代码都隐藏在这个辅助类中,我们可以在集成测试中愉快地编写干净的断言。

结论

Web 控制器有很多职责。如果我们想用有意义的测试覆盖一个 web 控制器,仅仅检查它是否返回正确的 HTTP 状态是不够的。
通过 @WebMvcTest,Spring Boot 提供了我们构建 Web 控制器测试所需的一切,但为了使测试有意义,我们需要记住涵盖所有职责。否则,我们可能会在运行时遇到丑陋的惊喜。
本文中的示例代码可在 GitHub 上找到。

使用 Spring Boot 进行单元测试

【注】本文译自: Unit Testing with Spring Boot – Reflectoring

编写好的单元测试可以被认为是一门难以掌握的艺术。但好消息是支持它的机制很容易学习。
本教程为您提供了这些机制,并详细介绍了编写良好的单元测试所必需的技术细节,重点是 Spring Boot 应用程序。
我们将看看如何以可测试的方式创建 Spring bean,然后讨论 Mockito 和 AssertJ 的用法,这两个库默认包含在 Spring Boot 中用于测试。
请注意,本文仅讨论单元测试。集成测试、Web 层测试和持久层测试将在本系列的后续文章中讨论。

 代码示例

本文附有 GitHub 上 的工作代码示例。

“使用 Spring Boot 进行测试”系列

本教程是系列的一部分:

  1. 使用 Spring Boot 进行单元测试
  2. 使用 Spring Boot 和 @WebMvcTest 测试 MVC Web Controller
  3. 使用 Spring Boot 和 @DataJpaTest 测试 JPA 查询
  4. 使用 Spring Boot 和 @SpringBootTest 进行测试

依赖关系

对于本教程中的单元测试,我们将使用 JUnit Jupiter (JUnit 5)、Mockito 和 AssertJ。我们还将包括 Lombok 以减少一些样板代码:

dependencies {
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
    testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

Mockito 和 AssertJ 是使用 spring-boot-starter-test 依赖项自动导入的,但我们必须自己包含 Lombok。

不要在单元测试中使用 Spring

如果你以前用 Spring 或 Spring Boot 写过测试,你可能会说我们不需要 Spring 来写单元测试。这是为什么?
考虑以下测试 RegisterUseCase 类的单个方法的“单元”测试:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {

    @Autowired
    private RegisterUseCase registerUseCase;

    @Test
    void savedUserHasRegistrationDate() {
        User user = new User("zaphod", "zaphod@mail.com");
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();
    }

}

这个测试在我电脑上的一个空 Spring 项目上运行大约需要 4.5 秒。
但是一个好的单元测试只需要几毫秒。否则它会阻碍由测试驱动开发(TDD)思想推动的“测试/代码/测试”流程。但即使我们不采用 TDD,等待太长时间的测试也会破坏我们的注意力。
执行上面的测试方法实际上只需要几毫秒。 剩下的 4.5 秒是由于 @SpringBootRun 告诉 Spring Boot 设置整个 Spring Boot 应用程序上下文。
所以我们启动了整个应用程序只是为了将 RegisterUseCase 实例自动装配到我们的测试中。一旦应用程序变大并且 Spring 不得不将越来越多的 bean 加载到应用程序上下文中,它将花费更长的时间。
那么,为什么我们不应该在单元测试中使用 Spring Boot 呢?老实说,本教程的大部分内容都是关于在没有 Spring Boot 的情况下编写单元测试。

创建可测试的 Spring Bean

然而,我们可以做一些事情来提高 Spring bean 的可测试性。

字段注入是不可取的

让我们从一个不好的例子开始。考虑以下类:

@Service
public class RegisterUseCase {

    @Autowired
    private UserRepository userRepository;

    public User registerUser(User user) {
        return userRepository.save(user);
    }

}

这个类不能在没有 Spring 的情况下进行单元测试,因为它没有提供传递 UserRepository 实例的方法。那么,我们需要按照上一节中讨论的方式编写测试,让 Spring 创建一个 UserRepository 实例并将其注入到用 @Autowired 注解的字段中。

这里的教训是不要使用字段注入

提供构造函数

实际上,我们根本不要使用 @Autowired 注解:

@Service
public class RegisterUseCase {

    private final UserRepository userRepository;

    public RegisterUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User registerUser(User user) {
        return userRepository.save(user);
    }

}

这个版本通过提供允许传入 UserRepository 实例的构造函数来允许构造函数注入。在单元测试中,我们现在可以创建这样一个实例(可能是我们稍后讨论的模拟实例)并将其传递给构造函数。
在创建生产应用程序上下文时,Spring 将自动使用此构造函数来实例化 RegisterUseCase 对象。注意,在 Spring 5 之前,我们需要在构造函数中添加 @Autowired 注解,以便 Spring 找到构造函数。
还要注意 UserRepository 字段现在是 final。这是有道理的,因为字段内容在应用程序的生命周期内永远不会改变。它还有助于避免编程错误,因为如果我们忘记初始化字段,编译器会报错。

减少样板代码

使用 Lombok 的 @RequiredArgsConstructor 注解,我们可以让构造函数自动生成:

@Service
@RequiredArgsConstructor
public class RegisterUseCase {

    private final UserRepository userRepository;

    public User registerUser(User user) {
        user.setRegistrationDate(LocalDateTime.now());
        return userRepository.save(user);
    }

}

现在,我们有一个非常简洁的类,没有样板代码,可以在普通的 java 测试用例中轻松实例化:

class RegisterUseCaseTest {

    private UserRepository userRepository = ...;

    private RegisterUseCase registerUseCase;

    @BeforeEach
    void initUseCase() {
        registerUseCase = new RegisterUseCase(userRepository);
    }

    @Test
    void savedUserHasRegistrationDate() {
        User user = new User("zaphod", "zaphod@mail.com");
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();
    }

}

然而,还缺少一点,那就是如何模拟我们被测类所依赖的 UserRepository 实例,因为我们不想依赖真实的东西,它可能需要连接到数据库。

使用 Mockito 来模拟依赖

现在事实上的标准模拟库是 Mockito。它至少提供了两种方法来创建模拟的 UserRepository 以填补前面代码示例中的空白。

使用普通 Mockito 模拟依赖项

第一种方法是以编程方式使用 Mockito:

private UserRepository userRepository = Mockito.mock(UserRepository.class);

这将创建一个从外部看起来像 UserRepository 的对象。默认情况下,当一个方法被调用时它什么都不做,如果该方法有返回值则返回 null
我们的测试现在将在 assertThat(savedUser.getRegistrationDate()).isNotNull() 处以 NullPointerException 失败,因为 userRepository.save(user) 现在返回 null
所以,我们必须告诉 Mockito 在调用 userRepository.save() 时返回一些东西。我们使用静态 when 方法来做到这一点:

    @Test
    void savedUserHasRegistrationDate() {
        User user = new User("zaphod", "zaphod@mail.com");
        when(userRepository.save(any(User.class))).then(returnsFirstArg());
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();
    }

这将使 userRepository.save() 返回传递给方法的相同用户对象。
Mockito 具有更多功能,可以进行模拟、匹配参数和验证方法调用。有关更多信息,请查看参考文档

使用 Mockito 的 @Mock 注解模拟依赖项

创建模拟对象的另一种方法是 Mockito 的 @Mock 注解与 JUnit Jupiter 的 MockitoExtension 相结合:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    private RegisterUseCase registerUseCase;

    @BeforeEach
    void initUseCase() {
        registerUseCase = new RegisterUseCase(userRepository);
    }

    @Test
    void savedUserHasRegistrationDate() {
        // ...
    }

}

@Mock 注解指定了 Mockito 应该注入模拟对象的字段。 @MockitoExtension 告诉 Mockito 评估那些 @Mock 注解,因为 JUnit 不会自动执行此操作。
结果和手动调用 Mockito.mock() 一样,选择使用哪种方式是品味问题。 但是请注意,通过使用 MockitoExtension 将我们的测试绑定到测试框架。
请注意,我们也可以在 registerUseCase 字段上使用 @InjectMocks 注解,而不是手动构造 RegisterUseCase 对象。然后 Mockito 会按照指定的算法为我们创建一个实例:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private RegisterUseCase registerUseCase;

    @Test
    void savedUserHasRegistrationDate() {
        // ...
    }

}

使用 AssertJ 创建可读断言

Spring Boot 测试支持自动附带的另一个库是 AssertJ。我们已经在上面使用它来实现我们的断言:

assertThat(savedUser.getRegistrationDate()).isNotNull();

然而,让断言更具可读性不是更好吗?例如:

assertThat(savedUser).hasRegistrationDate();

在很多情况下,像这样的小改动会使测试更容易理解。因此,让我们在测试源文件夹中创建我们自己的自定义断言

class UserAssert extends AbstractAssert<UserAssert, User> {

    UserAssert(User user) {
        super(user, UserAssert.class);
    }

    static UserAssert assertThat(User actual) {
        return new UserAssert(actual);
    }

    UserAssert hasRegistrationDate() {
        isNotNull();
        if (actual.getRegistrationDate() == null) {
            failWithMessage(
                    "Expected user to have a registration date, but it was null"
            );
        }
        return this;
    }
}

现在,如果我们从新的 UserAssert 类而不是从 AssertJ 库导入 assertThat 方法,我们就可以使用新的、更易于阅读的断言。
创建像这样的自定义断言似乎需要很多工作,但实际上只需几分钟即可完成。我坚信投入这些时间来创建可读的测试代码是值得的,即使之后它的可读性只是稍微好一点。毕竟,我们只编写一次测试代码,其他人(包括“未来的我”)必须在软件的生命周期中多次阅读、理解和操作代码
如果仍然觉得工作量太大,请查看 AssertJ 的断言生成器

结论

在测试中启动 Spring 应用程序是有原因的,但对于普通的单元测试来说,这是没有必要的。由于更长的周转时间,它甚至是有害的。相反,我们应该以一种易于支持为其编写简单单元测试的方式构建我们的 Spring bean。
Spring Boot Test Starter 附带 Mockito 和 AssertJ 作为测试库。
让我们利用这些测试库来创建富有表现力的单元测试!
最终形式的代码示例可在 github 上 找到。

Java ArrayList 与 LinkedList

【注】本文译自: Java ArrayList vs LinkedList | Baeldung

1. 概述

对于 collections (集合),Java 标准库提供了大量可供选择的选项。在这些选项中,有两个著名的 List 实现,称为 ArrayListLinkedList,每个实现都有自己的属性和用例。
在本教程中,我们将看到这两者是如何实现的。然后,我们将为评估每个应用的不同。

2. ArrayList

在内部,ArrayList 使用数组来实现 List 接口。由于数组在 Java 中是固定大小的,因此 ArrayList 创建一个具有一些初始容量的数组。在此过程中,如果我们需要存储比默认容量更多的项,它将用一个新的、更大的数组替换该数组。
为了更好地理解它的属性,让我们根据它的三个主要操作来评估这个数据结构:添加项、通过索引获取项和通过索引删除项。

2.1. 添加

当我们创建一个空的 ArrayList 时,它会使用默认容量(当前为 10)初始化其后备数组:

在该数组尚未满时添加新项目就像将该项分配给特定数组索引一样简单。这个数组索引由当前数组大小决定,因为我们实际上是附加到列表中:

backingArray[size] = newItem;
size++;

因此,在最佳和一般情况下,加法操作的时间复杂度为 O(1),这非常快。但是,随着后备数组变满,添加实现的效率会降低:

要添加新项目,我们应该首先初始化一个容量更大的全新数组,并将所有现有项目复制到新数组中。只有在复制当前元素后,我们才能添加新项目。因此,在最坏的情况下时间复杂度为 O(n),因为我们必须先复制 n 个元素。
从理论上讲,添加新元素的运行时间为摊销常数。也就是说,添加 n 个元素需要 O(n) 时间。但是,由于复制开销,某些单次添加可能表现不佳。

2.2. 按索引访问

通过索引访问项是 ArrayList 的真正亮点。要检索下标为 i 的项,我们只需要返回位于后备数组中第 i 个下标的项。因此,通过索引操作访问的时间复杂度始终为 O(1)

2.3. 通过索引删除

假设我们要从 ArrayList 中删除索引 6,它对应于我们的后备数组中的元素 15:

将所需元素标记为已删除后,我们应该将其后的所有元素向后移动一个索引。显然,元素越靠近数组的开头,我们应该移动的元素就越多。因此,时间复杂度在最佳情况下为 O(1),在平均和最坏情况下为 O(n)。

2.4. 应用和限制

.通常,当需要 List 实现时,ArrayList 是许多开发人员的默认选择。事实上,当读取次数远远超过写入次数时,这实际上是一个明智的选择
有时我们需要同样频繁的读取和写入。如果我们确实估计了可能项目的最大数量,那么使用 ArrayList 仍然有意义。如果是这种情况,我们可以使用初始容量初始化 ArrayList:

int possibleUpperBound = 10_000;
List<String> items = new ArrayList<>(possibleUpperBound);

这种估计可以防止大量不必要的复制和数组分配。

此外,数组由 Java 中的 int 值索引。因此,在 Java 数组中存储超过 2 的 32 次方个元素是不可能的,因此,在 ArrayList 中也是如此

3. LinkedList

LinkedList,顾名思义,使用链接节点的集合来存储和检索元素。例如,以下是添加四个元素后的 Java 实现:

每个节点维护两个指针:一个指向下一个元素,另一个指向前一个元素。对此进行扩展,双向链表有两个指向第一项和最后一项的指针
同样,让我们根据相同的基本操作来评估这个实现。

3.1. 添加

为了添加新节点,首先,我们应该将当前最后一个节点链接到新节点:

然后更新最后一个指针:

由于这两个操作都很简单,因此加法操作的时间复杂度始终为 O(1)

3.2. 通过索引访问

LinkedList 与 ArrayList 不同,不支持快速随机访问。因此,为了按索引查找元素,我们应该手动遍历列表的某些部分

在最好的情况下,当请求的项目接近列表的开头或结尾时,时间复杂度将与 O(1) 一样快。然而,在平均和最坏情况下,我们可能会以 O(n) 的访问时间结束,因为我们必须一个接一个地检查许多节点。

3.3. 通过索引删除

为了删除一项,我们应该首先找到请求的项,然后从列表中取消它的链接。因此,访问时间决定了时间复杂度——即在最佳情况下为 O(1),在平均和最坏情况下为 O(n)。

3.4. 应用

当添加率远高于读取率时,LinkedLists更适合。
此外,当大多数时候我们想要第一个或最后一个元素时,它可以用于读取密集的场景。值得一提的是,LinkedList 还实现了 Deque 接口——支持对集合两端的高效访问。
通常,如果我们知道它们的实现差异,那么我们可以轻松地为特定用例选择一个。
例如,假设我们将在类似列表的数据结构中存储大量时间序列事件。我们知道我们每秒都会收到突发事件。
此外,我们需要定期检查所有事件并提供一些统计数据。对于此用例,LinkedList 是更好的选择,因为添加速率远高于读取速率。
此外,我们会读取所有项目,因此我们无法超过 O(n) 上限。

4. 结论

在本教程中,我们首先深入研究了 ArrayListLinkLists 如何在 Java 中实现。
我们还评估了其中每一个的不同用例。

DevOps 发展史

【注】本文节译自:https://blog.devops4me.com/history-of-devops/

起源

  软件开发生命周期(SDLC)的发展迅速改变了组织如何将其产品发布/生产到生产环境的格局。当谈到组织如何管理产品交付时,传统的 SDLC 通常与瀑布(Waterfall)模式联系在一起。瀑布模式不能适应与组织的业务目标,他们希望为客户提供更快的速度和功能/产品。照此逻辑,他们需要一种新的方法来加快产品交付,但同时还要改善开发人员IT 运营团队。因此,DevOps 就出现了,2008 年多伦多敏捷会议上,Petrick Debois 介绍了“DevOps”一词。
  第一次会议名为 Devopsdays,于 2009 年在比利时根特举行。比利时顾问,项目经理和敏捷实践者 Patrick Debois 创立了会议。该会议现已传播到其他国家。2012年,DevOps 状态报告由 Puppet 的 Alanna Brown 起草并发布。截至 2014 年,Nicole Forsgren、Gene Kim、Jez Humble 等人发布了年度 DevOps 状态报告。在2014年,他们发现 DevOp 的采用正在加速。同样在 2014 年,Lisa Crispin 和 Janet Gregory 撰写了 More Agile Testing,其中包括有关测试和 DevOps 的章节。
  如果我将上面的时间事件放到时间轴中,如下图所示:

什么是 DevOps?

  DevOps 也是新兴技术和新兴商业文化的结合。转向 DevOps 文化的想法是建立开放的沟通,透明性和跨学科团队合作。DevOps背后的概念打破了孤岛,并为开发人员(DEV)与 IT 运营(OPS)之间的讨论和协作创造了更多空间。DevOps 的力量在于支持它的文化,使人们的思维方式从孤岛上移开了。它通常可以帮助您了解其来源、为什么变得流行以及什么使它流行。

目的是什么?

  在实施 DevOps 文化和方法时,DevOps 可以解决您组织面临的挑战,并且组织将获得:

  • 更快的服务交付:紧跟快速需求的敏捷版本。跨数据可见性:确保合规性和数据准确性。
  • 服务效率:提高质量和性能。
  • 经验丰富的专业DevOps:教您成功所需的工具。
  • 全面迎合组织的特定需求。

    DevOps 是:

  • 概念
  • 心态
  • 个人理解和拥护的共同态度
  • 必须培育和反复改进的文化
  • 可见度
  • 指导
  • 学习
  • 包容和开放的所有想法
  • 迭代
  • 持续
  • 协同合作
  • 自信地开发和交付软件的绝佳方法

    DevOps 不是:

  • 轻松实现或实施
  • 产品或工具链
  • 职务或职位
  • 云基础架构解决方案
  • 一项技术
  • 一种编程语言
  • 营销活动
  • CI / CD 流水线
  • Kubernetes
  • 容器 / Docker
  • 开源软件
  • 基础设施即代码
  • 自动化

    简而言之

      DevOps 认为 IT 行业急需概念上的不断学习和改进。而且,IT 社区无疑可以从 DevOps 历史中学到很多东西。这场 DevOps 革命不足为奇,而且随着创新的不断发展,其重要性在未来会不断提高。我们已经看到安全性与 DevOps 结合可以如何永远改变 Infosec 行业。

    结论

      将 DevOps 描述为一个旅程或愿望,而不是定义的目标或工具是合理的。DevOps 寻求持续的改进、更多的输出、更高的效率、甚至持续部署。支持 DevOps 的自动化工具还在不断发展。

DevOps教程:什么是DevOps

【注】本文译自: https://www.javatpoint.com/devops

  DevOps 是两个单词的复合,一个是 Development,另一个是 Operations。它是一种共同提升开发和运维过程的文化。
  DevOps 教程将帮助你学习 DevOps 基础知识并带你深入了解各种 DevOps 工具,譬如:Git、Ansible、Docker、Puppet、Jenkins、Chef、NagiosKubernetes

什么是 DevOps?

  DevOps 是两个单词的复合,其一是软件开发,其二是运维。这就允许一个团队掌握整个应用生命周期,从开发测试部署以及运维。DevOps 有助于减少软件开发工程师、质量保障(QA)工程师和系统管理者之间的断层。

  DevOps 提升开发和运维团队间的协作,通过自动化和可重复的方式将将代码更快地部署到生产。
  DevOps 有助于加快组织交付应用和服务的速度。它也使得组织更好地服务客户,以增强市场竞争力。
  DevOps 也可以被定义成开发和 IT 运维更好地沟通和协作的序列。
  DevOps 已经成为企业或组织最具价值的业务准则之一。在 DevOps 的帮助下,应用交付的质量和速度已经得到了极大的改善。
  DevOps 只是使“开发人员”和“运营人员”一起工作的一种实践或方法。DevOps 代表着 IT 文化的一种变化,它完全专注于在面向系统方法的上下文中通过采用敏捷实践来快速交付IT服务。
  DevOps 就是关于运营和开发流程的集成。 已采用DevOps的组织注意到,软件质量提高了22%,应用程序部署频率提高了17%,客户满意度提高了22%。 成功实施 DevOps 后,收入增长了19%。

为什么需要 DevOps?

  接下来,我们需要了解为什么我们需要 DevOps 而不是其他方法。

  • 运营和开发团队完全孤立地工作。
  • 在设计-构建之后,分别进行测试和部署。 这就使得他们比实际构建周期花费更多时间。
  • 在不使用 DevOps 的情况下,团队成员花费大量时间在设计,测试和部署上,而不是构建项目。
  • 手动代码部署会导致生产中的人为错误。
  • 编码团队和操作团队有各自的时间表、并且不同步,从而导致进一步的延迟。

    DevOps 历史

  • 2009年,第一届名为 DevOpsdays 的会议在比利时根特举行。 比利时顾问和 Patrick Debois 共同创立了此次会议。
  • 2012年,Puppet 的 Alanna Brown 提出并构思了 DevOps 状态报告。
  • 2014年,Nicole Forsgren、Jez Humble、Gene Kim 等人发布了年度 DevOps 状态报告。他们发现,DevOps 的采用也在 2014 年加速发展。
  • 2015年,妮可·福斯格伦(Nicole Forsgren)、吉恩·金(Gene Kim)和杰兹·汉布尔(Jez Humble)创立了 DORA(DevOps研究与任务)。
  • 2017年,妮可·福斯格伦(Nicole Forsgren),吉恩·金(Gene Kim)和杰兹·汉布尔(Jez Humble)发表了“加速:建立和扩展高性能技术组织”。

    DevOps 架构特性

      以下是 DevOps 架构的一些关键功能,例如:

1)自动化

  自动化可以减少时间消耗,尤其是在测试和部署阶段。 生产率提高了,并且自动化使发布更快。 这将导致迅速捕获错误,因此可以轻松修复它。 对于持续交付,每个代码都是通过自动化测试,基于云的服务和构建来定义的。 可以使用自动部署来促进生产。

2) 协作

  开发和运营团队作为 DevOps 团队进行协作,随着团队生产力的提高,生产力不断提高,从而改善了文化模型、增强了责任感和所有权。 这些团队分担责任并紧密同步工作,进而加快了生产部署速度。

3)集成

  应用程序需要与环境中的其他组件集成。 集成阶段是将现有代码与新功能结合起来,然后进行测试。 持续的集成和测试可以实现持续的开发。 发布和微服务的频率导致重大的运营挑战。 为了克服这些问题,就要实施持续集成和持续交付,以便以更快,更安全和可靠的方式交付。

4)配置管理

  配置管理确保应用程序仅与那些与其运行环境有关的资源进行交互。 在将应用程序的外部配置与源代码分开的情况下,不会创建配置文件。 配置文件可以在部署过程中编写,也可以在运行时加载,具体取决于运行环境。

DevOps 的优点和缺点

  以下是DevOps对业务可能具有的一些优点和缺点,例如:优点

  • DevOps 是快速开发和部署应用程序的绝佳方法。
  • 对市场变化做出更快的响应,以改善业务增长。
  • DevOps通过减少软件交付时间和运输成本来提升业务利润。
  • DevOps 清除了描述过程,从而使产品开发和交付更加清晰。
  • 改善了客户体验和满意度
  • DevOps 简化了协作,并将所有工具都放置在云中供客户访问。
  • DevOps 意味着集体责任,可以提高团队参与度和生产力。

缺点

  • DevOps 专业人士或专家的开发人员较少。
  • 使用 DevOps 进行开发非常昂贵。
  • 行业在短时间内很难采用新的DevOps技术。
  • 在自动化项目的持续集成中,缺乏DevOps知识可能是一个问题。

前提条件

  要学习 DevOps,您应该具有 Linux 的基本知识和至少一种脚本语言。

受众

  我们的 DevOps 教程旨在帮助初学者和专业人士。

ELK 教程 – 高效发现、分析和可视化你的数据


  随着越来越多的 IT 基础设施转身云计算,对公共云安全工具和日志分析平台的需求也在迅速增加。不管组织的规模有多大,每天都会生成大量的数据。这些数据中有相当一部分是由公司的 Web 服务器日志组成的。日志是最重要的信息来源之一, 但往往被忽视。每个日志文件都包含一些宝贵的信息,这些信息大多是非结构化的,没有任何意义。如果不对此日志数据进行详尽的分析,那么企业可能会忽视其周围的机会和威胁。这是日志分析工具非常有用的地方。ELK Stack 或 Elastic Stack 是完整的日志分析解决方案,它有助于深入搜索、分析和可视化不同机器生成的日志。通过本教程,我将为您提供相关见解。
  首先,让我们列出要讨论的主题:

  • 什么是 ELK Stack?
  • ELK Stack 架构
  • ELK Stack 安装
  • Elasticsearch 教程
  • Logstash 教程
  • Kibana 教程

  本教程将帮助您一起理解 Elasticsearch、Logstash 和 Kibana 的基础知识,并帮助您在 ELK Stack 中打下坚实的基础。
  首先让我们来了解什么是 ELK Stack。

什么是 ELK Stack?


  众所周知的 ELK Stack 最近被更名为 Elastic Stack。它是三个开源工具的强大集合:Elasticsearch、Logstash 和 Kibana。
  这三种不同的产品最常一起用于不同 IT 环境中的日志分析。使用 ELK Stack,您可以执行集中式日志记录,这有助于识别 Web 服务器或应用程序的问题。它使您可以在一个地方搜索所有日志,并通过在特定时间范围内关联多个服务器的日志来识别跨多个服务器的问题。
  现在让我们详细讨论这些工具。

Logstash

Logstash 是数据收集管道工具。它是 ELK Stack 的第一个组件,它收集数据输入并将其输入到 Elasticsearch。它可以一次从不同来源收集各种类型的数据,并立即提供以备将来使用。

Elasticsearch

Elasticsearch 是基于 Lucene 搜索引擎的 NoSQL 数据库,并使用 RESTful API 构建。它是一个高度灵活的分布式搜索和分析引擎。此外,它通过水平可伸缩性提供了简单的部署、最大的可靠性和易于管理的功能。它提供高级查询以执行详细分析,并集中存储所有数据以快速搜索文档。

Kibana

Kibana 是一种数据可视化工具。它用于可视化 Elasticsearch 文档,并帮助开发人员立即对其进行深入了解。Kibana 仪表板提供了各种交互式图表、地理空间数据、时间线和图表,以可视化使用 Elasticsearch 完成的复杂查询。使用 Kibana,您可以根据自己的特定需求创建和保存自定义图形。
  下一部分将讨论 ELK Stack 架构以及其中的数据流向。

ELK Stack 架构

  以下是 ELK Stack 的架构,显示了 ELK 中日志流的正确顺序。在此,Logstash 会根据提供的过滤条件来收集和处理从各种来源生成的日志。然后,Logstash 将这些日志通过管道传输到 Elasticsearch,然后 Elasticsearch 分析和搜索数据。最后,使用 Kibana,可以根据要求对日志进行可视化和管理。

ELK Stack 安装

第 I 步:打开 https://www.elastic.co/downloads

第 II 步:选择并下载 Elasticsearch。
第 III 步:选择并下载 Kibana。
第 IV 步:选择并下载 Logstash。
第 V 步:解压缩所有三个文件以获取对应文件夹的文件。

安装 Elasticsearch

第 VI 步:现在打开 elasticsearch 文件夹并转到 bin 文件夹
第 VII 步:双击 elasticsearch.bat 文件以启动 elasticsearch 服务器。

第 VIII 步:等待 elasticsearch 服务器启动。
第 IX 步:要检查服务器是否已启动,请转到浏览器并键入 localhost:9200

安装 Kibana

第 X 步:现在打开 kibana 文件夹 并转到 bin 文件夹
第 XI 步:双击 kibana.bat 文件以启动 kibana 服务器。

第 XII 步:等待 kibana 服务器启动。
第 XIII 步:要检查服务器是否已启动,请转到浏览器并键入 localhost:5601

安装 Logstash

第 XIV 步:现在打开 logstash 文件夹
第 XV 步:要测试您的 logstash 安装,请打开命令提示符,然后转到 logstash 文件夹。现在输入:

binlogstash -e 'input { stdin { } } output { stdout {} }'

第 XVI 步:等待,直到命令提示符上显示“ Pipeline main started”出现在命令提示符下。

第 XVII 步:现在,在命令提示符下输入一条消息,然后按 Enter 键。
第 XVIII 步:Logstash 将时间戳和 IP 地址信息附加到消息中,并将其显示在命令提示符下。
  既然我们完成了安装,那么现在让我们更深入地研究这些工具。让我们从 Elasticsearch 开始。

Elasticsearch

  如前所述,Elasticsearch 是一个高度可扩展的搜索引擎,它运行在基于 Java 的 Lucene 引擎之上。它基本上是一个 NoSQL 数据库。这意味着它将以非结构化格式存储数据,并且无法对任何类型的交易执行 SQL 查询。换句话说,它将数据存储在文档中,而不是表和模式中。为了获得更好的图像,请检查下表,该表显示了与数据库相比在 Elasticsearch 中的内容。

  现在让我们熟悉 Elasticsearch 的基本概念。
使用Elasticsearch时,需要遵循三个主要步骤:

  1. 索引
  2. 映射
  3. 搜索

  让我们一个一个详细地谈谈。

索引

  索引编制是添加数据 Elasticsearch 的过程。之所以称为“索引”,是因为将数据输入到 Elasticsearch 中后,它将被放入 Apache Lucene 索引中。然后,Elasticsearch 使用这些 Lucene 索引来存储和检索数据。索引编制与 CRUD 操作的创建和更新过程相似。   索引方案由 名称/类型/id 组成,其中名称和类型是必填字段。 如果您不提供任何 ID,Elasticsearch 将自己提供一个 ID。 然后,将整个查询附加到 HTTP PUT 请求中,最终 URL 如下:PUT name/type/id 与 HTTP 有效负载一起,还将发送包含字段和值的 JSON 文档。
  以下是创建一个美国客户的文档的示例,该文档及其字段中的详细信息。

PUT /customer/US/1 
{
    "ID": 101,
    "FName": "James",
    "LName": "Butt",
    "Email": "jbutt@gmail.com",
    "City": "New Orleans",
    "Type": "VIP"
}

  它会给你以下输出:

  这里显示文档已创建并添加到索引中。
  现在,如果您尝试在不更改标识的情况下更改字段详细信息,Elasticsearch 将使用当前详细信息覆盖现有文档。

PUT /customer/US/1
{
    "ID": 101,
    "FName": "James",
    "LName": "Butt",
    "Email": "jbutt@yahoo.com",
    "City": "Los Angeles",
    "Type": "VVIP"
}


  这里显示文档已经更新了索引的新细节。

映射

  映射是设置索引模式的过程。通过映射,您可以告诉 Elasticsearch 你的模式中属性的数据类型。如果在预索引时未针对特定对象进行映射,则 Elasticsearch 将动态地将泛型类型添加到该字段。但是这些泛型类型是非常基本的,大多数时候都不能满足查询的期望。
  现在让我们尝试映射查询。

PUT /customer/
{
    "mappings": {
        "US": {
            "properties": {
                "ID": {
                    "type": "long"
                },
                "FName": {
                    "type": "text"
                },
                "LName": {
                    "type": "text"
                },
                "Email": {
                    "type": "text"
                },
                "City": {
                    "type": "text"
                },
                "Type": {
                    "type": "text"
                }
            }
        }
    }
}


  当您执行查询时,您将获得这种类型的输出。

搜索

  具有特定索引和类型的一般搜索查询如下:

POST index/type/_search

  现在,让我们尝试搜索“customer”索引中存在的所有客户的详细信息。

POST /customer/US/_search

  当您执行此查询时,将生成以下结果:

  但是,当您要搜索特定结果时,Elasticsearch 提供了三种方法:

使用查询

  使用查询,您可以搜索一些特定的文档或条目。例如,让我们对属于“ VVIP”类别的客户执行搜索查询。

POST /customer/US/_search
{
    "query": {
        "match": {
            "Type": "VVIP"
        }
    }
}

使用过滤器

  使用过滤器,您可以进一步缩小搜索范围。以下是搜索 ID 为“ 101”的 VVIP 客户的示例:

POST /customer/_search
{
    "query": {
        "match": {
            "Type": "VVIP"
        }
    },
    "post_filter": {
        "match": {
            "ID": 101
        }
    }
}

  如果执行此查询,则会得到以下结果:

使用聚合

  聚合是一个框架,可帮助通过搜索查询聚合数据。小型聚合可以结合在一起,以构建所提供数据的复杂摘要。让我们执行一个简单的汇总,以检查索引中有多少类型的客户:

POST /customer/_search
{
    "size": 0,
    "aggs": {
        "Cust_Types": {
            "terms": {
                "field": "Type.keyword"
            }
        }
    }
}


  现在让我们看看如何从索引中检索数据集。

获取数据

  要检查索引中包含的文档列表,您只需要发送以下格式的 HTTP GET 请求:

GET index/type/id

  让我们尝试检索“ id”等于 2 的客户的详细信息:

GET /customer/US/2

  成功执行后,它将为您提供以下类型的结果。

  使用 Elasticsearch,您不仅可以浏览数据,还可以删除或删除文档。

删除数据

  使用删除约定,您可以轻松地从索引中删除不需要的数据并释放内存空间。要删除任何文档,您需要以以下格式发送 HTTP DELETE 请求:

DELETE index/type/id.

  现在让我们尝试删除 ID 为 2 的客户的详细信息。

DELETE /customer/US/2

  执行此查询时,您将获得以下类型的结果。

  至此,我们讲解了使用 Elasticsearch 的 CRUD 操作的基础知识,了解这些基本操作将帮助您执行不同类型的搜索。
  现在让我们开始学习 ELK Stack 的下一个工具 Logstash。

Logstash

  正如我已经讨论的那样,Logstash 是一种管道工具,通常用于收集和转发日志或事件。它是一个开源数据收集引擎,可以动态集成来自各种来源的数据并将其标准化到指定的目标位置。

  使用多个输入,过滤器和输出插件,Logstash 可以轻松转换各种事件。至少,Logstash 需要在其配置文件中指定的输入和输出插件来执行转换。以下是 Logstash 配置文件的结构:

input {
    ...
}

filter {
    ...
}

output {
    ...
}

  如您所见,整个配置文件分为三个部分,每个部分都包含一个或多个插件的配置选项。这三个部分是:

  1. input(输入)
  2. filter (过滤)
  3. output (输出)

  您也可以在配置文件中应用多个过滤器。在这种情况下,其应用程序顺序将与配置文件中的规范顺序相同。
  现在,让我们尝试配置 CSV 文件格式的美国客户数据集文件。

    file {
        path => "E:/ELK/data/US_Customer_List.csv"
        start_position => "beginning"
        sincedb_path => "/dev/null"
    }
}
filter {
    csv {
        separator => ","
        columns => ["Cust_ID", "Cust_Fname", "Cust_Lname", "Cust_Email", "Cust_City", "Cust_Type"]
    }
    mutate {
        convert => ["Cust_ID", "integer"]
    }
}
output {
    elasticsearch {
        hosts => "localhost"
        index => "customers"
        document_type => "US_Based_Cust"
    }
    stdout {}
}

  要将这个 CSV 文件数据插入 elasticsearch 中,您必须通知 Logstash 服务器。
  为此,请执行以下步骤:

  1. 打开命令提示符
  2. 进入 Logstash 的 bin 目录
  3. 输入:logstash –f X:/foldername/config_filename.config 然后按回车。一旦您的 logstash 服务器启动并运行,它将开始将文件中的数据传输到Elasticsearch 中。

      如果要检查是否成功插入了数据,请转到 Sense 插件并键入:GET /customers/
      它会为您提供已创建的文档数。
      现在,如果要可视化此数据,则必须使用 ELK Stack 的最后一个工具,即Kibana。因此,在本教程的下一部分中,我将讨论 Kibana 及其使用方式,以可视化您的数据。

    Kibana

      如前所述,Kibana 是一个开源的可视化和分析工具。它有助于可视化 Logstash 管道传输并存储到 Elasticsearch 中的数据。您可以使用 Kibana 来搜索,查看此存储的数据并与之交互,然后在各种图表,表格和地图中对其进行可视化。Kibana 的基于浏览器的界面简化了海量数据并反映了 Elasticsearch 查询中的实时变化。此外,您还可以轻松创建、自定义、保存和共享仪表板。
      一旦您了解了如何与 Elasticsearch 和 Logstash 一起使用,学习 Kibana 就不是什么大事了。在本教程的这一部分,我将向您介绍为了对数据进行分析所需的各种功能。

    管理页面

      在这里,您必须执行 Kibana 的运行时配置。 在此页面中,您需要指定一些搜索内容。请参见以下示例,在该示例中,我已经配置了“customer”索引的条目。

      如您所见,在“索引模式(Index Patterns)”字段中,您需要指定要使用的索引。确保在“时间过滤器字段名称”中将其选择为@timestamp。然后,您可以继续并单击创建以创建索引。如果索引创建成功,您将看到以下页面类型:

      在这里,您可以根据需要从下拉列表中选择不同的过滤器。此外,要释放内存,您还可以删除特定的索引。

    发现页面

      通过“发现”页面,您可以访问存在于每个与所选索引模式匹配的每个索引的文档。 您可以轻松地交互和浏览Kibana服务器上存在的所有数据。 此外,您可以查看文档中存在的数据并对其进行搜索查询。 下面你可以看到,我正在搜索来自“洛杉矶”的“ VIP”客户。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ANoSab7B-1619606511268)(https://d1jnx9ba8s6j9r.cloudfront.net/blog/wp-content/uploads/2017/11/28-1.png)]
      因此,如您所见,我们只有一位来自洛杉矶的VIP客户。

    可视化页面

      可视化页面使您可以可视化以图表、条形图、饼图等形式显示在 Elasticsearch 索引中的数据。您甚至可以在此处构建仪表板,这些仪表板将基于 Elasticsearch查询显示相关的可视化效果。通常,使用一系列 Elasticsearch 聚合查询来提取和处理数据。当您转到“可视化”页面并搜索保存的可视化时,或者可以创建一个新的可视化。

      您可以以任何形式汇总数据。为了方便用户,提供了不同类型的可视化选项。

      让我向您展示如何根据用户类型可视化美国客户数据。

      要执行可视化,请按照以下步骤操作:

  4. 选择可视化类型。 [这里我用的是饼图]
  5. 在汇总字段中,从下拉列表中选择“术语(term)”。
  6. 在“字段(field)”中,选择要执行搜索的字段类型。
  7. 您还可以指定可视化的顺序和大小。
  8. 现在单击执行按钮以生成饼图。

    仪表盘页面

      “仪表板”页面显示已保存的可视化的集合。在这里,您可以添加新的可视化效果,也可以使用任何保存的可视化效果。

    Timelion 页面

      Timelion 是一个时间序列数据可视化工具,它将完全独立的数据源整合到一个界面中。 它由一种单行表达语言驱动,可用于检索时间序列数据,执行计算以简化复杂问题并可视化结果。

    开发工具页面

      Kibana 的“开发工具”页面包含诸如“ Beta Sense”插件之类的开发工具,可用于与 Elasticsearch 中存在的数据进行交互。它通常被称为 Kibana 的控制台。以下是一个示例,其中我使用了 Kibana 的 Sense 插件来搜索类型为“ US_based_cust”的“客户(customers)”索引:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s4xVhcR2-1619606511273)(https://d1jnx9ba8s6j9r.cloudfront.net/blog/wp-content/uploads/2017/11/33-1.png)]
      本文到此结束。现在,您可以使用 Logstash、Elasticsearch 和 Kibana 对任何数据执行各种搜索和分析。


本文译自:https://www.edureka.co/blog/elk-stack-tutorial/