tu-huynh
tuhuynh
.com
$
Blog

Hands-on modern Restful API with Jiny

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, Ternary Operator)
  • Build tool (Gradle/Maven)
  • SQL

Setup Gradle

If you’re using macOS, with [Brew](https://brew.sh/ just run:

brew install gradle

If you’re on Windows, with choco simple run:

choco install gradle

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 evaluate 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