java -jar openapi-generator-cli-XXX.jar help
Table of Contents
- 1. Getting Started
- 2. What you will need
- 3. Solution
- 4. Installing OpenAPI Generator
- 5. Generating the Micronaut Client
- 6. Configure Client Authorization
- 7. Testing the Client
- 8. Using Client in a Controller
- 9. Running the Application
- 10. Generate a Micronaut Application Native Executable with GraalVM
- 11. Next Steps
Use OpenAPI Definition to Generate a Micronaut Client
Learn how to generate a Declarative Micronaut Client API from an OpenAPI definition and how to use it in your application
Authors: Andriy Dmytruk
Micronaut Version: 3.9.2
1. Getting Started
OpenAPI is a specification for describing REST APIs. Many web APIs provide definitions for the available endpoints and parameters in form of OpenAPI documents. OpenAPI infrastructure includes numerous tooling, including the OpenAPI generator.
OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported).
In this guide, we will use the OpenAPI Generator to generate a client API from an OpenAPI definition file. We will generate the client for Twitter API, however the steps could easily be replicated with any other API with an OpenAPI definition.
After generation, we will see how to use the client inside a Micronaut application, including performing API calls in controller methods.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Installing OpenAPI Generator
To use OpenAPI Generator, we need to install OpenAPI Generator CLI.
We will use the jar option. Download it and store it in the directory that you want to use for this project.
Next, open terminal in the same directory. To verify that generator works correctly run the help
command:
It should provide a description and a list of commands.
All the options for installation are given at the "CLI Installation" guide on the OpenAPI Generator website. In particular:
If you installed the generator with a package manager or bash launcher script, simply run
|
You can also use the OpenAPI Generator Online Service but its usage is not covered by this guide. |
5. Generating the Micronaut Client
We will generate a client for Twitter API.
To do this, we will need to download the official Twitter API OpenAPI definition file: https://api.twitter.com/2/openapi.json.
Save it as twitter-api-definition.json
and move into the directory that you intend to use for this project.
To generate the Micronaut client from the JSON definition, open the terminal in the directory where the definition file and the OpenAPI generator executable are located, and run:
java -jar openapi-generator-cli-5.4.0.jar generate \
-g java-micronaut-client \(1)
-i twitter-api-definition.json \(2)
-o ./ \(3)
-p apiPackage=example.micronaut.api \(4)
-p modelPackage=example.micronaut.model \(5)
-p build=gradle \(6)
-p test=junit(7)
1 | Specify that we will use the Java Micronaut client generator. |
2 | Specify our OpenAPI definition file as twitter-api-definition.json . |
3 | Specify the output directory to be the current directory (./ ).
You can specify it to be a different one if you want. |
4 | We provide generator-specific properties starting with -p .
We want all the API files to be generated in the example.twitter.api package. |
5 | We want all the models to be in the example.twitter.model package.
In this package classes for data models, that are used for communication with server will be generated. |
6 | We want to use gradle as build tool. The supported values are gradle , maven and all .
If nothing is specified, both Maven and Gradle files are generated. |
7 | We want to use JUnit 5 for testing. The supported values are junit (JUnit 5) and spock .
If nothing is specified, junit is used by default. |
After generation finishes, you should see the following directory structure:
./
├── docs
│ └── ... (1)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── example/twitter/
│ │ │ ├── api (2)
│ │ │ │ ├── GeneralApi.java
│ │ │ │ ├── SpacesApi.java
│ │ │ │ ├── TweetsApi.java
│ │ │ │ └── UsersApi.java
│ │ │ └── model (3)
│ │ │ ├── ...
│ │ │ ├── Tweet.java
│ │ │ ├── TweetSearchResponse.java
│ │ │ ├── User.java
│ │ │ └── ...
│ │ └── resources/
│ │ ├── application.yml (4)
│ │ └── logback.xml
│ └── test/
│ └── java/
│ └── example/twitter/ (5)
│ ├── api
│ │ ├── GeneralApiTest.java
│ │ ├── SpacesApiTest.java
│ │ ├── TweetsApiTest.java
│ │ └── UsersApiTest.java
│ └── model
│ └── ...
├── README.md
└── ...
1 | The docs/ directory contains automatically generated Markdown files with documentation about your API. |
2 | The example.twitter.api package contains API classes. |
3 | The example.twitter.model package contains data classes, that are used in parameters or bodies of both
requests and responses in the API. |
4 | The application.yml file contains the configuration of your Micronaut application. We will modify it later. |
5 | Templates for tests are generated. Test files for both APIs and models were created. |
The definition file is a document describing the Twitter API according to the OpenAPI Specification. If you want to learn about the structure of OpenAPI specification and how to simplify the creation of a Micronaut Server with it, read the "Use OpenAPI Defintion to Generate a Micronaut Server" guide or the OpenAPI guide. |
As you can see, four API files were generated for the Twitter API.
If you look inside the API files, you will see that method definitions were generated corresponding to different paths and operations available in the API. Using Micronaut`s features, we will be able to inject a client implementation of this interface and use it by calling the corresponding methods without the need to worry how client-server communication is handled.
If you want to view all the available parameters for micronaut server generator, run
|
6. Configure Client Authorization
In order to send requests to an API, you might be required to configure Authorization.
6.1. Create Twitter Development Account
In case of Twitter, we need to configure OAuth2 authorization. You can authorize with an API key via the OAuth2 authorization code flow or using a Bearer token.
We will use the API key option. To get your API key, you will need to sign up for a Twitter developer account.
To do this, follow the first 2 steps on the
"Getting Access to the Twitter API" guide.
Do not forget to save your API key
and API secret
values somewhere.
You can read about Twitter OAuth2 authentication in the "Authorization" page of the Twitter API documentation. The documentation specifies authentication with Bearer token and using an API key. |
If you forgot to save your
|
6.2. Configure Authorization for Micronaut Client
We will modify the application.yml
to include the following configuration:
micronaut:
application:
name: openapi-micronaut
server:
port: 8080
security:
oauth2:
clients:
twitter: (1)
client-id: '${TWITTER_AUTH_CLIENT_ID:}' (4)
client-secret: '${TWITTER_AUTH_CLIENT_SECRET:}'
client-credentials:
service-id-regex: '.*'
uri-regex: 'api.twitter.com/.*' (5)
token:
url: "${openapi-micronaut-client-base-path}oauth2/token" (3)
auth-method: "client_secret_basic" (2)
1 | We add a value to the miconaut.security.oauth2.clients . We will add it with the key of twitter . |
2 | Set the authorization flow to client_secret_basic . This authorization was created for applications that need
to access the resources on behalf of itself and requires an id and a secret parameter, and a token url. |
3 | Specify the url for retrieving tokens. We found it in the Twitter documentation. |
4 | Specify the client-id and client-secret parameters. You could directly put your id and secret directly as values
in the configuration, however this highly discouraged due to security concerns. We will point to environment variables
instead. |
5 | Specify that this authorization should be used for all requests to the Twitter api URL (starting with api.twitter.com ). Also, set it to be used for all service ids. |
Before running the application we will need to set the values of client-id
and client-secret
using environment
variables. To do it, paste your API key
and API secret
into the following command and run in terminal:
export TWITTER_AUTH_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXX
export TWITTER_AUTH_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Do not forget to add these lines before running any tests or application. If you are using an IDE, there probably should be an option to add environment variables in the run settings. View the documentation of your specific IDE for details.
Now, the client should authorize correctly, and we will be able to send requests.
7. Testing the Client
You can see that four APIs were generated in the project. These APIs have paths logically distributed between them.
You can look at Twitter API v2 documentation for description on
each of the paths, and notice that the sections are also logically split into Tweets
, Users
and Spaces
, as well.
We will show how we can use the Twitter API by writing some simple tests using the generated Micronaut client.
First, we will use the recent tweet counts API
to get the number of tweets about Toronto in the last 7 days. Open the TweetsApiTest
and rewrite the contents of the file with the following:
package example.twitter.api;
import example.twitter.model.SearchCount;
import example.twitter.model.Tweet;
import example.twitter.model.TweetCountsResponse;
import example.twitter.model.TweetSearchResponse;
import example.twitter.model.User;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* API tests for TweetsApi
*/
@MicronautTest (1)
@EnabledIfEnvironmentVariable(named = "TWITTER_AUTH_CLIENT_ID", matches = ".+") (2)
public class TweetsApiTest {
@Inject
TweetsApi api; (3)
/**
* Recent search counts
*/
@Test
public void tweetCountsRecentSearchTest() {
// WHEN
String query = "Toronto";
TweetCountsResponse response = api.tweetCountsRecentSearch(
query, null, null, null, null, null, null).block(); (4)
// THEN
assertNotNull(response);
// Calculate total count
assertNotNull(response.getData());
Integer totalCount = response.getData().stream()
.filter(Objects::nonNull)
.map(SearchCount::getTweetCount)
.reduce(0, Integer::sum);
// There should be more than 100 tweets with such query in the last 7 days
assertTrue(totalCount > 100); (5)
}
}
1 | Annotating the class with @MicronautTest will create a Micronaut context and allows utilizing various Micronaut features, like injection. More info. |
2 | Enable the test only if Twitter developer credentials are present. |
3 | Injection for TweetsApi . |
4 | Use the api to send a request to an API path for counting tweets with a specific query. We will count tweets that mentioned "Toronto" . |
5 | We will verify that the data is present. Using manipulations with the generated data models we will calculate the total number of tweets and verify that there are at least 100 (which reasonably should be always true). |
Now, we will test receiving the latest 10
tweets about New York City (NYC). We will use the
recent tweets lookup API.
To test the data models, we will get multiple properties of the tweet, and also properties of the user
who posted it. We will add the following test to the TwetsApiTest
class:
/**
* Recent search
*/
@Test
public void tweetsRecentSearchTest() {
// GIVEN
String query = "nyc"; (1)
OffsetDateTime startTime = null;
OffsetDateTime endTime = null;
String sinceId = null;
String untilId = null;
Integer maxResults = 10;
String nextToken = null;
Set<String> expansions = new HashSet<>(Collections.singletonList("author_id")); (2)
Set<String> tweetFields = new HashSet<>(Arrays.asList( (3)
"id", "author_id", "created_at", "attachments", "lang", "possibly_sensitive", "text", "source"
));
Set<String> userFields = new HashSet<>(Arrays.asList("description", "created_at", "username", "name")); (4)
Set<String> mediaFields = null;
Set<String> placeFields = null;
Set<String> pollFields = null;
// WHEN
TweetSearchResponse response = api.tweetsRecentSearch(
query, startTime, endTime, sinceId, untilId, maxResults, nextToken, expansions, tweetFields,
userFields, mediaFields, placeFields, pollFields).block(); (5)
// THEN
assertNotNull(response);
// Tweets should be present
List<Tweet> tweets = response.getData(); (6)
assertNotNull(tweets);
assertEquals(maxResults, tweets.size());
Tweet tweet = tweets.get(0);
assertNotNull(tweet);
assertNotNull(tweet.getAuthorId());
assertNotNull(tweet.getCreatedAt());
assertNotNull(tweet.getLang());
assertNotNull(tweet.getPossiblySensitive());
assertNotNull(tweet.getText());
// Users should be present
assertNotNull(response.getIncludes());
List<User> users = response.getIncludes().getUsers(); (7)
assertNotNull(users);
assertTrue(users.size() > 0);
User user = users.get(0);
assertNotNull(user);
assertNotNull(user.getId());
assertNotNull(user.getUsername());
}
1 | Set the search query to "nyc" . |
2 | Expand the query on the author_id property to additionally get information about the user based on their id.
You can read about expanding in the Twitter API expansions documentation page. |
3 | Specify the fields we want to get for each tweet. |
4 | Specify the fields we want to get for users (authors of tweets). |
5 | Retrieve the response from server. |
6 | Get all the tweets from the response and verify that some common properties are present. |
7 | Get users form a separate property of the response and verify that the information is present. |
Finally, we will test getting multiple pages using the same recent tweets lookup API:
/**
* Recent search with the nextToken parameter set (to retrieve batch)
*/
@Test
public void tweetsRecentSearchTestNextToken() {
// WHEN
String query = "Toronto";
int maxResults = 10;
TweetSearchResponse response = api.tweetsRecentSearch(query, null, null, null,
null, maxResults, null, null, null, null, null, null, null).block(); (1)
// THEN
assertNotNull(response);
assertNotNull(response.getData());
assertEquals(maxResults, response.getData().size());
Set<String> tweetIds = response.getData().stream().map(Tweet::getId).collect(Collectors.toSet());
assertNotNull(response.getMeta());
String nextToken = response.getMeta().getNextToken();
assertNotNull(nextToken);
// WHEN
TweetSearchResponse nextResponse = api.tweetsRecentSearch(query, null, null, null,
null, maxResults, nextToken, null, null, null, null,
null, null).block(); (2)
// THEN
assertNotNull(nextResponse);
assertNotNull(nextResponse.getData());
assertEquals(maxResults, nextResponse.getData().size());
// Calculate the intersection and verify that no match
Set<String> nextTweetIds = nextResponse.getData().stream().map(Tweet::getId).collect(Collectors.toSet());
nextTweetIds.retainAll(tweetIds);
assertTrue(nextTweetIds.isEmpty()); (3)
}
1 | Retrieve the first 10 tweets that match our query. |
2 | Retrieve the next 10 tweets that match the query using the nextToken parameter. |
3 | Verify that the no tweets on the two pages match. |
We will now proceed to use the API in a simple application. However, you would probably want to also implement tests for all other paths of the API, if you were planning to use them in your application.
To run the tests:
./gradlew test
Then open build/reports/tests/test/index.html
in a browser to see the results.
All the tests should run successfully.
8. Using Client in a Controller
We will now write a controller, that will search for most frequent words in the latest tweets on a given topic.
8.1. Creating an Introspected Class
We will first define an object to represent the response. Create a file for WordFrequency
class, that will have the word
itself, and the number it occurred as properties:
package example.micronaut.model;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Introspected (1)
public class WordFrequency {
@NonNull @NotBlank
private final String word;
@NonNull @NotNull
private final Integer numberOccurred; (2)
public WordFrequency(@NonNull String word, @NonNull Integer numberOccurred) {
this.word = word;
this.numberOccurred = numberOccurred;
}
@NonNull
public String getWord() {
return word;
}
@NonNull
public Integer getNumberOccurred() {
return numberOccurred;
}
}
1 | Annotate the class with @Introspected to generate BeanIntrospection metadata at compilation time. This information can be used, for example, to render the POJO as JSON using Jackson without using reflection. |
2 | Define the two required properties and create getters for them. |
8.2. Implementing the Controller
We will create the API of the controller. It will use the generated TweetApi
client, and the WordFrequency
class, which we just created.
Create a file for TwitterFrequentWordsController
class and paste the following:
package example.micronaut.controller;
import example.twitter.api.TweetsApi;
import example.twitter.model.TweetSearchResponse;
import example.micronaut.model.WordFrequency;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller("/twitter-frequent-words") (1)
public class TwitterFrequentWordsController {
public static int TWEETS_NUMBER = 50;
private final TweetsApi tweetsApi;
public TwitterFrequentWordsController(TweetsApi tweetsApi) { (2)
this.tweetsApi = tweetsApi;
}
@Get (3)
@Secured(SecurityRule.IS_ANONYMOUS)
public Flux<WordFrequency> get(@QueryValue("search-query") String searchQuery,
@QueryValue("words-n") Integer wordsNumber (4)
) {
return tweetsApi.tweetsRecentSearch(searchQuery,
null, null, null, null, TWEETS_NUMBER, null, null,
null, null, null, null, null)
.flatMapIterable(v -> getTopFrequentWords(v, wordsNumber)); (5)
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /twitter-frequent-words . |
2 | Injection for TweetsApi . |
3 | The @Get annotation maps the get method to an HTTP GET request on /twitter-frequent-words . |
4 | Use the @QueryValue annotation to define the search-query and words-n parameters to be provided in the query part of the URL. Micronaut will parse them from request and provide as arguments to the method. |
5 | Make a request to the Twitter api and process the data using a function that we will implement next. |
Now add the method responsible for processing the data to the controller. There is no need to get into the details of its implementation, but we will leave some description for a curious reader:
@NonNull
private static Map<String, Integer> getWordOccurrences(@NonNull TweetSearchResponse response) { (1)
Map<String, Integer> wordOccurrences = new HashMap<>(); (2)
response.getData().forEach(tweet ->
Arrays.stream(tweet.getText().split("[^a-zA-Z]+"))
.map(String::toLowerCase)
.filter(w -> w.length() > 3)
.forEach(word -> wordOccurrences.put(word, wordOccurrences.getOrDefault(word, 0) + 1)) (3)
);
return wordOccurrences;
}
@NonNull
private static List<WordFrequency> getTopFrequentWords(@NonNull TweetSearchResponse response,
@NonNull Integer wordsNumber) {
if (response.getData() == null) {
return Collections.emptyList();
}
return getWordOccurrences(response).entrySet().stream()
.sorted(Map.Entry.comparingByValue(TwitterFrequentWordsController::compareIntegersReversed))
.limit(wordsNumber)
.map(v -> new WordFrequency(v.getKey(), v.getValue()))
.collect(Collectors.toList()); (4)
}
private static Integer compareIntegersReversed(Integer v1, Integer v2) {
return v2 - v1;
}
1 | Create a method to calculate the number of occurrences for all the words. |
2 | Create a Map to store the number of occurrences. |
3 | Split the text of tweets on words (we will split by any non-alphabetic character), filter out all words shorter than four characters to remove articles and increment the counter for each word in the Map . |
4 | Sort the words by the number of occurrences, limit the number by certain amount, and convert the result to a list with instances of WordFrequency . |
9. Running the Application
To run the application, use the ./gradlew run
command, which starts the application on port 8080.
You can send a few requests to the path to test the application. We will use cURL for that.
-
The search for
20
most frequent words in tweets about"Ukraine"
returns words representing relevant topics at the time (beginning of 2022):curl -i 'localhost:8080/twitter-frequent-words?words-n=20&search-query=Ukraine'
[ {"word": "ukraine","numberOccurred": 40}, {"word": "https", "numberOccurred": 15}, {"word": "russian","numberOccurred": 12}, {"word": "with", "numberOccurred": 9}, {"word": "russia", "numberOccurred": 9}, {"word": "invasion", "numberOccurred": 7}, {"word": "from", "numberOccurred": 6}, {"word": "india", "numberOccurred": 5}, {"word": "that", "numberOccurred": 5}, {"word": "about", "numberOccurred": 5}, {"word": "border", "numberOccurred": 4}, {"word": "army", "numberOccurred": 4}, {"word": "putin", "numberOccurred": 4}, {"word": "people", "numberOccurred": 4}, {"word": "forces", "numberOccurred": 4}, {"word": "troops", "numberOccurred": 4}, {"word": "their", "numberOccurred": 4}, {"word": "will", "numberOccurred": 4}, {"word": "biden", "numberOccurred": 4}, {"word": "just", "numberOccurred": 3} ]
-
The results of search for
10
most common words in tweets containing the substring"farm"
also seem to be reasonable:curl -i 'localhost:8081/twitter-frequent-words?words-n=10&search-query=farm'
[ {"word": "farm", "numberOccurred": 47}, {"word": "workers", "numberOccurred": 17}, {"word": "https", "numberOccurred": 16}, {"word": "hello", "numberOccurred": 10}, {"word": "animal", "numberOccurred": 9}, {"word": "redvelvet", "numberOccurred": 9}, {"word": "those", "numberOccurred": 7}, {"word": "their", "numberOccurred": 7}, {"word": "temporary", "numberOccurred": 6}, {"word": "revi", "numberOccurred": 6} ]
It is also highly recommended, that you implement tests for all the controllers you write, but we will skip writing tests for our controller in this guide.
10. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
10.1. Native executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
To generate a native executable using Gradle, run:
./gradlew nativeCompile
The native executable is created in build/native/nativeCompile
directory and can be run with build/native/nativeCompile/micronautguide
.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('--verbose') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra arguments to build the native executable |
11. Next Steps
11.1. Learn How to Write OpenAPI Definition and Generate Server Based on It
Read the "Use OpenAPI Definition to Generate a Micronaut Server" Guide to learn how to:
-
understand OpenAPI definition files and write your own definition files,
-
generate server API based on the definitions,
-
implement the functionality of the server based on the API and write comprehensive tests utilizing Micronaut`s features.
11.2. Learn Micronaut
To learn more about Micronaut framework and its features visit Micronaut documentation or read one of the several Micronaut guides.
11.3. Micronaut OpenAPI
-
Use Micronaut OpenAPI module to generate OpenAPI definition documents from controllers with Micronaut annotations.