Spring Boot と Apache Thrift を使用したマイクロサービスの構築

# 1. はじめに
現代のマイクロサービスの世界では、サービスに厳密で多言語のクライアントを提供することが重要です。 API が自己文書化されている方がよいでしょう。
そのための最適なツールの 1 つは Apache Thrift です。
私のお気に入りのマイクロサービス プラットフォームである Spring Boot での使用方法を説明したいと思います。

すべてのプロジェクトのソース コードは GitHub で入手できます
https://github.com/bsideup/spring-boot-thrift

# 2. 電卓アプリのテンプレートファイルを作成する
Apache Thrift に慣れていなくても、そのテンプレートファイルは非常にわかりやすいです。
```text: calculate.thrift
namespace cpp com.example.calculator
namespace d com.example.calculator
namespace java com.example.calculator
namespace php com.example.calculator
namespace perl com.example.calculator
namespace as3 com.example.calculator

enum TOperation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}
exception TDivisionByZeroException {
}

service TCalculatorService {
   i32 calculate(1:i32 num1, 2:i32 num2, 3:TOperation op) throws (1:TDivisionByZeroException divisionByZero);
}
```

# 3. テンプレートファイルからジェネレートする
ここでは、calculate という 1 つのメソッドだけを使用して TCalculatorService を定義します。
タイプ TDivisionByZeroException の例外をスローできます。
すぐに使用できる言語の数に注意してください。
 (ただし、この例ではターゲットとして Java のみを使用します)
```text
thrift --gen java calculate.thrift
```

# 4. Spring Boot での電卓アプリの設定する
簡単な電卓アプリから始めましょう。プロトコルとアプリの2 つのモジュールがあります。プロトコルから始めます。プロジェクトは次のようになります。

ここでは、Spring Boot Web アプリのプロトコルと典型的なスターターに依存しています。
```xml:pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.thrift</groupId>
        <artifactId>libthrift</artifactId>
        <version>${thrift.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
```

CalculatorApplication はメイン クラスです。 この例では、同じファイルで Spring を構成しますが、アプリでは代わりに別の構成クラスを使用する必要があります。
```java
package com.example.calculator;

import com.example.calculator.handler.CalculatorServiceHandler;
import org.apache.thrift.protocol.*;
import org.apache.thrift.server.TServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.*;
import javax.servlet.Servlet;

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class CalculatorApplication {
    public static void main(String[] args) {
        SpringApplication.run(CalculatorApplication.class, args);
    }

    @Bean
    public TProtocolFactory tProtocolFactory() {
        //We will use binary protocol, but it's possible to use JSON and few others as well
        return new TBinaryProtocol.Factory();
    }
    
    @Bean
    public ServletRegistrationBean <HttpServlet> stateServlet(TProtocolFactory protocolFactory, CalculatorServiceHandler handler) {
        ServletRegistrationBean <HttpServlet> servRegBean = new ServletRegistrationBean <>();
        servRegBean.setServlet(new TServlet(new TCalculatorService.Processor <>(handler), protocolFactory));
        servRegBean.addUrlMappings("/calculator/*");
        servRegBean.setLoadOnStartup(1);
        return servRegBean;
    }
}
```

ここでは、/calculator/ が Servlet で呼び出されるようにURLマッピングを使って設定をしています。

その後、Thrift ハンドラー クラスが必要です。
```java
package com.example.calculator.handler;

import com.example.calculator.*;
import com.example.calculator.service.CalculatorService;
import org.apache.thrift.TException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CalculatorServiceHandler implements TCalculatorService.Iface {
    
    @Autowired
    CalculatorService calculatorService;
    
    @Override
    public int calculate(int num1, int num2, TOperation op) throws TException {
        switch(op) {
            case ADD:
                return calculatorService.add(num1, num2);
            case SUBTRACT:
                return calculatorService.subtract(num1, num2);
            case MULTIPLY:
                return calculatorService.multiply(num1, num2);
            case DIVIDE:
                try {
                    return calculatorService.divide(num1, num2);
                } catch(IllegalArgumentException e) {
                    throw new TDivisionByZeroException();
                }
            default:
                throw new TException("Unknown operation " + op);
        }
    }
}
```

この例では、Thrift ハンドラーが通常の Spring Bean であり、それに依存関係を注入できることを示したいと思います。

次に、CalculatorService 自体を実装する必要があります。
```java
package com.example.calculator.service;

import org.springframework.stereotype.Component;

@Component
public class CalculatorService {
    
    public int add(int num1, int num2) {
        return num1 + num2;
    }
    
    public int subtract(int num1, int num2) {
        return num1 - num2;
    }
    
    public int multiply(int num1, int num2) {
        return num1 * num2;
    }
    
    public int divide(int num1, int num2) {
        if(num2 == 0) {
            throw new IllegalArgumentException("num2 must not be zero");
        }
        
        return num1 / num2;
    }
}
```

それで最後になります。
サービスを何らかの方法でテストする必要があります。

通常、アプリケーションが JSON REST API を提供している場合でも、そのためのクライアントを実装する必要があります。 倹約はあなたのためにそれを行います。 気にする必要はありません。 また、さまざまなプロトコルをサポートします。 生成されたクライアントをテストで使用してみましょう。
```java
package com.example.calculator;

import org.apache.thrift.protocol.*;
import org.apache.thrift.transport.THttpClient;
import org.apache.thrift.transport.TTransport;
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = CalculatorApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port:0")
public class CalculatorApplicationTest {
    
    @Autowired
    protected TProtocolFactory protocolFactory;

    @Value("${local.server.port}")
    protected int port;
    
    protected TCalculatorService.Client client;

    @Before
    public void setUp() throws Exception {
        TTransport transport = new THttpClient("http://localhost:" + port + "/calculator/");
        
        TProtocol protocol = protocolFactory.getProtocol(transport);
        
        client = new TCalculatorService.Client(protocol);
    }

    @Test
    public void testAdd() throws Exception {
        assertEquals(5, client.calculate(2, 3, TOperation.ADD));
    }

    @Test
    public void testSubtract() throws Exception {
        assertEquals(3, client.calculate(5, 2, TOperation.SUBTRACT));
    }
    
    @Test
    public void testMultiply() throws Exception {
        assertEquals(10, client.calculate(5, 2, TOperation.MULTIPLY));
    }
    
    @Test
    public void testDivide() throws Exception {
        assertEquals(2, client.calculate(10, 5, TOperation.DIVIDE));
    }
    
    @Test(expected = TDivisionByZeroException.class)
    public void testDivisionByZero() throws Exception {
        client.calculate(10, 0, TOperation.DIVIDE);
    }
}
```

このテストでは、Spring Boot アプリケーションを実行し、ランダムなポートにバインドしてテストします。 すべてのクライアント/サーバー通信は、実際のクライアントと同じ方法で実行されます。

クライアント側からのサービスの使いやすさに注目してください。 メソッドを呼び出して例外をキャッチしているだけです。

# 5. 最後に
この記事では、Spring BootとApache Thriftを使用してマイクロサービスを構築する方法を説明しました。Apache Thriftは厳密で多言語のクライアントを提供し、APIが自己文書化される利点があります。簡単な電卓アプリケーションを例に、テンプレートファイルの作成、コード生成、Spring Bootでの設定、Thriftハンドラークラスの実装、およびテスト方法を示しました。これにより、クライアント/サーバー通信が実際のクライアントと同じ方法で実行され、サービスの使いやすさが向上します。

## 翻訳元サイト
https://dzone.com/articles/building-microservices-spring