1. Overview
Spring Boot is a significant addition to the Spring ecosystem. In this tutorial, we’ll discuss how to bootstrap a simple application using Spring Boot.
This tutorial is a starting point for Boot – a way to get started in a simple manner, with a basic web application.
We will go over some core configuration, a front-end, quick data manipulation, and exception handling.
2. Setup
First, let’s use Spring Initializr to generate the base for our project.
The generated project relies on the Boot parent:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> <relativePath /> </parent>
The initial dependencies are going to be quite simple:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency>
3. Application Configuration
Next, we’ll configure a simple main class for our application:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Notice how we’re using @SpringBootApplication as our primary application configuration class; behind the scenes, that’s equivalent to @Configuration, @EnableAutoConfiguration and @ComponentScan together.
Finally, we’ll define a simple application.properties file – which for now only has one property:
server.port=8081
server.port changes the server port from the default 8080 to 8081; there are of course many more Spring Boot properties available.
4. Simple Front-End
Let’s now add a simple front end using Thymeleaf.
First, we need to add the spring-boot-starter-thymeleaf dependency to our pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
That enables Thymeleaf by default – no extra configuration is necessary.
We can now configure it in our application.properties:
spring.thymeleaf.cache = false spring.thymeleaf.enabled=true spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.application.name = Bootstrap Spring Boot
Next, we’ll define a simple controller and a basic home page – with a welcome message:
@Controller public class SimpleController { @Value("${spring.application.name}") String appName; @RequestMapping("/") public String homePage(Model model) { model.addAttribute("appName", appName); return "home"; } }
Finally, here is our home.html:
<html> <head><title>Home Page</title></head> <body> <h1>Hello !</h1> <p>Welcome to <span th:text="${appName}">Our App</span></p> </body> </html>
Note how we used a property we defined in our properties – and then injected that so that we can show it on our home page.
5. Security
Next, let’s add security to our application – by first including the security starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
By now, you’re hopefully noticing a pattern – most Spring libraries are easily imported into our project with the use of simple Boot starters.
Once the spring-boot-starter-security dependency on the classpath of the application – Basic Authentication is enabled by default.
And, just as before, we’re going to do some simple configuration in our application.properties:
security.basic.enabled=true security.user.name=john security.user.password=123
If we don’t specify credentials explicitly – a default username will be used and password will be randomly generated by Boot at startup.
Of course, Spring Security is an extensive topic and one not easily covered in a couple of lines of configuration – so I definitely encourage you to go deeper into the topic.
6. Simple Persistence
On to persistence.
Let’s start by defining our data model – a simple Book entity:
@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Column(nullable = false, unique = true) private String title; @Column(nullable = false) private String author; }
And its repository, making good use of Spring Data here:
public interface BookRepository extends CrudRepository<Book, Long> { List<Book> findByTitle(String title); }
Finally, we need to of course configure our new persistence layer:
@EnableJpaRepositories("org.baeldung.persistence.repo") @EntityScan("org.baeldung.persistence.model") @SpringBootApplication public class Application { ... }
Note that we’re using:
- @EnableJpaRepositories to scan the specified package for repositories
- @EntityScan to pick up our JPA entities
To keep things simple, we’re using an H2 in-memory database here – so that we don’t have any external dependencies when we run the project.
Once we include H2 dependency, Spring Boot auto-detects it and sets up our persistence with no need for extra configuration, other than the datasource properties:
spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1 spring.datasource.username=sa spring.datasource.password=
Of course, like security, persistence is a broader topic than this basic set here, and one you should certainly explore further.
7. Web and the Controller
Next, let’s have a look at a web tier – and we’ll start that by setting up a simple controller – the BookController.
We’ll implement basic CRUD operations exposing Book resources with some simple validation:
@RestController @RequestMapping("/api/books") public class BookController { @Autowired private BookRepository bookRepository; @GetMapping public Iterable findAll() { return bookRepository.findAll(); } @GetMapping("/title/{bookTitle}") public List findByTitle(@PathVariable String bookTitle) { return bookRepository.findByTitle(bookTitle); } @GetMapping("/{id}") public Book findOne(@PathVariable Long id) { Book book = bookRepository.findOne(id); if (book == null) throw new BookNotFoundException(); return book; } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Book create(@RequestBody Book book) { return bookRepository.save(book); } @DeleteMapping("/{id}") public void delete(@PathVariable Long id) { Book book = bookRepository.findOne(id); if (book == null) throw new BookNotFoundException(); bookRepository.delete(id); } @PutMapping("/{id}") public Book updateBook(@RequestBody Book book, @PathVariable Long id) { if (book.getId() != id) { throw new BookIdMismatchException(); } Book old = bookRepository.findOne(id); if (old == null) { throw new BookNotFoundException(); } return bookRepository.save(book); } }
Given this aspect of the application is an API, we made use of the @RestController annotation here – which equivalent to a @Controller along with @ResponseBody – so that each method marshalls the returned resource right to the HTTP response.
Just one note worth pointing out – we’re exposing our Book entity as our external resource here. That’s fine for our simple application here, but in a real-world application, you will likely want to separate these two concepts.
8. Error Handling
Now that the core application is ready to go, let’s focus on a simple centralized error handling mechanism using @ControllerAdvice:
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ BookNotFoundException.class }) protected ResponseEntity<object> handleNotFound( Exception ex, WebRequest request) { return handleExceptionInternal(ex, "Book not found", new HttpHeaders(), HttpStatus.NOT_FOUND, request); } @ExceptionHandler({ BookIdMismatchException.class, ConstraintViolationException.class, DataIntegrityViolationException.class }) public ResponseEntity<object> handleBadRequest(Exception ex, WebRequest request) { return handleExceptionInternal(ex, ex.getLocalizedMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } }
Beyond the standard exceptions we’re handling here, we’re also using a custom exception:
BookNotFoundException:
public class BookNotFoundException extends RuntimeException { public BookNotFoundException(String message, Throwable cause) { super(message, cause); } // ... }
This should give you an idea of what’s possible with this global exception handling mechanism. If you’d like to see a full implementation, have a look at the in-depth tutorial.
Note that Spring Boot also provides an /error mapping by default. We can customize its view by creating a simple error.html:
<html lang="en"> <head><title>Error Occurred</title></head> <body> <h1>Error Occurred!</h1> <b>[<span th:text="${status}">status</span>] <span th:text="${error}">error</span> </b> <p th:text="${message}">message</p> </body> </html>
Like most other aspects in Boot, we can control that with a simple property:
server.error.path=/error2
9. Testing
Finally, let’s test our new Books API.
We’ll immediately make use of @SpringBootTest to load the application context:
@RunWith(SpringRunner.class) @SpringBootTest(classes = { Application.class }, webEnvironment = WebEnvironment.DEFINED_PORT) public class LiveTest { private static final String API_ROOT = "http://localhost:8081/api/books"; @Before public void setUp() { RestAssured.authentication = preemptive().basic("john", "123"); } private Book createRandomBook() { Book book = new Book(); book.setTitle(randomAlphabetic(10)); book.setAuthor(randomAlphabetic(15)); return book; } private String createBookAsUri(Book book) { Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); return API_ROOT + "/" + response.jsonPath().get("id"); } }
First, we can try to find books using variant methods:
@Test public void whenGetAllBooks_thenOK() { Response response = RestAssured.get(API_ROOT); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); } @Test public void whenGetBooksByTitle_thenOK() { Book book = createRandomBook(); createBookAsUri(book); Response response = RestAssured.get( API_ROOT + "/title/" + book.getTitle()); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertTrue(response.as(List.class) .size() > 0); } @Test public void whenGetCreatedBookById_thenOK() { Book book = createRandomBook(); String location = createBookAsUri(book); Response response = RestAssured.get(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertEquals(book.getTitle(), response.jsonPath() .get("title")); } @Test public void whenGetNotExistBookById_thenNotFound() { Response response = RestAssured.get(API_ROOT + "/" + randomNumeric(4)); assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode()); }
Next, we’ll test creating a new book:
@Test public void whenCreateNewBook_thenCreated() { Book book = createRandomBook(); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); assertEquals(HttpStatus.CREATED.value(), response.getStatusCode()); } @Test public void whenInvalidBook_thenError() { Book book = createRandomBook(); book.setAuthor(null); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .post(API_ROOT); assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatusCode()); }
Update an existing book:
@Test public void whenUpdateCreatedBook_thenUpdated() { Book book = createRandomBook(); String location = createBookAsUri(book); book.setId(Long.parseLong(location.split("api/books/")[1])); book.setAuthor("newAuthor"); Response response = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(book) .put(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); response = RestAssured.get(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); assertEquals("newAuthor", response.jsonPath() .get("author")); }
And delete a book:
@Test public void whenDeleteCreatedBook_thenOk() { Book book = createRandomBook(); String location = createBookAsUri(book); Response response = RestAssured.delete(location); assertEquals(HttpStatus.OK.value(), response.getStatusCode()); response = RestAssured.get(location); assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode()); }
10. Conclusion
This was a quick but comprehensive intro to Spring Boot.
We of course barely scratched the surface here – there’s a lot more to this framework that we can cover in a single intro article.
That’s exactly why we don’t just have a single article about Boot on the site.
The full source code of our examples here is, as always, over on GitHub.