Building Microservices with Apache Thrift and Spring Boot
Overview
When building microservices, it is important to provide strict service contracts and clients for multiple languages. Apache Thrift is an efficient tool for this because the API is defined in an IDL and can be treated as self-documenting.
This article explains how to apply Apache Thrift in a Spring Boot application.
Creating a Spring Boot Project
Create an initial Spring Boot project with curl.
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 -
This creates a Java 11 web project based on Spring Boot 2.7.12.
Gradle Build Script
Adding Thrift Libraries
Add the Thrift and httpcore libraries to build.gradle.
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
The Gradle Thrift plugin compiles Thrift IDL files through the Thrift compiler.
plugins {
id "org.jruyi.thrift" version "0.4.2"
}
compileThrift {
recurse true
generator 'html'
generator 'java', 'private-members'
}
After running compileThrift, generated Java files are created under:
build/generated-sources/thrift
Thrift Template
Create 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);
}
This defines TCalculatorService with a single calculate method. The method can throw TDivisionByZeroException.
The generated files include TCalculatorService.java, TDivisionByZeroException.java, and TOperation.java.
Creating Classes
Service Class
First, implement the calculation logic as a normal Spring service.
@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 Handler
Next, create a handler that implements the generated 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);
}
}
}
The Thrift handler is a normal Spring bean, so dependencies can be injected.
Thrift Configuration
Register a TServlet and map it to /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;
}
}
Test Code
Even when an application provides a JSON REST API, a client normally has to be implemented. Thrift makes this easy because it generates the client 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);
}
}
Tests call the generated client methods and verify the result, including division-by-zero handling with TDivisionByZeroException. Communication is performed in the same way as a real client-server call.
Conclusion
This article showed how to build a microservice with Spring Boot and Apache Thrift. Using a calculator example, it covered IDL creation, code generation, Spring Boot configuration, handler implementation, and testing. Apache Thrift provides strict contracts, multilingual clients, and a self-documenting API.
References
- Building Microservices with Spring Boot and Apache Thrift. Part 1
- jruyi/thrift-gradle-plugin | GitHub