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:
mn --version
| 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 mavenBom
import:
We'll need some configuration properties in our application, so modify /resources/application.yml to add some placeholders for the client-id
, client-secret
and base-url
:
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
Which outputs:
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:
{"username":"ironman","firstName":"Anthony","lastName":"Stark","createdOn":null}
{"username":"ironman","first_name":"Anthony","last_name":"Stark","created_on":null}
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 offset
, limit
, count
, hasMore
and a List
of User
objects.
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 Single
and 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 UnqiueUsernameValidator
:
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 201 Created
):
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
Photo by Andrew Gloor on Unsplash
I've written many blog posts about connecting to an Autonomous DB instance in the past. Best practices evolve as tools, services, and frameworks become...
Email delivery is a critical function of most web applications in the world today. I've managed an email server in the past - and trust me - it's not fun...
In my last post, we looked at the technical aspects of my Brain to the Cloud project including much of the code that was used to collect and analyze the...