Skip to content

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 版是否正常工作。