Cursos que formaram meu caráter: Desenvolvimento web com Quarkus — API First com o OpenAPI Generator

Arthur Magalhaes Fonseca
14 min readDec 17, 2022

“Imaginação é o começo da criação. Você imagina o que deseja, deseja o que imagina e, por fim, cria o que deseja”. George Bernard Shaw

Neste quinto post falaremos sobre um tema que estudo desde 2012 quando tive uma grande experiência com SOA (Service Oriented Architecture).

Falaremos sobre pontos positivos e negativos da abordagem, uma vez que tudo tem trade-offs.

Para trabalhar com Quarkus vamos precisar usar o JAX-RS como ponte para geração de nossos contratos. Como grande fã do Spring vamos entrar em algumas discussões sobre estar ou não atrelado à especificação do Java. Vamos então comparar vantagens e desvantagens de se utilizar o JAX-RS ou abstrações Spring.

Spoiler alert

Prefiro a solução com Spring. E irei explicar o porque.

Esse artigo faz parte de uma série, abaixo é possível encontrar a lista completa de artigos.

Estamos nos baseando no curso Desenvolvimento web com Quarkus do Vinicius Ferraz Campos Florentino.

O repositório que estamos utilizando é:

Caso você tenha interesse em outros posts sobre SOA, escrevi alguns insights que podem ser úteis:

Em 2012 entrei em um instituto de pesquisas, o IBTI. Um instituto que foi bem importante para as minhas concepções arquiteturais.

Entrei em um laboratório de SOA e me lembro de pensar que se tratava de um plugin do Java na época.

SOA me influenciou tanto que um de seus princípios, a Padronização de contrato de serviços acabou me atrasando a entrar no mundo REST. Para mim, se eu não utilizasse Contract-First não estava bom.

Me lembro inclusive de um projeto que escrevi uma vez, que eu tinha definido um projeto multi módulo em que um módulo trabalhava com REST e esse chamava outro módulo que trabalhava com SOAP só porque eu utilizava Contract First lá.

Facepalm

Fico pensando hoje no overhead dessa decisão arquitetural.

São coisas que olho para trás e apesar de não concordar hoje, sei que me ajudaram a chegar aonde estou hoje.

Como dito anteriormente, em um instituto de pesquisas o que é necessário fazer é experimentar novas coisas. E experimentar quer dizer provar coisas que você não sabe se estão certas ou não.

OpenAPI abordagens

Partirei do pré-suposto que você já ouviu falar sobre OpenAPI ou Swagger daqui para frente.

Como abordagens de APIs com Swagger ou OpenAPI podemos dividir em 3 abordagens:

Escrita de contrato e implementação em uma linguagem de programação

Nesse abordagem é definido um contrato e a equipe de desenvolvimento segue a especificação em seu código de uma forma de-para.

Geração de contrato a partir de código

Nessa abordagem iniciamos por nosso código para gerar o nosso contrato.

Geração de código a partir de contrato

Nessa abordagem iniciamos por nosso contrato para a geração de um código. Essa abordagem é conhecida como Contract-First.

Prós e contra de cada abordagem

Na primeira abordagem o principal problema que possuímos é o fato de a implementação ser manual, sendo assim, o programador pode se confundir ou esquecer de algo da especificação.

Na segunda abordagem o principal problema no caso do Java são questões com a serialização e desserialização de classes que especificamos. Podemos utilizar classes que não implementam serialização para gerar nosso Swagger ou OpenAPI.

Ainda na segunda abordagem, a vantagem está em não estarmos atrelados a outras soluções e utilizar implementações que a maioria da comunidade utiliza.

Na terceira abordagem o principal problema é estar atrelado a alguma solução, que pode não ter todas as funcionalidades que precisamos.

Ainda na terceira abordagem, a vantagem é estarmos sempre alinhados a um contrato especificado, evitando a quebra do mesmo.

Nesse post iremos utilizar a terceira abordagem, mesmo tendo o problema de estar atrelado a uma solução de geração de código.

Já escrevi sobre o OpenAPI Generator, porém, precisei fazer alguns ajustes para trabalhar com isso no Quarkus e é sobre isso que vamos iniciar agora.

Mais à frente falaremos sobre outros problemas dessa abordagem.

Principais configuração em nosso projeto

Podemos ver a configuração utilizando API-First em nosso projeto nos arquivos applications/cadastro/build.gradle:

apply from: "$rootDir/plugins/openapigen_cadastro.gradle"

E no arquivo plugins/openapigen_cadastro.gradle:

import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

buildscript {
repositories {
mavenLocal()
maven { url "https://repo1.maven.org/maven2" }
}
dependencies {
classpath "org.openapitools:openapi-generator-gradle-plugin:$openApiGenVersion"
}
}

apply plugin: 'org.openapi.generator'

def apiServerOutput = "$buildDir/generated/openapi-code-server".toString()

task generateApiServer(type: GenerateTask) {
generatorName = "jaxrs-spec"
inputSpec = "$projectDir/src/main/resources/openapi/restaurantes_v1.yml".toString()
outputDir = apiServerOutput
apiPackage = "org.openapi.server.v1.restaurantes.api"
invokerPackage = "org.openapi.v1.restaurantes.invoker"
modelPackage = "org.openapi.v1.restaurantes.model"
configOptions = [
"dateLibrary" : "java8",
"hideGenerationTimestamp": "true",
"interfaceOnly" : "true",
"performBeanValidation" : "true",
"returnResponse" : "true",
"serializableModel" : "true",
"useBeanValidation" : "true",
"useOptional" : "true",
"useSwaggerAnnotations" : "false"
]
}

compileJava.dependsOn(
generateApiServer
)

sourceSets.main.java.srcDir "$apiServerOutput/src/gen/java"

Na configuração apiServerOutput definimos um destino para no qual serão criadas nossas classes com base em nossa OpenAPI.

Na configuração do plugin definimos que utilizaremos o JAX-RS como especificação de nossa API. Lembrando que existem outras abordagens para o generatorName tanto para server quanto para client.

Na configuração inputSpec utilizamos o caminho para nossa OpenAPI no projeto.

apiPackage, invokerPackage e modelPackage são as configurações de nossos packages da aplicação que será gerada:

Sobre as configurações utilizadas em configOptions do jaxrs-spec podemos encontrar mais detalhes no seguinte link.

Definimos então as bibliotecas do java8 para configuração de nossas bibliotecas de data. Não se confunda com o fato de o Java possuir outras versões para além do 8! Para o plugin isso se refere ao fato de usarmos LocalDate, classe que apareceu a partir do Java 8.

hideGenerationTimestamp diz respeito à geração do timestamp de geração estar na interface gerada. Como estamos gerando nossas classes no package build essa é uma configuração que não impacta muito na forma como usamos o plugin.

interfaceOnly diz respeito à geração de interfaces para serem implementadas.

Para nossa abordagem não precisaremos de classes concretas, porque iremos implementar a interface, mas para entender o que mudaria no caso desse parâmetro ser false, pense nos projetos gerados a partir do site https://editor.swagger.io/ > Generate Server > jaxrs-spec. Nesse caso será criado uma classe concreta para ser evoluída.

Para entender nossa abordagem, estamos gerando uma interface Java com base em um contrato OpenAPI, essas classes geradas não precisarão ser versionadas, pois podem ser criadas e recriadas dado evolução da nossa API. O que buscamos é ter a certeza que estamos implementando todos os métodos da interface, e caso mudemos nossa OpenAPI perceber que precisamos estar em compliance com o contrato descrito.

A configuração performBeanValidation e useBeanValidation dizem respeito a utilizar o Bean Validation ou não nas classes geradas, bem como realizar sua validação em camada de Controller. Isso inclusive diz respeito a uma diferença da abordagem do Vinicius Ferraz Campos Florentino. Estamos delegando ao plugin essa validação, porém isso nos trás um problema, não estamos mais controlando o Validation das nossas classes, e com isso estamos atrelados aos lados bons e ruins do plugin. Na solução utilizada pelo Vinícius, ele conseguia ajustar coisas em seus DTOs, no nosso caso estamos delegando isso às classes de modelo geradas pelo plugin. Existem até formas de sobrescrevermos como o plugin gera as classes com o Mustache, mas para fins desse post não pensei em criar configurações próprias, preferi utilizar a geradas pelo próprio plugin.

A configuração returnResponse é uma das principais reclamações que tenho com relação ao JAX-RS. Caso essa solução seja definida como false os métodos gerados terão em sua assinatura as classes da especificação e não a classe javax.ws.rs.core.Response. As vantagens do returnResponse false é a geração de métodos como:

@Path("/restaurantes")
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen")
public interface RestaurantesApi {

...

@POST
@Consumes({ "application/json" })
void cadastraRestaurante(@Valid CadastroRestaurante cadastroRestaurante);

@GET
@Path("/{idRestaurante}/pratos")
@Produces({ "application/json" })
List<Prato> recuperaPratosRestaurante(@PathParam("idRestaurante") Long idRestaurante);

...
}

Os primeiros "problemas" com OpenAPI e JAX-RS

A desvantagem inicial que vi foi no retorno de HTTP Status para 201 Created, pois no caso de métodos de retorno void eu não consegui alterar o HTTP Status, o retorno era sempre 200 Ok. Até vi uma outra abordagem, mas para isso, porém, eu precisaria ajustar a forma como o gerador de código funciona com o Mustache para a adição do parâmetro HttpServletResponse response nos métodos gerados como mostrado por Pierre Henry nessa dúvida do Stack Overflow.

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public User addUser(User user, @Context final HttpServletResponse response){

User newUser = ...

//set HTTP code to "201 Created"
response.setStatus(HttpServletResponse.SC_CREATED);
try {
response.flushBuffer();
}catch(Exception e){}

return newUser;
}

Por outro lado, utilizar a opção returnResponse como true o retorno é a classe javax.ws.rs.core.Response, o problema é que diferente da solução do Spring o Response do JAX-RS não utiliza generics. Observe a diferença do JAX-RS e do Spring para o mesmo problema:

import javax.ws.rs.core.Response;

public interface RestaurantesApi {
@PUT
@Path("/{idRestaurante}/pratos/{idPrato}")
@Consumes({ "application/json" })
Response atualizaPratoRestaurante(
@PathParam("idPrato") Long idPrato,
@PathParam("idRestaurante") Long idRestaurante,
@Valid AtualizacaoPrato atualizacaoPrato
);
@GET
@Path("/{idRestaurante}/pratos")
@Produces({ "application/json" })
Response recuperaPratosRestaurante(@PathParam("idRestaurante") Long idRestaurante);
}
import org.springframework.http.ResponseEntity;

public interface RestaurantesApi {

default ResponseEntity<Void> atualizaPratoRestaurante(
@Parameter(name = "idPrato", description = "", required = true) @PathVariable("idPrato") Long idPrato,
@Parameter(name = "idRestaurante", description = "", required = true) @PathVariable("idRestaurante") Long idRestaurante,
@Parameter(name = "AtualizacaoPrato", description = "") @Valid @RequestBody(required = false) AtualizacaoPrato atualizacaoPrato
) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

}

default ResponseEntity<List<Prato>> recuperaPratosRestaurante(
@Parameter(name = "idRestaurante", description = "", required = true) @PathVariable("idRestaurante") Long idRestaurante
) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"preco\" : 6.027456183070403, \"nome\" : \"nome\", \"id\" : 0, \"descricao\" : \"descricao\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

}
}

Observe que ResponseEntity do Spring utiliza generics e Response do JAX-RS não. Com isso, acho a solução do Spring para o problema muito mais elegante, pois garanto o tipo de entidade do retorno, diferente da opção Response em que poderíamos alterar o objeto de retorno sem que houvesse um erro de compilação.

O código utilizado como referência se encontra em applications/cadastro/src/main/java/com/gitlab/arthurfnsc/ifood/cadastro/RestauranteResource:

    @Override
@Counted(name = "Quantidade buscas restaurante")
@SimplyTimed(name = "Tempo simples de busca")
@Timed(name = "Tempo completo de busca")
public Response recuperaPratosRestaurante(Long idRestaurante) {
Optional<Restaurante> restauranteOp = Restaurante.findByIdOptional(
idRestaurante
);
if (restauranteOp.isEmpty()) {
throw new NotFoundException("Restaurante não existe");
}
List<Prato> pratos = Prato.list("restaurante", restauranteOp.get());
return Response.ok(pratoMapper.paraListaPratoApi(pratos)).build();
}

No exemplo acima, poderíamos colocar qualquer coisa dentro de Response.ok(), porém, isso não geraria erro de compilação:

    @Override
@Counted(name = "Quantidade buscas restaurante")
@SimplyTimed(name = "Tempo simples de busca")
@Timed(name = "Tempo completo de busca")
public Response recuperaPratosRestaurante(Long idRestaurante) {
return Response.ok(LocalDate.now()).build();
}

Lembrando que analisando friamente esse é um problema decorrente da minha escolha arquitetural. O Vinícius não teve esse problema na resolução dele.

Mais “problemas” com OpenAPI e JAX-RS

Outro problema que encontrei foi com anotações do Swagger.

Observe uma classe gerada em um projeto SpringBoot:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

public interface RestaurantesApi {
@Operation(
operationId = "atualizaPratoRestaurante",
tags = { "prato", "restaurante" },
responses = {
@ApiResponse(responseCode = "204", description = "No Content")
},
security = {
@SecurityRequirement(name = "ifood_auth", scopes={ "write:restaurantes" })
}
)
@RequestMapping(
method = RequestMethod.PUT,
value = "/restaurantes/{idRestaurante}/pratos/{idPrato}",
consumes = { "application/json" }
)
default ResponseEntity<Void> atualizaPratoRestaurante(
@Parameter(name = "idPrato", description = "", required = true) @PathVariable("idPrato") Long idPrato,
@Parameter(name = "idRestaurante", description = "", required = true) @PathVariable("idRestaurante") Long idRestaurante,
@Parameter(name = "AtualizacaoPrato", description = "") @Valid @RequestBody(required = false) AtualizacaoPrato atualizacaoPrato
) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}

Nessa abordagem vemos que é documentado o Request e Response do nosso método em nossa interface. Temos um “problema” com o JAX-RS, o Swagger não faz parte da especificação do Java, e precisaríamos utilizar o Microprofile para isso.

“Yes, we are based on microprofile open API and does not support swagger. You can remove the swagger dependency from your classpath”, phillip-kruger

Até vi a issue 795 aberta no OpenAPIGen.

Além de duas abordagens, uma com o Apache Geronimo:

E outra com o MicroGen:

Mas não cheguei a testar nenhuma das duas.

Na abordagem que estamos seguindo, para minha filosofia a escrita de org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema ou org.eclipse.microprofile.openapi.annotations.tags.Tag na nossa classe de Resource configura uma intervenção manual que pode levar a algum problema futuro se eu esquecer de evoluir esse métodos. Por isso, eu optei por não realizar tal configuração.

Por esse motivo também desabilitei a configuração useSwaggerAnnotations, uma vez que não são as anotações que desejamos expor.

Lembrando que com relação os problemas que eu levantei estão atrelados a decisões arquiteturais que eu decidi seguir. O Response do JAX-RS é um que acho que poderia ser melhorado, mas os demais estão mais atrelados à minha escolha pelo OpenAPI Generation.

Demais configurações

As próximas configurações do plugin dizem respeito ao vínculo da geração de código à task de compilação de Java, bem como colocar as classes geradas no nosso sourceSet.

compileJava.dependsOn(
generateApiServer
)

sourceSets.main.java.srcDir "$apiServerOutput/src/gen/java"

Uma vez que essas configurações estejam definidas basta executar a task generateApiServer ou mesmo a task compileJava ou mesmo alguma que dependa dela, como build, test.

Com isso, nossas classes serão geradas em “$buildDir/generated/openapi-code-server”.toString() com base no OpenAPI definido em applications/cadastro/src/main/resources/openapi/restaurantes_v1.yml.

Na nossa classe de Resource applications/cadastro/src/main/java/com/gitlab/arthurfnsc/ifood/cadastro/RestauranteResource implentamos a interface gerada:

import org.openapi.server.v1.restaurantes.api.RestaurantesApi;

public class RestauranteResource implements RestaurantesApi {

}

Isso já será o suficiente para recebermos um erro de compilação, pois precisamos implementar os métodos da interface que estamos implementando.

Essa é uma das vantagens do conceito de API First! Imagine que adicionemos outros métodos em nossa OpenAPI ou que removamos um método ou que adicionemos mais parâmetros em um método. Quando executarmos a task de geração de código receberemos erro de compilação, seja por métodos que removemos, seja por métodos que precisamos adicionar ou adicionar parâmetros.

Conclusão e observações da decisão arquitetural

Decision

A escolha arquitetural da utilização de API First com Open API Generator nos trouxe alguns benefícios como:

  • Acoplamento com o contrato de serviço e suas evoluções.
  • Facilidade na descoberta de quebra de contrato de APIs definidas.

Porém, também nos trouxe alguns desafios que não foram passados pelo Vinicius Ferraz Campos Florentino dado o fato de outra abordagem arquitetural:

  • Liberdade de implementação de endpoints
  • Gerador do JAX-RS não estar 100% alinhado com nossas expectativas.

Para além disso, gostaria de trazer mais alguns pontos que já testei com a abordagem e coisas que percebi que ela não resolve:

OpenAPI Generator sempre vai estar defasado com relação à especificação

Um ponto interessante para gerar uma discussão sobre o OpenAPI Generator é que ele invariavelmente vai estar defasado com relação ao número de features da própria especificação OpenAPI, afinal, quando uma abordagem como essa é criada, o foco inicial é na resolução dos problemas mais comuns e não de todos os problemas.

Pegue por exemplo uma coisa bem bacana do OpenAPI 3, a reutilização de responses com component responses. O código a seguir está na seção Reusing Responses:

/users:
get:
summary: Gets a list of users.
response:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ArrayOfUsers'
'401':
$ref: '#/components/responses/Unauthorized' # <-----
/users/{id}:
get:
summary: Gets a user by ID.
response:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'401':
$ref: '#/components/responses/Unauthorized' # <-----
'404':
$ref: '#/components/responses/NotFound' # <-----
# Descriptions of common components
components:
responses:
NotFound:
description: The specified resource was not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
# Schema for error response body
Error:
type: object
properties:
code:
type: string
message:
type: string
required:
- code
- message

Veja que dessa forma podemos reusar as seguintes especificações:

        '401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'

Isso tornaria nossa especificação mais enxuta. Há alguns anos atrás quando testei isso não funcionava com o gerador. Testando hoje, vi que funciona, porém, é bem comum ver pontos de melhoria e adequação solicitados nas issues do projeto.

Invariavelmente ficamos atrelados ao gerador para o bem ou para o mal.

OpenAPI Generator definitivamente não combina com Serverless

Se você clonou o projeto e importou em sua IDE, provavelmente percebeu vários erros decorrentes das classes que precisam ser geradas para que sejam encontradas. Por isso colocamos o seguinte código na nossa configuração.

compileJava.dependsOn(
generateApiServer
)

Se por um acaso você leu o README e foi executar o projeto, percebeu o seguinte output:

./gradlew clean :applications:cadastro:quarkusDev
################################################################################
# Thanks for using OpenAPI Generator. #
# Please consider donation to help us maintain this project 🙏 #
# https://opencollective.com/openapi_generator/donate #
################################################################################
Successfully generated code to /home/arthurfnsc/repos/udemy/ifood/applications/cadastro/build/generated/openapi-code-server
Listening for transport dt_socket at address: 5005

Note que para executar o projeto do zero precisamos criar algumas classes: Successfully generated code to /home/arthurfnsc/repos/udemy/ifood/applications/cadastro/build/generated/openapi-code-server.

AWS Lambda Function

No exemplo acima, imagine que o nosso projeto AWS Lambda Function utilize essa estratégia. Iremos perder um tempo para executar a requisição porque vamos precisar gerar código antes. E, mesmo depois disso, se nossa Lambda entrar em um contexto de inativação, será necessário gerar novamente código, onerando o processo.

No demais, lembrem-se, não existe bala de prata!

No próximo post falaremos da abordagem de API First em projetos de APIs reativas

Esse post faz parte de uma série sobre Cursos que formaram meu caráter: Desenvolvimento web com Quarkus.

A série completa é:

Original post published at https://dev.to on December 15th, 2022.

--

--