In our last post, we exposed our user table with a complete set of REST endpoints that can be used for CRUD operations. The good news is, from this point on, we're done writing SQL for this microservice. From here on out we'll use Micronaut with Java to interface with the endpoints and provide validation for our persistence operations.
If you haven't yet worked with Micronaut, the first thing you'll want to do is install the Micronaut CLI which we can use to scaffold our our application and some additional classes as needed. Simple instructions for installing the CLI, taken from the Micronaut site are as follows:
We'll deviate from their instructions at this point and install a specific version of the CLI:
sdk install micronaut 1.2.0.RC2
Confirm the install with:
| Micronaut Version: 1.2.0.RC2
Now we can create our application with the following CLI command. We're adding the Graal native image feature, but don't worry about that for now.
mn create-app codes.recursive.cnms.ords.user-service-ords --features graal-native-image
This creates our basic application structure and gives us some configuration files, our
build.gradle and our
Application.java which is used to launch our service.
Let's change the
build.gradle file as follows to use the latest build snapshot of Micronaut. We're adding the Sonatype repo and changing the version number in the
We'll need some configuration properties in our application, so modify /resources/application.yml to add some placeholders for the
If we set environment variables before launching our application using underscores, Micronaut will automatically map them to the relevant configuration values. Set some environment variables in your terminal like so (see the last post in this series if you're not sure what values should be substituted here):
Now let's create a configuration class so that our configuration values are properly typed within our application:
Now let's create a controller:
mn create-controller UserController
Let's open up the controller and take a look at it.
Here we have a basic controller that will listen on
/user with a simple endpoint that will return a
200 OK response. We'll enhance it later on, but for now let's move to our model.
For our model, create a new package called
model and create a new class called
User.java in the model package. If you followed along with the Helidon portion of this series, you'll notice that the User model looks similar, but there are a few notable changes here. First, we're going to use some Jackson annotations to map the properties that will be returned from the ORDS service to properties within our model object. Our validation annotations are the same as the last project and will be used to make sure our properties are valid before persistence operations. We also have a property called "
links" that maps to an array of items that is passed back from ORDS, but note that we're annotating that with
@JsonIgnore so it will not be included in our serialized objects. Lastly, note that we have an annotation called @UniqueUsername at the class level. This refers to a custom validator that we'll create later on to make sure that the username is unique. The entire class looks like this:
Note that I've specified a
@JsonAlias on some properties. These are used during deserialization and would allow, for example, a user to be created from either of the following JSON strings:
Note: The underscore version will always be used for serialization when returning as JSON.
We'll also need to model what a paginated result set will look like, so let's create a
PaginatedUserResult object. This object contains properties for
hasMore and a
This is where things start to get pretty cool. Micronaut supports declarative HTTP clients which means we can create a simple interface (or abstract class) that represents our ORDS endpoints and Micronaut will take care of the actual implementation of that client behind the scenes for us. This means our ORDS endpoints can be represented very simply and we can get our microservice up and running with minimal effort (and, as stated earlier, zero SQL). We simply tell Micronaut the base URL for the client and represent each operation with an abstract method stub. Note that we're taking advantage of Micronaut's non-blocking, async HTTP client by returning reactive types like
Maybe from our client methods. The
getToken() method returns a
Map, which means it will operate in a blocking manner. This is by design and we'll discuss that in a bit. Here's how we represent the ORDS endpoints that we created in the last post.
If you remember in our last post we secured our endpoints and each call must include an auth token to avoid authorization failures. We've modeled an endpoint here for
/ords/usersvc/oauth/token that can be used to generate the token, but how can we ensure that each of the other calls include that token (generating a valid one first, if necessary)? Well, we could switch to a concrete HTTP client implementation, but that would mean we can't utilize the declarative client and we'd have to implement each call manually. A better solution would be to use an HTTP client filter that will intercept our calls, generate a token if necessary and set that token into our calls. We need the token before we move forward with the current request, so that's why our
getToken() method in our client returned a
Map. Here's how the filter looks:
Next, let's create our
UniqueUsername annotation and
The validator performs some logic based on whether or not the operation is an insert or update. We inject the client so that we can check for existing users with the given username (in a blocking manner, so we get the result immediately) to say whether or not the current username is valid:
So now that we've got our client configured we can inject it into our controller and create endpoints matching up to each operation. Adding methods for each operation is very simple, we just call our client and return the proper reactive type and Micronaut will take care of the rest for us.
To get a user by ID (
Maybe will return a
404 if no user is returned):
To get all users:
To get all users (with pagination):
To get a user by username:
To save a new user (returns
To update an existing user:
To delete a user (returns
204 No Content if successful, or
404 Not Found):
If you'd like to see verbose output for the HTTP client, you can update your
/resources/logback.xml file to set the log level like so (notice line #16):
We're now at a point where we can compile and test out our microservice. Compile it with
gradle assemble and run the application with
java -jar build/libs/user-service-ords-0.1.jar. Now we can test the endpoints with CURL.
Save (POST) a new user:
Save a new user with invalid data (will return 400 and validation errors):
Update (PUT) an existing user:
Get the new user
List all users (defaults to max 25 records):
List all users (paginated):
Delete a user:
Confirm delete (same GET by ID will return 404):
Get user by username:
At this point, we've got a full blown microservice that performs validation and exposes our ORDS endpoints. In our next post we'll look at deploying this service in a Docker image to Kubernetes as well as a Graal native image for reduced startup times, memory and CPU utilization.
Note: The code for this project is available on GitHub at https://github.com/cloud-native-microservices/user-svc-micronaut-ords