Tutoriel Spring Boot - Bootstrap une application simple

1. Vue d'ensemble

Spring Boot est un ajout à la plate-forme Spring, axé sur la convention sur la configuration, très utile pour démarrer avec un minimum d'effort et créer des applications autonomes de niveau production.

Ce tutoriel est un point de départ pour Boot - un moyen de démarrer de manière simple, avec une application Web de base.

Nous allons passer en revue une configuration de base, un frontal, une manipulation rapide des données et une gestion des exceptions.

2. Configuration

Tout d'abord, utilisons Spring Initializr pour générer la base de notre projet.

Le projet généré repose sur le parent Boot:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE  

Les dépendances initiales vont être assez simples:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-data-jpa   com.h2database h2 

3. Configuration de l'application

Ensuite, nous allons configurer une classe principale simple pour notre application:

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

Remarquez comment nous utilisons @SpringBootApplication comme classe de configuration d'application principale; dans les coulisses, cela équivaut à @Configuration , @EnableAutoConfiguration et @ComponentScan ensemble.

Enfin, nous définirons un simple fichier application.properties - qui pour l'instant n'a qu'une seule propriété:

server.port=8081 

server.port change le port du serveur de la valeur par défaut 8080 à 8081; il existe bien sûr de nombreuses autres propriétés Spring Boot disponibles.

4. Vue MVC simple

Ajoutons maintenant une interface simple en utilisant Thymeleaf.

Tout d'abord, nous devons ajouter la dépendance spring-boot-starter-thymeleaf à notre pom.xml :

 org.springframework.boot spring-boot-starter-thymeleaf  

Cela active Thymeleaf par défaut - aucune configuration supplémentaire n'est nécessaire.

Nous pouvons maintenant le configurer dans notre 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 

Ensuite, nous définirons un contrôleur simple et une page d'accueil de base - avec un message de bienvenue:

@Controller public class SimpleController { @Value("${spring.application.name}") String appName; @GetMapping("/") public String homePage(Model model) { model.addAttribute("appName", appName); return "home"; } } 

Enfin, voici notre home.html :

 Home Page  

Welcome to Our App

Notez comment nous avons utilisé une propriété que nous avons définie dans nos propriétés - puis nous l'avons injectée afin de pouvoir l'afficher sur notre page d'accueil.

5. Sécurité

Ensuite, ajoutons de la sécurité à notre application - en incluant d'abord le démarreur de sécurité:

 org.springframework.boot spring-boot-starter-security  

À présent, vous remarquerez, espérons-le, un modèle - la plupart des bibliothèques Spring sont facilement importées dans notre projet à l'aide de simples démarreurs de démarrage .

Une fois la dépendance spring-boot-starter-security sur le chemin de classe de l'application - tous les points de terminaison sont sécurisés par défaut, en utilisant httpBasic ou formLogin basé sur la stratégie de négociation de contenu de Spring Security.

C'est pourquoi, si nous avons le démarreur sur le chemin de classe , nous devons généralement définir notre propre configuration de sécurité personnalisée en étendant la classe WebSecurityConfigurerAdapter :

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .permitAll() .and().csrf().disable(); } }

Dans notre exemple, nous autorisons un accès illimité à tous les points de terminaison.

Bien sûr, Spring Security est un sujet étendu et difficile à couvrir en quelques lignes de configuration - je vous encourage donc vivement à approfondir le sujet.

6. Persistance simple

Commençons par définir notre modèle de données - une simple entité Book :

@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; }

Et son référentiel, faisant bon usage de Spring Data ici:

public interface BookRepository extends CrudRepository { List findByTitle(String title); }

Enfin, nous devons bien sûr configurer notre nouvelle couche de persistance:

@EnableJpaRepositories("com.baeldung.persistence.repo") @EntityScan("com.baeldung.persistence.model") @SpringBootApplication public class Application { ... }

Notez que nous utilisons:

  • @EnableJpaRepositories pour analyser le package spécifié à la recherche de référentiels
  • @EntityScan pour récupérer nos entités JPA

Pour simplifier les choses, nous utilisons ici une base de données en mémoire H2 - afin que nous n'ayons aucune dépendance externe lorsque nous exécutons le projet.

Une fois que nous avons inclus la dépendance H2, Spring Boot la détecte automatiquement et configure notre persistance sans avoir besoin de configuration supplémentaire, autre que les propriétés de la source de données:

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) { 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:

@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ BookNotFoundException.class }) protected ResponseEntity 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 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:

 Error Occurred  [status] error 

message

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 can make use of @SpringBootTest to load the application context and verify there are no errors when running the app:

@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:

public class SpringBootBootstrapLiveTest { private static final String API_ROOT = "//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:

@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.

Nous avons bien sûr à peine effleuré la surface ici - il y a beaucoup plus dans ce cadre que nous pouvons couvrir dans un seul article d'introduction.

C'est exactement pourquoi nous n'avons pas qu'un seul article sur Boot sur le site.

Le code source complet de nos exemples ici est, comme toujours, sur GitHub.