IK.AM

@making's tech note


Apache ThriftのサーバーサイドをSpring Bootで書く

🗃 {Programming/Java/org/springframework/boot}
🏷 Java 🏷 Spring 🏷 Spring Boot 🏷 Thrift 
🗓 Updated at 2015-01-03T15:00:05Z  🗓 Created at 2015-01-03T15:00:05Z   🌎 English Page

Apache ThriftのサーバーサイドをSpring Bootで書いてみます。

Spring Bootとの連携は基本的にThrift ServletをBean定義するだけです。Spring MVCは不要。

今回の題材は公式チュートリアルでthriftファイルはこんな感じ。

namespace cpp demo.calculator
namespace java demo.calculator
namespace php demo.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);
}

依存関係は

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
            </exclusion>
        </exclusions>
    </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>

でSpring MVCを抜きます(起動を軽くするため)。

エントリポイント兼Bean定義は以下のようにします。

package demo;

import demo.calculator.TCalculatorService;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.TServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    TProtocolFactory protocolFactory() {
        return new TBinaryProtocol.Factory();
    }
    
    @Bean /* Register Thrift Service */
    TServlet calculator(TCalculatorService.Iface calcService) {
        return new TServlet(new TCalculatorService.Processor<>(calcService), protocolFactory());
    }
}

あとはTCalculatorService.Ifaceの実装を書いてコンポーネントスキャン対象にすればOK。

package demo;

import demo.calculator.TCalculatorService;
import demo.calculator.TDivisionByZeroException;
import demo.calculator.TOperation;
import org.apache.thrift.TException;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.function.IntBinaryOperator;

@Component
public class CalculatorServiceImpl implements TCalculatorService.Iface {
    private static final EnumMap<TOperation, IntBinaryOperator> opMap = new EnumMap<TOperation, IntBinaryOperator>(TOperation.class) {{
        put(TOperation.ADD, (x, y) -> x + y);
        put(TOperation.SUBTRACT, (x, y) -> x - y);
        put(TOperation.MULTIPLY, (x, y) -> x * y);
        put(TOperation.DIVIDE, (x, y) -> x / y);
    }};

    @Override
    public int calculate(int num1, int num2, TOperation op) throws TException {
        if (opMap.containsKey(op)) {
            try {
                return opMap.get(op).applyAsInt(num1, num2);
            } catch (ArithmeticException e) {
                throw new TDivisionByZeroException();
            }
        } else {
            throw new TException("Unknown operation " + op);
        }
    }
}

例外ハンドリングが雑。当然ここでSpringのDI機能をフルに活用できます。

これでmvn spring-boot:runで実行できますし、mvn packagethrift serverの実行可能jarができます。

結合テストも簡単。

package demo;

import demo.calculator.TCalculatorService;
import demo.calculator.TDivisionByZeroException;
import demo.calculator.TOperation;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.transport.THttpClient;
import org.apache.thrift.transport.TTransport;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
@WebAppConfiguration
@IntegrationTest({"server.port:0"})
public class CalculatorTest {
    @Autowired
    TProtocolFactory protocolFactory;
    @Value("${local.server.port}")
    int port;
    TCalculatorService.Client client;

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

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

    @Test
    public void testSubtract() throws Exception {
        assertThat(client.calculate(2, 3, TOperation.SUBTRACT), is(-1));
    }

    @Test
    public void testMultiply() throws Exception {
        assertThat(client.calculate(2, 3, TOperation.MULTIPLY), is(6));
    }

    @Test
    public void testDivide() throws Exception {
        assertThat(client.calculate(10, 5, TOperation.DIVIDE), is(2));
    }

    @Test(expected = TDivisionByZeroException.class)
    public void testDivisionByZero() throws Exception {
        client.calculate(10, 0, TOperation.DIVIDE);
    }
}

Thrift ServiceをSpring Boot実行可能jar方式にするのとても良いと思います。

ソースコードはGithubに。

元ネタはこちらでした。


✒️️ Edit  ⏰ History  🗑 Delete