系统组件
文档的 设计架构 页面讨论了 系统组件 和 UI组件。本页面提供有关系统组件以及如何将其转换为Java代码的更多信息。
Vaadin应用程序由系统组件组成,其中一个是用户界面。系统组件的数量取决于应用程序的大小和复杂程度。一个小型应用程序可能只有一个组件。而一个大型应用程序可能拥有十到二十个组件。
以下是包含三个组件的Vaadin应用程序示例:
在此示例中,_Views_组件与_Services_组件交互。接着_Services_组件会与_Entities_组件交互,而_Entities_组件使用JPA与关系型数据库通信。
Java中的组件
Java编程语言中并不存在组件构造概念。取而代之的是,您可以使用Java包来建模组件。因此,上面的组件图对应于以下 Java 包结构:
-
com.example.application是一个根包,它包含了Spring Boot应用程序类。 -
com.example.application.views是对应于 _views_系统组件的包。 -
com.example.application.services是对应于 _services_系统组件的包。 -
com.example.application.entities是对应于 _entities_系统组件的包。
若系统组件较小,则每个组件使用一个包即可。然而,在更复杂的情况下,您通常需要创建子包以保证代码的组织性。您也可能需要将各个系统组件包本身组织成父级包。
应用程序编程接口(API)
系统组件可以具有允许其他组件_调用_它的 应用程序编程接口 (API)。然而,系统组件并非必须使自身提 供给其他组件使用。例如,用户界面系统组件通常只会被Web浏览器调用,并不会被其他系统组件调用。因此,它根本不需要API。
在Java语言中,API即系统组件包内的所有_public_类、接口和方法。换句话说,最简单的具有API的组件仅需一个包含单一public Java类的包,而该类只需有一个public方法。
所有未被视为API一部分的类或方法,应当使用public之外的可见性,例如包私有(package private)。
一个系统组件可以继承其所依赖的另一个组件的API。例如,示例中的服务组件便可以继承实体组件的API,如下所示:
Source code
Java
package com.example.application.services;
import com.example.application.entities.OrderRepository;
import com.example.application.entities.DraftOrder;
import com.example.application.entities.CompletedOrder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService { 1
private final OrderRepository repository;
OrderService(OrderRepository repository) { 2
this.repository = repository;
}
@Transactional
public CompletedOrder completeOrder(DraftOrder draftOrder) { 3
var completedOrder = draftOrder.complete();
return repository.save(completedOrder);
}
}-
OrderService是API的一部分,因此是_public_。 -
服务系统组件负责实例化服务本身。构造方法为_包私有_,因为它不属于API的一部分。
-
completeOrder是API的一部分,因此是_public_。DraftOrder和CompletedOrder是从实体系统组件的API继承而来的。
如您所见,除非您需要或希望使用Java接口,否则不需要为API专门定义接口。
服务提供接口(SPI)
系统组件还可以拥有允许其他组件插入的_服务提供接口_(SPI)。如果组件需要与外部系统交互或需要将某些业务规则外部化到其他组件中,这将非常 有用。
一个SPI是由一个组件_声明_,另一个组件_实现_的接口。在Java中,它至少包含一个Java接口,可能还有该接口所需的其他类型。例如,如果此接口需要一个Java类或Java记录类型作为输入参数或返回值,这类型也会成为SPI的一部分。SPI也可以包含来自系统组件API的类型。
示例中,服务组件可能需要与外部系统集成。无需将所有代码放在单一组件内,服务组件声明一个SPI。然后,再新建一个系统集成组件实现该SPI,并处理与外部系统的实际交互:
这种做法不仅实现了关注点的分离,也保护应用程序免受外部系统变化的影响。如果外部系统的API发生改变,您只需要修改系统集成组件。其他系统组件无需更改。
为区分API和SPI类及接口,您可以将SPI类及接口放入名为`spi`的子包中。示例中,SPI可能如下所示:
Source code
Java
package com.example.application.services.spi; 1
import com.example.application.entities.CompletedOrder;
public interface ShippingSystem {
void shipCompletedOrder(CompletedOrder completedOrder); 2
}-
接口位于`spi`子包中,这清楚表示其意图为由其他系统组件实现。
-
`CompletedOrder`类从实体系统组件的API继承过来,也可用于SPI。
有时,一个接口可以同时作为组件的API和SPI。一个典型的示例是领域模型组件的Repository接口:
Repository接口既是领域模型API的一部分,可由服务组件调用。同时,它也作为领域模型的SPI,由持久化组件实现。持久化组件则与数据库通信。在这种情况下,使用子包`spi`反而会令人困惑。取而代之,应该使用JavaDoc来说明该接口的角色。实际开发中需要灵活变通。
组件的实例化
由于Java不存在组件构造概念,因此组件实例在运行时被表示为普通的Java对象。这些对象由Spring实例化,Spring还通过依赖注入来设置对象之间的依赖关系。应当使用 构造方法注入 到 _final_字段 ,而不是自动装配到可变字段,如下:
Source code
Java
@Service
public class InvoiceGenerationService {
private final InvoiceRepository invoiceRepository;
private final AccountingSystem accountingSystem;
private final ApplicationEventPublisher eventPublisher;
InvoiceGenerationService(InvoiceRepository invoiceRepository,
AccountingSystem accountingSystem,
ApplicationEventPublisher eventPublisher) {
this.invoiceRepository = invoiceRepository;
this.accountingSystem = accountingSystem;
this.eventPublisher = eventPublisher;
}
}构造方法注入拥有多项优点。首先,它清楚地表明类的依赖项是什么。其次,不可能在缺少必需依赖情况下实例化此类。第三,无法在实例化之后意外修改依赖项。如果构造函数参数过多,意味着该类承担了太多职责,您应当将其拆分成更小的部分。
通常情况下,使用Spring的组件扫描以及如`@Component`或`@Service`的注解足以实例化系统组件中的所有对象。然而,如果需要对对象创建过程进行更细粒度的控制,则可以使用Spring基于Java的容器配置。在组件内部,创建一个带有`@Configuration`注解的类,并使用`@Bean`方法创建对象。
除非您需要使用`@Import`注解将配置类导入其他配置类中,否则可以将该类设为包私有,以明确表示此配置类不属于系统组件的API。
若您对Spring基于Java的容器配置不熟悉或希望了解更多,请阅读 Spring Framework 官方文档。

