Hands-on modern Restful API with Jiny
Hands-on modern Restful API with Jiny
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?
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.
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.
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
.
You can open the browser and evaluate http://localhost:1234/
and see “Hello World”:
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();
}
}
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