tu-huynh
Tu Huynh a Software Engineer guy
Blog

Hands-on modern Restful API with Jiny

wrote

In this Java tutorial, I’m going to help you understand the process of coding a basic Java SE RESTful application that manages a collection of books with the basic feature: list, insert, update, delete (or CRUD operations - Create, Update, Read and Delete).

Why Jiny?

Learn more com.jinyframework

Enterprise application servers such as Glassfish and JBoss are monolithic and needlessly complex, providing a wealth of configuration options and services that are mostly dormant. More lightweight application servers like Tomcat and Jetty are easier to configure, but still have a learning curve to do so properly.

Jiny was built on Java SE (yes, just Java’s standard lib, no Servlets) thus you only need to add the dependency and write a single line of code to create and start a server. Essentially, there is no implicit magic implemented under-the-hood, which makes it easy to reason about the program’s logic flows.

Requirements

  • Know basic Java, JavaSE 8 APIs (Functional Interface, Double Colon Operator, Tenary Operator)
  • Build tool (Gradle/Maven)
  • SQL

Setup Gradle

If you’re using macOS, just run:

brew install gradle

It’s done. (Here is how to setup Brew if you don’t have it)

If you’re Window user, please follow this tutorial.

When you’re done, make sure you can run gradle -v from your command line.

1

Init Gradle Project

Create an empty directory, then open a command line at that directory.

Run: gradle init to init Gradle Project.

Gradle CLI will ask you “Select type of project to generate”, you have to select 2: application by entering 2, then enter 3 for selecting Java, and then enter 1 for choosing Groovy for DSL.

2

Open your project using IntelliJ_IDEA/NetBean/Eclipse/VSCode/Vim/Emacs or your-favorite-editor, you have your initial build.gradle.

Install dependencies

We will install dependencies that needs for the server development:

  • Jiny: as HTTP Server framework
  • Lombok: for syntax shorthand
  • Google GSON: for JSON decoding and encoding
  • MySQL Driver: to connect the MySQL Server
  • Logback: for logging

Then you need to add to dependencies section in build.gradle:

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.12'
    annotationProcessor 'org.projectlombok:lombok:1.18.12'

    compile group: 'com.jinyframework', name: 'jiny', version: '0.2.6'
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
    compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.49'
    compile group: 'ch.qos.logback', name:'logback-classic', version: '1.0.9'
    compile group: 'ch.qos.logback', name:'logback-core', version: '1.0.9'

    // ... other default dependencies
}

Run “Hello World” server

Modify your main class to:

import com.jinyframework.HttpServer;
import com.jinyframework.core.RequestBinderBase.HttpResponse;
import lombok.val;

import java.io.IOException;

public class App {
    public static void main(String[] args) throws IOException {
        val server = HttpServer.port(1234);
        server.get("/", ctx -> HttpResponse.of("Hello World"));
        server.start();
    }
}

Then run gradle run to start the server on port 1234.

3

You can open the browser and visit http://localhost:1234/ and see “Hello World”:

4

Create MySQL Utils class

The util method below helps us to init a connection to MySQL Server:

package com.tuhuynh.utils;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.sql.Connection;
import java.sql.DriverManager;

@Slf4j
public class MySQLUtils {
    private static Connection connectionInstance = null;

    public static Connection getConnection() {
        if (connectionInstance == null) {
            connectionInstance = initConnection();
        }
        return connectionInstance;
    }

    @SneakyThrows
    public static Connection initConnection() {
        Class.forName("com.mysql.jdbc.Driver").newInstance();
        return DriverManager
                .getConnection("jdbc:mysql://localhost/tutorial?user=root&password=example");
    }
}

Create MySQL database and table

For simplicity, we have only one table. Execute the following MySQL script to create a database named Book:

CREATE DATABASE tutorial;
CREATE TABLE `tutorial`.`book` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255) NOT NULL,
  `author` VARCHAR(255) NOT NULL,
  `price` FLOAT NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `id_UNIQUE` (`id` ASC),
  UNIQUE INDEX `name_UNIQUE` (`name` ASC));

You can use either MySQL Command Line Client or MySQL Workbench tool to create the database.

Create entity class

Then, create a Java class named Book.java (in package entities) to model a book entity in the database with the following code:

package com.tuhuynh.entities;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Book {
    private int id;
    private String name;
    private String author;
    private float price;
}

Coding DAO class

Next, we need to implement a Data Access Layer (DAO) class provides CRUD (Create, Read, Update, Delete) operations for the table book in the database. Here’s the full source code of the BookDAO class:

package com.tuhuynh.daos;

import com.tuhuynh.entities.Book;
import com.tuhuynh.utils.MySQLUtils;
import lombok.Cleanup;
import lombok.SneakyThrows;
import lombok.val;

import java.sql.Connection;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class BookDAO {
    private static final Connection conn = MySQLUtils.getConnection();

    @SneakyThrows
    public static int createBook(final Book book) {
        @Cleanup val stmt = conn.prepareStatement("INSERT INTO book (NAME, AUTHOR, PRICE) VALUES (?,?,?)", Statement.RETURN_GENERATED_KEYS);
        stmt.setString(1, book.getName());
        stmt.setString(2, book.getAuthor());
        stmt.setFloat(3, book.getPrice());
        stmt.execute();

        int generatedId = -1;
        @Cleanup val rs = stmt.getGeneratedKeys();
        if (rs.next()) {
            generatedId = rs.getInt(1);
        }

        return generatedId;
    }

    @SneakyThrows
    public static List<Book> listBook() {
        @Cleanup val stmt = conn.prepareStatement("SELECT * FROM book");
        @Cleanup val rs = stmt.executeQuery();
        val books = new ArrayList<Book>();
        while (rs.next()) {
            val id = rs.getInt("id");
            val name = (String) rs.getObject("name");
            val author = (String) rs.getObject("author");
            val price = rs.getFloat("price");
            val book = new Book(id, name, author, price);
            books.add(book);
        }
        return books;
    }

    @SneakyThrows
    public static Book getBook(final int bookId) {
        @Cleanup val stmt = conn.prepareStatement("SELECT * FROM book WHERE id = ?");
        stmt.setInt(1, bookId);
        @Cleanup val rs = stmt.executeQuery();
        if (rs.next()) {
            val id = rs.getInt("id");
            val name = (String) rs.getObject("name");
            val author = (String) rs.getObject("author");
            val price = rs.getFloat("price");
            return new Book(id, name, author, price);
        }

        return null;
    }

    @SneakyThrows
    public static boolean updateBook(final int id, final Book book) {
        @Cleanup val stmt = conn.prepareStatement("UPDATE book SET name = ?, author = ?, price = ? WHERE id = ?");
        stmt.setString(1, book.getName());
        stmt.setString(2, book.getAuthor());
        stmt.setFloat(3, book.getPrice());
        stmt.setInt(4, id);
        val result = stmt.executeUpdate();
        return result >= 1;
    }

    @SneakyThrows
    public static boolean deleteBook(final int bookId) {
        @Cleanup val stmt = conn.prepareStatement("DELETE FROM book WHERE id = ?");
        stmt.setInt(1, bookId);
        val result = stmt.executeUpdate();
        return result >= 1;
    }
}

Coding handler class

Also, we need to implement HTTP handlers:

package com.tuhuynh.handlers;

import com.google.gson.Gson;
import com.jinyframework.core.RequestBinderBase.Context;
import com.jinyframework.core.RequestBinderBase.HttpResponse;
import com.tuhuynh.daos.BookDAO;
import com.tuhuynh.entities.Book;
import com.tuhuynh.utils.ResponseMessage;
import lombok.val;

public class BookHandler {
    private static final Gson gson = new Gson();

    public static HttpResponse listBook(Context ctx) {
        return HttpResponse.of(BookDAO.listBook());
    }

    public static HttpResponse getBook(Context ctx) {
        val id = Integer.parseInt(ctx.pathParam("id"));
        return HttpResponse.of(BookDAO.getBook(id));
    }

    public static HttpResponse createBook(Context ctx) {
        val body = ctx.getBody();
        val newBook = gson.fromJson(body, Book.class);
        val newBookId = BookDAO.createBook(newBook);
        return newBookId != -1 ? HttpResponse.of(new ResponseMessage("Inserted book id: " + newBookId))
                : HttpResponse.of(new ResponseMessage("Failed to insert book")).status(400);
    }

    public static HttpResponse updateBook(Context ctx) {
        val id = Integer.parseInt(ctx.pathParam("id"));
        val body = ctx.getBody();
        val updatedBook = gson.fromJson(body, Book.class);
        val result = BookDAO.updateBook(id, updatedBook);
        return result ? HttpResponse.of(new ResponseMessage("Updated book id: " + id))
                : HttpResponse.of(new ResponseMessage("Failed to update book id: " + id)).status(400);
    }

    public static HttpResponse deleteBook(Context ctx) {
        val id = Integer.parseInt(ctx.pathParam("id"));
        val result = BookDAO.deleteBook(id);
        return result ? HttpResponse.of(new ResponseMessage("Deleted book id: " + id))
                : HttpResponse.of(new ResponseMessage("Failed to delete book id: " + id)).status(400);
    }
}

Wire all handlers to server

To help the server response JSON, we need to set the transformer by using .useTransformer:

package com.tuhuynh;

import com.google.gson.Gson;
import com.jinyframework.HttpServer;
import com.jinyframework.core.RequestBinderBase.HttpResponse;
import com.tuhuynh.handlers.BookHandler;
import lombok.val;

import java.io.IOException;

public class App {
    public static void main(String[] args) throws IOException {
        val gson = new Gson();
        val server = HttpServer.port(1234)
                .useTransformer(gson::toJson);
        server.get("/", ctx -> HttpResponse.of("Hello World"));
        server.get("/books", BookHandler::listBook);
        server.get("/books/:id", BookHandler::getBook);
        server.post("/books", BookHandler::createBook);
        server.put("/books/:id", BookHandler::updateBook);
        server.delete("/books/:id", BookHandler::deleteBook);
        server.start();
    }
}

5

Build/Deploy

You need to install a plugin to build fat JAR:

plugins {
    // ... other plugins
    id 'com.github.johnrengelman.shadow' version '6.0.0'
}

Then run: gradle build, a fat jar will be built. Finally, you can run your jar file (~2MB) standalone:

(*) Notice that for the sake of simplicity, this naive CRUD Server is missing some functions such as CORS or ConnectionPool

java -jar build/libs/example-all.jar

code

View full source code on Github

Read more Jiny’s docs