Tutorial de inicialização do Spring - Bootstrap uma aplicação simples

Tutorial de inicialização do Spring - Bootstrap uma aplicação simples

1. Visão geral

O Spring Boot é uma adição opinativa e focada na convenção sobre configuração à plataforma Spring - altamente útil para começar com o mínimo esforço e criar aplicativos independentes e com nível de produção.

This tutorial is a starting point for Boot - uma maneira de começar de maneira simples, com um aplicativo web básico.

Veremos algumas configurações básicas, um front-end, manipulação rápida de dados e tratamento de exceções.

Leitura adicional:

Como alterar a porta padrão no Spring Boot

Veja como você pode alterar a porta padrão em um aplicativo Spring Boot.

Read more

Introdução aos Spring Boot Starters

Uma rápida visão geral dos Spring Boot Starters mais comuns, além de exemplos de como usá-los em um projeto do mundo real.

Read more

2. Configuração

Primeiro, vamos usarSpring Initializr para gerar a base para nosso projeto.

O projeto gerado depende do pai de inicialização:


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

As dependências iniciais serão bastante simples:


    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-starter-data-jpa


    com.h2database
    h2

3. Configuração da Aplicação

A seguir, vamos configurar uma classemain simples para nosso aplicativo:

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

Observe como estamos usando@SpringBootApplication como nossa classe de configuração de aplicativo principal; nos bastidores, isso é equivalente a@Configuration,@EnableAutoConfiguration e@ComponentScan juntos.

Por fim, definiremos um arquivoapplication.properties simples - que por enquanto tem apenas uma propriedade:

server.port=8081

server.port altera a porta do servidor do padrão 8080 para 8081; é claro que existem muitos maisSpring Boot properties available.

4. Visualização MVC Simples

Vamos agora adicionar um front end simples usando Thymeleaf.

Primeiro, precisamos adicionar a dependênciaspring-boot-starter-thymeleaf ao nossopom.xml:


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

Isso habilita o Thymeleaf por padrão - nenhuma configuração extra é necessária.

Agora podemos configurá-lo em nossoapplication.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 seguir, definiremos um controlador simples e uma página inicial básica - com uma mensagem de boas-vindas:

@Controller
public class SimpleController {
    @Value("${spring.application.name}")
    String appName;

    @GetMapping("/")
    public String homePage(Model model) {
        model.addAttribute("appName", appName);
        return "home";
    }
}

Finalmente, aqui está nossohome.html:


Home Page

Hello !

Welcome to Our App

Observe como usamos uma propriedade que definimos em nossas propriedades - e depois a injetamos para que possamos mostrá-la em nossa página inicial.

5. Segurança

A seguir, vamos adicionar segurança ao nosso aplicativo - primeiro incluindo o iniciador de segurança:


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

Agora, você deve estar notando um padrão -most Spring libraries are easily imported into our project with the use of simple Boot starters.

Uma vez que a dependência despring-boot-starter-security no caminho de classe do aplicativo - todos os endpoints são protegidos por padrão, usandohttpBasic ouformLogin com base na estratégia de negociação de conteúdo do Spring Security.

É por isso que, se tivermos o iniciador no caminho de classe, devemos geralmente definir nossa própria configuração de segurança personalizada estendendo a classeWebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and().csrf().disable();
    }
}

Em nosso exemplo, estamos permitindo acesso irrestrito a todos os terminais.

 

Claro, Spring Security é um tópico extenso e não é facilmente abordado em algumas linhas de configuração - então, eu definitivamente encorajo você ago deeper into the topic.

6. Persistência Simples

Vamos começar definindo nosso modelo de dados - uma entidadeBook simples:

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

E seu repositório, fazendo bom uso dos Spring Data aqui:

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

Por fim, é claro que precisamos configurar nossa nova camada de persistência:

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

Observe que estamos usando:

  • @EnableJpaRepositories para escanear o pacote especificado para repositórios

  • @EntityScan para pegar nossas entidades JPA

Para manter as coisas simples, estamos usando um banco de dados H2 em memória aqui - para que não tenhamos dependências externas quando executamos o projeto.

Uma vez que incluímos a dependência H2,Spring Boot auto-detects it and sets up our persistence sem necessidade de configuração extra, a não ser as propriedades da fonte de dados:

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=

Claro, como segurança, persistência é um tópico mais amplo do que este conjunto básico aqui, e você devecertainly explore further.

7. Web e o controlador

A seguir, vamos dar uma olhada em uma camada da web - e vamos começar configurando um controlador simples - oBookController.

Implementaremos operações CRUD básicas expondo recursosBook com algumas validações simples:

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

Dado que este aspecto do aplicativo é uma API, usamos a anotação @RestController aqui - que equivale a@Controller junto com@ResponseBody - de modo que cada método empacote o recurso retornado corretamente para a resposta HTTP.

Apenas uma observação que vale a pena destacar - estamos expondo nossa entidadeBook como nosso recurso externo aqui. Isso é bom para nosso aplicativo simples aqui, mas em um aplicativo do mundo real, você provavelmente desejaráseparate these two concepts.

8. Manipulação de erros

Agora que o aplicativo principal está pronto para funcionar, vamos nos concentrar ema simple centralized error handling mechanism usando@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);
    }
}


Além das exceções padrão que estamos tratando aqui, também estamos usando uma exceção personalizada:

BookNotFoundException:

public class BookNotFoundException extends RuntimeException {

    public BookNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    // ...
}

Isso deve dar uma ideia do que é possível com este mecanismo de tratamento de exceção global. Se você gostaria de ver uma implementação completa, dê uma olhada emthe in-depth tutorial.

Observe que Spring Boot também fornece um mapeamento/error por padrão. Podemos personalizar sua visualização criando umerror.html simples:


Error Occurred

    

Error Occurred!

[status] error

message

Como a maioria dos outros aspectos do Boot, podemos controlar isso com uma propriedade simples:

server.error.path=/error2

9. Teste

Por fim, vamos testar nossa nova API de livros.

Usaremos@SpringBootTest imediatamente para carregar o contexto do aplicativo:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { Application.class }, webEnvironment
  = WebEnvironment.DEFINED_PORT)
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");
    }
}

Primeiro, podemos tentar encontrar livros usando métodos variantes:

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

A seguir, testaremos a criação de um novo livro:

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

Atualize um livro existente:

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

E exclua um livro:

@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. Conclusão

Esta foi uma introdução rápida, mas abrangente, ao Spring Boot.

É claro que mal tocamos a superfície aqui - há muito mais nesta estrutura que podemos cobrir em um único artigo de introdução.

O código-fonte completo de nossos exemplos aqui é, como sempre,over on GitHub.