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