用Quarkus实现干净清晰的Clean架构 - Sourced Blog


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到数据库的完整应用程序。

更详细测试点击标题见原文


 

你好,有源码学习吗

可以在Github上找到本文的代码