Thrift-API mit Spring: Wie integriert man beides?

Print Friendly, PDF & Email

Wenn man eine Server-Schnittstelle plant, denkt man heutzutage als Erstes an eine REST-API. Für öffentliche Schnittstellen ist REST oft auch eine gute Wahl. Für nicht-öffentliche Schnittstellen lohnt sich jedoch auch ein Blick auf Apache Thrift. Möchte man Thrift dann zusammen mit dem Spring-Framework verwenden, stellt man fest, dass die Integration etwas sperrig ist.

Öffentliche Schnittstellen sollten möglichst technologie- und plattformneutral und für Client-Anwendungen einfach zu nutzen sein. REST erfüllt diese Anforderungen, daher sind REST-basierte APIs weit verbreitet. Die Kehrseite der Technologieneutralität ist jedoch, dass REST keine Hilfe bei der Implementierung einer Anbindung bietet. Auf der Client-Seite müssen alle Server-Aufrufe ausprogrammiert werden.

In einer geschlossenen System-Landschaft sind die Vorteile von REST geringer, vor allem, wenn die Landschaft homogen ist, das heißt, dieselben Technologien verwendet werden. Dann bieten proprietäre Ansätze wie z.B. RMI für Java mehr Komfort (jedoch natürlich auch eigene Probleme). In einer heterogenen geschlossenen System-Landschaft kommen solche Lösungen aber kaum in Frage.

Vor fünfzehn Jahren wäre dann Corba die erste Wahl gewesen, vor zehn Jahren war es SOAP. Aber der eher schwergewichtige Ansatz von beiden ist aus der Mode gekommen. Wer nach Alternativen sucht, findet in erster Linie Protocol Buffers, Avro und Thrift. Protocol Buffers bietet „nur“ eine performante Datenserialisierung, jedoch keine Möglichkeiten, eine API zu spezifizieren. Avro und Thrift hingegen bieten sowohl Datenserialisierung als auch Unterstützung für Remote Procedure Calls.

Thrift bietet gegenüber Avro zwei große Vorteile: Thrift generiert zum einen auf allen unterstützten Plattformen Code für die Schnittstellen und die Datenobjekte. Und zum anderen beinhaltet Thrift ein Versionierungskonzept, mit dem sich die eigenen APIs leicht rückwärtskompatibel verändern lassen.

Aber die Grundlagen von Thrift und seine spezifischen Vor- und Nachteile sollen an dieser Stelle gar nicht weiter diskutiert werden. Das gibt es schon an anderer Stelle:

Interessant wird es, wenn man sich für Thrift entscheidet und dann versucht, Thrift in eine Spring-basierte Anwendung zu integrieren.

Integration in ein Spring-Projekt

Thrift in ein Spring-Projekt einzubinden, ist gar nicht so einfach. Und erstaunlicherweise gibt es im Web dazu nur ein konkretes Beispiel, leider auf der Grundlage von Spring Boot, das nicht jedermanns Sache ist. Aber es geht auch ohne Spring Boot.

Thrift bringt eine eigene, in sich geschlossene Infrastruktur für die Kommunikation zwischen Clients und Server mit. Wenn man beispielsweise einen Thrift-basierten Dienst per http-Protokoll zugänglich machen möchte, dann muss man dazu ein Thrift-eigenes TServlet verwenden. Die Schwierigkeit bei der Integration von Thrift in Spring liegt darin, Thrifts Kommunikationsinfrastruktur in einer Spring-Anwendung zu initialisieren und gleichzeitig die Service-Implementierungen durch Spring verwalten zu lassen, sodass man z.B. Springs Dependency-Injection-Mechanismen nutzen kann.

Von hier an zeigen Code-Beispielen, wie man Thrift in Spring integrieren kann. Das vollständige Beispiel-Projekt mit allen Sourcen befindet sich auf Github.

Build-Prozess

Thrift generiert Source-Code aus einer Schnittstellen-Beschreibung in einem proprietären Format. Diesen Generierungsschritt sollte man in den Build-Prozess des eigenen Projektes verankern. Dafür benötigt man zunächst eine lokale Installation von Thrift. Zur Installation von Thrift folge man den Anweisungen auf thrift.apache.org. Die Generierung des Java-Sourcecodes geschieht beispielsweise mit einem Maven-Plugin:

<plugin>
    <groupId>org.apache.thrift.tools</groupId>
    <artifactId>maven-thrift-plugin</artifactId>
    <version>0.1.11</version>
    <executions>
        <execution>
            <id>thrift-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <generator>java</generator>
                <outputDirectory>${basedir}/target/generated-sources/thrift</outputDirectory>
                <compileOutput>true</compileOutput>
            </configuration>
            <goals>
                <goal>compile</goal>
            </goals>
    </execution>
    </executions>
</plugin>

In der Grundeinstellung durchsucht das Plugin das Verzeichnis src/main/thrift und erzeugt Code für alle dort gefundenen thrift-Dateien.

Damit die generierten Sourcen im Projekt sichtbar werden, empfiehlt es sich, zusätzlich das build-helper-Plugin zu verwenden:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.9.1</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>${project.build.directory}/generated-sources/thrift</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

Schließlich braucht man eine Abhängigkeit auf die Thrift-Bibliothek:

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.9.2</version>
</dependency>

Der vollständigen POM liegt auf Github.

Implementierung des Thrift-Dienstes

Das Beispiel-Projekt implementiert einen simplen Ping-Pong-Dienst, wie er in der folgenden Datei example.thrift deklariert ist:

namespace java de.timwellhausen.example.spring.thriftintegration.api

struct Ping {
    1: string message;
}

struct Pong {
    1: string answer;
}

service PingPongService {
    Pong knock(1: Ping ping);
}

Die Code-Generierung erzeugt daraus unter anderem die Java-Schnittstelle PingPongService. Eine Implementierung dieser Schnittstelle als Spring-Service schaut dann beispielsweise so aus:

@Service
public class PingPongServiceBean implements PingPongService.Iface {

    @Override
    public Pong knock(Ping ping) throws TException {

        String message = ping.getMessage();
        String answer = StringUtils.reverse(message);

        Pong pong = new Pong();
        pong.setAnswer(answer);

        return pong;
    }
}

Bereitstellen des Dienstes

Damit der implementierte Dienst als Thrift-Endpunkt verfügbar wird, muss er über die Thrift-eigene API explizit bekannt gemacht werden. Wenn der Dienst als http-Endpunkt erreichbar sein soll, muss man eine Instanz von TServlet initialisieren. Als Ort dafür bietet sich eine Configuration-Klasse an (alternativ ist auch eine reine XML-Deklaration möglich):

@Configuration
public class SpringConfiguration {

    @Bean
    public TServlet thriftServlet(PingPongService.Iface pingPongService) {

        TMultiplexedProcessor multiplexedProcessor = new TMultiplexedProcessor();

        Processor pingPongProcessor = new PingPongService.Processor(pingPongService);
        multiplexedProcessor.registerProcessor("PingPongService", pingPongProcessor);

        Factory protocolFactory = new TBinaryProtocol.Factory();
        TServlet thriftServlet = new TServlet(multiplexedProcessor, protocolFactory);

        return thriftServlet;
    }
}

Hier wird das Thrift-Servlet mit einem TMultiplexedProcessor und einem binären Protokoll initialisiert. Das Multiplexing erlaubt es, dass das Servlet mehrere Dienste anbieten und verarbeiten kann (auch wenn dies hier nicht benötigt wird).

Initialisierung von Spring

Als letzter Baustein fehlt noch, wie das Thrift-Servlet in einer Spring-Anwendung dem Web-Container bekannt gemacht wird. Denn Spring selbst verarbeitet üblicherweise alle http-Anfragen mit seinem eigenen DispatcherServlet.

Ohne Spring Boot zu nutzen, kann man Springs WebApplicationInitializer verwenden. Jede Klasse, die diese Schnittstelle implementiert, wird beim Hochfahren des Web-Containers aufgerufen. Dieser Ansatz erlaubt es, Spring hochzufahren, ohne eine web.xml-Datei erstellen zu müssen. An dieser Stelle lässt sich das Thrift-Servlet in den Servlet-Kontext einbetten:

public class WebApplicationInitializer implements org.springframework.web.WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        // Setup Spring
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(SpringConfiguration.class);
        rootContext.refresh();
        servletContext.addListener(new ContextLoaderListener(rootContext));

        // Register Thrift servlet as endpoint
        TServlet thriftServlet = rootContext.getBean("thriftServlet", TServlet.class);
        ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("apiServlet", thriftServlet);
        servletRegistration.setLoadOnStartup(2);
        servletRegistration.addMapping("/api");
    }
}

Wenn alle diese Bausteine ineinandergefügt sind, kann man die Anwendung deployen und hochfahren und der Thrift-Dienst ist verfügbar.

Die Client-Seite

Der Vollständigkeit halber ist hier noch ein Beispiel, wie der neue Dienst von einem Client-Programm aus aufgerufen werden kann. Spring ist dafür nicht notwendig:

public class Client {

    public static void main(String[] args) throws Exception {

        THttpClient transport = new THttpClient("http://localhost:8080/api");
        transport.open();
        TProtocol protocol = new TBinaryProtocol(transport);
        PingPongService.Client client = new PingPongService.Client(new TMultiplexedProtocol(protocol, "PingPongService"));

        Ping ping = new Ping();
        ping.setMessage("Hello");
        Pong pong = client.knock(ping);
    }
}

Fazit

Wenn man weiß, wie es geht, lässt sich Thrift relativ einfach in eine Spring-Anwendung integrieren. Die Tatsache, dass dieses Wissen schwer zu finden ist, lässt darauf schließen, dass die Kombination von Thrift und Spring wenig verwendet wird. Aber vielleicht hilft ja dieser Artikel, dass das Gespann von Thrift und Spring etwas mehr Verbreitung findet.

Schreibe einen Kommentar

Fügen Sie die notwendige Nummer ins freie Feld ein *