[JAVA] Create a web app that is just right for learning [Spring Boot + Thymeleaf + PostgreSQL]

1. Target audience

・ Learning Java / Spring Boot / Thymeleaf ・ I want to create a simple web application for practice

2. Goal

Create a simple web app that allows you to manage customers (register / update / delete / list). Certification / authorization / unit testing is omitted because it will be a lot. However, it may be better to add unit tests if you have time.

The completed image is as follows. app.gif

3. Finished product

If you want to see the finished product first, please.

The source is here.

The one that works is here.

4. Assumptions

It is assumed that the development environment and PostgreSQL have been installed. If you have not installed it, please follow the link below.

5. Overall configuration

The overall configuration of the web application created this time is as follows. overview.png

5.1 Technology used

Roughly speaking ...

Spring Boot
A Java framework that makes it easy to create web applications.
Thymeleaf
Template engine that can create screens (HTML + JS + CSS).
Bootstrap
CSS framework. Used for design.
Gradle
Build tool.
PostgreSQL
Open source database.

5.2 Configuration

Model
A class of roles for manipulating DB data. There are the following two types.

(1) Entity class
Class corresponding to DB table definition.
Basically, class name = table name and field name = column name correspond.

(2) Repository class
A class that has methods to refer / register / update / delete DB.

View
The screen to be displayed to the user.
Specifically, it is an HTML template defined in Thymeleaf.
Model data is flowed by Controller.

Controller
A class of roles that receives input from the user and returns the next screen.
Specifically, instruct Model to refer to / update DB in response to HTTP request, and
Pass the data to View and create the HTTP response for the next screen.

Configuration file
Holds settings such as DB connection information.

External libraries
Various libraries such as Spring Boot / JPA / JDBC.

Build configuration file (build.gradle)
Holds the settings of the external library to be used.

6. Functional design

Since development is the main theme, functional design is easy to write.

6.1 Screen design

Screen list

No screen name Description
1 the top screen The screen that serves as the entrance to each screen.
2 New registration screen A screen for newly registering customer information.
3 Update screen A screen to change customer information.
4 List screen A screen to list and delete customer information.

Screen layout / screen transition

As per [2. Goal](# 2-Goal).

Screen item definition / interaction

I think you can understand it, so I omitted it.

6.2 URL design

No URL HTTP method Description
1 / GET the top screen
2 /customers GET List screen
3 /customers/create GET New registration screen
4 /customers/create POST New registration execution
5 /customers/{id}/update GET Update screen
6 /customers/{id}/update POST Update execution
7 /customers/{id}/delete GET Delete execution

Designed with reference to here.

6.3 DB design

Table list

No table name Description
1 customer Manage customer information.

Table definition (customer)

No Column name PK Non-null Description
1 id Customer ID. Automatic numbering in sequence.
2 name Customer name.
  • The sequence name is "customer_id_seq"
  • If you want to increase the number of rows, you can easily increase it later. I made it simple so that it wouldn't be too much.

7. Development

Now let's move on to the development of the main subject.

7.0 Overall flow

Download the template from the Spring official website and develop it. The table is automatically generated from Model by the function of Spring JPA. flow.png

7.1 Template download

flow1.png

Create a template for your web app project. It's easy to download from Spring Initializr. You can download it by entering the following and pressing the "Generate" button.

spring01.PNG

7.1.1 Explanation of input contents

Project
Select
Gradle. If you like Maven, you can use Maven.
Group / Artifact / Name
The project name and package name. It's a practice, so it's OK.
Dependencies
Specify the library to use.

7.1.2 Description of Dependencies

Set the external libraries to be used.

Spring Boot DevTools
A library for improving development efficiency.
Every time the source is changed, it will be automatically built and restarted to reflect the changes.

lombok
A library for improving development efficiency.
You can clearly describe standard methods such as getter / setter / equals with annotations.

Spring Configuration Processor
A library for improving development efficiency.
Code completion can be performed in the configuration file (application.yml), and description mistakes can be suppressed.

Spring Web
A library for web apps / web APIs.

Thymeleaf
HTML template engine. As mentioned above, it is the role of View.

Spring Data JPA
DB related library. You can perform JPA-based DB operations.
To explain JPA roughly, it is a mechanism that allows you to perform simple DB operations without writing SQL.

PostgreSQL Driver
JDBC driver for PostgreSQL. Without this, PostgreSQL cannot be used.

7.1.3 Supplement

You can do the same from the Eclipse menu. Specifically, they are "File", "New", "Other", "Spring Boot", and "Spring Starter Project".

7.2 build

flow2.png

Import the downloaded template into Eclipse. The specific procedure is as follows.

  1. Unzip the downloaded template (zip) to the folder containing the Eclipse workspace.
  2. Start Eclipse.
  3. "File" "Import" "Gradle" "Existing Gradle project" "Next"
  4. Specify 1 folder in "Project Root Directory" and click "Finish".

When imported, it will be built automatically, as per the dependencies in build.gradle, External libraries will be downloaded.

7.2.1 Addition of external library

Some external libraries are not included in Spring Initializr. Specifically, the bootstrap java library is not included. Open the build.gradle file in Eclipse and add as follows.

build.gradle


dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.webjars:jquery:3.3.1'        //add to
	implementation 'org.webjars:bootstrap:4.3.1'     //add to
	implementation 'org.webjars:font-awesome:5.13.0' //add to
//abridgement. The full version is https://github.com/tk230to/tksystem

[Supplement] Description of the added external library

org.webjars:jquery
jQuery java library.
org.webjars:bootstrap
Bootstrap library for java.
org.webjars:font-awesome
Font Awesome (using icons) java library.

[Supplement] How to find an external library

Look for these libraries in the Maven Repository (https://mvnrepository.com/).

7.3 Creating each class

flow3.png

Create each class and configuration file.

7.3.1 Creation target

Create a Model / View / Controller / configuration file. What was this file? In that case, please read [5.2 Configuration](# 52-Configuration) again.

7.3.2 Model (Entity class) creation

Customer class

Create a class corresponding to [customer table](# 63-db design). Class name = table name, field name = column name.

Customer.java


package com.example.tksystem.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.validation.constraints.NotBlank;

import lombok.Data;

/**
 *Customer class.
 */
@Entity
@Data
public class Customer {

  /**Sequence name*/
  private static final String SEQUENCE_NAME = "customer_id_seq";

  /** ID */
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE_NAME)
  @SequenceGenerator(name = SEQUENCE_NAME, sequenceName = SEQUENCE_NAME, allocationSize = 1)
  private Long id;

  /**name*/
  @NotBlank
  private String name;
}
@Entity
Annotation indicating that it is an Entity class.
@Data
lombok annotation that automatically generates getter / setter etc.
@Id
Annotation indicating that it is a PK column.
@GeneratedValue
Annotation indicating automatic numbering in the sequence.
@SequenceGenerator
Annotation for creating a sequence.

7.3.3 Model (Repository class) creation

CustomerRepository class

Create a class to do CRUD operations on the customer table. It just inherits from JpaRepository.

CustomerRepository.java


package com.example.tksystem.model;

import org.springframework.data.jpa.repository.JpaRepository;

/**
 *Customer repository class.
 */
public interface CustomerRepository extends JpaRepository<Customer, Long> {

}

The following methods (excerpts) will be available.

List<Customer> findAll()
Refers to all records in the table (SELECT) and returns it as a list.
Customer getOne(Long id)
Refers to the record corresponding to the customer ID (SELECT) and returns the result.
Customer save(Customer entity)
Register / update (INSERT / UPDATE) customer information of
argument and return result record.
void deleteById(Long id)
Delete (DELETE) the record corresponding to the customer ID.

7.3.4 View creation

Create an HTML template for each screen. Since the navigation bar at the top is common to all screens, it will be shared as a common layout.

Common layout

layout.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:fragment="head(title)">

  <title th:text="'Customer management- ' + ${title}">tksystem</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

  <link rel="stylesheet" th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}" />
  <link rel="stylesheet" th:href="@{/webjars/font-awesome/5.13.0/css/all.min.css}" />

  <script th:src="@{/webjars/jquery/3.3.1/jquery.min.js}"></script>
  <script th:src="@{/webjars/bootstrap/4.3.1/js/bootstrap.min.js}"></script>

</head>

<body>
  <nav class="navbar navbar-dark bg-dark navbar-expand-sm mb-3" th:fragment="navbar">

    <a class="navbar-brand" href="/">Customer management</a>

    <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#Navbar" aria-controls="Navbar" aria-expanded="false" aria-label="Switching navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div id="Navbar" class="collapse navbar-collapse">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" th:href="@{/customers/create/}">
            <i class="fas fa-plus fa-lg" aria-hidden=”true”></i>sign up
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link" th:href="@{/customers/}">
            <i class="fas fa-list fa-lg" aria-hidden=”true”></i>List
          </a>
        </li>
      </ul>
    </div>

  </nav>
</body>

</html>
th:fragment
Allows replacement with th: replace on other screens.
Other Thymeleaf grammars Please refer to
here .
Bootstrap grammar Please refer to
official document .

the top screen

index.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:replace="fragments/layout :: head('Home')">
</head>

<body>
  <div th:replace="fragments/layout :: navbar"></div>

  <div class="container-fluid">
    <h5>menu</h5>
    <hr>

    <div class="row">
      <div class="col-sm-6">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">
              <a class="nav-link" th:href="@{/customers/create}">
                <i class="fas fa-plus fa-lg" aria-hidden=”true”></i>sign up
              </a>
            </h5>
            <p class="card-text">Register a new customer.</p>
          </div>
        </div>
      </div>

      <div class="col-sm-6">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">
              <a class="nav-link" th:href="@{/customers/}">
                <i class="fas fa-list fa-lg" aria-hidden=”true”></i>List
              </a>
            </h5>
            <p class="card-text">Display a list of customers.</p>
          </div>
        </div>
      </div>

    </div>
  </div>

</body>

</html>

New registration screen

create.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:replace="fragments/layout :: head('sign up')">
</head>

<body>
  <div th:replace="fragments/layout :: navbar"></div>

  <div class="container-fluid">
    <h5>sign up</h5>
    <hr>

    <div class="row">
      <div class="col-sm-12">
        <form action="#" th:action="@{/customers/create/}" th:object="${customer}" method="post">
          <div class="form-group">
            <label for="name">name<span class="badge badge-danger">Mandatory</span></label>
            <input type="text" id="name" class="form-control" placeholder="(Example)Yamada Taro" th:field="*{name}" />
            <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color: red"></span>
          </div>

          <button type="submit" class="btn btn-primary">Confirm</button>  
        </form>
      </div>
    </div>
  </div>
</body>
</html>

Update screen

It is almost the same as the registration screen. The finished product is here

List screen

list.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head th:replace="fragments/layout :: head('List')">
</head>

<body>
  <div th:replace="fragments/layout :: navbar"></div>

  <div class="container-fluid">
    <h5>List</h5>
    <hr>

    <div class="row">
      <div class="col-sm-12">

        <table class="table table-bordered table-hover">
          <thead class="thead-dark">
            <tr>
              <th width="10%">ID</th>
              <th width="80%">name</th>
              <th width="10%">Delete</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="customer:${customer}">
              <td th:text="${customer.id}"></td>
              <td>
                <a th:text="${customer.name}" th:href="@{/customers/{id}/update/(id=${customer.id})}">
                </a>
              </td>
              <td>
                <a th:href="@{/customers/{id}/delete/(id=${customer.id})}" onClick="return window.confirm('Are you sure you want to delete?')">
                  <i class="far fa-trash-alt fa-lg" aria-hidden=”true”></i>
                </a>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</body>

</html>

7.3.5 Creating a Controller

IndexController class

Create a class to handle top screen requests.

IndexController.java


package com.example.tksystem.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 *The controller class for the index screen.
 */
@Controller
@RequestMapping("/")
public class IndexController {

  /**
   *index screen
   *
   * @param model model
   * @return Transition destination
   */
  @RequestMapping("index")
  public String index(Model model) {
    return "index";
  }
}
@Controller
Annotation indicating that it is a Controller class.

@RequestMapping
Map the URL of the HTTP request to the class / method.
In this example, when the URL is "/ index /", the index (Model model) method is called.

Method return value
The return string indicates the Thymeleaf HTML file name on the next screen.
For "index", it indicates /src/main/resources/templates/index.html.

CustomerController class

Create a class to handle requests on the customer screen (new registration / update / list screen).

CustomerController.java


package com.example.tksystem.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.tksystem.model.Customer;
import com.example.tksystem.model.CustomerRepository;

/**
 *Customer screen controller class.
 */
@Controller
@RequestMapping("/customers")
public class CustomerController {

  /**Registration/update/Redirect destination URL after deletion is completed*/
  private static final String REDIRECT_URL = "redirect:/customers/";

  /**HTML path*/
  private static final String PATH_LIST = "customer/list";
  private static final String PATH_CREATE = "customer/create";
  private static final String PATH_UPDATE = "customer/update";

  /**Model attribute name*/
  private static final String MODEL_ATTRIBUTE_NAME = "customer";

  /**Customer repository*/
  @Autowired
  private CustomerRepository customerRepository;

  /**
   *Display the list screen.
   *
   * @param model model
   * @return Transition destination
   */
  @GetMapping(value = "/")
  public String list(Model model) {
    model.addAttribute(MODEL_ATTRIBUTE_NAME, customerRepository.findAll(Sort.by("id")));
    return PATH_LIST;
  }

  /**
   *Display the registration screen.
   *
   * @param model model
   * @return Transition destination
   */
  @GetMapping(value = "/create")
  public String create(Model model) {
    model.addAttribute(MODEL_ATTRIBUTE_NAME, new Customer());
    return PATH_CREATE;
  }

  /**
   *Perform registration.
   *
   * @param customer Customer screen input value
   * @param result Input check result
   * @return Transition destination
   */
  @PostMapping(value = "/create")
  public String create(@Validated @ModelAttribute(MODEL_ATTRIBUTE_NAME) Customer customer,
      BindingResult result) {

    if (result.hasErrors()) {
      return PATH_CREATE;
    }

    customerRepository.save(customer);
    return REDIRECT_URL;
  }

  /**
   *Display the update screen.
   *
   * @param id customer ID
   * @param model model
   * @return Transition destination
   */
  @GetMapping(value = "/{id}/update")
  public String update(@PathVariable("id") Long id, Model model) {
    model.addAttribute(MODEL_ATTRIBUTE_NAME, customerRepository.getOne(id));
    return PATH_UPDATE;
  }

  /**
   *Perform the update.
   *
   * @param customer Customer screen input value
   * @param result Input check result
   * @return Transition destination
   */
  @PostMapping(value = "/{id}/update")
  public String update(@Validated @ModelAttribute(MODEL_ATTRIBUTE_NAME) Customer customer,
      BindingResult result) {

    if (result.hasErrors()) {
      return PATH_UPDATE;
    }

    customerRepository.save(customer);
    return REDIRECT_URL;
  }

  /**
   *Perform deletion.
   *
   * @param id customer ID
   * @return Transition destination
   */
  @GetMapping(value = "/{id}/delete")
  public String list(@PathVariable("id") Long id) {
    customerRepository.deleteById(id);
    return REDIRECT_URL;
  }
}
@GetMapping/@PostMapping
Same as
@ RequestMapping.
@Validated
Annotation indicating that input is checked.
BindingResult result
Contains the input check result.

7.3.6 Creating a configuration file

application.yml This file is used to set DB connection information, etc. url / username / password is the default for PostgreSQL. If it is different from the default, change it.

application.yml


spring:
  datasource:

    #Postgres IP address/port number/DB name
    url: jdbc:postgresql://localhost:5432/postgres

    #Postgres username
    username: postgres

    #Postgres password
    password: postgres

    #Postgres JDBC driver
    driver-class-name: org.postgresql.Driver

  jpa:
    hibernate:

      # @Always drop the table corresponding to Entity&create.
      ddl-auto: create-drop
spring.jpa.hibernate.ddl-auto
See here .

hibernate.properties This setting avoids exceptions that occur in PostgreSQL x Spring JPA. There is no problem in operation without it, but it is unpleasant to get an exception every time, so it is better to set it. Details of the exception here

hibernate.properties


# PgConnection.createClob()Avoid method warnings
hibernate.jdbc.lob.non_contextual_creation = true

ValidationMessages.properties The message definition for the input check error message. I will make my own because the Japanese message has not been provided yet. A pull request has been captured at here, so I think it will be provided soon.

ValidationMessages.properties


javax.validation.constraints.AssertFalse.message     =Please set to false
javax.validation.constraints.AssertTrue.message      =Please set to true
javax.validation.constraints.DecimalMax.message      = {value} ${inclusive == true ? 'Please set the following values' : 'Please make it smaller'}
javax.validation.constraints.DecimalMin.message      = {value} ${inclusive == true ? 'Please set the value above' : 'Please set a larger value'}
javax.validation.constraints.Digits.message          =The value should be in the following range(<integer{integer}digit>.<After the decimal point{fraction}digit>)
javax.validation.constraints.Email.message           =Please use the correct format for your email address
javax.validation.constraints.Future.message          =Please set to a future date
javax.validation.constraints.FutureOrPresent.message =Set to current or future date
javax.validation.constraints.Max.message             = {value}Please set the following values
javax.validation.constraints.Min.message             = {value}Please set the value above
javax.validation.constraints.Negative.message        =Must be less than 0
javax.validation.constraints.NegativeOrZero.message  =Please set the value to 0 or less
javax.validation.constraints.NotBlank.message        =White space is not allowed
javax.validation.constraints.NotEmpty.message        =Empty elements are not allowed
javax.validation.constraints.NotNull.message         =null is not allowed
javax.validation.constraints.Null.message            =Please make it null
javax.validation.constraints.Past.message            =Please set to a past date
javax.validation.constraints.PastOrPresent.message   =Set to current or past date
javax.validation.constraints.Pattern.message         =Regular expressions"{regexp}"Please match to
javax.validation.constraints.Positive.message        =Must be greater than 0
javax.validation.constraints.PositiveOrZero.message  =Must be greater than or equal to 0
javax.validation.constraints.Size.message            = {min}From{max}Please make the size between

WebConfig Make settings to use the above ValidationMessages.properties. See here for more information.

WebConfig.java


package com.example.tksystem;

import java.nio.charset.StandardCharsets;

import org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 *Web configuration class.
 *
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public Validator getValidator() {

    // ValidationMessages.UTF to properties-Allows you to set with 8.
    ReloadableResourceBundleMessageSource messageSource =
        new ReloadableResourceBundleMessageSource();
    messageSource.setBasename(AbstractMessageInterpolator.USER_VALIDATION_MESSAGES);
    messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());

    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setValidationMessageSource(messageSource);

    return validator;
  }
}

7.4 Start & table automatic generation

flow4.png

Now that all the files are complete, run the project in Eclipse. The procedure is to select the project and select "Right click" "Run" "Spring Boot application".

By setting ddl-auto set in application.yml The table is automatically generated according to the created Entity class.

Please access [http: // localhost: 8080](http: // localhost: 8080) and check the operation. If it works like [2. Goal](# 2-Goal), it is successful.

Thank you for your hard work.

Recommended Posts