"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.
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 @viniciusfcf.
O repositório que estamos utilizando é:
SOA so far
Caso você tenha interesse em outros posts sobre SOA, escrevi alguns insights que podem ser úteis:
- Desacoplamento em APIs com API First
- Fundamentos, Objetivos e Benefícios estratégicos de SOA (parte 1)
- Fundamentos, Objetivos e Benefícios estratégicos de SOA (parte 2)
- Princípios SOA
- Não confunda alhos com bugalhos: SOA e SOAP
- Contract-First em REST com Swagger
- Contract-First em REST
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á.
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 o 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 @viniciusfcf. 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;
}
I'm writing a REST web app (NetBeans 6.9, JAX-RS, TopLink Essentials) and trying to return JSON and HTTP status code. I have code ready and working that returns JSON when the HTTP GET method is called from the client. Essentially:
@Path("get/id")
@GET
@Produces("application/json")
public M_機械 getMachineToUpdate(@PathParam("id") String id) {
//
…
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.
Quarkus, Swagger Ignore method/Class #22429
Describe the bug
Hello,
I have a Quarkus app and i am using Swagger interface for those REST API, and all my methods are visible, i want to ignore some of them to dot be visible when i am using quarkus and i have tried like below, and also added some other @ApiOperation and so one,
When i am using Swagger all my methods are visible on Swagger interface so i dont know how to ignore some of them.
@GET @Produces(MediaType.TEXT_PLAIN) @Operation(hidden = true) @Schema(hidden=true) public TemplateInstance getGetSMTest(@QueryParam("name") String name){ LOG.info("called page /getSMTest"); return getSMTest.data("name", name); }
Best Regards, Daniel Sanchi
Expected behavior
No response
Actual behavior
No response
How to Reproduce?
No response
uname -a
or ver
Output of No response
java -version
Output of No response
GraalVM version (if different from Java)
No response
Quarkus version or git rev
No response
mvnw --version
or gradlew --version
)
Build tool (ie. output of No response
Additional information
No response
"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:
prior to using Eclipse Microprofile we've generated our open-api files via the io.openapitools.swagger:swagger-maven-plugin
This plugin relies on the io.swagger.core.v3:swagger-annotations
maven dependency.
Eclipse Microprofile though comes with a different dependency which does not seem to be enough for the openapitools plugin, therfore the generated open-api file does not contain any…
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
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 @viniciusfcf 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.
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 é:
- Talk is cheap show me the code
- Utilizando Gradle ao invés de Maven
- Plugins do Gradle: Gerenciador de versões de bibliotecas com Versions
- Plugins do Gradle: Saudades do Maven, relatórios com Project-report
- Plugins do Gradle: API First com o OpenAPI Generator
- Plugins do Gradle: API First com o OpenAPI Generator para APIs reativas
- Plugins do Gradle: Validação de vulnerabilidades de dependências com OWASP Dependency Check
- Plugins do Gradle: Lint com Spotless [não publicado]
- GitLab: Motivação para a escolha [não publicado]
- GitLab: Considerações sobre o Prettier em pipelines [não publicado]
- Gitmoji [não publicado]
- SDK Man [não publicado]
- Pré commit hook [não publicado]
- application.yml: Utilizando YML em propriedades de aplicação [não publicado]
- application.yml: Utilizando variáveis de ambiente [não publicado]
- application.yml: Sobrescrevendo opções em teste [não publicado]
- Mapstruct: Utilizando expressões para mapeamentos [não publicado]
- Microprofile SecurityScheme: Sobrescrevendo tokenUrl [não publicado]
- Prometheus: O problema simples que me custou algumas horas [não publicado]
- quarkus-hibernate-reactive-panache: Utilizando as vantagens do Hibernate em projetos reativos [não publicado]
- Testcontainers: Configuração para projetos reativos [não publicado]
Top comments (0)