Getting stared with the Spring Framework

Table of Contents

What is the Spring Framework

Spring is the world's most popular Java framework and makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simplicity, and productivity.

What can Spring do

  • Microservices: Quickly deliver production‑grade features with independently evolvable microservices.
  • Reactive: Spring's asynchronous, nonblocking architecture means you can get more from your computing resources.
  • Cloud: Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.
  • Web apps: Frameworks for fast, secure, and responsive web applications connected to any data store.
  • Serverless: The ultimate flexibility. Scale up on demand and scale to zero when there’s no demand.
  • Event Driven: Integrate with your enterprise. React to business events. Act on your streaming data in realtime.
  • Batch: Automated tasks. Offline processing of data at a time to suit you.

Getting started

You can select and generate your Spring Package online: https://start.spring.io/

Lets make a Gradle Project, use Java v17 and add those dependencies:

  • Spring Boot DevTools Developer Tools
  • Lombok Developer Tools
  • Spring Web Web
  • Thymeleaf Template Engines
  • Spring Data JPA SQL
  • H2 Database SQL
  • Validation I/O

Click "Generate" and unzip the file to your IDE Projects and open it with your preferred IDE.

In the IDE run the PeopledbWebApplication.java Class and open your browser, go to http://localhost:8080/ you should get a page like this:

Go back to your IDE and create a new index.html File in the Folder /src/main/resources/static/index.html with this content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
Hello World
</body>
</html>

Rebuild your Project in the IDE and reopen your page on http://localhost:8080/ and you should see this now:

Congratulations you just generated your first Spring Web App.

Display dynamic content

Let's create a new package called web.controller and lets create a Java class called PeopleController with this content:

package ch.finecloud.peopledbweb.web.controller;

import ch.finecloud.peopledbweb.biz.model.Person;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

@Controller
@RequestMapping("/people")
public class PeopleController {

    @GetMapping
    public String getPeople(Model model) {
        List<Person> people = List.of(
                new Person(10l, "Jake", "Snake", LocalDate.of(1950, 1, 8), new BigDecimal(50000)),
                new Person(20l, "Sara", "Smith", LocalDate.of(1960, 2, 7), new BigDecimal(60000)),
                new Person(30l, "Johnny", "Jackson", LocalDate.of(1970, 3, 6), new BigDecimal(70000)),
                new Person(40l, "Bonny", "Norris", LocalDate.of(1980, 4, 5), new BigDecimal(80000))
        );
        model.addAttribute("people", people);
        return "people";
    }
}

We're going to create a new HTML file under the resources/templates folder. We call it people.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>People</title>
</head>
<body>
here are some people
</body>
</html>

If we not open our browser to http://localhost:8080/people we will see that html file loaded. What is the difference between putting an HTML file under static versus under templates? The basic idea is: if you have HTML content that is dynamic and controlled from our Java code, then you need to put that HTML under templates. This will allow us to feed some dynamically generated content into this page and make a more interesting web page. Essentially, whereas static web pages we put under the static folder here are not capable of being fed data from our Java code directly.

Let's actually put sort of put a little bit of something dynamic in here. Let's make another package under the main  package, and call this one biz.model with a Java Class Person with the following content: 

package ch.finecloud.peopledbweb.biz.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDate;

@Data
@AllArgsConstructor
public class Person {
    private Long id;
    private String firstName;
    private String lastName;
    private LocalDate dob;
    private BigDecimal salary;

}

Normally we would create or generate getters and setters and all that good stuff in that Java Class. However, now that we're using the spring framework with spring boot and we got all this cool added functionality here, we can do something a little more interesting. We're going to use a special annotation from a library called Lombok, and the annotation we will use is called @Data. This annotation will will allow the Lombok Library to scan this class, find these fields and generate getters and setters and a constructor and an equals and a hash code and a two string method, all for us whenever we need it.

Next we also need to change our people.html file like so:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>People</title>
</head>
<body>
here are some people:
<ol>
    <li th:each="person: ${people}" th:text="${person}">My Junk Person</li>
</ol>
</body>
</html>

If we now rebuild our Project and refresh our people page we receive this dynamic content:

Add some Bootstrap

Let's make the Table more beautiful by using the bootstrap css framework:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>People</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8" crossorigin="anonymous"></script>
</head>
<body>
<div class="col-8 mx-auto">
    <table class="table">
        <thead>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">Last Name</th>
            <th scope="col">First Name</th>
            <th scope="col">DOB</th>
            <th scope="col">Salary</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="person : ${people}">
            <th scope="row" th:text="${person.id}">1</th>
            <td th:text="${person.lastName}">Mark</td>
            <td th:text="${person.firstName}">Otto</td>
            <td th:text="${person.dob}">@mdo</td>
            <td th:text="${person.salary}">@mdo</td>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>

If we now rebuild our Project and refresh our people page we receive this dynamic content:

Formatting Dates and Numbers

To format the Dates and Currency (Salary) we should not modify our Person.java Class, since this Class is part of the Service Layer, not the Presentation Layer. What we should do instead is create new Formatter Classes which make use of the Spring Formatter and can act directory on. the Presentation Layer:

peopledbweb/web/formatter/BigDecimalFormatter.java

package ch.finecloud.peopledbweb.web.formatter;

import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

@Component
public class BigDecimalFormatter implements Formatter<BigDecimal> {

    @Override
    public BigDecimal parse(String text, Locale locale) throws ParseException {
        return null;
    }

    @Override
    public String print(BigDecimal object, Locale locale) {
        return NumberFormat.getCurrencyInstance(locale).format(object);
    }
}

peopledbweb/web/formatter/LocalDateFormatter.java

package ch.finecloud.peopledbweb.web.formatter;

import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;

import java.text.ParseException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

@Component
public class LocalDateFormatter implements Formatter<LocalDate> {

    private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd. MMMM, yyyy");

    @Override
    public LocalDate parse(String text, Locale locale) throws ParseException {
        return LocalDate.parse(text, dateTimeFormatter);
    }

    @Override
    public String print(LocalDate object, Locale locale) {
        return dateTimeFormatter.format(object);
    }
}

we also need to put the dob and salary table declaration into one more pair of curly braces:

            <td th:text="${{person.dob}}">@mdo</td>
            <td th:text="${{person.salary}}">@mdo</td>

If we not rebuild our Project we get beck those values nicely formatted:

Introducing Spring Data

Until now we have worked with very static data. Lets change this by adding real CRUD functionality.

Now we're going to introduce the third part of our three tier architecture. And that is the data. Lets create a new package called data. Under the data package, we create a new Java Interfacee called PersonRepository with this content:

package ch.finecloud.peopledbweb.data;

import ch.finecloud.peopledbweb.biz.model.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}

The first will be the type of the domain model class that I want this repository to be responsible for or work with, and that would be our person class. The other type, though, which I will separate with a comma. The second type will be the data type of the ID property of the person class, that would be, in our case, long. And then I need to annotate this interface so that when spring is starting up or in this case, spring
data is starting up. It can scan through all of the classes that I have here, and it will find this interface and know that it should treat this interface as an actual data repository.
To do that, we will use another annotation of the spring framework, which is @Repository. That's all I have to do to make a repository with full crud capabilities.

We need to go into the person class now and make a few little alterations here:

package ch.finecloud.peopledbweb.biz.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
import java.time.LocalDate;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    private LocalDate dob;
    private BigDecimal salary;
}

Lets create a Class to load the Persons:

package ch.finecloud.peopledbweb.data;

import ch.finecloud.peopledbweb.biz.model.Person;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

@Component
public class PersonDataLoader implements ApplicationRunner {

    private PersonRepository personRepository;

    public PersonDataLoader(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (personRepository.count() == 0) {
            List<Person> people = List.of(
                    new Person(null, "Pete", "Snake", LocalDate.of(1950, 1, 8), new BigDecimal(50000)),
                    new Person(null, "Jennifer", "Smith", LocalDate.of(1960, 2, 7), new BigDecimal(60000)),
                    new Person(null, "Mark", "Jackson", LocalDate.of(1970, 3, 6), new BigDecimal(70000)),
                    new Person(null, "Vishnu", "Norris", LocalDate.of(1971, 3, 6), new BigDecimal(70000)),
                    new Person(null, "Alice", "Jane", LocalDate.of(1972, 3, 6), new BigDecimal(70000)),
                    new Person(null, "Daniel", "Norris", LocalDate.of(1980, 4, 5), new BigDecimal(80000))
            );
            personRepository.saveAll(people);
        }
    }
}

we also need to modify our PeopleController:

package ch.finecloud.peopledbweb.web.controller;

import ch.finecloud.peopledbweb.biz.model.Person;
import ch.finecloud.peopledbweb.data.PersonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/people")
public class PeopleController {

    private PersonRepository personRepository;

    public PeopleController(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @ModelAttribute("people")
    public Iterable<Person> getPeople() {
        return personRepository.findAll();
    }
    @GetMapping
    public String showPeoplePage() {
        return "people";
    }
}

Saving a Person

To provide the function to also add new Persons, we first need to create a form in the WebUI to create new People Records:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>People</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8" crossorigin="anonymous"></script>
</head>
<body>
<div class="col-md-8 col-sm-10 mx-auto mt-5 my-5">
    <h2>People List</h2>
    <table class="table table-bordered table-sm">
        <thead>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">Last Name</th>
            <th scope="col">First Name</th>
            <th scope="col">DOB</th>
            <th scope="col">EMail</th>
            <th scope="col">Salary</th>
        </tr>
        </thead>
        <tbody>
        <tr th:if="${#lists.isEmpty(people)}">
            <td colspan="6" class="text-center">No Data</td>
        </tr>
        <tr th:each="person : ${people}">
            <th scope="row" th:text="${person.id}">1</th>
            <td th:text="${person.lastName}">Mark</td>
            <td th:text="${person.firstName}">Otto</td>
            <td th:text="${{person.dob}}">@mdo</td>
            <td th:text="${person.email}">Otto</td>
            <td th:text="${{person.salary}}">@mdo</td>
        </tr>
        </tbody>
    </table>
    <h2>Person Form</h2>
<form th:object="${person}" method="post" novalidate>
    <div class="mb-3">
        <label for="firstName" class="form-label">First Name</label>
        <input type="text" class="form-control" id="firstName" th:field="*{firstName}" th:errorclass="is-invalid" aria-describedby="firstNameHelp">
        <div id="validationFirstName" class="invalid-feedback" th:errors="*{firstName}">
            Please choose a username.
        </div>
    </div>
    <div class="mb-3">
        <label for="lastName" class="form-label">Last Name</label>
        <input type="text" class="form-control" id="lastName" th:field="*{lastName}" th:errorclass="is-invalid" aria-describedby="lastNameHelp">
        <div id="validationLastName" class="invalid-feedback" th:errors="*{lastName}">
            Please choose a username.
        </div>
    </div>
    <div class="mb-3">
        <label for="dob" class="form-label">Date of Birth</label>
        <input type="date" class="form-control" id="dob" th:field="*{dob}" th:errorclass="is-invalid" aria-describedby="dobHelp">
        <div id="validationDOB" class="invalid-feedback" th:errors="*{dob}">
            Please choose a username.
        </div>
    </div>
    <div class="mb-3">
        <label for="email" class="form-label">Email address</label>
        <input type="email" class="form-control" id="email" th:field="*{email}" th:errorclass="is-invalid" aria-describedby="emailHelp">
        <div id="validationEmail" class="invalid-feedback" th:errors="*{email}">
            Please choose a username.
        </div>
    </div>
    <div class="mb-3">
        <label for="salary" class="form-label">Salary</label>
        <input type="number" class="form-control" id="salary" th:field="*{salary}" th:errorclass="is-invalid" aria-describedby="salaryHelp">
        <div id="validationSalary" class="invalid-feedback" th:errors="*{salary}">
            Please choose a username.
        </div>
    </div>
    <button type="submit" class="btn btn-primary">Save Person</button>
</form>
</div>
</body>
</html>

lets also perform some validation of the entered data:

package ch.finecloud.peopledbweb.biz.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDate;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;

    @NotEmpty(message = "First name can not be empty")
    private String firstName;

    @NotEmpty(message = "Last name can not be empty")
    private String lastName;

    @Past(message = "Date of Birth must be in the Past")
    @NotNull(message = "Date of Birth can not be empty")
    private LocalDate dob;

    @Email(message = "Email must be valid")
    @NotEmpty(message = "Email can not be empty")
    private String email;

    @DecimalMin(value = "1000.00", message = "Salary must be at least 1000.00")
    @NotNull(message = "salary can not be empty")
    private BigDecimal salary;
}