Статьи

Создание микросервисов с помощью Spring Boot и Apache Thrift. Часть 1

В современном мире микросервисов важно предоставлять клиентам строгих клиентов и полиглотов. Лучше, если ваш API самодокументирован. Одним из лучших инструментов для этого является Apache Thrift . Я хочу объяснить, как использовать его с моей любимой платформой для микросервисов — Spring Boot .

Весь исходный код проекта доступен на GitHub: https://github.com/bsideup/spring-boot-thrift

Скелет проекта

Я буду использовать Gradle для создания нашего приложения. Во-первых, нам нужен наш основной файл build.gradle:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
    }
}

allprojects {
    repositories {
        jcenter()
    }

    apply plugin:'base'
    apply plugin: 'idea'
}

subprojects {
    apply plugin: 'java'
}

Ничего особенного для проекта Spring Boot. Затем нам понадобится файл Gradle для модулей Thrift Protocol (мы будем использовать его в следующей части):

        

import org.gradle.internal.os.OperatingSystem

repositories {
    ivy {
        artifactPattern "http://dl.bintray.com/bsideup/thirdparty/[artifact]-[revision](-[classifier]).[ext]"
    }
}

buildscript {
    repositories {
        jcenter()
    }
  
    dependencies {
        classpath "ru.trylogic.gradle.plugins:gradle-thrift-plugin:0.1.1"
    }
}

apply plugin: ru.trylogic.gradle.thrift.plugins.ThriftPlugin

task generateThrift(type : ru.trylogic.gradle.thrift.tasks.ThriftCompileTask) {
    generator = 'java:beans,hashcode'
    destinationDir = file("generated-src/main/java")
}

sourceSets {
    main {
        java {
            srcDir generateThrift.destinationDir
        }
    }
}

clean {
    delete generateThrift.destinationDir
}

idea {
    module {
        sourceDirs += [file('src/main/thrift'), generateThrift.destinationDir]
    }
}

compileJava.dependsOn generateThrift

dependencies {
    def thriftVersion = '0.9.1';
    Map platformMapping = [
            (OperatingSystem.WINDOWS) : 'win',
            (OperatingSystem.MAC_OS) : 'osx'
    ].withDefault { 'nix' }

    thrift "org.apache.thrift:thrift:$thriftVersion:${platformMapping.get(OperatingSystem.current())}@bin"

    compile "org.apache.thrift:libthrift:$thriftVersion"

    compile 'org.slf4j:slf4j-api:1.7.7'
}

Мы используем мой плагин Thrift для Gradle . Thrift сгенерирует исходный код для каталога «generate-src / main / java». По умолчанию Thrift использует slf4j v1.5.8, а Spring Boot использует v1.7.7. Это приведет к ошибке во время выполнения, когда вы запустите ваше приложение, поэтому мы должны принудительно установить зависимость API SLF4J.

Калькулятор сервис

Начнем с простого сервиса калькулятора. Он будет иметь 2 модуля: протокол и приложение. Начнем с протокола. Ваш проект должен выглядеть следующим образом:

       

  • калькулятор/

    • протокол /                               
      • SRC /                                      
        • главный/                                             
          • бережливость /                                                            
            • calculator.thrift
      • build.gradle
  • build.gradle
  • settings.gradle
  • thrift.gradle

Где калькулятор / протокол / build.gradle содержит только одну строку:

apply from: rootProject.file('thrift.gradle')

        

Не забудьте поместить эти строки в settings.gradle, иначе ваши модули не будут видны Gradle:

include 'calculator:protocol'
include 'calculator:app'

Протокол калькулятора

        

Даже если вы не знакомы с Thrift, файл описания протокола ( calculator / protocol / src / main / thrift / calculator.thrift ) должен быть вам очень понятен:

namespace cpp com.example.calculator
namespace d com.example.calculator
namespace java com.example.calculator
namespace php com.example.calculator
namespace perl com.example.calculator
namespace as3 com.example.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);
}

 

Здесь мы определяем TCalculatorService только одним методом — вычисления. Может выдать исключение типа TDivisionByZeroException. Обратите внимание, сколько языков мы поддерживаем «из коробки» (в этом примере мы будем использовать только Java в качестве цели)

Теперь запустите ./gradlew generateThrift, вы получите сгенерированный исходный протокол Java в папке calculator / protocol / generate-src / main / java / .

Приложение калькулятор

Далее нам нужно создать само приложение-службу. Просто создайте калькулятор / приложение / папку со следующей структурой:

  • SRC /        

    • главный/                          
      • Джава/                                      
        • ком /                                                   
          • пример/                                                              
            • калькулятор/                                                                            
              • обработчик /                  
                • CalculatorServiceHandler.java                                       
              • оказание услуг/                                                                                                            
                • CalculatorService.java                                           
              • CalculatorApplication.java
  • build.gradle

Наш файл build.gradle для модуля приложения должен выглядеть следующим образом:

apply plugin: 'spring-boot'

dependencies {
    compile project(':calculator:protocol')

    compile 'org.springframework.boot:spring-boot-starter-web'

    testCompile 'org.springframework.boot:spring-boot-starter-test'
}

Здесь у нас есть зависимость от протокола и типичных стартеров для веб-приложения Spring Boot.

        

CalculatorApplication — наш основной класс. В этом примере я настрою Spring в том же файле, но в ваших приложениях вы должны использовать другой класс конфигурации.

package com.example.calculator;

import com.example.calculator.handler.CalculatorServiceHandler;
import org.apache.thrift.protocol.*;
import org.apache.thrift.server.TServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.*;
import javax.servlet.Servlet;

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class CalculatorApplication {
    public static void main(String[] args) {
        SpringApplication.run(CalculatorApplication.class, args);
    }

    @Bean
    public TProtocolFactory tProtocolFactory() {
        //We will use binary protocol, but it's possible to use JSON and few others as well
        return new TBinaryProtocol.Factory();
    }
    
    @Bean
    public Servlet calculator(TProtocolFactory protocolFactory, CalculatorServiceHandler handler) {
        return new TServlet(new TCalculatorService.Processor<CalculatorServiceHandler>(handler), protocolFactory);
    }
}

Вы можете спросить, почему бин сервлета Thrift называется «калькулятор». В Spring Boot он зарегистрирует ваш bean-компонент сервлета в контексте имени bean-компонента, и наш сервлет будет доступен в / calculator /.

После этого нам нужен класс обработчика Thrift:

package com.example.calculator.handler;

import com.example.calculator.*;
import com.example.calculator.service.CalculatorService;
import org.apache.thrift.TException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CalculatorServiceHandler implements TCalculatorService.Iface {
    
    @Autowired
    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, и вы можете вставлять в него зависимости.

Теперь нам нужно реализовать CalculatorService:

package com.example.calculator.service;

import org.springframework.stereotype.Component;

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

        

Вот и все. Ну, почти. Нам все еще нужно как-то протестировать наш сервис. И это должен быть интеграционный тест.

Обычно, даже если ваше приложение предоставляет JSON REST API, вам все равно придется реализовать клиент для него. Thrift сделает это за вас. Мы не должны заботиться об этом. Также он будет поддерживать разные протоколы. Давайте использовать сгенерированный клиент в нашем тесте:

package com.example.calculator;

import org.apache.thrift.protocol.*;
import org.apache.thrift.transport.THttpClient;
import org.apache.thrift.transport.TTransport;
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.*;
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.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = CalculatorApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port:0")
public class CalculatorApplicationTest {
    
    @Autowired
    protected TProtocolFactory protocolFactory;

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

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

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

    @Test
    public void testSubtract() throws Exception {
        assertEquals(3, client.calculate(5, 2, TOperation.SUBTRACT));
    }
    
    @Test
    public void testMultiply() throws Exception {
        assertEquals(10, client.calculate(5, 2, TOperation.MULTIPLY));
    }
    
    @Test
    public void testDivide() throws Exception {
        assertEquals(2, client.calculate(10, 5, TOperation.DIVIDE));
    }
    
    @Test(expected = TDivisionByZeroException.class)
    public void testDivisionByZero() throws Exception {
        client.calculate(10, 0, TOperation.DIVIDE);
    }
}

        

Этот тест запустит ваше приложение Spring Boot, свяжет его со случайным портом и протестирует его. Все клиент-серверные коммуникации будут выполняться так же, как и клиенты реального мира.

Обратите внимание, как легко пользоваться нашим сервисом со стороны клиента. Мы просто вызываем методы и ловим исключения.