Quick Tip: Records With MicroProfile Rest Client
Mon Nov 11 15:42:47 EST 2024
One of my favorite features of the XPages JEE Support project is the inclusion of the MicroProfile Rest Client. This framework makes consuming remote APIs (usually JSON, but not always) much, much better than in traditional XPages or even in most other frameworks.
MicroProfile Rest Client
If you're not familiar with it, the way it works is that you can use Jakarta REST annotations to describe the remote API. For example, say you want to connect to a vendor's service at "https://api.example.com/v1/users", which allows for GET requests that list existing users and POST requests to create new ones. With the MP Rest Client, you'd create an interface like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package com.example.api; import java.util.List; import com.example.api.model.User; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("v1") public interface ExampleApi { @Path("users") @GET @Produces(MediaType.APPLICATION_JSON) List<User> getUsers(); @Path("users") @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) User createUser(User newUser); } |
Then, in your Java code, you can use it like this (among other potential ways):
1 2 3 4 5 6 7 8 9 10 | ExampleApi api = RestClientBuilder.newBuilder() .baseUri(URI.create("https://api.example.com")) .build(ExamplApi.class); List<User> existingUsers = api.getUsers(); // ... User foo = makeSomeUser(); api.createUser(foo); |
The MP Rest Client code takes care of all the actual plumbing of turning that interface into a usable REST client, translating between JSON and custom classes (like User
there), and other housekeeping work. Things can get much more complicated (authorization, interceptors, etc.), but that will do for now.
Records
What has been a nice addition with Domino 14 and recent releases of the XPages JEE project is that now I can also use Java records with these interfaces. Records were introduced in Java 14 and are meant for a couple of reasons - for our needs, they cut down on boilerplate code, but they also have beneficial implications with multithreading.
In our example above, we might define the User
class as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package com.example.api.model; import jakarta.json.bind.annotation.JsonbProperty; public class User { private String username; @JsonbProperty("first_name") private String firstName; @JsonbProperty("last_name") private String lastName; public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } |
This is a standard old Java bean class, and it's fine. Ideally, we'd add more to it - equals(...)
, hashCode()
, and toString()
in particular - and this version only has three fields. It'd keep growing dramatically the more we add: more fields, bean validation annotations, and so forth.
For cases like this, particularly where you likely won't be writing constructor code much (unless they add derivation syntax), records are perfect. In this case, we could translate the class to:
1 2 3 4 5 6 7 8 9 10 11 12 | package com.example.api.model; import jakarta.json.bind.annotation.JsonbProperty; public record User( String username, @JsonbProperty("first_name") String firstName, @JsonbProperty("last_name") String lastName ) { } |
This is dramatically smaller and will scale much better, lines-of-code-wise. And, since the important parts of the stack support it, it's nearly a drop-in replacement: JSON conversion knows about it, so it works in the MP Rest Client and in your own REST services, and Expression Language can bind to it, so it's usable in Pages and elsewhere.
Since records are read-only, they're not a universal replacement for bean-format classes (in particular, beans used in XPages forms), but they're extremely convenient when they fit and they're another good tool to have in your toolbelt.