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 package
でthrift 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に。
元ネタはこちらでした。