engineering

April 07, 2020   |   10min read

What is Spring Boot? First Steps with Spring

Introduction

Spring Boot is a good tool to know if you want to start your journey into the development of Java web-based applications. In this post, I’ll first briefly explain what Spring Boot is and then show you how to get started with it in a short tutorial.

To properly explain Spring Boot, let’s first cover the Spring framework itself:

Spring is an open-source framework for web app development. It takes care of a lot of “plumbing” between application components, handles HTTP requests, and employs inversion of control to make the integration between business logic and the inner mechanisms of the application easier. Spring applications are configured by either using text files (XML or YAML) or configuration classes and annotations.

Spring Boot Explained

With that in-a-nutshell primer out of the way, let’s learn something about Spring Boot. Spring Boot is built on top of the Spring framework. With it, creating an application is even simpler by including some default configuration and satisfying some non-functional requirements like an embedded application server, metrics, health check, or security out of the box. Boot’s focus and goal is to reduce the quantity of code required to create and configure a Spring application while still maintaining flexibility. For instance, when users start to configure more and more of their application, Boot’s autoconfiguration and defaults get turned off, allowing an advanced user more control over their project.

But how does the Spring Boot project achieve that, you might ask.

The answer is surprisingly simple—with a whole lot of autoconfiguration and conditionals. Boot contains basic configurations for a multitude of commonly used services, data sources, messaging queues, application servers, etc., which are activated upon detection of those tools in the project. A Spring Boot project also automatically tries to read configuration .properties files from 17 different locations. Dependencies for any and all supported tools are downloaded by build tools—Maven or Gradle. This way, the user can focus their efforts on providing business logic and a lot of tools—JSON parsers, logging libraries, and many, many more are available without much fuss.

Building a web app with Spring Boot

Since using Spring Boot is so easy, let’s check it out for ourselves and build a simple web app.

Pivotal, the creators of Spring and Spring Boot, provided us with a really cool tool—Spring Initializr. It also has a web API and a dedicated plugin in IntelliJ IDEA. We’ll be using this one. Our application will be a simple movie database, so let’s call it “movies.”

Spring Initializr Window

In the project creation dialog, select “Spring Initializr” from the left-hand menu and pick your preferred Java SDK for the project (I picked 11, you can go with whatever version you’re comfortable with).

Spring Boot Java SDK Choice

Name the project in the next window. Select the desired Java version and build system—Gradle or Maven.

Spring Boot New Project Set Up

In the next window, select the components you want to include in the project. For this tutorial, pick Lombok, Spring Web, Spring Data JPA, and H2 Database. In the final window, name the project and select its location on disk. Click “Finish” and wait until everything gets downloaded and indexed.

Spring Boot New Project Name

As you can see, there’s not much in terms of code here—just a single Application class, but if you click “Run” or type ./gradlew bootRun in the terminal, the application will start and run in an embedded Tomcat server exposing its API on the default port 8080. It won’t serve anything other than 404 errors, but it will be there.

Now let’s add some functionality. We’ll start with a simple Movie entity. For the sake of simplicity of the tutorial, we’ll omit any separation of domain and persistence models.

package com.example.movies.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Movie {

  @Id @GeneratedValue private long id;

  @Column(unique = true, nullable = false)
  private String title;

  private String director;
  private int rating = 0;
}

We’re using annotations from Lombok for quick generation of getters, setters, and a constructor and javax.persistence to mark the object as an entity that will be saved in the database later. Next, we’re adding a Repository of movies. Thanks to Spring Data and Boot’s autoconfigs, this interface is everything we need to save, read, and edit our objects.

package com.example.movies.repositories;

import com.example.movies.model.Movie;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

public interface MovieRepository extends CrudRepository<Movie, Long> {
  Optional<Movie> findByTitle(String title);
}

Now that we have our persistence layer ready to go, it’s time to actually use the Spring Boot framework to configure a REST controller:

package com.example.movies.controllers;

import com.example.movies.exceptions.MovieIdMismatchException;
import com.example.movies.exceptions.MovieNotFoundException;
import com.example.movies.model.Movie;
import com.example.movies.repositories.MovieRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/movies")
public class MovieController {
  final MovieRepository repository;

  public MovieController(MovieRepository repository) {
    this.repository = repository;
  }

  @GetMapping
  public Iterable<Movie> findAll() {
    return repository.findAll();
  }

  @GetMapping(path = "/title/{title}")
  public Movie findMovieByTitle(@PathVariable String title) {
    return repository.findByTitle(title).orElseThrow(MovieNotFoundException::new);
  }

  @GetMapping(path = "/{id}")
  public Movie findMovieById(@PathVariable Long id) {
    return repository.findById(id).orElseThrow(MovieNotFoundException::new);
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public Movie create(@RequestBody Movie movie) {
    return repository.save(movie);
  }

  @DeleteMapping(path = "/{id}")
  public void delete(@PathVariable Long id) {
    Movie movie = repository.findById(id).orElseThrow(MovieNotFoundException::new);
    repository.delete(movie);
  }

  @PutMapping("/{id}")
  public Movie update(@PathVariable Long id, @RequestBody Movie movie) {
    if (id != movie.getId()) {
      throw new MovieIdMismatchException();
    }
    repository.findById(id).orElseThrow(MovieNotFoundException::new);
    return repository.save(movie);
  }
}

To set up a controller, we used @RestController annotation—it’s a convenience annotation that combines @Controller and @ResponseBody annotations. It makes Spring treat the class as a controller of web requests and treat all methods handling requests as if they had a @ResponseBody annotation; the objects returned by handler methods will be bound to HTTP response bodies. @RequestMapping sets the base path of an HTTP request for the whole class.

The next category of annotations used in this controller is request mappings. Default @RequestMapping maps all HTTP request methods for a given address and requires configuration to restrict them. Instead of doing this, we’ll use @GetMapping, @PutMapping, @DeleteMapping, and @PostMapping, which map only the methods in annotation names.

As you can see, just by @Autowiring our MovieRepository, we can perform operations on the in-memory H2 database.

To use proper HTTP response codes, we’ll use some custom exceptions and configure the Spring application to emit proper responses when they are thrown.

package com.example.movies.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Movie not found")
public class MovieNotFoundException extends RuntimeException {}

The @ResponseStatus annotation informs Spring to catch this runtime exception and respond with a given response code and status instead of exiting with an error. By throwing this exception, we’ll respond with 404 Not Found when the user requests an entity that doesn’t exist in the database.

package com.example.movies.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class MovieIdMismatchException extends RuntimeException {}

With this exception, we’ll handle bad requests for object modification. The principle is the same as above.

With the controller created and exceptions handled, let’s create an integration test that will check the correctness of responses to requests.

package com.example.movies.controllers;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.example.movies.model.Movie;
import com.example.movies.repositories.MovieRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class MovieControllerTest {

  @Autowired private MockMvc mockMvc;
  private static final String URL_ROOT = "/api/movies/";
  @Autowired private MovieRepository repository;
  @Autowired ObjectMapper mapper;

  @BeforeEach
  void cleanDatabase() {
    repository.deleteAll();
  }

  @Test
  void findAll_returnsOK() throws Exception {
    mockMvc.perform(get(URL_ROOT)).andExpect(status().isOk());
  }

  @Test
  void findMovieByTitle_ReturnsMovie() throws Exception {
    Movie expected = saveMovie();
    mockMvc
        .perform(get(URL_ROOT + "title/" + expected.getTitle()))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("$.title").value(expected.getTitle()))
        .andExpect(jsonPath("$.director").value(expected.getDirector()))
        .andExpect(jsonPath("$.rating").value(expected.getRating()));
  }

  @Test
  void findMovieByTitle_ThrowsError() throws Exception {
    mockMvc
        .perform(get(URL_ROOT + "title/" + "nonsense"))
        .andExpect(status().isNotFound())
        .andExpect(status().reason("Movie not found"));
  }

  @Test
  void findMovieById_ReturnsMovie() throws Exception {
    Movie expected = saveMovie();
    mockMvc
        .perform(get(URL_ROOT + expected.getId()))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("$.title").value(expected.getTitle()))
        .andExpect(jsonPath("$.director").value(expected.getDirector()))
        .andExpect(jsonPath("$.rating").value(expected.getRating()));
  }

  @Test
  void findMovieById_ThrowsException() throws Exception {
    mockMvc
        .perform(get(URL_ROOT + "5"))
        .andExpect(status().isNotFound())
        .andExpect(status().reason("Movie not found"));
  }

  @Test
  void create_returnsMovie() throws Exception {
    Movie toCreate = createAMovie();
    mockMvc
        .perform(
            post(URL_ROOT)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(toCreate)))
        .andExpect(status().isCreated())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("$.title").value(toCreate.getTitle()))
        .andExpect(jsonPath("$.director").value(toCreate.getDirector()))
        .andExpect(jsonPath("$.rating").value(toCreate.getRating()));
  }

  @Test
  void delete_removesRecord() throws Exception {
    Movie movie = saveMovie();
    mockMvc.perform(delete(URL_ROOT + movie.getId())).andExpect(status().isOk());
    mockMvc.perform(get(URL_ROOT + movie.getId())).andExpect(status().isNotFound());
  }

  @Test
  void delete_throwsError() throws Exception {
    mockMvc.perform(delete(URL_ROOT + "5")).andExpect(status().isNotFound());
  }

  @Test
  void update_changesData() throws Exception {
    Movie movie = saveMovie();
    movie.setRating(1);
    mockMvc
        .perform(
            put(URL_ROOT + movie.getId())
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(movie)))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("$.title").value(movie.getTitle()))
        .andExpect(jsonPath("$.director").value(movie.getDirector()))
        .andExpect(jsonPath("$.rating").value(movie.getRating()));
  }

  @Test
  void update_throwsNotFound() throws Exception {
    Movie movie = saveMovie();
    movie.setId(25);
    mockMvc
        .perform(
            put(URL_ROOT + movie.getId())
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(movie)))
        .andExpect(status().isNotFound());
  }

  @Test
  void update_throwsBadRequest() throws Exception {
    Movie movie = saveMovie();
    long movieId = movie.getId();
    movie.setId(9999);
    mockMvc
        .perform(
            put(URL_ROOT + movieId)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(movie)))
        .andExpect(status().isBadRequest());
  }

  private Movie saveMovie() {
    return repository.save(createAMovie());
  }

  private Movie createAMovie() {
    Movie movie = new Movie();
    movie.setRating(5);
    movie.setTitle("Armageddon");
    movie.setDirector("Michael Bay");
    return movie;
  }
}

In this test, we’re using @SpringBootTest and @AutoConfigureMockMvc—the first one enables Spring to inject application context into the test class. This way we can use the database normally, and we can autowire objects into the test. The second annotation configures the MockMvc object so that we can simulate HTTP requests to our application.

Since there’s no business logic in the application, we won’t be unit testing it. The only thing we can test is the interactions between request handling and database layer.

That’s it for the tutorial, thanks for sticking around to the end. All the code is available on GitHub.

If you have any questions regarding the development of web-based applications or have a project in mind, don’t hesitate to contact us.

Michał Walenia

Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!