15. [TD]:为 Web 服务创建客户端
关键词:多层架构、Spring、依赖注入、Web 服务 / JSON、客户端 / 服务器
15.1. 支持
![]() |
本章的项目位于 [support / chap-15] 文件夹中。
15.2. 客户端/服务器架构
我们希望构建以下客户端/服务器架构:
![]() |
[ui] 层将采用第 9 节和第 10 节中已开发的内容。之所以能够实现这一点,是因为上层的 [business] 层将实现与第 8 节中的 [business] 层相同的接口 [IElectionsMetier]:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// get the lists in competition
public ListeElectorale[] getListesElectorales();
// the number of seats to be filled
public int getNbSiegesAPourvoir();
// the electoral threshold
public double getSeuilElectoral();
// recording results
public void recordResultats(ListeElectorale[] listesElectorales);
// calculating seats
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
在第7节中,[DAO]层与数据库管理系统(DBMS)交换数据。在此,[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.business] 包包含 [业务] 层
- [elections.client.ui] 包包含 [UI] 层
15.5.1. [业务]层的配置
![]() |
[MetierConfig] 类负责处理 [business] 层的 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 {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// mapper jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
该代码已在第13.6.3.1节中进行过说明。由于这里无需管理JSON过滤器,因此代码更为简洁。
15.5.2. 实体
![]() |
[DAO] 和 [business] 层处理的实体就是它们与 Web 服务/JSON 交换的对象。这些是 [ElectionsConfig] 和 [VoterList] 类型的对象。在服务器端,这些实体带有 JPA 持久化注解。在此,这些注解已被移除。我们再次列出实体代码以供参考:
[摘要实体]
package spring.webjson.client.entities;
public abstract class AbstractEntity {
// properties
protected Long id;
protected Long version;
// manufacturers
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// redefine [equals] and [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();
}
// getters and setters
...
}
[ElectionsConfig]
package elections.webjson.client.entities;
public class ElectionsConfig extends AbstractEntity {
// fields
private int nbSiegesAPourvoir;
private double seuilElectoral;
// manufacturers
public ElectionsConfig() {
}
public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
this.nbSiegesAPourvoir = nbSiegesAPourvoir;
this.seuilElectoral = seuilElectoral;
}
// getters and setters
...
}
[选民列表]
package elections.webjson.client.entities;
public class ListeElectorale extends AbstractEntity {
// fields
private String nom;
private int voix;
private int sieges;
private boolean elimine;
// manufacturers
public ListeElectorale() {
}
public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
setNom(nom);
setVoix(voix);
setSieges(sieges);
setElimine(elimine);
}
// getters and setters
...
}
15.5.3. [DAO] 层接口
![]() |
[DAO] 层具有以下 [IClientDao] 接口:
package elections.client.dao;
public interface IClientDao {
// generic request
String getResponse(String url, String jsonPost);
}
该接口仅有一个方法 [getResponse]:
- 第一个参数是要查询的服务器 URL;
- 第二个参数是要发送的 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 {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// generic request
@Override
public String getResponse(String url, String jsonPost) {
try {
// url : URL to contact
// jsonPost: the jSON value to be posted
// request execution
RequestEntity<?> request;
if (jsonPost != null) {
// query POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// query GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// execute the query
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. [业务]层的实现
![]() |
如前所述,[业务]层与第8.4节中的接口[IElectionsMetier]相同:
package elections.client.metier;
import elections.client.entities.ListeElectorale;
public interface IElectionsMetier {
// get the lists in competition
public ListeElectorale[] getListesElectorales();
// the number of seats to be filled
public int getNbSiegesAPourvoir();
// the electoral threshold
public double getSeuilElectoral();
// recording results
public void recordResultats(ListeElectorale[] listesElectorales);
// calculating seats
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;
// election configuration
private ElectionsConfig electionsConfig;
@PostConstruct
public void init() {
// mappers jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// request
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
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) {
...
}
// list of exception error messages
private List<String> getMessagesForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
}
第37行使用的[Response]类型是第14.7节中描述的Web服务器/JSON响应;
任务:根据第13.6.3.7节,完成[ElectionsMetier]类;
15.7. JUnit 测试
![]() |
[JUnit]层[1]通过层[2–4]与服务器的[业务]层[5]进行通信。通过确保[业务]层[2]和[5]具有相同的接口,我们使层[2–4]变得透明。层[1]看起来似乎直接与层[5]进行通信。 值得注意的是,在 [1] 中,我们将能够使用之前用于测试 [业务] 层 [5] 的 JUnit 测试。
![]() |
任务:运行该项目的 JUnit 测试,以验证您对服务器及其客户端的实现。
15.8. [UI] 层的实现
让我们回到我们要构建的架构:
![]() |
既然 [业务] 层 [2] 已经构建并经过测试,我们就可以构建 [UI] 层 [1] 了。
![]() |
由于[业务]层的[IElectionsMetier]接口与第8段所述项目中的接口完全一致,我们可以[3]复制第10段中的[UI]层项目。该项目原本是NetBeans项目。只需将NetBeans中的相关Java类复制并粘贴到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 context retrieval
protected AnnotationConfigApplicationContext ctx;
public void run() {
// instantiation layer [ui]
IElectionsUI electionsUI = null;
try {
// spring context retrieval
ctx = new AnnotationConfigApplicationContext(UiConfig.class);
// ui] layer recovery
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 行:导入来自 [business] 层的 Bean;
- 第 7 行:我们声明 [elections.client.ui] 包中包含 Spring Bean;
任务:验证 [ui] 层的控制台版和 Swing 版是否正常工作。















