O framework Quarkus: como a arquitetura limpa é implementada

Olá, Habr!



À medida que continuamos nossa exploração de novos frameworks Java e seu interesse no livro Spring Boot, estamos olhando para o novo framework Quarkus para Java. Você pode encontrar uma descrição detalhada dele aqui , e hoje oferecemos a você a leitura de um artigo simples que demonstra como é conveniente aderir a uma arquitetura limpa usando o Quarkus .



O Quarkus está rapidamente ganhando o status de uma estrutura que não pode ser evitada. Por isso, resolvi repassá-lo mais uma vez e verificar o quanto ele se dispõe a aderir aos princípios da Arquitetura Pura.



Como ponto de partida, peguei um projeto Maven simples que tem 5 módulos padrão para criar um aplicativo CRUD REST seguindo princípios de arquitetura limpa:



  • domain: objetos de domínio e interfaces de gateway para esses objetos
  • app-api: interfaces de aplicação correspondentes a casos práticos
  • app-impl: implementação desses casos por meio da área disciplinar. Depende de app-apie domain.
  • infra-persistence: Implementa gateways que permitem ao domínio interagir com a API do banco de dados. Depende domain.
  • infra-web: Abre os casos considerados para interagir com o mundo externo usando REST. Depende app-api.


Além disso, criaremos um módulo main-partitionque servirá como um artefato de aplicativo implementável.



Ao planejar trabalhar com o Quarkus, a primeira etapa é adicionar a especificação da BOM ao arquivo POM do seu projeto. Este BOM gerenciará todas as versões de dependências que você usa. Você também precisará configurar plug-ins padrão para projetos maven em sua ferramenta de gerenciamento de plug-ins, como o plug-in surefire . Ao trabalhar com o Quarkus, você também configurará o plugin de mesmo nome aqui. Por último, mas não menos importante, aqui você precisa configurar o plugin para trabalhar com cada um dos módulos (em <build> <plugins> ... </plugins> </build>), ou seja, o plugin Jandex... Como o Quarkus usa CDI, o plugin Jandex adiciona um arquivo de índice a cada módulo; o arquivo contém registros de todas as anotações usadas neste módulo e links que indicam onde quais anotações são usadas. Como resultado, o CDI é muito mais fácil de manusear, com muito menos trabalho a ser feito posteriormente.



Agora que a estrutura básica está pronta, você pode começar a construir um aplicativo completo. Para fazer isso, você precisa garantir que a partição principal crie o aplicativo executável Quarkus. Este mecanismo é ilustrado em qualquer exemplo de “início rápido” fornecido no Quarkus.



Primeiro, configuramos a construção para usar o plugin Quarkus:



<build>
  <plugins>
    <plugin>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-maven-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>build</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>


A seguir, vamos adicionar dependências a cada um dos módulos do aplicativo, onde estarão junto com as dependências quarkus-resteasye quarkus-jdbc-mysql. Na última dependência, pode-se substituir o banco de dados pelo que você mais gosta (considerando que mais tarde iremos para o caminho de desenvolvimento nativo e, portanto, não podemos usar um banco de dados embutido, por exemplo, H2).



Como alternativa, você pode adicionar um perfil para que possa construir o aplicativo nativo posteriormente. Para fazer isso, você realmente precisa de um suporte de desenvolvimento adicional (GraalVM native-imagee XCode se estiver usando OSX).



<profiles>
  <profile>
    <id>native</id>
    <activation>
      <property>
        <name>native</name>
      </property>
    </activation>
    <properties>
      <quarkus.package.type>native</quarkus.package.type>
    </properties>
  </profile>
</profiles>


Agora, se você executar a mvn package quarkus:devpartir da raiz do projeto, terá um aplicativo Quarkus funcionando! Não há muito para ver ainda, uma vez que ainda não temos controladores nem conteúdo.



Adicionando um controlador REST



Neste exercício, vamos da periferia para o centro. Primeiro, vamos criar um controlador REST que retornará os dados do usuário (neste exemplo, é apenas o nome).



Para usar a API JAX-RS, uma dependência deve ser adicionada ao POM de infra-web:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>


O código do controlador mais simples se parece com este:



@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
    @GET
    public List<JsonCustomer> list() {
        return getCustomers.getCustomer().stream()
                .map(response -> new JsonCustomer(response.getName()))
                .collect(Collectors.toList());
    }

    public static class JsonCustomer {
        private String name;

        public JsonCustomer(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


Se executarmos o aplicativo agora, podemos chamar localhost : 8080 / customer e vê-lo Joeno formato JSON.



Adicione um caso específico



A seguir, vamos adicionar um caso e uma implementação para este caso prático. Vamos app-apidefinir o seguinte caso:



public interface GetCustomers {
    List<Response> getCustomers();

    class Response {
        private String name;

        public Response(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}


Eles app-implcriam uma implementação simples dessa interface.



@UseCase
public class GetCustomersImpl implements GetCustomers {
    private CustomerGateway customerGateway;

    public GetCustomersImpl(CustomerGateway customerGateway) {
        this.customerGateway = customerGateway;
    }

    @Override
    public List<Response> getCustomers() {
        return Arrays.asList(new Response("Jim"));
    }
}


Para que o CDI veja o componente GetCustomersImpl, você precisa de uma anotação personalizada UseCaseconforme definido abaixo. Você também pode usar o ApplicationScoped e a anotação padrão Transactional, mas ao criar sua própria anotação, você obtém a capacidade de agrupar o código de maneira mais lógica e separar o código de implementação de estruturas como CDI.



@ApplicationScoped
@Transactional
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
}


Para usar anotações CDI, você deve adicionar as seguintes dependências ao arquivo POM app-impl, além das dependências app-apie domain.



<dependency>
  <groupId>jakarta.enterprise</groupId>
  <artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
  <groupId>jakarta.transaction</groupId>
  <artifactId>jakarta.transaction-api</artifactId>
</dependency>


Em seguida, precisamos modificar o controlador REST para usá-lo nos casos app-api.



...
private GetCustomers getCustomers;

public CustomerResource(GetCustomers getCustomers) {
    this.getCustomers = getCustomers;
}

@GET
public List<JsonCustomer> list() {
    return getCustomers.getCustomer().stream()
            .map(response -> new JsonCustomer(response.getName()))
            .collect(Collectors.toList());
}
...


Se agora você executar o aplicativo e chamar localhost : 8080 / customer, você o verá Jimno formato JSON.



Definição e implementação do domínio



A seguir, vamos nos concentrar no domínio. A essência aqui é domainbastante simples, consiste em Customeruma interface de portal por onde receberemos os consumidores.



public class Customer {
	private String name;

	public Customer(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}
}
public interface CustomerGateway {
	List<Customer> getAllCustomers();
}


Também precisamos fornecer uma implementação do gateway antes de começarmos a usá-lo. Fornecemos essa interface em infra-persistence.



Para esta implementação, usaremos o suporte JPA disponível no Quarkus, e também usaremos o framework Panache , o que tornará nossa vida um pouco mais fácil. Além do domínio, temos que adicionar a infra-persistenceseguinte dependência para:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>


Primeiro, definimos a entidade JPA correspondente ao consumidor.



@Entity
public class CustomerJpa {
	@Id
	@GeneratedValue
	private Long id;
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}


Ao trabalhar com o Panache, você pode escolher uma das duas opções: ou suas entidades herdarão PanacheEntity ou você usará o padrão repositório / DAO. Não sou um fã do padrão ActiveRecord, então eu mesmo paro no repositório, mas você decide com o que você vai trabalhar.



@ApplicationScoped
public class CustomerRepository implements PanacheRepository<CustomerJpa> {
}


Agora que temos nossa entidade e repositório JPA, podemos implementar o gateway Customer.



@ApplicationScoped
public class CustomerGatewayImpl implements CustomerGateway {
	private CustomerRepository customerRepository;

	@Inject
	public CustomerGatewayImpl(CustomerRepository customerRepository) {
		this.customerRepository = customerRepository;
	}

	@Override
	public List<Customer> getAllCustomers() {
		return customerRepository.findAll().stream()
				.map(c -> new Customer(c.getName()))
				.collect(Collectors.toList());
	}
}


Agora você pode alterar o código na implementação do nosso caso, para que ele use o gateway.



...
private CustomerGateway customerGateway;

@Inject
public GetCustomersImpl(CustomerGateway customerGateway) {
    this.customerGateway = customerGateway;
}

@Override
public List<Response> getCustomer() {
    return customerGateway.getAllCustomers().stream()
            .map(customer -> new GetCustomers.Response(customer.getName()))
            .collect(Collectors.toList());
}
...


Não podemos iniciar nosso aplicativo ainda, porque o aplicativo Quarkus ainda precisa ser configurado com os parâmetros de persistência exigidos. No src/main/resources/application.propertiesmódulo, main-partitionadicione os seguintes parâmetros.



quarkus.datasource.url=jdbc:mysql://localhost/test
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.MySQL8Dialect
quarkus.datasource.username=root
quarkus.datasource.password=root
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql


Para visualizar os dados originais, também adicionaremos o arquivo import.sqlao mesmo diretório do qual os dados foram adicionados.



insert into CustomerJpa(id, name) values(1, 'Joe');
insert into CustomerJpa(id, name) values(2, 'Jim');


Se você agora executar o aplicativo e chamada localhost : 8080 / cliente, você vai ver Joeque Jimem formato JSON também. Portanto, temos uma aplicação completa, desde REST até banco de dados.



Opção nativa



Se você deseja construir um aplicativo nativo, você precisa fazer isso usando o comando mvn package -Pnative. Isso pode levar cerca de alguns minutos, dependendo de qual é o seu estande de desenvolvimento. O Quarkus é bastante rápido na inicialização e sem suporte nativo, inicia em 2-3 segundos, mas quando compilado em um executável nativo usando GraalVM, o tempo correspondente é reduzido para menos de 100 milissegundos. Para um aplicativo Java, isso é extremamente rápido.



Testando



Você pode testar o aplicativo Quarkus usando a estrutura de teste Quarkus correspondente. Se você anotar o teste @QuarkusTest, o JUnit primeiro iniciará o contexto do Quarkus e, em seguida, executará o teste. Um teste de todo o aplicativo main-partitionserá mais ou menos assim:



@QuarkusTest
public class CustomerResourceTest {
	@Test
	public void testList() {
		given()
				.when().get("/customer")
				.then()
				.statusCode(200)
				.body("$.size()", is(2),
						"name", containsInAnyOrder("Joe", "Jim"));
	}
}


Conclusão



Em muitos aspectos, o Quarkus é um competidor feroz do Spring Boot. Na minha opinião, algumas coisas no Quarkus são ainda melhor resolvidas. Mesmo que app-impl tenha uma dependência de framework, é apenas uma dependência para anotações (no caso do Spring, quando adicionamos spring-contextpara obter @Component, adicionamos várias dependências centrais do Spring). Se você não gosta disso, também pode adicionar um arquivo Java à seção principal, usando a anotação @ProducesCDI e criando o componente lá; neste caso, você não precisa de dependências adicionais em app-impl. Mas, por alguma razão, jakarta.enterprise.cdi-apiquero ver o vício aí menos do que o vício spring-context.



Quarkus é rápido, muito rápido. É mais rápido que o Spring Boot com este tipo de aplicativo. Visto que, de acordo com a Clean Architecture, a maioria (senão todas) das dependências do framework deve residir fora do aplicativo, a escolha entre Quarkus e Spring Boot se torna óbvia. A este respeito, a vantagem do Quarkus é que foi imediatamente criado tendo em conta o suporte do GraalVM e, portanto, ao custo de um esforço mínimo, permite transformar a aplicação nativa. Spring Boot ainda está atrasado em relação ao Quarkus nesse aspecto, mas não tenho dúvidas de que logo o alcançará.



No entanto, experimentar o Quarkus também me ajudou a perceber os muitos infortúnios que aguardam aqueles que tentam usar o Quarkus com os servidores de aplicativos clássicos Jakarta EE. Embora ainda não haja muito que possa ser feito com o Quarkus, seu gerador de código suporta uma variedade de tecnologias que ainda não são fáceis de usar no contexto de Jakarta EE com um servidor de aplicativos tradicional. O Quarkus cobre todos os fundamentos que as pessoas familiarizadas com Jakarta EE precisarão, e o desenvolvimento nele é muito mais suave. Será interessante ver como o ecossistema Java pode lidar com esse tipo de competição.



Todo o código deste projeto está postado no Github .



All Articles