Skip to content

Commit

Permalink
Add the Thrift tutorial (#4865)
Browse files Browse the repository at this point in the history
Motivation:

- Provide a tutorial for beginners who wants to use Apache Thrift with Armeria

Modifications:

- Add a tutorial for writing an Apache Thrift service with Armeria

Result:

- Adds the tutorial in the "THRIFT SERVICE" menu in armeria.dev site.

Co-authored-by: jrhee17 <[email protected]>
  • Loading branch information
haneepark and jrhee17 authored Jun 21, 2023
1 parent 3e05423 commit a575329
Show file tree
Hide file tree
Showing 16 changed files with 1,438 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package example.armeria.server.blog.thrift;

import java.net.URI;
import java.util.List;

import org.apache.thrift.TException;

import com.linecorp.armeria.client.logging.LoggingClient;
import com.linecorp.armeria.client.thrift.ThriftClients;

import example.armeria.blog.thrift.BlogPost;
import example.armeria.blog.thrift.BlogService;
import example.armeria.blog.thrift.CreateBlogPostRequest;
import example.armeria.blog.thrift.DeleteBlogPostRequest;
import example.armeria.blog.thrift.GetBlogPostRequest;
import example.armeria.blog.thrift.ListBlogPostsRequest;
import example.armeria.blog.thrift.UpdateBlogPostRequest;

public final class BlogClient {

private final BlogService.Iface blogService;

BlogClient(URI uri, String path) {
blogService = ThriftClients.builder(uri)
.path(path)
.decorator(LoggingClient.newDecorator())
.build(BlogService.Iface.class);
}

BlogPost createBlogPost(String title, String content) throws TException {
final CreateBlogPostRequest request =
new CreateBlogPostRequest().setTitle(title)
.setContent(content);
return blogService.createBlogPost(request);
}

BlogPost getBlogPost(int id) throws TException {
final GetBlogPostRequest request =
new GetBlogPostRequest().setId(id);
return blogService.getBlogPost(request);
}

List<BlogPost> listBlogPosts(boolean descending) throws TException {
return blogService.listBlogPosts(new ListBlogPostsRequest().setDescending(descending))
.getBlogs();
}

BlogPost updateBlogPost(int id, String newTitle, String newContent) throws TException {
final UpdateBlogPostRequest request = new UpdateBlogPostRequest().setId(id)
.setTitle(newTitle)
.setContent(newContent);
return blogService.updateBlogPost(request);
}

void deleteBlogPost(int id) throws TException {
final DeleteBlogPostRequest request = new DeleteBlogPostRequest().setId(id);
blogService.deleteBlogPost(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public void updateBlogPost(UpdateBlogPostRequest request, AsyncMethodCallback<Bl
final BlogPost oldBlogPost = blogPosts.get(request.getId());
if (oldBlogPost == null) {
resultHandler.onError(
new BlogNotFoundException("The blog post does not exist. ID: " + request.getId()));
new IllegalArgumentException("The blog post does not exist. ID: " + request.getId()));
} else {
final BlogPost newBlogPost = oldBlogPost.deepCopy()
.setTitle(request.getTitle())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,18 @@
import java.util.List;

import org.apache.thrift.TException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.linecorp.armeria.client.logging.LoggingRpcClient;
import com.linecorp.armeria.client.thrift.ThriftClients;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.thrift.THttpService;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;

import example.armeria.blog.thrift.BlogNotFoundException;
import example.armeria.blog.thrift.BlogPost;
import example.armeria.blog.thrift.BlogService;
import example.armeria.blog.thrift.CreateBlogPostRequest;
import example.armeria.blog.thrift.DeleteBlogPostRequest;
import example.armeria.blog.thrift.GetBlogPostRequest;
import example.armeria.blog.thrift.ListBlogPostsRequest;
import example.armeria.blog.thrift.ListBlogPostsResponse;
import example.armeria.blog.thrift.UpdateBlogPostRequest;

@TestMethodOrder(OrderAnnotation.class)
class BlogServiceTest {
Expand All @@ -43,42 +33,34 @@ protected void configure(ServerBuilder sb) throws Exception {
}
};

static BlogService.Iface client;

@BeforeAll
static void beforeAll() {
client = ThriftClients.builder(server.httpUri())
.path("/thrift")
.rpcDecorator(LoggingRpcClient.newDecorator())
.build(BlogService.Iface.class);
}

@Test
@Order(1)
void createBlogPost() throws TException {
final CreateBlogPostRequest request = new CreateBlogPostRequest()
.setTitle("My first blog")
.setContent("Hello Armeria!");
final BlogPost response = client.createBlogPost(request);
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final BlogPost response = client.createBlogPost("My first blog", "Hello Armeria!");
assertThat(response.getId()).isGreaterThanOrEqualTo(0);
assertThat(response.getTitle()).isEqualTo(request.getTitle());
assertThat(response.getContent()).isEqualTo(request.getContent());
assertThat(response.getTitle()).isEqualTo("My first blog");
assertThat(response.getContent()).isEqualTo("Hello Armeria!");
System.out.println(response);
}

@Test
@Order(2)
void getBlogPost() throws TException {
final BlogPost blogPost = client.getBlogPost(new GetBlogPostRequest().setId(0));
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final BlogPost blogPost = client.getBlogPost(0);

assertThat(blogPost.getTitle()).isEqualTo("My first blog");
assertThat(blogPost.getContent()).isEqualTo("Hello Armeria!");
System.out.println(blogPost);
}

@Test
@Order(3)
void getInvalidBlogPost() {
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final Throwable exception = catchThrowable(() -> {
client.getBlogPost(new GetBlogPostRequest().setId(Integer.MAX_VALUE));
client.getBlogPost(Integer.MAX_VALUE);
});
assertThat(exception).isInstanceOf(BlogNotFoundException.class)
.extracting("reason")
Expand All @@ -89,15 +71,10 @@ void getInvalidBlogPost() {
@Test
@Order(4)
void listBlogPosts() throws TException {
final CreateBlogPostRequest newBlogPost = new CreateBlogPostRequest()
.setTitle("My second blog")
.setContent("Armeria is awesome!");
client.createBlogPost(newBlogPost);
final ListBlogPostsResponse
response = client.listBlogPosts(new ListBlogPostsRequest()
.setDescending(false));

final List<BlogPost> blogs = response.getBlogs();
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
client.createBlogPost("My second blog", "Armeria is awesome!");

final List<BlogPost> blogs = client.listBlogPosts(false);
assertThat(blogs).hasSize(2);
final BlogPost firstBlog = blogs.get(0);
assertThat(firstBlog.getTitle()).isEqualTo("My first blog");
Expand All @@ -106,26 +83,54 @@ void listBlogPosts() throws TException {
final BlogPost secondBlog = blogs.get(1);
assertThat(secondBlog.getTitle()).isEqualTo("My second blog");
assertThat(secondBlog.getContent()).isEqualTo("Armeria is awesome!");
System.out.println(blogs);
}

@Test
@Order(5)
void updateBlogPosts() throws TException {
final UpdateBlogPostRequest request = new UpdateBlogPostRequest()
.setId(0)
.setTitle("My first blog")
.setContent("Hello awesome Armeria!");
final BlogPost updated = client.updateBlogPost(request);
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final BlogPost updated = client.updateBlogPost(0, "My first blog", "Hello awesome Armeria!");
assertThat(updated.getId()).isZero();
assertThat(updated.getTitle()).isEqualTo("My first blog");
assertThat(updated.getContent()).isEqualTo("Hello awesome Armeria!");
System.out.println(updated);
}

@Test
@Order(6)
void badRequestExceptionHandlerWhenTryingDeleteMissingBlogPost() {
void updateInvalidBlogPost() {
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final Throwable exception = catchThrowable(() -> {
final BlogPost updated = client.updateBlogPost(Integer.MAX_VALUE, "My first blog",
"Hello awesome Armeria!");
});
assertThat(exception).isInstanceOf(BlogNotFoundException.class)
.extracting("reason")
.asString()
.isEqualTo("The blog post does not exist. ID: " + Integer.MAX_VALUE);
}

@Test
@Order(7)
void deleteBlogPost() throws TException {
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
client.deleteBlogPost(1);
final Throwable exception = catchThrowable(() -> {
client.getBlogPost(1);
});
assertThat(exception).isInstanceOf(BlogNotFoundException.class)
.extracting("reason")
.asString()
.isEqualTo("The blog post does not exist. ID: 1");
}

@Test
@Order(8)
void deleteInvalidBlogPost() {
final BlogClient client = new BlogClient(server.httpUri(), "/thrift");
final Throwable exception = catchThrowable(() -> {
client.deleteBlogPost(new DeleteBlogPostRequest().setId(100));
client.deleteBlogPost(100);
});
assertThat(exception).isInstanceOf(BlogNotFoundException.class)
.extracting("reason")
Expand Down
2 changes: 2 additions & 0 deletions site/src/components/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import prismTheme from 'react-syntax-highlighter/dist/esm/styles/prism/cb';
// Prism syntaxes
/* eslint-disable import/no-extraneous-dependencies */
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
import cpp from 'react-syntax-highlighter/dist/esm/languages/prism/cpp';
import graphql from 'react-syntax-highlighter/dist/esm/languages/prism/graphql';
import groovy from 'react-syntax-highlighter/dist/esm/languages/prism/groovy';
import http from 'react-syntax-highlighter/dist/esm/languages/prism/http';
Expand Down Expand Up @@ -40,6 +41,7 @@ none.aliases = [] as string[];

const supportedLanguages = {
bash,
cpp,
graphql,
groovy,
http,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions site/src/pages/tutorials/thrift/blog/create-service.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
menuTitle: "Create a service"
order: 1
category: thrift
type: step
targetLang: java
---

# Creating a service

As the first step of the tutorial, we'll create a simple service with a dummy method in Thrift and implement the service in Java.

<TutorialSteps current={1} />

## What you need

No preparation is required for this step. Do check that you've prepared the [prerequisites](/tutorials/thrift/blog/#prerequisites).

## 1. Create a thrift file

Create a thrift file, `blog.thrift` in the `{project_root}/src/main/thrift` folder as follows.
In the thrift file, let's define the `BlogService` service with the `hello()` method.

```cpp filename=blog.thrift
namespace java example.armeria.blog.thrift

service BlogService {
string hello()
}
```
<Tip>
See [Sample service structure](/tutorials/thrift/blog#sample-service-structure) for the overall folder structure.
</Tip>
## 2. Compile the thrift file
Compile the `blog.thrift` file to generate Java code.
You can refer to the full [build.gradle](https://github.com/line/armeria-examples/tree/main/tutorials/thrift/build.gradle) file for generating code with [Gradle Thrift Plugin](https://github.com/jruyi/thrift-gradle-plugin).
```bash
./gradlew compileThrift
```

You'll see the generated Java code in the `{project_root}/build/generated-sources/thrift/gen-java/example/armeria/blog/thrift/` folder.

## 3. Implement the service

Now, let's implement the service in Java.

1. Create a file, `BlogServiceImpl.java`.
2. Declare the `BlogServiceImpl` class implementing the `BlogService` service we defined earlier in Thrift.
```java filename=BlogServiceImpl.java
package example.armeria.server.blog.thrift;

import example.armeria.blog.thrift.BlogService;

public class BlogServiceImpl implements BlogService.AsyncIface {}
```
3. In the `BlogServiceImpl` class, override the `hello()` method, a dummy method for temporary use to test the connection between server and client.
```java filename=BlogServiceImpl.java
import org.apache.thrift.async.AsyncMethodCallback;
...
@Override
public void hello(AsyncMethodCallback<String> resultHandler) {
resultHandler.onComplete("Hello, Armeria!");
}
```

<Tip>

Although here we implement the asynchronous interface `BlogService.AsyncIface`, note that implementing the synchronous interface `BlogService.Iface` isn't very different.
See [Running a Thrift service](/docs/server-thrift) for more information.

</Tip>

## What's next

In this step, we've created a simple service with a dummy method.

Next, at [Step 2. Run a server](/tutorials/thrift/blog/run-server), we'll create and run a server with the service we've created.
Also, we'll run a client to make a call to the service.

<TutorialSteps current={1} />
Loading

0 comments on commit a575329

Please sign in to comment.