随着我们继续我们的新的Java框架的探索和你的兴趣春天启动的书,我们正在寻找在Java的新Quarkus框架。您可以在此处找到它的详细描述,今天,我们建议阅读一篇简单文章的翻译,以证明使用Quarkus坚持使用干净的体系结构有多方便。
Quarkus正在迅速获得无法回避的框架的地位。因此,我决定再次检查它,并检查它在多大程度上遵循了纯体系结构的原则。
首先,我采用了一个简单的Maven项目,该项目具有5个标准模块,以遵循干净的体系结构原理创建CRUD REST应用程序:
domain
:域对象和这些对象的网关接口app-api
:与实际情况相对应的应用程序接口app-impl
:通过主题领域实施这些案例。取决于app-api
和domain
。infra-persistence
:实现允许域与数据库API交互的网关。取决于domain
。infra-web
:打开考虑的案例,以便使用REST与外界进行交互。取决于app-api
。
另外,我们将创建一个模块
main-partition
,该模块将用作可部署的应用程序工件。
当计划使用Quarkus时,第一步是将BOM规范添加到项目的POM文件中。该BOM将管理您使用的所有版本的依赖项。您还需要在插件管理工具中为maven项目配置标准插件,例如surefire插件。在使用Quarkus时,您还将在此处配置同名的插件。最后但并非最不重要的一点,在这里您需要配置插件以与每个模块(在<build> <plugins> ... </ plugins> </ build>中)一起使用,即Jandex插件...由于Quarkus使用CDI,因此Jandex插件向每个模块添加了一个索引文件。该文件包含此模块中使用的所有注释的记录以及指示在何处使用注释的链接。结果,CDI易于处理,以后的工作也少得多。
现在基本结构已经准备就绪,您可以开始构建完整的应用程序。为此,您需要确保主分区创建了Quarkus可执行应用程序。 Quarkus提供的任何“快速启动”示例中都说明了这种机制。
首先,我们将构建配置为使用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>
接下来,让我们向每个应用程序模块添加依赖项,它们将与依赖项
quarkus-resteasy
和一起出现quarkus-jdbc-mysql
。在最后一个依赖项中,您可以用最喜欢的数据库替换该数据库(考虑到稍后我们将采用本机开发路径,因此我们不能使用嵌入式数据库,例如H2)。
或者,您可以添加配置文件,以便以后可以构建本机应用程序。为此,您确实需要一个额外的开发平台(GraalVM
native-image
和XCode(如果使用的是OSX))。
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
现在,如果您
mvn package quarkus:dev
从项目根目录运行,您将拥有一个可运行的Quarkus应用程序!没什么可看的,因为我们既没有控制器也没有内容。
添加REST控制器
在本练习中,让我们从外围到核心。首先,让我们创建一个REST控制器,该控制器将返回用户数据(在本示例中,这只是名称)。
要使用JAX-RS API,必须将依赖项添加到Web POM:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
最简单的控制器代码如下所示:
@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;
}
}
如果现在运行该应用程序,则可以调用localhost:8080 / customer并
Joe
以JSON格式查看它。
添加特定案例
接下来,让我们为该实际案例添加案例和实现。让我们
app-api
定义以下情况:
public interface GetCustomers {
List<Response> getCustomers();
class Response {
private String name;
public Response(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
将
app-impl
创建一个简单的实现这个接口。
@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"));
}
}
为了使CDI能够看到该组件
GetCustomersImpl
,您需要一个自定义注释UseCase
,如下所示。您也可以使用标准的ApplicationScoped和批注Transactional
,但是通过创建自己的批注,您可以从逻辑上对代码进行分组,并从CDI之类的框架中分离实现代码。
@ApplicationScoped
@Transactional
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
}
要使用CDI批注,
app-impl
除了app-api
和依赖项外,还必须将以下依赖项添加到POM文件中domain
。
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
</dependency>
接下来,我们需要修改REST控制器以在case中使用它
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());
}
...
如果现在运行该应用程序并调用localhost:8080 / customer,您将以
Jim
JSON格式看到它。
领域的定义和实施
接下来,我们将专注于领域。这里的本质
domain
非常简单,它包含Customer
一个网关接口,通过该接口我们将接收消费者。
public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public interface CustomerGateway {
List<Customer> getAllCustomers();
}
在开始使用网关之前,我们还需要为网关提供一个实现。我们在中提供了这样的接口
infra-persistence
。
对于此实现,我们将使用Quarkus中提供的JPA支持,并使用Panache框架,这将使我们的生活更加轻松。除了域,我们还必须添加
infra-persistence
以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
首先,我们定义与使用者相对应的JPA实体。
@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;
}
}
使用Panache时,可以选择以下两个选项之一:您的实体将继承PanacheEntity,或者您将使用存储库/ DAO模式。我不喜欢ActiveRecord模式,因此我自己会停在存储库中,但是您将要使用什么。
@ApplicationScoped
public class CustomerRepository implements PanacheRepository<CustomerJpa> {
}
现在我们有了JPA实体和存储库,我们可以实现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());
}
}
现在您可以在我们的案例的实现中更改代码,以便它使用网关。
...
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());
}
...
我们还不能启动我们的应用程序,因为Quarkus应用程序仍然需要使用必需的持久性参数进行配置。在
src/main/resources/application.properties
模块中,main-partition
添加以下参数。
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
要查看原始数据,我们还将文件添加
import.sql
到添加数据的同一目录中。
insert into CustomerJpa(id, name) values(1, 'Joe');
insert into CustomerJpa(id, name) values(2, 'Jim');
如果现在运行该应用程序并调用localhost:8080 / customer,您也会以JSON格式看到
Joe
它Jim
。因此,我们有了一个完整的应用程序,从REST到数据库。
本机选项
如果要构建本机应用程序,则需要使用命令来执行此操作
mvn package -Pnative
。这可能需要几分钟,具体取决于您的开发立场。Quarkus在没有本机支持的情况下启动速度非常快,启动时间为2-3秒,但是使用GraalVM编译为本机可执行文件时,相应的时间减少到不到100毫秒。对于Java应用程序,这非常快。
测试中
您可以使用相应的Quarkus测试框架来测试Quarkus应用程序。如果您对测试进行注释
@QuarkusTest
,则JUnit将首先启动Quarkus上下文,然后执行测试。对整个应用程序的测试main-partition
将如下所示:
@QuarkusTest
public class CustomerResourceTest {
@Test
public void testList() {
given()
.when().get("/customer")
.then()
.statusCode(200)
.body("$.size()", is(2),
"name", containsInAnyOrder("Joe", "Jim"));
}
}
结论
在许多方面,Quarkus是Spring Boot的激烈竞争者。我认为,Quarkus中的某些问题甚至可以更好地解决。即使app-impl具有框架依赖关系,也仅是注释的依赖关系(在Spring的情况下,当我们添加
spring-context
到get时@Component
,我们会添加很多Spring核心依赖关系)。如果您不喜欢这样,还可以使用@Produces
CDI的注释并在其中创建组件,将Java文件添加到主体部分。在这种情况下,您不需要中的任何其他依赖项app-impl
。但是出于某种原因,jakarta.enterprise.cdi-api
我希望那里的瘾比瘾少spring-context
。
Quarkus非常快。使用这种类型的应用程序,它比Spring Boot更快。由于根据Clean Architecture,框架的大多数(如果不是全部)依赖项都应驻留在应用程序的外部,因此Quarkus和Spring Boot之间的选择变得显而易见。在这方面,Quarkus的优势在于它是在考虑到GraalVM支持的情况下立即创建的,因此,以最小的努力为代价,它使您可以将应用程序转换为本地应用程序。在这方面,Spring Boot仍然落后于Quarkus,但我毫不怀疑它将很快赶上。
没错,尝试Quarkus还帮助我意识到了许多不幸,等待那些尝试将Quarkus与经典Jakarta EE应用程序服务器一起使用的人。尽管Quarkus尚无法完成很多工作,但其代码生成器支持多种技术,这些技术在Jakarta EE与传统应用程序服务器的环境中尚不易使用。Quarkus涵盖了熟悉Jakarta EE的人们将需要的所有基础知识,并且在此基础上的开发更加顺畅。看看Java生态系统如何处理这种竞争将很有趣。
该项目的所有代码都发布在Github上。