Implementing CREATE operation
Table of contents
In this step, you'll write a service method for creating a blog post and a test method to verify the service method.
By completing this step, you'll learn to map your service with the HTTP POST (@Post
) method,
make your own request converter (@RequestConverter
), and utilize a server extension for testing (ServerExtension
).
What you need
You need to have the following files obtained from previous steps. You can always download the full version, instead of creating one yourself.
Main.java
BlogPost.java
BlogService.java
1. Map HTTP method
Let's start mapping the HTTP POST method with our service method:
- Declare a service method,
createBlogPost()
, in the classBlogService
. - Map this service method with the HTTP POST method by adding the
@Post
annotation. - Bind the endpoint
/blogs
to the method.
import com.linecorp.armeria.server.annotation.Post;
public final class BlogService {
...
@Post("/blogs")
public void createBlogPost(BlogPost blogPost) {}
}
2. Handle parameters
Let's receive blog post information through a request body. Armeria's request converter converts request parameters in HTTP messages into Java objects for you. In the request converter, we define what keys of a JSON object to map with what properties of a Java object.
Let's first write a request converter and then register the request converter to the service method.
Write a request converter
Armeria's request converter converts a request body from a client into a Java object for you.
We can use Armeria's default JacksonRequestConverterFunction
as is, but here let's give a go at customizing a request converter for our blog post requests. We want to convert blog post details into a Java object.
Create a
BlogPostRequestConverter.java
file and declare a class, implementing theRequestConverterFunction
interface. For the sake of simplicity, generate impromptu IDs for this tutorial.BlogRequestConverter.javapackage example.armeria.server.blog; import com.fasterxml.jackson.databind.ObjectMapper; import com.linecorp.armeria.server.annotation.RequestConverterFunction; import java.util.concurrent.atomic.AtomicInteger; final class BlogPostRequestConverter implements RequestConverterFunction { private static final ObjectMapper mapper = new ObjectMapper(); private AtomicInteger idGenerator = new AtomicInteger(); // Blog post ID }
Add a method retrieving a value of a given key in a JSON object:
BlogRequestConverter.javaimport com.fasterxml.jackson.databind.JsonNode; final class BlogPostRequestConverter implements RequestConverterFunction { ... static String stringValue(JsonNode jsonNode, String field) { JsonNode value = jsonNode.get(field); if (value == null) { throw new IllegalArgumentException(field + " is missing!"); } return value.textValue(); } }
Customize the default
convertRequest()
method as follows.BlogRequestConverter.javaimport com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.common.AggregatedHttpRequest; import com.linecorp.armeria.common.annotation.Nullable; import java.lang.reflect.ParameterizedType; final class BlogPostRequestConverter implements RequestConverterFunction { ... @Override public Object convertRequest(ServiceRequestContext ctx, AggregatedHttpRequest request, Class<?> expectedResultType, @Nullable ParameterizedType expectedParameterizedResultType) throws Exception { if (expectedResultType == BlogPost.class) { JsonNode jsonNode = mapper.readTree(request.contentUtf8()); int id = idGenerator.getAndIncrement(); String title = stringValue(jsonNode, "title"); String content = stringValue(jsonNode, "content"); return new BlogPost(id, title, content); // Create an instance of BlogPost object } return RequestConverterFunction.fallthrough(); } ... }
Register a request converter
In this step, assign the request converter we customized to our service method. Annotate the service method with @RequestConverter
and specify the RequestConverterFunction
class as BlogPostRequestConverter.class
.
import com.linecorp.armeria.server.annotation.RequestConverter;
public final class BlogService {
...
@Post("/blogs")
@RequestConverter(BlogPostRequestConverter.class)
public void createBlogPost(BlogPost blogPost) {
// Implement blog service
}
}
3. Implement service code
When the request for creation is received, our request converter creates an instance of a blog post object for us. We want to save the blog post object in the map (blogPosts
) created in the BlogService
class.
Let's store the blog post information in the map by adding line 4, in the createBlogPost()
method.
1@Post("/blogs")
2@RequestConverter(BlogPostRequestConverter.class)
3public void createBlogPost(BlogPost blogPost) {
4 blogPosts.put(blogPost.getId(), blogPost);
5}
4. Return response
Now, it's time to return a response to our client. As the response, return the information received, with additional information including the ID of the post, created time, plus the modified time which would be identical to the created time.
Let's return a response for blog post creation:
Replace the return type of the
createBlogPost()
method fromvoid
toHttpResponse
.Create and return an HTTP response using Armeria's
HttpResponse
with the information of the post created.BlogService.javaimport com.linecorp.armeria.common.HttpResponse; public final class BlogService { ... public HttpResponse createBlogPost(BlogPost blogPost) { ... return HttpResponse.ofJson(blogPost); } }
5. Create a test file
Let's start writing test code. We'll use test code to verify what we implement along the way.
Create the BlogServiceTest.java
file as follows.
You can see the full version of the file here.
package example.armeria.server.blog;
import com.fasterxml.jackson.databind.ObjectMapper;
class BlogServiceTest {
private static final ObjectMapper mapper = new ObjectMapper();
}
6. Register a ServerExtension
Armeria's ServerExtension
automatically handles set-up and tear-down of a server for testing.
This is convenient as it eliminates the need to execute the main method to set up a server before running our tests.
In the BlogServiceTest
class, register a ServerExtension
as follows.
Note that the service instance is added to the configuration.
import org.junit.jupiter.api.extension.RegisterExtension;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
class BlogServiceTest {
...
@RegisterExtension
static final ServerExtension server = new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.annotatedService(new BlogService());
}
};
}
7. Test creating a blog post
Let's test if we can create a blog post.
In the
BlogServiceTest
class, add a private method as follows.BlogServiceTest.javaimport java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.MediaType; ... private static HttpRequest createBlogPostRequest(Map<String, String> content) throws JsonProcessingException { return HttpRequest.builder() .post("/blogs") .content(MediaType.JSON_UTF_8, mapper.writeValueAsString(content)) .build(); }
Add a test method as follows to test creating a blog post.
BlogServiceTest.javaimport static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; import org.junit.jupiter.api.Test; import com.linecorp.armeria.client.WebClient; import com.linecorp.armeria.common.AggregatedHttpResponse; ... @Test void createBlogPost() throws JsonProcessingException { final WebClient client = WebClient.of(server.httpUri()); final HttpRequest request = createBlogPostRequest(Map.of("title", "My first blog", "content", "Hello Armeria!")); final AggregatedHttpResponse res = client.execute(request).aggregate().join(); final Map<String, Object> expected = Map.of("id", 0, "title", "My first blog", "content", "Hello Armeria!"); assertThatJson(res.contentUtf8()).whenIgnoringPaths("createdAt", "modifiedAt") .isEqualTo(mapper.writeValueAsString(expected)); }
Run the test case on your IDE or using Gradle.
./gradlew test
The service worked as expected if you see the test case passed.
You can test this also with Armeria's Documentation service. See Using DocService after adding service methods for instructions.
Next step
In this step, we've written a method to implement a CREATE operation and used Armeria's annotations; @Post
and @RequestConverter
.
We've also registered ServerExtension
to our test and written a test method.
Next, at Step 5. Implement READ, we'll implement a READ operation to read a single post and also multiple posts.