Spring Boot Tutorial – Bootstrap a Simple Application

This tutorial is a starting point for Boot – a way to get started in a simple manner, with a basic web application.

We"ll 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:

1
2
3
4
5
6
< parent >
     < groupId >org.springframework.boot</ groupId >
     < artifactId >spring-boot-starter-parent</ artifactId >
     < version >2.1.6.RELEASE</ version >
     < relativePath />
</ parent >

The initial dependencies are going to be quite simple:

1
2
3
4
5
6
7
8
9
10
11
12
< 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:

1
2
3
4
5
6
@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:

1
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 MVC View

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:

1
2
3
4
<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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class SimpleController {
     @Value ( "${spring.application.name}" )
     String appName;
 
     @GetMapping ( "/" )
     public String homePage(Model model) {
         model.addAttribute( "appName" , appName);
         return "home" ;
     }
}

Finally, here is our home.html:

1
2
3
4
5
6
7
< 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:

1
2
3
4
< 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 – all endpoints are secured by default, using either httpBasic or formLogin based on Spring Security"s content-negotiation strategy.

That"s why, if we have the starter on the classpath, we should usually define our own custom Security configuration by extending the WebSecurityConfigurerAdapter class:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
             .anyRequest()
             .permitAll()
             .and().csrf().disable();
     }
}

In our example, we"re allowing unrestricted access to all endpoints.

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

Let"s start by defining our data model – a simple Book entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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:

1
2
3
public interface BookRepository extends CrudRepository<Book, Long> {
     List<Book> findByTitle(String title);
}

Finally, we need to of course configure our new persistence layer:

1
2
3
4
5
6
@EnableJpaRepositories ( "com.baeldung.persistence.repo" )
@EntityScan ( "com.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 data source properties:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@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) {
         return bookRepository.findById(id)
           .orElseThrow(BookNotFoundException:: new );
     }
 
     @PostMapping
     @ResponseStatus (HttpStatus.CREATED)
     public Book create( @RequestBody Book book) {
         return bookRepository.save(book);
     }
 
     @DeleteMapping ( "/{id}" )
     public void delete( @PathVariable Long id) {
         bookRepository.findById(id)
           .orElseThrow(BookNotFoundException:: new );
         bookRepository.deleteById(id);
     }
 
     @PutMapping ( "/{id}" )
     public Book updateBook( @RequestBody Book book, @PathVariable Long id) {
         if (book.getId() != id) {
           throw new BookIdMismatchException();
         }
         bookRepository.findById(id)
           .orElseThrow(BookNotFoundException:: new );
         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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
< 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:

1
server.error.path= /error2

9. Testing

Finally, let"s test our new Books API.

We can make use of @SpringBootTest to load the application context and verify there are no errors when running the app:

1
2
3
4
5
6
7
8
@RunWith (SpringRunner. class )
@SpringBootTest
public class SpringContextTest {
 
     @Test
     public void contextLoads() {
     }
}

Next, let"s add a JUnit test that verifies the calls to the API we"re written, using RestAssured:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SpringBootBootstrapLiveTest {
 
     private static final String API_ROOT
       = "http://localhost:8081/api/books" ;
 
     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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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:

1
2
3
4
5
6
7
8
9
10
11
@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.