Skip to content

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 版是否正常运行。