When thinking of Java web applications, what comes to mind first? Spring, endless annotations, XML configurations, containers, layers upon layers of abstractions, and boilerplate code. A lot of boilerplate code. Node.js approach seems to be so much simpler, and this is probably one of the reasons newbies prefer Node.js to Java. But what if I told you Java can be that simple too?
To not sound unfounded and make everything illustrative, let’s rewrite a personal blog from this article and make it slightly different. Instead of Node.js for backend, we will write it in Java using DataKernel framework. The other parts of the implementation will stay the same. So, you can easily compare both approaches for the same task.
The final result will still look like this:
Note, the frontend part won’t be described here as we just leave it the same as in the original source, thanks to the author. You can add it later on by yourself.
Let’s nail out the basics. You should probably have some experience with Java to successfully complete the tutorial. Also make sure that you have all the necessary prerequisites installed:
At this point we’re ready to dive into backend implementation. Create the new Maven project in the IDE. Your project structure should look like this:
Then add Maven dependency to your project by configuring your pom.xml
file in the following way:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tutorial</groupId>
<artifactId>blog-tutorial</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.datakernel</groupId>
<artifactId>datakernel-launchers-http</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>io.datakernel</groupId>
<artifactId>datakernel-codec</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
</project>
We are going to set up our server in the src/main/java
folder. Let’s create a BlogLauncher.java class here. It will
incorporate the main logic of our app and take care of application lifecycle.
Instead of middleware chains, DataKernel provides another concept named servlets. You don’t need an Express Router anymore, we will use RoutingServlet to handle all the endpoints. To handle requests independently and preserve modularity we create three async servlets:
So your BlogLauncher should look like this:
public class BlogLauncher extends HttpServerLauncher {
@Provides
@Named("app")
Config config() {
return ofClassPathProperties("blog.properties");
}
@Override
protected Module getBusinessLogicModule() {
return new DbModule();
}
@Provides
Executor executor() {
return newCachedThreadPool();
}
@Provides
@Named("static")
AsyncServlet staticServlet(Executor executor) {
return StaticServlet.ofClassPath(executor, "dist")
.withIndexHtml();
}
@Provides
@Named("failed")
AsyncServlet failServlet() {
return $ -> notFound404();
}
@Provides
public AsyncServlet rootServlet(@Named("failed") AsyncServlet failServlet,
@Named("static") AsyncServlet staticServlet,
ArticleSchemaDao articleDao) {
return mapException(Objects::nonNull, failServlet)
.serve(RoutingServlet.create()
.map("/*", staticServlet)
.map("/api/articles/*", RoutingServlet.create()
.map(GET, "/", $ -> ok200()
.withJson(ofList(ARTICLE_CODEC_FULL),
articleDao.findAll()
.stream()
.sorted(reverseOrder())
.collect(Collectors.toList())))
.map(POST, "/", loadBody().serve(request -> {
try {
return ok200()
.withJson(ARTICLE_CODEC_FULL,
articleDao.save(fromJson(ARTICLE_CODEC, request.getBody().getString(UTF_8))));
} catch (ParseException e) {
return ofCode(400);
}
}))
.map(GET, "/:id", request -> {
ArticleSchema foundArticle = articleDao.findById(request.getPathParameter("id"));
return foundArticle == null ?
ofCode(404) :
ok200().withJson(ARTICLE_CODEC_FULL, foundArticle);
})
.map(DELETE, "/:id", request -> {
articleDao.delete(request.getPathParameter("id"));
return ok200();
})
.map(PATCH, "/:id", loadBody().serve(request -> {
try {
ArticleSchema updatedArticle = articleDao.update(request.getPathParameter("id"),
fromJson(ARTICLE_CODEC, request.getBody().getString(UTF_8)));
return updatedArticle == null ?
ofCode(400) :
ok200().withJson(ARTICLE_CODEC_FULL, updatedArticle);
} catch (ParseException e) {
return ofCode(400);
}
}))));
}
public static void main(String[] args) throws Exception {
HttpServerLauncher launcher = new BlogLauncher();
launcher.launch(args);
}
}
Looking at the above code, you can note:
@Provides
annotation and all the required components will be created automatically.GET
processing, Promise.of() will return a successfully created
Promise with the given content.To make this example look similar to the application from the Node.js tutorial we will use MongoDB too, but with the Java
client.
Let’s add some new classes, so the structure of src/main/java
directory will eventually look like this:
Add a blog resource bundle in the resources directory with the following content:
mongo.url=mongodb://localhost:27017
mongo.database=blog-database
In the DbModule.java we will provide MongoDB configurations:
public class DbModule extends AbstractModule {
@Provides
MongoClient mongoClient(MongoClientSettings settings) {
return MongoClients.create(settings);
}
@Provides
MongoClientSettings settings(@Named("app") Config config, CodecRegistry codecRegistry) {
return MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(config.get("mongo.url")))
.codecRegistry(codecRegistry)
.build();
}
@Provides
CodecRegistry codecRegistry() {
return fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
fromProviders(PojoCodecProvider.builder().automatic(true).build()));
}
@Provides
MongoDatabase database(MongoClient mongoClient, @Named("app") Config config) {
return mongoClient.getDatabase(config.get("mongo.database"));
}
@Provides
MongoCollection<ArticleSchemaDao.ArticleSchema> articlesCollection(MongoDatabase database) {
return database.getCollection("articles", ArticleSchemaDao.ArticleSchema.class);
}
@Provides
@Export
ArticleSchemaDao schemaDaoDb(MongoCollection<ArticleSchemaDao.ArticleSchema> collection) {
return new ArticleSchemaDaoDb(collection);
}
}
Note, schemaDaoDb() is marked with @Export
annotation which allows us to explicitly say which instances we want to be
exported from the module. ArticleSchema, ArticleSchemaDao and ArticleSchemaDaoDb that are used here will be shown next.
In ArticleSchemaDao.java we will simply create an interface which will be implemented in the ArticleSchemaDaoDb, and also the ArticleSchema - a plain java class with fields, constructors, and accessors.
ArticleSchemaDao:
public interface ArticleSchemaDao {
ArticleSchema save(ArticleSchema articleSchema);
List<ArticleSchema> findAll();
@Nullable
ArticleSchema findById(String id);
void delete(String id);
@Nullable
ArticleSchema update(String id, ArticleSchema articleSchema);
@SuppressWarnings({"unused"})
final class ArticleSchema implements Comparable<ArticleSchema> {
private ObjectId id;
private String title;
private String body;
private String author;
private Date createdAt;
private Date updatedAt;
public ArticleSchema() {
}
public ArticleSchema(String title, String body, String author) {
long now = System.currentTimeMillis();
this.id = new ObjectId();
this.title = title;
this.body = body;
this.author = author;
this.createdAt = new Date(now);
this.updatedAt = new Date(now);
}
public ArticleSchema(ObjectId id, String title, String body, String author, Date createdAt, Date updatedAt) {
this.id = id;
this.title = title;
this.body = body;
this.author = author;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public ObjectId getId() {
return id;
}
public ArticleSchema setId(ObjectId id) {
this.id = id;
return this;
}
public String getTitle() {
return title;
}
public ArticleSchema setTitle(String title) {
this.title = title;
return this;
}
public String getBody() {
return body;
}
public ArticleSchema setBody(String body) {
this.body = body;
return this;
}
public String getAuthor() {
return author;
}
public ArticleSchema setAuthor(String author) {
this.author = author;
return this;
}
public Date getCreatedAt() {
return createdAt;
}
public ArticleSchema setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
return this;
}
public Date getUpdatedAt() {
return updatedAt;
}
public ArticleSchema setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
return this;
}
public void update() {
updatedAt.setTime(System.currentTimeMillis());
}
@Override
public int compareTo(@NotNull ArticleSchema articleSchema) {
return createdAt.getTime() > articleSchema.createdAt.getTime() ? 1 : -1;
}
}
}
ArticleSchemaDaoDb:
public class ArticleSchemaDaoDb implements ArticleSchemaDao {
private final MongoCollection<ArticleSchema> collection;
public ArticleSchemaDaoDb(MongoCollection<ArticleSchema> collection) {
this.collection = collection;
}
@Override
public ArticleSchema save(ArticleSchema articleSchema) {
collection.insertOne(articleSchema);
return articleSchema;
}
@Override
public List<ArticleSchema> findAll() {
ArrayList<ArticleSchema> articles = new ArrayList<>();
collection.find().forEach((Consumer<? super ArticleSchema>) articles::add);
return articles;
}
@Nullable
@Override
public ArticleSchema findById(String id) {
return collection.find(new BasicDBObject("_id", new ObjectId(id))).first();
}
@Override
public void delete(String id) {
collection.deleteOne(new BasicDBObject("_id", new ObjectId(id)));
}
@Nullable
@Override
public ArticleSchema update(String id, ArticleSchema articleSchema) {
collection.updateOne(new BasicDBObject("_id", new ObjectId(id)),
new BasicDBObject("$set", new BasicDBObject()
.append("title", articleSchema.getTitle())
.append("body", articleSchema.getBody())
.append("author", articleSchema.getAuthor())
.append("updatedAt", new Date())));
return findById(id);
}
}
Another important point is that DataKernel provides a Codec module which encodes and decodes custom objects. That’s quite important in our example since we work with JSON and want to encode/decode ArticleSchema objects to/from JSONs. For this purpose, let’s write down the following code in the ArticleCodecs:
@SuppressWarnings("WeakerAccess")
public final class ArticleCodecs {
public static final StructuredCodec<ArticleSchema> ARTICLE_CODEC = object(ArticleSchema::new,
"title", ArticleSchema::getTitle, STRING_CODEC,
"body", ArticleSchema::getBody, STRING_CODEC,
"author", ArticleSchema::getAuthor, STRING_CODEC);
private static final StructuredCodec<Date> DATE_CODEC = LONG_CODEC.transform(Date::new, Date::getTime);
private static final StructuredCodec<ObjectId> OBJECT_ID_STRUCTURED_CODEC = STRING_CODEC.transform(ObjectId::new, ObjectId::toHexString);
public static final StructuredCodec<ArticleSchema> ARTICLE_CODEC_FULL = object(ArticleSchema::new,
"_id", ArticleSchema::getId, OBJECT_ID_STRUCTURED_CODEC,
"title", ArticleSchema::getTitle, STRING_CODEC,
"body", ArticleSchema::getBody, STRING_CODEC,
"author", ArticleSchema::getAuthor, STRING_CODEC,
"createdAt", ArticleSchema::getCreatedAt, DATE_CODEC,
"updatedAt", ArticleSchema::getUpdatedAt, DATE_CODEC)
.transform(value -> value.setTitle("article"), articleSchema -> articleSchema);
}
All we need is to add the frontend part and that’s it! Create a new client directory alongside src directory and
fill it with this content. Don’t forget to install
dependencies with npm i
in the client directory. It should look like this:
Now let’s test the application.
sudo service mongod start
npm run build
from the client directory.src/main
.Congratulations, you’ve just created a Java backend for the personal blog app. It wasn’t that hard, right?
To see how you can use DataKernel, take a look at DataKernel tutorials or docs. Thank you for reading this tutorial and if you have faced any problems, take into account the full example sources on GitHub.