Tutorial de Spring Boot - Bootstrap una aplicación simple

1. Información general

Spring Boot es una adición obstinada y centrada en la convención sobre la configuración para la plataforma Spring, muy útil para comenzar con el mínimo esfuerzo y crear aplicaciones independientes de grado de producción.

Este tutorial es un punto de partida para Boot , una forma de comenzar de manera sencilla, con una aplicación web básica.

Repasaremos algunas configuraciones centrales, un front-end, manipulación rápida de datos y manejo de excepciones.

2. Configuración

Primero, usemos Spring Initializr para generar la base para nuestro proyecto.

El proyecto generado se basa en el padre Boot:

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

Las dependencias iniciales van a ser bastante simples:

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

3. Configuración de la aplicación

A continuación, configuraremos una clase principal simple para nuestra aplicación:

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

Observe cómo estamos usando @SpringBootApplication como nuestra clase de configuración de aplicación principal; detrás de escena, eso es equivalente a @Configuration , @EnableAutoConfiguration y @ComponentScan juntos.

Finalmente, definiremos un archivo application.properties simple , que por ahora solo tiene una propiedad:

server.port=8081 

server.port cambia el puerto del servidor del 8080 predeterminado al 8081; Por supuesto, hay muchas más propiedades Spring Boot disponibles.

4. Vista MVC simple

Ahora agreguemos una interfaz simple usando Thymeleaf.

Primero, necesitamos agregar la dependencia spring-boot-starter-thymeleaf a nuestro pom.xml :

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

Eso habilita Thymeleaf de forma predeterminada; no es necesaria ninguna configuración adicional.

Ahora podemos configurarlo en nuestra 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 

A continuación, definiremos un controlador simple y una página de inicio básica, con un mensaje de bienvenida:

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

Finalmente, aquí está nuestro home.html :

 Home Page  

Welcome to Our App

Observe cómo usamos una propiedad que definimos en nuestras propiedades y luego la inyectamos para que podamos mostrarla en nuestra página de inicio.

5. Seguridad

A continuación, agreguemos seguridad a nuestra aplicación, incluyendo primero el iniciador de seguridad:

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

A estas alturas, es de esperar que esté notando un patrón: la mayoría de las bibliotecas de Spring se importan fácilmente a nuestro proyecto con el uso de iniciadores de arranque simples .

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:

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

@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 { List findByTitle(String title); }

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

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

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.

Por supuesto, apenas tocamos la superficie aquí: hay mucho más en este marco que podemos cubrir en un solo artículo de introducción.

Esa es exactamente la razón por la que no tenemos un solo artículo sobre Boot en el sitio.

El código fuente completo de nuestros ejemplos aquí está, como siempre, en GitHub.