In my writing a simple MicroProfile application article, I went through the steps in creating a simple meeting coordination application. In this article I’ll take you through how to update that MicroProfile application to add persistence.
At the time this article was written the MicroProfile has not defined a persistence mechanism. This isn’t because MicroProfile doesn’t view persistence as important, but indicates the many equally valid choices that could be made for persistence in microservices. In this article, I use MongoDB but there are other equally valid options like JPA, Cloudant, Cassandra.
The sample application solves a real problem that the Liberty development team had. The globally-distributed Liberty development team has a lot of online meetings using IBM Connections Cloud Meetings. IBM Connections Cloud provides meeting rooms to individual employees, which is a problem for team meetings if the person who initially set up the meeting room can’t make it (e.g. they were called into another meeting, are on vacation, or sick). The sample application provides a single URL for a meeting, which can then be ‘started’ by one person and everyone else gets redirected.
The meeting application is available in GitHub. Each branch in the repository maps to an article. The starting point for the app for this article is branch part1
. By the end of this article, your code should match the code in branch part2
.
This article assumes you are using Eclipse and WebSphere Developer Tools (WDT). To install WDT, download and start Eclipse, then drag and drop
on to the toolbar to start the WDT installer.
Getting the source into Eclipse from GitHub
The files for this application are available in GitHub. To follow this article, you want to clone the repository, check out the part1
branch of the repository, and import the project into Eclipse. You can do this from the command line or directly from Eclipse.
From the command line
If you prefer to clone the Git repository from the command line:
- Run the following commands:
git clone https://github.com/WASdev/sample.microprofile.meetingapp.git cd sample.microprofile.meetingapp git checkout part1
- In Eclipse, import the project as an existing project.
From Eclipse
If you prefer to clone the Git repository from Eclipse:
- In Eclipse, switch to the Git perspective.
- Click Clone a Git repository from the Git Repositories view.
- Enter URI
https://github.com/WASdev/sample.microprofile.meetingapp.git
- Click Next, then click Next again accepting the defaults.
- From the Initial branch drop-down list, click part1.
- Select Import all existing Eclipse projects after clone finishes, then click Finish.
- Switch to the Java EE perspective.
The meetings
project is automatically created in the Project Explorer view.
Installing MongoDB
Depending on what platform you are on the installation instructions may be different. For this exercise you should get the community version of MongoDB from the MongoDB download-center.
Once installed you can run the MongoDB database daemon using:
mongod -dbpath <path to database>
The database needs to be running for the application to work. If it isn’t running there will be a lot of noise in the server logs.
Updating the application to compile against the MongoDB API
To start writing code, the Maven pom.xml
needs to be updated to indicate the dependency on MongoDB:
- Open the
pom.xml
. - On the editor select the Dependencies tab.
- On the dependencies tab there are two sections, one for Dependencies, and the other for Dependency Management. Just to the right of the Dependencies box there is a Add button. Click the Add button.
- Enter a groupdId of
org.mongodb
- Enter a artifactId of
mongo-java-driver
- Enter a version of
2.14.3
- In the scope drop-down, select provided. This will allow the application to compile, but will prevent the Maven WAR packager putting the API in the WAR file. Later the build will be configured to make it available to the server.
- Click OK
- Save the
pom.xml
.
Update MeetingsUtil to convert between MongoDB and JSON-Processing objects
When updating the application, we need to convert between the MongoDB representation of data and the JSON-Processing one. Rather than scatter this around this can be placed in the MeetingsUtil
class:
- Open the
MeetingsUtil
class from meetings > Java Resources > src/main/java > net.wasdev.samples.microProfile.meetings > MeetingsUtil.java - The first method that is needed takes a MongoDB
DBObject
and returns aJsonObject
. At the beginning of the file, after the class definition, add the method declaration:public static JsonObject meetingAsJsonObject(DBObject obj) { // method body will go here }
- This introduces one new class, the
DBObject
class is in thecom.mongodb
package:import com.mongodb.DBObject;
- The first step in creating a new
JsonObject
is to create aJsonObjectBuilder
using theJSON
class. All of these are already used in the class so no new imports will be required. Add the following line to the start of the method:public static JsonObject meetingAsJsonObject(DBObject obj) { JsonObjectBuilder builder = Json.createObjectBuilder();
- The JSON object for a meeting uses a field called
id
. MongoDB uses_id
for the same function, soid
in JSON will map to_id
in MongoDB. To extract theid
field from theDBObject
and add it to the JSON, add this line to the method:builder.add("id", (String) obj.get("_id"));
- The
title
field can be directly mapped from JSON to the MongoDB object. Thetitle
is a String but, to ensure the right add method is called, cast it to a String. Add this line:builder.add("title", (String) obj.get("title"));
- The
duration
field is a Long. As before it needs to be cast to a Long to ensure the rightadd
method is called:builder.add("duration", (Long) obj.get("duration"));
- If a meeting is running it’ll have a
meetingURL
field. If it isn’t running it’ll return null. If there is nomeetingURL
field, we don’t want to add it to the meeting returned so we want to only add ifmeetingURL
is non-null. Add this:String meetingURL = (String) obj.get("meetingURL"); if (meetingURL != null) builder.add("meetingURL", meetingURL);
- Finally we need to return a JsonObject. This can be obtained by calling the build method on the
JsonObjectBuilder
. Add this:return builder.build();
- Next, we need a method that does the opposite, mapping from a
JsonObject
to aDBObject
. This method serves two purposes: It creates a newDBObject
and it merges aJsonObject
to an existingDBObject
, so that it has two parameters rather than one. After the end of the previous method add this:public static DBObject meetingAsMongo(JsonObject json, DBObject mongo) { // method body will go here }
- The first thing to do in this new method is check if the
DBObject
passed in is null. If it is null, a newDBObject
is created.DBObject
is abstract so aBasicDBObject
is what will be instantiated. At the beginning of the method add this (there will be a compile error after this but don’t worry, it’ll be fixed soon):if (mongo == null) { mongo = new BasicDBObject();
- Next, the
id
needs to be moved from theJsonObject
to the newDBObject
. This should only be done when a newDBObject
is created because, otherwise, a disconnect between the URL and theJsonObject
could result in anid
being incorrectly overwritten. Add this:mongo.put("_id", json.getString("id")); }
- This introduced a new class, the
BasicDBObject
which is in thecom.mongodb
package but needs to be imported:import com.mongodb.BasicDBObject;
- The
title
field is a direct mapping from theJsonObject
to theDBObject
but theJsonObject
contains aJsonString
which needs to be converted to aString
. ThetoString
method can’t be used for this because it wraps theString
literal with quotes, which isn’t required here. FortunatelyJsonObject
provides a convenientgetString
method for this:mongo.put("title", json.getString("title"));
- The
duration
field is also a direct mapping but it’s aJsonNumber
in theJsonObject
, which needs to be converted to a Long to go into theDBObject
:mongo.put("duration", ((JsonNumber) json.get("duration")).longValue());
- This introduced the
JsonNumber
, which needs to be imported:import javax.json.JsonNumber;
- We want to get the
meetingURL
but, since it might not be there, you can’t use thegetString
method because it’ll throw aNullPointerException
if there is no field with that name. To get around this, use theget
method, which returns aJsonString
. A null check can then be performed and only if it is non-null will it be added to the JSON. ThegetString
method must be used sincetoString
wraps the string in quotes:JsonString jsonString = json.getJsonString("meetingURL"); if (jsonString != null) { mongo.put("meetingURL", jsonString.getString()); }
- This introduced the
jsonString
, which needs to be imported:import javax.json.JsonString;
- Finally return the mongo object and save the file.
return mongo;
Updating the MeetingManager
The MeetingManager
currently makes use of a ConcurrentMap
to store the meetings. All the code that integrates with this needs to be updated. In this section this is done one step at a time; as a result there will be compililation errors until you get to the end:
- Open the
MeetingManager
class from meetings > Java Resources > src/main/java > net.wasdev.samples.microProfile.meetings > MeetingManager.java. - After the class definition delete the following line from the file:
private ConcurrentMap<String, JsonObject> meetings = new ConcurrentHashMap<>();
- To interact with MongoDB, an instance of
DB
needs to be injected. This can be done using the@Resource
annotation which can identify which resource from JNDI to inject. Add the code where theConcurrentMap
was removed (in the previous step):@Resource(name="mongo/sampledb") private DB meetings;
- This pulls in two new classes. The MongoDB
DB
class and the Java EE@Resource
annotation. These need to be imported:import javax.annotation.Resource; import com.mongodb.DB;
- MongoDB stores entries in a collection in the database. The first thing to do when writing or reading is to select the collection. To simplify code later on, let’s create a convenience method to get the collection:
public DBCollection getColl() { return meetings.getCollection("meetings"); }
- This introduces a new class, the
DBCollection
class, which is in thecom.mongodb
package. This needs to be imported:import com.mongodb.DBCollection;
-
Find and edit the
add
method:- Remove the method body from the
add
method. This will be replaced to update the database.public void add(JsonObject meeting) { // code will be added here }
- First get the collection.
DBCollection coll = getColl();
- The method is given a
JsonObject
and we need aDBObject
for mongoDB, so we need to convert. The API can take an existing entry from the database, so first lets see if we can find something from the database using the findOne method:DBObject existing = coll.findOne(meeting.getString("id"));
- This introduces a new class, the
DBObject
class which is in thecom.mongodb
package. This needs to be imported:import com.mongodb.DBObject;
- Next call the
MeetingsUtil
convenience method to convert fromJsonObject
toDBObject
:DBObject obj = MeetingsUtil.meetingAsMongo(meeting, existing);
- Finally save the new or changed
DBObject
back into the database:coll.save(obj);
- Remove the method body from the
- Find and update the
get
method:- Remove the method body from the
get
method (this will be replaced to fetch from the database):public JsonObject get(String id) { // code will be added here }
- To get a single entry the collection needs to be obtained, an entry found by
id
, and then converted to aJsonObject
using the utility method created earlier. Add the following line to theget
method:return MeetingsUtil.meetingAsJsonObject(getColl().findOne(id));
- Remove the method body from the
- Find and update the
list
method. Thelist
method is slightly more complicated to update. The general structure stays the same but for loop will change. To iterate over entries in a collection aDBCursor
is used and that returns aDBObject
. TheDBObject
then needs to be converted to aJsonObject
. Replace the existing loop that looks like this:for (JsonObject meeting : meetings.values()) { results.add(meeting); }
with this one
for (DBObject meeting : getColl().find()) { results.add(MeetingsUtil.meetingAsJsonObject(meeting)); }
-
Find and update the
startMeeting
method:- This change will radically simplify the code because there is no need to create and merge multiple
JsonObject
s. Instead, you can simply add themeetingURL
to the existingDBObject
. Theid
andurl
will still need to be fetched from theJsonObject
. Remove the following lines from thestartMeeting
method:JsonObject existingMeeting = meetings.get(id); JsonObject updatedMeeting = MeetingsUtil.createJsonFrom(existingMeeting).add("meetingURL", url).build(); meetings.replace(id, existingMeeting, updatedMeeting);
- After the
id
andurl
are fetched, replace the removed code with the following four lines to get a collection, find the meeting entry, set themeetingURL
, and then save it back to the database:DBCollection coll = getColl(); DBObject obj = coll.findOne(id); obj.put("meetingURL", url); coll.save(obj);
- This change will radically simplify the code because there is no need to create and merge multiple
- Save the file.
The MeetingManager is now able to persist to a MongoDB database and back. However, the server still needs to be configured to enable it. This consists of two parts: First, server configuration and, second, getting the server runtime set up.
Updating the Server Configuration
The server configuration is part of the project so first let’s configure that:
- Open the
server.xml
from src > main > liberty > config > server.xml. - There are several lines commented out. These need to be uncommented and then modified. The following lines should have the surrounding comment markers removed:
<featureManager> <feature></feature> </featureManager>
- Between the open and close feature tag add
mongodb-2.0
. It should look like this:<featureManager> <feature>mongodb-2.0</feature> </featureManager>
- A shared library needs to be defined to be used by the application and the runtime for defining the MongoDB resources:
<library id="mongodriver"> <file name="${shared.resource.dir}/mongo-java-driver.jar"/> </library>
- Next the
mongo
needs to be defined. This tells the server where themongo
server instance is running:<mongo id="mongo" libraryRef="mongodriver"> <ports>27017</ports> <hostNames>localhost</hostNames> </mongo>
- Next, define the database:
<mongoDB databaseName="meetings" jndiName="mongo/sampledb" mongoRef="mongo"/>
- Finally, configure the application so it can see the MongoDB classes:
<webApplication location="meetings-${project.version}.war"> <classloader commonLibraryRef="mongodriver"/> </webApplication>
- Save the file.
The next step is to configure the Maven build to make sure all the resources end up in the right place.
Updating the Maven POM
The Maven POM needs to be updated to do a few things: It needs to copy the MongoDB Java driver into the Liberty server, define an additional bootstrap property, copy the application to a different location, and ensure that the mongodb-2.0
feature is installed:
- Open the
pom.xml
in the root of the project. - Select the
pom.xml
tab in the editor.
Copy the MongoDB Java driver
- Search the file for the string
maven-dependency-plugin
you should see this in the file:<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>copy-server-files</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <includeArtifactIds>server-snippet</includeArtifactIds> <prependGroupId>true</prependGroupId> <outputDirectory>${project.build.directory}/wlp/usr/servers/${wlpServerName}/configDropins/defaults</outputDirectory> </configuration> </execution> </executions>
This is copying server snippets from dependencies into the server configuration directory. We are going to add to it instructions to download the
mongo-java-driver
, copy it to theusr/shared/resources
folder, and strip the version off the JAR file name. This last part means we don’t have to remember to update theserver.xml
every time the dependency version is upgraded. -
To add these additional instructions, add the following lines after the
</execution>
closing tag:<execution> <id>copy-mongodb-dependency</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <includeArtifactIds>mongo-java-driver</includeArtifactIds> <outputDirectory>${project.build.directory}/wlp/usr/shared/resources/</outputDirectory> <stripVersion>true</stripVersion> </configuration> </execution>
Place project.version
into bootstrap.properties
The server.xml
references the WAR by artifact name and version. The version is referenced using a variable which needs to be provided to the server. This can easily be done using the bootstrap.properties
:
- Search the
pom.xml
file for the string<bootstrapProperties>
. You should see this in the file:<bootstrapProperties> <default.http.port>${testServerHttpPort}</default.http.port> <default.https.port>${testServerHttpsPort}</default.https.port> </bootstrapProperties>
- Before the closing
</bootstrapProperties>
add the following line:<project.version>${project.version}</project.version>
Copy the application to the apps
folder
The Maven POM deploys the application into the dropins
folder of the Liberty server but this doesn’t allow a shared library to be used. So, instead, the application needs to be copied to the apps
folder:
- Search in the
pom.xml
for the string ‘dropins’, you should see this:<goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/wlp/usr/servers/${wlpServerName}/dropins</outputDirectory>
- Update the word dropins to be apps. It should look like this:
<configuration> <outputDirectory>${project.build.directory}/wlp/usr/servers/${wlpServerName}/apps</outputDirectory>
Install the mongodb-2.0
feature from the Liberty repository
The mongodb-2.0
feature is not in the Liberty server installations that are stored in the Maven repository so it needs to be downloaded from the Liberty repository at build time:
- Search the
pom.xml
for the stringpackage-server
, you should see this:<execution> <id>package-app</id> <phase>package</phase> <goals> <goal>package-server</goal> </goals> </execution> </executions>
- Just before the
</executions>
tag, add the following lines to cause themongodb-2.0
feature to be installed:<execution> <id>install-feature</id> <phase>package</phase> <goals> <goal>install-feature</goal> </goals> <configuration> <features> <acceptLicense>true</acceptLicense> <feature>mongodb-2.0</feature> </features> </configuration> </execution>
- Save the
pom.xml
.
Running the application
There are two ways to get the application running from within WDT:
- The first is to use Maven to build and run the project:
- Run the Maven
install
goal to build and test the project: Right-click pom.xml in themeetings
project, click Run As… > Maven Build…, then in the Goals field typeinstall
and click Run. The first time you run this goal, it might take a few minutes to download the Liberty dependencies. - Run a Maven build for the
liberty:start-server
goal: Right-click pom.xml, click Run As… > Maven Build, then in the Goals field, typeliberty:start-server
and click Run. This starts the server in the background. - Open the application, which is available at
http://localhost:9080/meetings/
. - To stop the server again, run the
liberty:stop-server
build goal.
- Run the Maven
- The second way is to right-click the
meetings
project and select Run As… > Run on Server but there are a few things to note if you do this. WDT doesn’t automatically add the MicroProfile features as you would expect so you need to manually add those. Also, any changes to the configuration insrc/main/liberty/config
won’t be picked up unless you add an include.
Find out more about MicroProfile and WebSphere Liberty.
Related articles
Alasdair takes his simple MicroProfile application and adds WebSockets and CDI Events to notify the client about changes to the meeting. All code is in GitHub.
Continue reading Writing a simple MicroProfile application (4): Using WebSockets and CDI events
Alasdair takes his simple MicroProfile application and adds persistence using MongoDB as the persistence mechanism. All code is in GitHub.
Continue reading Writing a simple MicroProfile application (3): Using Java EE concurrency
Alasdair introduces MicroProfile by explaining how to write a simple application based around the mundane but real-life requirement to book meetings.
Continue reading Writing a simple MicroProfile application (1)