In this blog post, we will embark on an exciting journey of building a full-fledged Spring Boot Monolith application implementing CRUD (Create, Read, Update, Delete) operations on a Person
entity. We’ll leverage JPA
for data persistence, Swagger
for API documentation, Postgres
as our database, and Vue.js
3 for the front-end. All of this will be achieved using the simplicity of JBang
in a single Java file!
You might also want to check out our previous article on How to Build a Spring Boot Rest Api with JBang in a Single Java File. In that post, we showed you how to use JBang to create a simple Rest web service that exposes a “Hello, World!” endpoint. We also explained how to use JBang features such as dependencies and scripts to simplify the development and execution of your Java application.
- Prerequisites
- Getting Started
- Run the Application
- Access the Application
- Access the Application Rest Api
- Details About the Implementation
- Conclusion
Prerequisites
Before we dive into the development process, ensure you have:
- read the previous article How to Build a Spring Boot Rest Api with JBang in a Single Java File
- JBang installed on your system. You can install it from JBang’s official website.
- Docker and Docker Compose installed for setting up the Postgres database.
You can clone the https://github.com/dmakariev/examples
repository.
git clone https://github.com/dmakariev/examples.git
cd examples/jbang/spring-boot-jpa-vue
Getting Started
Let’s create the files for the Spring Boot Monolith. Follow these steps:
Initialize a New Directory
Create a new directory for your project and navigate to it using your terminal. Then, create :
- an empty JBang script file with a
.java
extension, e.g.,springbootJpaVue.java
. - an empty file with
.html
extension for the Vue.js UI app, e.g.,index-fetch.html
. - an empty
Dockerfile
- an empty Docker Compose file
compose.yaml
$ mkdir spring-boot-jpa-vue
$ cd spring-boot-jpa-vue
$ touch springbootJpaVue.java
$ touch index-fetch.html
$ touch Dockerfile
$ touch compose.yaml
Write the Spring Boot Code
Open the springbootJpaVue.java
file in your favorite text editor or integrated development environment (IDE) and add the following code.
//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 22
//DEPS org.springframework.boot:spring-boot-dependencies:3.2.4@pom
//DEPS org.springframework.boot:spring-boot-starter-web
//DEPS org.springframework.boot:spring-boot-starter-data-jpa
//DEPS org.springframework.boot:spring-boot-starter-actuator
//DEPS com.h2database:h2
//DEPS org.postgresql:postgresql
//DEPS org.projectlombok:lombok
//DEPS org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0
//JAVA_OPTIONS -Dserver.port=8080
//JAVA_OPTIONS -Dspring.datasource.url=jdbc:h2:mem:person-db;MODE=PostgreSQL;
//JAVA_OPTIONS -Dspring.h2.console.enabled=true -Dspring.h2.console.settings.web-allow-others=true
//JAVA_OPTIONS -Dmanagement.endpoints.web.exposure.include=health,env,loggers
//FILES META-INF/resources/index.html=index-fetch.html
//REPOS mavencentral,sb_snapshot=https://repo.spring.io/snapshot,sb_milestone=https://repo.spring.io/milestone
package com.makariev.examples.jbang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Optional;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@SpringBootApplication
public class springbootJpaVue {
public static void main(String[] args) {
SpringApplication.run(springbootJpaVue.class, args);
}
}
@Component
@RequiredArgsConstructor
class InitialRecords {
private final PersonRepository personRepository;
@EventListener(ApplicationReadyEvent.class)
public void exercise() {
if (personRepository.count() > 0) {
return;
}
List.of(
new Person(1L, "Ada", "Lovelace", 1815),
new Person(2L, "Niklaus", "Wirth", 1934),
new Person(3L, "Donald", "Knuth", 1938),
new Person(4L, "Edsger", "Dijkstra", 1930),
new Person(5L, "Grace", "Hopper", 1906),
new Person(6L, "John", "Backus", 1924)
).forEach(personRepository::save);
}
}
@RestController
class HiController {
@GetMapping("/hi")
public String sayHi(@RequestParam(required = false, defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
}
@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
class PersonController {
private final PersonRepository personRepository;
@GetMapping
public Page<Person> findAll(Pageable pageable) {
return personRepository.findAll(pageable);
}
@GetMapping("{id}")
public Optional<Person> findById(@PathVariable("id") Long id) {
return personRepository.findById(id);
}
@PostMapping
public Person create(@RequestBody Person person) {
return personRepository.save(person);
}
@PutMapping("{id}")
public Person updateById(@PathVariable("id") Long id, @RequestBody Person person) {
var loaded = personRepository.findById(id).orElseThrow();
loaded.setFirstName(person.getFirstName());
loaded.setLastName(person.getLastName());
loaded.setBirthYear(person.getBirthYear());
return personRepository.save(loaded);
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
personRepository.deleteById(id);
}
}
@Data
@Entity
@Table(name = "person")
@NoArgsConstructor
@AllArgsConstructor
class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private int birthYear;
}
interface PersonRepository extends JpaRepository<Person, Long> {
}
Write the Vue.js Code
Open the index-fetch.html
file in your favorite text editor or integrated development environment (IDE) and add the following code.
Write the Dockerfile
Open the Dockerfile
file in your favorite text editor and add the following code.
FROM public.ecr.aws/docker/library/amazoncorretto:21-alpine AS build
RUN apk --no-cache add bash
RUN apk --no-cache add curl
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN curl -Ls https://sh.jbang.dev | bash -s - export portable springbootJpaVue.java
FROM public.ecr.aws/docker/library/amazoncorretto:21-alpine
RUN mkdir /app/
RUN mkdir /app/lib
COPY --from=build /app/springbootJpaVue.jar /app/springbootJpaVue.jar
COPY --from=build /app/lib/* /app/lib/
WORKDIR /app
ENTRYPOINT ["java","-jar","springbootJpaVue.jar"]
Write the Docker Compose file
Open the compose.yaml
file in your favorite text editor and add the following code.
services:
backend:
build: .
ports:
- 8080:8088
environment:
- SERVER_PORT=8088
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/example
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=pass-example
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
networks:
- spring-postgres
db:
image: postgres
restart: always
volumes:
- db-data:/var/lib/postgresql/data
networks:
- spring-postgres
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD=pass-example
expose:
- 5432
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: admin_not_used@user.com
PGADMIN_DEFAULT_PASSWORD: admin_not_used
PGADMIN_CONFIG_SERVER_MODE: 'False'
volumes:
- pgadmin:/var/lib/pgadmin
ports:
- "5050:80"
networks:
- spring-postgres
restart: always
volumes:
db-data:
pgadmin:
networks:
spring-postgres:
Run the Application
We have created the Spring Boot Monolith application. It consists of exactly two source files and two configuration files for docker.
springbootJpaVue.java
is the backend, implemented as Spring Boot Java application, it contains also some default valuesindex-fetch.html
is the frontend, implemented with Vue.js as standalone script
The way the two files are related is with this JBang directive
//FILES META-INF/resources/index.html=index-fetch.html
The application has a single jpa entity Person
that could be stored in a database.
Return to your terminal. Navigate to the directory containing your springbootJpaVue.java
The application could be configured to run with one of two databases
- H2 Database in memory
$ jbang -Dspring.datasource.url=jdbc:h2:mem:person-db \ springbootJpaVue.java
- H2 Database filesystem - database data is stored in file
$ jbang -Dspring.datasource.url=jdbc:h2:file:./person-db-data \ -Dspring.jpa.hibernate.ddl-auto=update \ springbootJpaVue.java
- Postgres, it needs localhost instance of Postgres
$ jbang -Dspring.datasource.url=jdbc:postgresql://localhost:5432/example \ -Dspring.datasource.username=postgres \ -Dspring.datasource.password=postgres \ -Dspring.jpa.hibernate.ddl-auto=update springbootJpaVue.java
to run it with default settings file and execute any the following commands:
$ jbang springbootJpaVue.java
$ sh springbootJpaVue.java
if you allow executable permissions for springbootJpaVue.java
by executing
$ chmod +x springbootJpaVue.java
you could even execute the application like that
$ ./springbootJpaVue.java
you could build a fatJar
$ jbang export fatjar springbootJpaVue.java
and then run it as
$ jbang springbootJpaVue-fatjar.jar
or like normal java application
$ java -jar springbootJpaVue-fatjar.jar
you could create a portable
jar file with ./lib
folder containing all dependencies
$ jbang export portable springbootJpaVue.java
and then run it as
$ jbang springbootJpaVue.jar
or like normal java application
$ java -jar springbootJpaVue.jar
docker compose
$ docker compose up
In all of the cases above, JBang will download the required Spring Boot dependencies and start the application. You will see output indicating that the Spring Boot application is running.
Access the Application
Web-based User Interface Built with Vue.JS
You could access the UI at http://localhost:8080/
H2 SQL Console Application
You could access the SQL database using a browser interface at http://localhost:8080/h2-console
OpenAPI Definition
You could access it at http://localhost:8080/v3/api-docs
Swagger UI
You could access it at http://localhost:8080/swagger-ui/index.html
Spring Boot Actuator Endpoints
You could access it at http://localhost:8080/actuator
Web Version of PgAdmin
When executed with docker compose, the application provides access to web version of PgAdmin, that lets you access a SQL database using a browser interface.
You could access it at http://localhost:5050/
Access the Application Rest Api
In the Web Browser
Open your web browser and navigate to http://localhost:8080/hi
. You should see the “Hello, World!” message displayed in your browser.
Or if you prefer more personalized message, then navigate to http://localhost:8080/hi?name=Joe
. You should see the “Hello, Joe!” message displayed in your browser.
In the Terminal/CLI with Curl
To create a new person, use the POST method with the person data as a JSON body:
$ curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1919}' \
http://localhost:8080/api/persons
To get a list of all persons, use the GET method:
$ curl -X GET http://localhost:8080/api/persons
To get a specific person by id, use the GET method with the id as a path variable:
$ curl -X GET http://localhost:8080/api/persons/1
To update an existing person by id, use the PUT method with the person data as a JSON body:
$ curl -X PUT -H "Content-Type: application/json" \
-d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1918}' \
http://localhost:8080/api/persons/1
In the Terminal/CLI with HTTPIE
you could download alternative Terminal/CLI client from here https://httpie.io/cli
To create a new person, use the POST method with the person data as a JSON body:
$ http POST http://localhost:8080/api/persons firstName=Alice lastName=Smith birthYear=1996
To get a list of all persons, use the GET method:
$ http GET http://localhost:8080/api/persons
To get a specific person by id, use the GET method with the id as a path variable:
$ http GET http://localhost:8080/api/persons/1
To update an existing person by id, use the PUT method with the person data as a JSON body:
$ http PUT http://localhost:8080/api/persons/1 firstName=Bob lastName=Jones birthYear=1990
To delete an existing person by id, use the DELETE method with the id as a path variable:
$ http DELETE http://localhost:8080/api/persons/1
Details About the Implementation
Spring Data Jpa Dependencies
To enable JPA, which is the Java/Jakarta Persistence API, we need
//DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4
we also need a database, so we will add dependency for H2 Database the section becomes
//DEPS org.springframework.boot:spring-boot-starter-web:3.1.4
//DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4
//DEPS com.h2database:h2:2.2.224
to minimize the boilerplate code, we’ll add also Lombok
//DEPS org.springframework.boot:spring-boot-starter-web:3.1.4
//DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4
//DEPS com.h2database:h2:2.2.224
//DEPS org.projectlombok:lombok:1.18.30
JBang supports importing of .pom
files,
let’s change the dependencies to
//DEPS org.springframework.boot:spring-boot-dependencies:3.1.4@pom
//DEPS org.springframework.boot:spring-boot-starter-web
//DEPS org.springframework.boot:spring-boot-starter-data-jpa
//DEPS com.h2database:h2
//DEPS org.projectlombok:lombok
as you can see the dependency versions are removed, the Spring Boot version is defined only once.
Persistence : Person
Entity and Repository
This is the JPA entity and the data repository
@Data
@Entity
@Table(name = "person")
@NoArgsConstructor
@AllArgsConstructor
class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private int birthYear;
}
interface PersonRepository extends JpaRepository<Person, Long> {
}
Rest Api: PersonController
This is the rest controller
@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
class PersonController {
private final PersonRepository personRepository;
@GetMapping
public Page<Person> findAll(Pageable pageable) {
return personRepository.findAll(pageable);
}
@GetMapping("{id}")
public Optional<Person> findById(@PathVariable("id") Long id) {
return personRepository.findById(id);
}
@PostMapping
public Person create(@RequestBody Person person) {
return personRepository.save(person);
}
@PutMapping("{id}")
public Person updateById(@PathVariable("id") Long id, @RequestBody Person person) {
var loaded = personRepository.findById(id).orElseThrow();
loaded.setFirstName(person.getFirstName());
loaded.setLastName(person.getLastName());
loaded.setBirthYear(person.getBirthYear());
return personRepository.save(loaded);
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
personRepository.deleteById(id);
}
}
OpenAPI Support and Enable Swagger UI
We are using the springdoc
project. To enable it, all we have to do is add the following dependency
//DEPS org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0
after restarting the application, you are going to get the Swagger UI at the following URL http://localhost:8080/swagger-ui/index.html
Enable H2 Console Application
The H2 Console Application lets you access a SQL database using a browser interface. To activate it we need to add the following configuration right after the dependency section
//JAVA_OPTIONS -Dspring.h2.console.enabled=true
//JAVA_OPTIONS -Dspring.h2.console.settings.web-allow-others=true
//JAVA_OPTIONS -Dspring.datasource.url=jdbc:h2:mem:person-db;MODE=PostgreSQL;
Conclusion
In this blog post, we demonstrated how to create a Spring Boot Monolith using just a single Java file for the backend and a single HTML file for the frontend and JBang
. This approach can be handy for quick prototyping, lightweight applications, or when you want to reduce the complexity of your development environment. As your application grows in complexity, you can always transition to a more traditional project structure. JBang
provides a flexible and efficient way to develop Java applications without the need for heavyweight project setups.
Explore further and build even more sophisticated Spring Boot
applications using JBang
.
Happy coding!