15. [TD]:为 Web 服务创建客户端
关键词:多层架构、Spring、依赖注入、Web服务 / jSON、客户端 / 服务器
15.1. Support
![]() |
本章的项目位于文件夹 [support / chap-15] 中。
15.2. 客户端/服务器架构
我们要构建以下客户端/服务器架构:
![]() |
[ui]层即第9和第10节中已开发的层。 之所以能够实现这一点,是因为上文的 [métier] 层将实现与第 8 段中 [métier] 层相同的 [IElectionsMetier] 接口:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// 获取参赛名单
public ListeElectorale[] getListesElectorales();
// 待填补席位数
public int getNbSiegesAPourvoir();
// 选举门槛
public double getSeuilElectoral();
// 结果登记
public void recordResultats(ListeElectorale[] listesElectorales);
// 席位计算
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
在第7段中,[DAO]层与SGBD进行数据交换。在此,[DAO]层与Web服务器/jSON进行数据交换。
首先,我们将关注以下架构:
![]() |
15.3. Eclipse 项目
Eclipse 项目如下:
![]() | ![]() | ![]() |
该结构与第 13.6.1 节中的示例项目结构相同。我们将遵循相同的步骤。
15.4. Maven 配置
即第13.6.2节中描述的配置。
15.5. [DAO] 层的实现
![]() |
![]() |
- 包 [elections.client.config] 包含 [DAO] 层的 Spring 配置;
- 包 [elections .client.dao] 包含 [DAO] 层的实现;
- 包 [elections .client.entities] 包含与 Web 服务 / jSON 交换的对象;
- 包 [elections .client.metier] 包含层 [métier]
- 包 [elections .client.ui] 包含层 [UI]
15.5.1. [métier] 层的配置
![]() |
类 [MetierConfig] 负责对层 [métier] 进行 Spring 配置。其代码如下:
package elections.client.config;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
@ComponentScan({ "elections.client.dao","elections.client.metier" })
public class MetierConfig {
// 常量
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// 组件创建 RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// 数据交换超时
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// 结果
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// 映射器 jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
该代码已在第 13.6.3.1 节中进行过说明。由于此处无需管理 jSON 过滤器,因此代码更为简洁。
15.5.2. 实体
![]() |
由 [DAO] 和 [métier] 层处理的实体,正是它们与 Web 服务 / jSON 交换的实体。 这些是类型为 [ElectionsConfig] 和 [ListeElectorale] 的对象。在服务器端,这些实体带有持久化注解 JPA。在此,这些注解已被移除。为便于回顾,我们再次给出实体的代码:
[AbstractEntity]
package spring.webjson.client.entities;
public abstract class AbstractEntity {
// 属性
protected Long id;
protected Long version;
// 构造函数
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// 重写 [equals] 和 [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// 获取器和设置器
...
}
[ElectionsConfig]
package elections.webjson.client.entities;
public class ElectionsConfig extends AbstractEntity {
// 字段
private int nbSiegesAPourvoir;
private double seuilElectoral;
// 构造函数
public ElectionsConfig() {
}
public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
this.nbSiegesAPourvoir = nbSiegesAPourvoir;
this.seuilElectoral = seuilElectoral;
}
// getter 和 setter
...
}
[ListeElectorale]
package elections.webjson.client.entities;
public class ListeElectorale extends AbstractEntity {
// 字段
private String nom;
private int voix;
private int sieges;
private boolean elimine;
// 构造函数
public ListeElectorale() {
}
public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
setNom(nom);
setVoix(voix);
setSieges(sieges);
setElimine(elimine);
}
// getter 和 setter
...
}
15.5.3. [DAO] 层接口
![]() |
[DAO] 层具有以下 [IClientDao] 接口:
package elections.client.dao;
public interface IClientDao {
// 泛型查询
String getResponse(String url, String jsonPost);
}
该接口仅有一个方法 [getResponse]:
- 第一个参数是要查询的服务器地址;
- 第二个参数是要发送的值的 jSON,若无内容发送则为 null;
- 结果是对象 [Response<T>] 的字符串 jSON,其中类 [Response] 已在第 14.7 节中描述;
15.5.4. 与 Web 服务的交互实现 / jSON
![]() |
类 [ClientDao] 以如下方式实现接口 [IClientDao]:
package elections.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import elections.client.entities.ElectionsException;
@Component
public class ClientDao implements IClientDao {
// 数据
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// 通用请求
@Override
public String getResponse(String url, String jsonPost) {
try {
// 网址:URL 联系
// jsonPost:待发布的值 jSON
// 执行请求
RequestEntity<?> request;
if (jsonPost != null) {
// 请求 POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// 请求 GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// 正在执行查询
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new ElectionsException(200, e1);
} catch (RuntimeException e2) {
throw new ElectionsException(201, e2);
}
}
}
该代码已在第 13.6.3.6 节中进行了描述。
15.6. [métier] 层的实现
![]() |
如前所述,[métier] 层具有与第 8.4 节中相同的 [IElectionsMetier] 接口:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// 获取参选名单
public ListeElectorale[] getListesElectorales();
// 待填补席位数
public int getNbSiegesAPourvoir();
// 选举门槛
public double getSeuilElectoral();
// 结果登记
public void recordResultats(ListeElectorale[] listesElectorales);
// 席位计算
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
该接口由以下 [ElectionsMetier] 类实现:
package elections.client.metier;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.client.dao.IClientDao;
import elections.client.entities.ElectionsConfig;
import elections.client.entities.ElectionsException;
import elections.client.entities.ListeElectorale;
@Component
public class ElectionsMetier implements IElectionsMetier {
@Autowired
private IClientDao dao;
@Autowired
private ApplicationContext context;
// 选举配置
private ElectionsConfig electionsConfig;
@PostConstruct
public void init() {
// 映射器jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// 查询
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// 错误?
if (response.getStatus() != 0) {
// 抛出 1 个异常
throw new ElectionsException(response.getStatus(), response.getMessages());
} else {
electionsConfig = response.getBody();
}
} catch (ElectionsException e1) {
throw e1;
} catch (Exception e2) {
throw new ElectionsException(100, getMessagesForException(e2));
}
}
@Override
public ListeElectorale[] getListesElectorales() {
...
}
@Override
public int getNbSiegesAPourvoir() {
return electionsConfig.getNbSiegesAPourvoir();
}
@Override
public double getSeuilElectoral() {
return electionsConfig.getSeuilElectoral();
}
@Override
public void recordResultats(ListeElectorale[] listesElectorales) {
...
}
@Override
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales) {
...
}
// 异常的错误消息列表
private List<String> getMessagesForException(Exception exception) {
// 获取异常的错误消息列表
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// 仅当消息不为空且不为空字符串时才获取消息
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// 下一个原因
cause = cause.getCause();
}
return erreurs;
}
}
第 37 行使用的类型 [Response] 是第 14.7 节中描述的 Web 服务器响应 / jSON;
待完成任务:按照第 13.6.3.7 节的说明,完善 [ElectionsMetier] 类;
15.7. JUnit 测试
让我们回到正在构建的客户端/服务器架构:
![]() |
[JUnit] 和 [1] 层通过 [2-4] 层与服务器 [5] 的 [Métier] 层进行通信。 通过使 [Métier]、[2] 和 [5] 层具有相同的接口,使得 [2-4] 层变得透明。 [1]层感觉上似乎直接与[5]层通信。 有趣的是,在 [1] 中,我们将能够使用 JUnit 测试,该测试曾用于测试 [Métier] 和 [5] 层。
![]() |
待办事项:运行该项目的 JUnit 测试,以验证您的实现以及服务器和客户端的运行情况。
15.8. [UI] 层的实现
让我们回到我们要构建的架构:
![]() |
既然 [métier] [2] 层已经构建并测试完毕,我们可以构建 [ui] [1] 层。
![]() |
由于 [métier] 层的 [IElectionsMetier] 接口与第 8 段所述项目中的接口完全相同, 因此,在 [3] 中,我们可以复制第 10 段中 [ui] 层的项目。该项目原本是一个 NetBeans 项目。只需将相关的 Java 类从 NetBeans 复制并粘贴到 Eclipse 中即可。完成此操作后,需要对包和导入语句进行调整。
对于 [elections.client.boot] 和 [4] 包中的可执行类,也将采用同样的方法。
[AbstractBootElections]类如下:
package elections.client.boot;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import elections.client.config.UiConfig;
import elections.client.entities.ElectionsException;
import elections.client.ui.IElectionsUI;
public abstract class AbstractBootElections {
// 获取 Spring 上下文
protected AnnotationConfigApplicationContext ctx;
public void run() {
// 实例化 [ui] 层
IElectionsUI electionsUI = null;
try {
// 获取 Spring 上下文
ctx = new AnnotationConfigApplicationContext(UiConfig.class);
// 获取 [ui] 层
electionsUI = getUI();
...
- 第 19 行:由配置类 [UiConfig] 定义的 Spring 上下文被实例化。该类如下所示:
![]() |
类 [UiConfig] 如下所示:
package elections.client.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.client.ui" })
public class UiConfig {
}
- 第 6 行:导入 [métier] 层的 Bean;
- 第 7 行:声明 [elections.client.ui] 包中包含 Spring Bean;
待完成任务:请验证 [ui] 层的控制台版和 Swing 版是否正常运行。















