Setup Inicial
Vamos criar um API Rest para avaliação de livros, que será compatível com nossa API de livros criada nos tutoriais do nosso primeiro projeto Rest em SpringBoot.
Precisaremos criar um novo projeto no maven, o qual usaremos para configurar e codificar a nossa API.
Poderíamos criar o projeto do maven via IDE (Eclipse, IntelliJ, NetBeans e etc..), ou via linha de comando como o exemplo abaixo:
1 2 3 4 5 |
mvn archetype:generate \ -DgroupId=br.com.marcelferry.workshop \ -DartifactId=rating-rest \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false |
Por ambos os métodos, teríamos de remover as classes desnecessárias e fazer as configurações das dependências.
Mas nesse tutorial, vamos utilizar a ferramenta Spring Initializr que auxilia muito na geração de projetos SpringBoot, reduzindo muito o nosso tempo de bootstrap. Vamos acessá-lo em:
1 2 |
https://start.spring.io/ |
Teremos acesso uma página simples e bem clara:
Digite o nome do grupo:
1 2 |
br.com.marcelferry.workshop |
Digite o nome do artefato:
1 2 |
rating-rest |
Selecione nas dependências, a Web:
Selecione JPA:
E clique em Generate Project.
O Navegador fará o download de um arquivo compactado contendo um projeto maven que poderá ser importado na sua IDE.
Vejamos o pom.xml gerado automaticamente pelo site:
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 47 48 49 50 51 52 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>br.com.marcelferry.workshop</groupId> <artifactId>rating-rest</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>rating-rest</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
Observe que o projeto já contém as dependencias necessária para a construção e execução de testes unitários.
Veja também que esse projeto criado já tem nossa classe principal do SpringBoot para inicialização de nossa API. Busque por RatingRestApplication.java, ela deverá ser parecida com o código abaixo:
1 2 3 4 5 6 7 |
@SpringBootApplication public class RatingRestApplication { public static void main(String[] args) { SpringApplication.run(RatingRestApplication.class, args); } } |
Vejá também que o projeto já tem uma classe de teste base para criarmos nossos testes unitários. Busque por RatingRestApplicationTests.java:
1 2 3 4 5 6 7 8 9 |
@RunWith(SpringRunner.class) @SpringBootTest public class RatingRestApplicationTests { @Test public void contextLoads() { } } |
Para podermos usar o projeto junto com o demais vamos alterar a porta utilizada por padrão pelo projeto, alterando o application.properties que já veio em nosso projeto. Vamos adicionar a seguinte propriedade no arquivo:
1 |
server.port=8084 |
API REST
Banco de Dados
Nós vamos utilizar o H2 Database, que permite que prototipemos projetos rapidamente, já que em um única biblioteca temos o Driver JDBC e o próprio Database Manager. Vamos insirir o bloco de dependência abaixo no pom.xml
1 2 3 4 5 |
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.193</version> </dependency> |
Agora, vamos criar nossa classe de Entidade conforme abaixo:
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 47 48 49 50 |
@Entity public class Rating { @Id private Long id; @Column(name = "book_id") private Long bookId; private int stars; public Rating() { } public Rating(Long bookId, int stars) { super(); this.bookId = bookId; this.stars = stars; } public Rating(Long id, Long bookId, int stars) { super(); this.id = id; this.bookId = bookId; this.stars = stars; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getBookId() { return bookId; } public void setBookId(Long bookId) { this.bookId = bookId; } public int getStars() { return stars; } public void setStars(int stars) { this.stars = stars; } } |
Com base nessa classe, vamos agora criar uma nova classe em nosso projeto que será responsável pelas chamadas ao banco de dados.
1 2 3 |
public interface RatingRepository extends JpaRepository<Rating, Long> { List<Rating> findByBookId(@Param("bookId") Long bookId); } |
Dessa forma toda a camada de persistência estará pronta para ser usada.
Vamos agora acertar os últimos detalhes do banco de dados. Adicione os seguintes itens no seu application.properties
.
1 2 3 4 5 6 7 8 |
# Datasource spring.datasource.url=jdbc:h2:file:~/ratings spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.datasource.initialize=true spring.datasource.schema=classpath:schema.sql spring.jpa.hibernate.ddl-auto=update |
Rest Controller
Vamos criar uma classe para servir de Controller de nosso projeto REST conforme exemplo abaixo:
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 |
@RestController @RequestMapping("/ratings") public class RatingController { @Resource RatingRepository ratingRepository; @GetMapping("/") public List<Rating> findAllRatings() { return ratingRepository.findAll(); } @PostMapping("/") @ResponseStatus(HttpStatus.CREATED) public Rating insertRatings(@RequestBody Rating rating) { return ratingRepository.save(rating); } @DeleteMapping("/{ratingId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteRating(@PathVariable("ratingId") Long ratingId) { ratingRepository.delete(ratingId); } @GetMapping("/search/findByBookId") public List<Rating> findByBookId(@RequestParam("bookId") Long bookId) { return ratingRepository.findByBookId(bookId); } } |
Podemos testar nossa aplicação rodando nossa aplicação usando:
1 |
mvn spring-boot:run |
E acesse o sistema em:
1 2 |
http://localhost:8084/ratings/ |
Documentação com Swagger
O Spring Fox é o framework construído no topo da API Swagger e integra-se ao framework do Spring. Spring Fox fornecerá as anotações que para integração com o framework do Swagger.
Em nosso projeto vamos usar o Swagger para documentar automaticamente nossa API.
Para isso, vamos adicionar as dependências necessárias no POM.xml.
1 2 3 4 5 6 7 8 9 10 11 |
<!-- Adicionar documentação automatizada de Swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> |
Para utilizarmos o SpringFox em nosso projeto iremos criar uma classe de configuração com o nome SwaggerConfig, contendo a anotação @Configuration junto com o anotação @EnableSwagger2 com o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build(); } } |
Podemos rodar nosso projeto agora e vamos o serviço disponibilizado pelo Swagger:
1 |
http://localhost:8084/v2/api-docs |
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
{ "swagger":"2.0", "info":{ "description":"Api Documentation", "version":"1.0", "title":"Api Documentation", "termsOfService":"urn:tos", "contact":{ }, "license":{ "name":"Apache 2.0", "url":"http://www.apache.org/licenses/LICENSE-2.0" } }, "host":"localhost:8084", "basePath":"/", "tags":[ /* ... */ { "name":"health-mvc-endpoint", "description":"Health Mvc Endpoint" }, /* ... */ ], "paths":{ /* ... */ "/health":{ "get":{ "tags":[ "health-mvc-endpoint" ], "summary":"invoke", "operationId":"invokeUsingGET_16", "consumes":[ "application/json" ], "produces":[ "application/vnd.spring-boot.actuator.v1+json", "application/json" ], "responses":{ "200":{ "description":"OK", "schema":{ "type":"object" } }, "401":{ "description":"Unauthorized" }, "403":{ "description":"Forbidden" }, "404":{ "description":"Not Found" } } } }, "/health.json":{ "get":{ "tags":[ "health-mvc-endpoint" ], "summary":"invoke", "operationId":"invokeUsingGET_17", "consumes":[ "application/json" ], "produces":[ "application/vnd.spring-boot.actuator.v1+json", "application/json" ], "responses":{ "200":{ "description":"OK", "schema":{ "type":"object" } }, "401":{ "description":"Unauthorized" }, "403":{ "description":"Forbidden" }, "404":{ "description":"Not Found" } } } }, /* ... */ }, /* ... */ } |
O retorno será um json enorme, a princípio incompreensível. O próprio Swagger nos fornece uma interface amigável para visualizarmos esse json.
1 |
http://localhost:8084/swagger-ui.html |
Vamos alterar agora a nossa classe SwaggerConfig.java para exibir apenas os endpoints que precisamos:
1 2 3 4 5 6 7 8 9 10 11 |
public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(apis()) .paths(PathSelectors.any()) .build(); } private Predicate<RequestHandler> apis() { return RequestHandlerSelectors.basePackage("br.com.marcelferry.workshop.ratingrest"); } |
Veja agora como ficou nossa interface de documentação:
Vamos agora personalizar as informações gerais da API. Alteremos a classe SwaggerConfig:
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 |
public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(apis()) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private Predicate<RequestHandler> apis() { return RequestHandlerSelectors.basePackage("br.com.marcelferry.workshop.ratingrest"); } private ApiInfo apiInfo() { ApiInfo apiInfo = new ApiInfoBuilder() .title ("API de Avaliação de Livros") .description ("Essa é a API de Avaliação de Livros.") .license("Apache License Version 2.0") .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0") .termsOfServiceUrl("/service.html") .version("1.0.0") .contact(new Contact("Marcel Ferry","www.marcelferry.com.br", "marcelferry@marcelferry.com.br")) .build(); return apiInfo; } |
O resultado será algo parecido com esse:
Vamos agora alterar a classe RatingController
1 2 3 4 5 6 7 8 9 10 |
@ApiOperation(value = "Lista todas as Avaliações", notes = "Lista todas as Avaliações", response = Rating.class, responseContainer = "List" ) @ApiResponses(value = { @ApiResponse(code = 200, message = "Avaliações Listadas com sucesso"), @ApiResponse(code = 401, message = "You are not authorized to view the resource"), @ApiResponse(code = 403, message = "Accessing the resource you were trying to reach is forbidden"), @ApiResponse(code = 404, message = "The resource you were trying to reach is not found") }) @GetMapping public List<Rating> findAllRatings() { return ratingRepository.findAll(); } |
Veremos o resultado no método /ratings via GET.
Vamos agora alterar todos os demais métodos conforme fizemos com o primeiro:
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 |
@ResponseStatus(HttpStatus.CREATED) @ApiOperation(value = "Insere uma Avaliação", notes = "Insere uma Avaliação", response = Rating.class ) @ApiResponses({ @ApiResponse(code = 201, message = "Inclusão com sucesso de uma avaliação") }) @PostMapping public Rating insertRatings(@RequestBody Rating rating) { return ratingRepository.save(rating); } @ResponseStatus(HttpStatus.NO_CONTENT) @ApiOperation(value = "Exclui uma Avaliação", notes = "Exclui uma Avaliação") @ApiResponses({ @ApiResponse(code = 204, message = "Exclusão com sucesso de uma avaliação") }) @DeleteMapping("{ratingId}") public void deleteRating(@PathVariable("ratingId") Long ratingId) { ratingRepository.delete(ratingId); } @ResponseStatus(HttpStatus.NO_CONTENT) @ApiOperation(value = "Atualizar Avaliação", notes = "Atualizar Avaliação") @ApiResponses({ @ApiResponse(code = 204, message = "Atualização com sucesso de uma avaliação") }) @PutMapping("{ratingId}") public void updateRating(@PathVariable("ratingId") Long ratingId, @RequestBody Rating rating) { ratingRepository.delete(ratingId); } @ResponseStatus(HttpStatus.OK) @ApiOperation(value = "Obtém uma Avaliação", notes = "Obtém uma Avaliação") @ApiResponses({ @ApiResponse(code = 200, message = "Existe uma avaliação") }) @GetMapping("search/findByBookId") public List<Rating> findByBookId(@RequestParam("bookId") Long bookId) { return ratingRepository.findByBookId(bookId); } |
Agora podemos atualizar e ver nossos endpoints:
Para aumentar os detalhes vamos acrescentar novas informações em nossa classe Rating.java:
1 2 3 4 5 6 7 8 |
@Id @ApiModelProperty(notes = "Identificador único da avalição", required = true) private Long id; @Column(name = "book_id") @ApiModelProperty(notes = "Identificador do livro ao qual a avaliação se refere", required = true) private Long bookId; @ApiModelProperty(notes = "Valor da avaliação", required = true) private int stars; |
Veja o resultado nessa mudança no método POST:
Muito obrigado cara, Sou o Gilberto da Generation, e eu fico feliz por saber que a turma em que eu faço parte, está usando o seu modelo como exemplo
Obrigada por compartilhar esse tutorial conosco.