Apache ThriftとSpring Bootでマイクロサービスを構築する

概要

マイクロサービスを構築するうえでは、厳密なサービス契約と多言語クライアントを提供することが重要である。Apache ThriftはIDLでAPIを定義でき、API自体をドキュメントとして扱いやすいため、この目的に適したツールである。

ここでは、Spring BootアプリケーションにApache Thriftを適用する方法を説明する。

Spring Bootプロジェクトを作成する

curlコマンドでSpring Bootの初期プロジェクトを作成する。

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.7.12 \
-d dependencies=web \
-d baseDir=spring-thrift \
-d groupId=com.devkuma \
-d artifactId=spring-thrift \
-d packageName=com.devkuma.calculator \
-d applicationName=CalculatorApplication \
-d packaging=jar \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -

このコマンドにより、Java 11、Spring Boot 2.7.12ベースのWebプロジェクトが生成される。

Gradleビルドスクリプト

Thriftライブラリを追加する

build.gradleにThriftとhttpcoreのライブラリを追加する。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.apache.thrift:libthrift:0.18.1'
    implementation 'org.apache.httpcomponents:httpcore:4.4.16'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Thrift Plugin

Gradle Thriftプラグインは、Thriftコンパイラを使ってIDLファイルをコンパイルする。

plugins {
    id "org.jruyi.thrift" version "0.4.2"
}

compileThrift {
    recurse true
    generator 'html'
    generator 'java', 'private-members'
}

compileThriftを実行すると、生成されたJavaファイルは次のディレクトリに作成される。

build/generated-sources/thrift

Thriftテンプレート

src/main/thrift/calculate.thriftを作成する。

namespace java com.devkuma.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);
}

ここでは、calculateメソッドを1つ持つTCalculatorServiceを定義している。このメソッドはTDivisionByZeroExceptionを送出できる。

生成されるファイルには、TCalculatorService.javaTDivisionByZeroException.javaTOperation.javaが含まれる。

クラスを作成する

サービスクラス

まず、計算ロジックを通常のSpringサービスとして実装する。

@Service
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;
    }
}

Thriftハンドラクラス

次に、生成されたTCalculatorService.Ifaceを実装するハンドラを作成する。

@Component
public class CalculatorServiceHandler implements TCalculatorService.Iface {

    @Autowired
    private 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なので、依存性注入を利用できる。

Thrift設定クラス

TServletを登録し、/calculator/*へマッピングする。

@Configuration
public class ThriftConfig {

    @Bean
    public TProtocolFactory tProtocolFactory() {
        return new TBinaryProtocol.Factory();
    }

    @Bean
    public ServletRegistrationBean<HttpServlet> stateServlet(
            TProtocolFactory tProtocolFactory,
            CalculatorServiceHandler handler) {
        ServletRegistrationBean<HttpServlet> bean = new ServletRegistrationBean<>();
        bean.setServlet(new TServlet(
            new TCalculatorService.Processor<>(handler), tProtocolFactory));
        bean.addUrlMappings("/calculator/*");
        bean.setLoadOnStartup(1);
        return bean;
    }
}

テストコード

アプリケーションがJSON REST APIを提供する場合でも、通常はクライアント実装が必要になる。ThriftではクライアントAPIが生成されるため、この作業が簡単になる。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class CalculatorApplicationTests {

    @Autowired
    protected TProtocolFactory protocolFactory;

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

    protected TCalculatorService.Client client;

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

テストでは、生成されたクライアントメソッドを呼び出して結果を確認し、ゼロ除算ではTDivisionByZeroExceptionを検証する。通信は実際のクライアント・サーバー呼び出しと同じ方法で行われる。

まとめ

ここでは、Spring BootとApache Thriftを使ってマイクロサービスを構築する方法を説明した。計算機アプリケーションを例に、IDL作成、コード生成、Spring Boot設定、ハンドラ実装、テストまでを扱った。Apache Thriftを使うと、厳密な契約、多言語クライアント、自己文書化されたAPIを提供できる。

参考