lectures

Java Spring

What is Spring Boot?

Spring Boot is a Java framework for building web applications and microservices.

Use cases of Spring Boot

Which web frameworks developer you use? Alt text
Reference: Java Dev Ecosystem 2023

Creating a Spring Boot Project

Spring Initializr

Alt text
URL: Spring Initializr

The Tree Structure of a Spring Boot Project for a BookStore REST API project:

my-spring-boot-app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── bookstore/
│   │   │               ├── BookstoreApplication.java
│   │   │               ├── controller/
│   │   │               │   └── BookController.java
│   │   │               ├── model/
│   │   │               │   └── Book.java
│   │   │               ├── repository/
│   │   │               │   └── BookRepository.java
│   │   │               └── service/
│   │   │                   └── BookService.java
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── static/
│   │       └── templates/
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── bookstore/
│                       ├── BookstoreApplicationTests.java
│                       ├── controller/
│                       ├── repository/
│                       └── service/
├── target/
├── mvnw
├── mvnw.cmd
├── pom.xml
└── README.md

Description of Key Components:

In Java Spring, the Main Application Class is located in the src/main/java directory.

@SpringBootApplication
public class BookstoreApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookstoreApplication.class, args);
    }

}

“Hello World” program

In this section, we will create a simple Spring Boot application that prints “Hello World!” when we send a GET / request to the application.

REST Controllers: In Spring Boot, REST controllers are used to handle incoming web requests and returning responses using HTTP.

Here is an example of creating a Basic REST Controller that print “Hello World!”

@RestController
public class HelloController {
    @GetMapping("/")
    public String index() {
        return "Hello World!";
    }
}

In Java, an annotation is a form of syntactic metadata that can be added to Java source code.

To start the Spring app, we can run the following command in the terminal:

mvn spring-boot:run

Here is the sample output of the Rest API when send a GET / request to the Spring App:

"Hello World!"

Defining a REST API for Book Service

In this section, we will define a REST API for a simple Book Service. The Book Service will be used to store and retrieve books from a database. The Book Service will expose a REST API that allows clients to perform CRUD operations on books.

Defining the Book Model

In Spring Boot , model classes represent the data in the application.

Here is the sample code for a simple model class for a book:

package com.example.helloworld;

public class Book {
    private int bookid;
    private String title;
    private String author;

    // No-argument constructor 
    public Book() {
    }

    // Constructor with fields
    public Book(int bookid, String title, String author) {
        this.bookid = bookid;
        this.title = title;
        this.author = author;
    }

    // Getters and Setters
    public int getBookid() {
        return bookid;
    }

    public void setBookid(int bookid) {
        this.bookid = bookid;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

Explanation of Sample Code:

Example:

@RestController
public class BookController {
    @GetMapping("/book")
    public Book getBook() {
        return new Book(1, "The Alchemist", "Paulo Coelho");
    }
}

Explanation of Sample Code:

Here is a sample HTTP request to create a new book:

GET /book HTTP/1.1

Here is the sample HTTP response:

HTTP/1.1 200
Content-Type: application/json

{
    "bookid": 1,
    "title": "The Alchemist",
    "author": "Paulo Coelho"
}

If we want to return a list of books, we can use the List interface:

@RestController
public class BookController {
    @GetMapping("/books")
    public List<Book> getBooks() {
        List<Book> books = new ArrayList<>();
        books.add(new Book(1, "Spring in Action", "Craig Walls"));
        books.add(new Book(2, "Effective Java", "Joshua Bloch"));
        books.add(new Book(3, "Java Concurrency in Practice", "Brian Goetz"));
        return books;
    }
}

Here is the sample output of the Rest API when send a GET /books request to the Spring App:

[
    {
        "bookid": 1,
        "title": "Spring in Action",
        "author": "Craig Walls"
    },
    {
        "bookid": 2,
        "title": "Effective Java",
        "author": "Joshua Bloch"
    },
    {
        "bookid": 3,
        "title": "Java Concurrency in Practice",
        "author": "Brian Goetz"
    }
]

The JSON response is a list of books, where each book is represented as a JSON object.

Service Layer

In Spring Boot, the service layer is used to encapsulate the business logic of the application and is typically used to interact with the data source. In this section, we will introduce the service layer by refining the Book Service example from the previous section.

Let’s define the class BookService to encapsulate the business logic of the Book Service.

Sample Code:

@Service
public class BookService {
    // Keeps a Collection of books
    private final Set<Book> books;

    // Constructor
    public BookService() {
        books = new HashSet<>();
        books.add(new Book(1, "Spring in Action", "Craig Walls"));
        books.add(new Book(2, "Effective Java", "Joshua Bloch"));
        books.add(new Book(3, "Java Concurrency in Practice", "Brian Goetz"));
    }

    // Returns all books in the collection
    public Set<Book> findAllBooks() {
        return books;
    }
}

Explanation of Sample Code:

The BookService class is now responsible for managing the books, while the BookController class is responsible for handling web requests and delegating business logic to the service layer.

Here is how the BookController class looks like after the introduction of the service layer:

@RestController
public class BookController {
    @autoWired
    private BookService bookService;    

    // Returns all books in the collection
    @GetMapping("/books")
    public Set<Book> getBooks() {
        return bookService.findAllBooks();
    }
}

Repository for Persistent Data Storage

In our previous example, we used a collection of books as the data source. However, in real-world applications, we need a persistent data storage solution.

The following diagram illustrate the architecture of a typical Spring Boot application with a persistent data storage solution.

Alt text

Remark: In Java Spring, the *Application Context is a container that contains all the beans (objects) in the application.*

In Spring Boot,

We first define the Book model as an entity class.

    @Entity
    public class Book {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int bookid;
        private String title;
        private String author;

        // Define the Constructors, getters, setters, toString() ...

    }

Explanation:

For the Book entity class, we should also define the constructor, getters, setters, and toString() methods…


Next, we will define the BookRepository interface, which extends JpaRepository to provide CRUD methods for Book entities.

The following is the sample code for the BookRepository interface:


```java
public interface BookRepository extends JpaRepository<Book, Integer> {
}

The Spring Framework will create the following table in relationship databases based on the Book entity class: | Column | Type | Key | |——–|———|———–| | id | Integer | Primary | | title | String | | | author | String | |

Remark:

You can customize the BookRepository interface to support additional custom queries. For example, to support the operation find books by author, we can define the following method in the BookRepository interface:

public interface BookRepository extends JpaRepository<Book, Integer> {
    List<Book> findByAuthor(String author); // Find books by author
}

Here is the sample code for the BookService class after integrating the BookRepository:

@Service
public class BookService {
    @Autowired
    private final BookRepository repo;

    public Set<Book> findAllBooks() {
        return new HashSet<>(repo.findAll());
    }
}

Explanation:

With the changes, our Spring app will now return all books from the database when we send a GET /books request to the app.

Database Configuration

There are two ways to configure the database in Spring Boot:

  1. Using application.properties: We can configure the database in the application.properties file. For example, we can specify the JDBC URL, username, and password in the application.properties file.
  2. Include the MySQL JDBC driver in your pom.xml: We can also configure the database by including the MySQL JDBC driver in the pom.xml file. For example, we can specify the JDBC URL, username, and password in the pom.xml file.

Example: Configure DataSource in application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=myusername
spring.datasource.password=mypassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Explanation:

Introduction to ORM in Spring Boot with JPA

Object-Relational Mapping (ORM) in Spring Boot, facilitated by JPA (Java Persistence API), allows seamless mapping of Java objects to relational database tables. This approach bridges the gap between the object-oriented domain model and the relational database.

Let’s consider the scenario where we have two entities: Book and Author. Suppose that a book can only be written by one author, but an author can write multiple books. In this case, we have a one-to-many relationship between Book and Author.

The following code define the Book entity class to respesent a book in the database:

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne // Many books can be written by one author
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;

    // Getters and setters...
}

The following code define the Author entity class to respesent an author in the database:

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author") // One author can write many books
    private Set<Book> books;

    // Getters and setters...
}

When the Spring App is started, the Hibernate ORM framework will automatically create the Book and Author tables in the database based on the entity classes.

It will also automatically create the foreign key constraints to maintain the relationship between the two tables.

Here is an example of Database Tables with sample data

Author Table

id name
1 John Doe
2 Jane Smith

Book Table

id title author_id
1 Spring in Action 1
2 JPA for Beginners 1
3 Hibernate Basics 2

The following BookService class illustrates how to create a new book and link up with an author from the database.

@Service
public class BookService {
    // Autowired repositories...
    public Book createBook(Long authorId, Book book) {
        Author author = authorRepository.findById(authorId).orElseThrow();
        book.setAuthor(author);
        return bookRepository.save(book);
    }
    // Additional methods...
}

ORM with JPA in Spring Boot simplifies managing relational data in an object-oriented manner.

Dependency Injection in Spring

Overview

Spring Framework supports three primary methods of dependency injection to manage and inject dependencies into classes: Constructor Injection, Setter Injection, and Field Injection. These approaches provide flexibility in how dependencies are provided to an object.

Types of Dependency Injection in Spring

Field Injection

**Field Injection **involves injecting dependencies directly into fields.

Constructor Injection

Constructor Injection involves injecting dependencies through the class constructor.

  @RestController
  public class BookController {
      private final BookService bookService;

      @Autowired
      public BookController(BookService bookService) {
          this.bookService = bookService;
      }
  }

Setter Injection

Setter Injection involves injecting dependencies through setter methods.

@RestController
public class BookController {
    private BookService bookService;

    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
}

Case Study: MessageService

In this section, we will demonstrate how to use Dependency Injection in a Spring Boot application.

We will define the followin classes:

Alt text

The MessageService interface defines the contract for sending messages. It includes a method to send messages to a specified recipient.

public interface MessageService {
    void sendMessage(String message, String recipient);
}

We now create the following implementation classes for the MessageService interface:

Here is the sample code for the EmailMessageService class:

@Component
public class EmailMessageService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending email to " + recipient + ": " + message);
    }
}

Here is the sample code for the SMSMessageService class:

@Service
public class SMSMessageService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending SMS to " + recipient + ": " + message);
    }
}

We will now define the client class NotificationService, which uses the MessageService to send messages.

@Component
public class NotificationService {
    @Autowired
    private final MessageService messageService;

    public void sendNotification(String message, String recipient) {
        messageService.sendMessage(message, recipient);
    }
}

When the Spring app is started, Spring will automatically create an instance of NotificationService and inject an implementation of MessageService into it.


Here are the possible ways to resolve this ambiguity:

  1. Use of @Qualifier Annotation
  2. Marking a Bean as @Primary
  3. Configurations in application.properties

Option 1: Use of @Qualifier Annotation

We can explicitly specify the bean to be used for autowiring using the @Qualifier annotation. In the code below, the NotificationService class is annotated with @Qualifier("emailMessageService"), which tells Spring to inject the EmailMessageService implementation.

@Component
@Qualifier("emailMessageService")
public class NotificationService {
    @Autowired
    private final MessageService messageService;

    public void sendNotification(String message, String recipient) {
        messageService.sendMessage(message, recipient);
    }
}

Option 2: Marking a Bean as @Primary

We can designate one of the implementations as the primary bean. In the code below, the EmailMessageService class is annotated with @Primary, which tells Spring to give preference to the primary bean when resolving the dependency.

@Component
@Primary
public class EmailMessageService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending email to " + recipient + ": " + message);
    }
}

Option 3: Configurations in application.properties

In the code below, the application.properties file is configured to inject the EmailMessageService implementation.

  1. Define a Property in application.properties:
    • Set a property in application.properties that determines which implementation to use. For example:
       messaging.service.type=email
      
  2. Conditional Bean Creation in Configuration Class:
    • Use the @ConditionalOnProperty annotation in your Java configuration class to create beans conditionally based on the property value.
    • Example:
       @Configuration
       public class MessagingConfig {
      
         @Bean
         @ConditionalOnProperty(name = "messaging.service.type", havingValue = "email")
         public MessageService emailMessageService() {
             return new EmailMessageService();
         }
      
         @Bean
         @ConditionalOnProperty(name = "messaging.service.type", havingValue = "sms")
         public MessageService smsMessageService() {
             return new SMSMessageService();
         }
       }
      
  3. Injecting the Selected Implementation:
    • In your service or controller where MessageService is required, simply autowire the MessageService. Spring will automatically inject the correct implementation based on the configuration.
    @Service
    public class NotificationService {
         @Autowired
         private final MessageService messageService;
    }
    

In this section, we demonstrated how to use Dependency Injection in Spring Boot applications.

Extending the REST API for Book Service

Overview

The current implementation of our Book Service can retrieve all books from the database. However, to enhance its functionality and make it a comprehensive tool for managing books, we may extend the REST API to support full CRUD (Create, Read, Update, Delete) operations.

The following table summarizes the REST API endpoints for the Book Service:

Operation HTTP Method Endpoint Request Body Success Response Failure Response
Create a new book POST /api/books JSON object with book details 201 (Created) with book details 400 (Bad Request) if input is invalid
Read all books GET /api/books N/A 200 (OK) with list of books 500 (Internal Server Error)
Read a single book GET /api/books/{id} N/A 200 (OK) with book details 404 (Not Found) if book ID doesn’t exist
Update a book PUT /api/books/{id} JSON object with updated book details 200 (OK) with updated book details 404 (Not Found) if book ID doesn’t exist
Delete a book DELETE /api/books/{id} N/A 204 (No Content) 404 (Not Found) if book ID doesn’t exist

Create a New Book

To support the creation of a new book, we may extend our BookService class with a createBook() method. In the code below,

@Service
public class BookService {
    @Autowired
    private final BookRepository repo;

    // existing code ...

    // Create a new book
    public Book createBook(Book book) {
        return repo.save(book);
    }
}

We should also update the BookController class to handle the POST /api/books request. - In the code below, the createBook() method takes the book to be created as a parameter. It calls the createBook() method in BookService to create a new book.

@RestController
public class BookController {
    @Autowired
    private BookService bookService;

    // existing code ...

    // Create a new book
    @PostMapping("/api/books")
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book newBook = bookService.createBook(book);
        return new ResponseEntity<>(newBook, HttpStatus.CREATED);
    }
}

In the sample code:

Here is a sample HTTP request to create a new book:

POST /api/books HTTP/1.1
Content-Type: application/json

{
    "title": "The Alchemist",
    "author": "Paulo Coelho"
}

Here is the sample HTTP response:

HTTP/1.1 201
Content-Type: application/json

{
    "bookid": 4,
    "title": "The Alchemist",
    "author": "Paulo Coelho"
}

Get a Single Book

Our Book API currently supports retrieving all books from the database. However, we may also want to retrieve a specific book by its ID. In this section, we will extend the Book API to support retrieving a single book by its ID.

We may extend our BookService class with a getBookById() method.

@Service
public class BookService {
    @Autowired
    private final BookRepository repo;

    // existing code ...

    // Get a book by ID
    public Book getBookById(Long id) {
        return repo.findById(id).orElse(null);
    }
}

We should also update the BookController class to handle the GET /api/books/{id} request.

@RestController
public class BookController {
    @Autowired
    private BookService bookService;

    // existing code ...

    // Get a book by ID
    @GetMapping("/api/books/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        // Call the getBookById() method in the bookService and return the book
        Book book = bookService.getBookById(id);

        // If the book exists, return the book
        if (book != null) {
            return new ResponseEntity<>(book, HttpStatus.OK); // 200 (OK)
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 (Not Found)
        }
    }
}

In the sample code,

Retrieving the details of a specific book is done via the GET /api/books/{id} endpoint. For instance, to retrieve the details of the book with ID 1, we can send a GET /api/books/1 request to the Book API.

Here is a sample HTTP request to retrieve the details of a book with ID 1:

GET /api/books/1 HTTP/1.1

Here is the sample HTTP response:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "bookid": 1,
    "title": "1984",
    "author": "George Orwell"
}

Update a Book

Updating a book involves modifying its details like title or author. This is achieved through the PUT /api/books/{id} endpoint.

We should extend our BookService class with an updateBook() method. In the code below, the updateBook() method takes two parameters: id and bookDetails. The id parameter is the ID of the book to be updated, and the bookDetails parameter is the updated book details.

@Service
public class BookService {
    @Autowired
    private final BookRepository repo;

    // existing code ...

    // Update a book
    public Book updateBook(Long id, Book bookDetails) {
        // Get the book by ID from the database
        Book book = repo.findById(id).orElse(null);

        // If the book exists, update the book details and save the changes
        if (book != null) {
            book.setTitle(bookDetails.getTitle());
            book.setAuthor(bookDetails.getAuthor());
            return repo.save(book);
        }
        return null;
    }
}

Explanation:

We should also update the BookController class to handle the PUT /api/books/{id}.

@RestController
public class BookController {
    @Autowired
    private BookService bookService;

    // existing code ...

    // Update a book
    @PutMapping("/api/books/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
        // Call the updateBook() method in the bookService and return the updated book
        Book updatedBook = bookService.updateBook(id, bookDetails);

        // If the book exists, return the book
        if (updatedBook != null) {
            return new ResponseEntity<>(updatedBook, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Here is the sample HTTP request and response to update the details of a book with ID 1:

PUT /api/books/1 HTTP/1.1
Content-Type: application/json

{
    "title": "1984",
    "author": "George Orwell"
}
HTTP/1.1 200 OK
Content-Type: application/json

{
    "bookid": 1,
    "title": "1984",
    "author": "George Orwell"
}

Delete a Book

To delete a book, we can extend our BookService class with a deleteBook() method. In the code below, the deleteBook() method takes the ID of the book to be deleted as a parameter. It first checks if the book exists in the database using the existsById() method provided by BookRepository. If the book exists, it deletes the book using the deleteById() method provided by BookRepository.

@Service
public class BookService {
    @Autowired
    private final BookRepository repo;

    // existing code ...

    // Delete a book
    public boolean deleteBook(Long id) {
        if (repo.existsById(id)) {
            repo.deleteById(id);
            return true;
        }
        return false;
    }
}

We should also update the BookController class to handle the DELETE /api/books/{id} request. In the code below, the deleteBook() method takes the ID of the book to be deleted as a parameter. It first checks if the book exists in the database using the existsById() method provided by BookRepository. If the book exists, it deletes the book using the deleteBook() method provided by BookService.

@RestController
public class BookController {
    @Autowired
    private BookService bookService;

    // existing code ...

    // Delete a book
    @DeleteMapping("/api/books/{id}")
    public ResponseEntity<HttpStatus> deleteBook(@PathVariable Long id) {
        // Call the deleteBook() method in the bookService and return the HTTP status code
        if (bookService.deleteBook(id)) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT); // 204 (No Content)
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 (Not Found)
        }
    }
}

References