Quarkus迅速成为一个不容忽视的框架,因此,我决定再次尝试一下,以查看在编写Quarkus应用程序时可以在多大程度上坚持Clean Architecture(CA)原则。
我的起点是一个基本的Maven项目,该项目在执行CA时具有用于CRUD REST应用程序的5个标准模块:
- domain:领域实体和这些实体的网关接口
- app-api:应用程序的用例接口
- app-impl:使用领域来实现那些用例。取决于app-api和domain。
- infra-persistence:使用数据库API实现域定义的网关。取决于domain。
- infra-web:使用REST将用例暴露于外界。取决于app-api。
此外,我们将创建一个main-partition模块,该模块将用作应用程序的可部署工件。
当您要使用Quarkus时,要做的第一件事是将BOM添加到项目的父POM中。此BOM将管理您将使用的依赖项的所有版本。您还需要在插件管理中为maven项目配置标准插件,例如compile和surefire插件。当我们将使用Quarkus时,您还可以在插件管理中在此处配置Quarkus插件。最后但并非最不重要的一点是,您将配置一个插件来为每个模块运行(如所示<build><plugins>...</plugins></build>),即Jandex插件。由于Quarkus使用CDI,因此Jandex插件会向每个模块添加一个索引文件,该文件包含该模块中使用的每个注释以及指向其使用位置的链接。这将使使用CDI的生活更加轻松,并减少以后的工作量。
现在已经构建了基本结构,我们可以从构建功能正常的应用程序开始。为此,我们首先要确保main-partition创建了一个可执行的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,您需要向infra-webPOM 添加一个依赖项:
<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; } }
|
如果我们现在运行该应用程序,您将能够调用http://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看到GetCustomersImplBean,您需要使用UseCase如下定义的自定义注释。您还可以使用标准ApplicationScoped和Transactional批注,但是创建自己的批注可以使您对这些批注进行逻辑分组,并使实现代码与CDI之类的框架脱钩。
@ApplicationScoped @Transactional @Stereotype @Retention(RetentionPolicy.RUNTIME) public @interface UseCase { }
|
为了使用CDI注解,你必须以下依存增加的POM app-impl除了依赖于app-api和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控制器以使用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()); } ...
|
如果现在运行该应用程序并调用http://localhost:8080/customer,您将看到JimJSON格式。
定义和实施领域
因此,接下来:业务领域。在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除了之外,您还必须添加以下依赖项domain:
<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实体和存储库,我们可以实现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
|
如果现在运行该应用程序并调用http://localhost:8080/customer,您将看到Joe和JimJSON格式。现在,我们有了从REST到数据库的完整应用程序。
更详细测试点击标题见原文