创建和测试一个简单的微服务的实例

80 阅读6分钟

事件驱动的Hello World程序

本文展示了创建和测试一个简单的微服务的概要,它为易于部署和维护的微服务提供了基础。

事件驱动的微服务在实施、测试和维护之前可以直接进行描述。它们对新信息的实时响应速度也很高,在Java中,99.99%的时间内延迟都低于10微秒,这取决于独立部署的小型微服务的功能。

在这篇介绍性文章中,我们用一个 事件驱动的Hello World程序(一种程序流程由事件决定的编程范式)来逐步实现行为驱动的开发,我们首先将业务需要的行为描述为测试数据,并编写一个非常简单的微服务,将像这样的输入事件:

say: Hello World

变成这样的输出,通过添加一个感叹号:

say: Hello World! # <- adds an exclamation point

这个例子的所有代码都可以在GitHub上找到

当对事件驱动系统进行建模时,一个有用的模式是有事件驱动的核心系统与连接到可能不是事件驱动的外部系统的网关。为了保持清晰的关注点分离,业务逻辑 ,如根据市场数据做出决定,或处理订单,被放在事件驱动的微服务中,因为这些微服务最容易测试,而连接到外部客户和系统的网关则尽可能薄,所以它们只关注作为适配器的作用,避免包含重要的业务逻辑。

领域驱动设计 是对确定领域专家的要求的关注。他们的要求被进一步划分为事件驱动的微服务。其中,信息在微服务之间以一系列事件的形式传递。

每个内部微服务的要求都可以用YAML来描述行为驱动开发。

所有的例子都在Chronicle-Queen-Demo/hello-world模块中。

一个简单的事件驱动合约

我们将事件建模为没有参数或一对多参数的异步方法调用,例如

public interface Says {

   void say(String words);

}

这是最简单的Hello World例子,可以开始使用。我们可以在这个接口上添加具有多个参数的其他事件类型(方法)。参数不一定只是基元,它们也可以是复杂的数据结构,如数据传输对象。

对于微服务产生的事件将如何被处理,没有任何假设。它可能被记录下来,但其他方面被忽略,目前,它可能被单个微服务立即处理,或在以后的某个时候被多个下游微服务读取。 因此,它不会返回一个值。任何结果都将作为事件从各自的事件处理程序中发射出来。在编程中,事件处理程序是一个回调例程,可以异步操作。

外部事件生产者和消费者

通常我们需要与客户的外部系统集成。由于这是一个简单的 "Hello World "的例子,让我们想象一下,我们没有通过网关连接的外部系统,而是有一个简单的程序从控制台读取输入以提供上游事件,另一个简单的程序写到控制台,作为下游网关。

public class SaysInput {

   public static void input(Says says) throws IOException {

       BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

       for (String line; ((line = br.readLine()) != null); )

           says.say(line);

   }

}

public class SaysOutput implements Says {

   public void say(String words) {

       System.out.println(words);

   }

}

这些可以很容易地整合,因为一个的输出与另一个的输入是连在一起的:

public class RecordInputToConsoleMain {

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

       // Writes text in each call to say(line) to the console

       final Says says = new SaysOutput();

       // Takes each line input and calls say(line) each time

       SaysInput.input(says);

   }

}

我们还可以将生产者执行的所有操作记录到YAML中,以便以后建立测试:

public class RecordInputAsYamlMain {

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

       // obtains a proxy that writes to the PrintStream the method calls and their  arguments

       final Says says = Wires.recordAsYaml(Says.class, System.out);

       // Takes each line input and calls say(theLine) each time

       SaysInput.input(says);

   }

}

使用下面的方法来重放文件的输出:

public class ReplayOutputMain {

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

    // Reads the content of a Yaml file specified in args[0] and feeds it to SaysOutput.

     Wires.replay(args[0], new SaysOutput());

   }

}

RecordAsYaml和Replay方法的单元测试

为了单独测试recordAsYaml和replay方法的功能,并验证它们是否像上面建议的那样工作,我们开发了以下单元测试。 在单元测试中有大量的文本是很麻烦的,在下一节中,你可以看到这些文本是如何从文件中提取的:

public class WiresTest extends WireTestCommon {



@Test

public void recordAsYaml() {

   ByteArrayOutputStream baos = new ByteArrayOutputStream();

   PrintStream ps = new PrintStream(baos);

   Says says = Wires.recordAsYaml(Says.class, ps);

   says.say("One");

   says.say("Two");

   says.say("Three");



   assertEquals("" +

           "---\n" +

           "say: One\n" +

           "...\n" +

           "---\n" +

           "say: Two\n" +

           "...\n" +

           "---\n" +

           "say: Three\n" +

           "...\n",

           new String(baos.toByteArray(), StandardCharsets.ISO_8859_1));

}



@Test

public void replay() throws IOException {

   ByteArrayOutputStream baos = new ByteArrayOutputStream();

   PrintStream ps = new PrintStream(baos);

   Says says = Wires.recordAsYaml(Says.class, ps);

   says.say("zero");

   Wires.replay("=" +

           "---\n" +

           "say: One\n" +

           "...\n" +

           "---\n" +

           "say: Two\n" +

           "...\n" +

           "---\n" +

           "say: Three\n" +

           "...\n",says);



   assertEquals("" +

           "---\n" +

           "say: zero\n" +

           "...\n" +

           "---\n" +

           "say: One\n" +

           "...\n" +

           "---\n" +

           "say: Two\n" +

           "...\n" +

           "---\n" +

           "say: Three\n" +

           "...\n", new String(baos.toByteArray(), StandardCharsets.ISO_8859_1));

}



interface Says {

   void say(String word);

}

}

通过使用YAML进行记录和重放,我们的微服务可以很容易地编写、测试和调试,而不需要消息层的参与。

让我们添加一个作为数据处理器的微服务作为一个类,它可以有一个或多个事件类型。这个微服务以文本消息的形式获取输入事件,并在其上添加感叹号,然后将其转发到输出网关上。

public class AddsExclamation implements Says {

   private final Says out;



   public AddsExclamation(Says out) {

       this.out = out;

   }



   public void say(String words) {

       this.out.say(words + "!");

   }

}

一个单线程的事件驱动流程

我们可以在一个进程中结合这些所有的阶段,一个线程。虽然这在生产中不太可能有用,但把微服务放到一个单线程中,更容易测试和调试。

public class DirectWithExclamationMain {

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

       SaysInput.input(new AddsExclamation(new SaysOutput()));

   }

}

测试单个事件驱动的服务

我们可以读取资源文件,而不是在测试中嵌入大量的文本。这使得它们更容易阅读和维护。

public class AddsExclamationTest {

   @Test

   public void say() throws IOException {

YamlTester yt = YamlTester.runTest(AddsExclamation.class, "says");

assertEquals(yt.expected(), yt.actual());

   }

}

让我们更新一下输入,看看维护这个测试是多么容易。我将把第二个输入改为Hello World,并再次运行测试:

src/test/resources/says/in.yaml
---

say: One

...

---

say: Hello World

...

---

say: Three

...

不仅测试失败了,而且我还可以在差异上清楚地看到原因。

在这一点上,我可以修复测试,也可以通过在out.yaml文件中把实际结果复制粘贴到预期结果上,来接受这个变化。

在下一篇文章中,我们将看到如何实现一个更现实的处理订单的例子,并从配置中自动化许多微服务测试。 这为创建高性能、确定性、冗余的微服务奠定了基础。

本文展示了创建和测试一个简单的微服务的概要,它为易于部署和维护的微服务提供了基础。