18. 为 /jSON Web 服务编写的客户端
既然 [dbproduitscategories] 数据库已在 Web 上提供,我们将编写一个使用该数据库的应用程序。届时我们将拥有以下客户端/服务器架构:
![]() |
客户端应用程序将包含三个层:
- 一个 [HTTP 客户端] [3] 层,用于与暴露数据库的 /jSON Web 应用程序进行通信;
- 一个 [DAO] 层 [2],其接口与 [DAO] 层 [4] 保持一致;
- 一个 JUnit 测试层 [1],用于验证客户端和服务器是否运行正常;
18.1. Eclipse 项目
客户端的 Eclipse 项目结构如下:
![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
- [spring.webjson.client.config] 包包含 [DAO] 层的 Spring 配置;
- [spring.webjson.client.dao] 包包含 [DAO] 层的实现;
- [spring.webjson.client.entities] 包包含与 Web 服务/JSON 交换的对象。我们对它们都很熟悉;
- [spring.webjson.client.infrastructure] 包包含项目中使用的异常类。我们对它们都了如指掌;
18.2. 项目的 Maven 配置
该项目是一个由以下 [pom.xml] 文件配置的 Maven 项目:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-webjson-client-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<name>spring-webjson-client-generic</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 第 16–20 行:父级 Maven 项目 [spring-boot-starter-parent],它允许我们定义多个依赖项而无需指定其版本,因为这些版本已在父项目中定义;
- 第 24–27 行:尽管我们并非在编写 Web 应用程序,但仍需 [spring-web] 依赖项,其中包含 [RestTemplate] 类,该类可轻松实现与 Web 应用程序或 JSON 的交互;
- 第 29–36 行:一个 JSON 库;
- 第 38–41 行:一个允许我们为客户端的 HTTP 请求设置超时的依赖项。超时是指等待服务器响应的最长时限。超过此时间后,客户端将通过抛出异常来触发超时错误;
- 第 43–48 行:Google Guava 库;
- 第 50–53 行:日志记录库;
- 第 54–64 行:用于 JUnit 测试的依赖项。它包含测试所需的 JUnit 4 库。这些依赖项带有 [<scope>test</scope>] 属性,表明它们仅在测试阶段需要。它们不会包含在最终的项目归档中;
18.3. Spring 配置
![]() |
[AppConfig] 类负责处理 HTTP 客户端的 Spring 配置。其代码如下:
package spring.webjson.client.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8081";
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// converter jSON
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
}
- 第 20 行:该类是一个 Spring 配置类;
- 第 21 行:其他 Spring 组件位于 [spring.webjson.client.dao] 包中;
- 第 25 行:设置了 1 秒(1000 毫秒)的超时;
- 第 88–91 行:返回此值的 Bean;
- 第 26 行:Web 服务 / JSON 的 URL;
- 第 93–96 行:返回此值的 Bean;
- 第 72–86 行:处理与 Web 服务/JSON 通信的 [RestTemplate] 类的配置。如果不需要配置,可以在代码中通过简单的 [new RestTemplate()] 进行实例化。在此,我们希望设置与 Web 服务/JSON 通信的超时时间。 第 89 行的 [timeout] Bean 作为参数传递给了第 73 行的 [restTemplate] 方法;
- 第 75 行:[HttpComponentsClientHttpRequestFactory] 组件正是用于设置通信超时(第 82–83 行)的组件;
- 第 76 行:使用该组件构造 [RestTemplate] 类。由于它依赖该组件与 Web 服务/JSON 进行通信,因此通信确实会受超时限制;
- 第 78–80 行:我们将一个 JSON 转换器与 [RestTemplate] 类关联起来。 我们在研究 Web 服务时已讨论过这一点。客户端和服务器之间交换的是文本行。转换器将对象序列化为文本,并将文本反序列化回对象。[RestTemplate] 类可以关联多个转换器,具体选用哪个取决于服务器发送的 HTTP 头部。这里我们只使用了一个 JSON 转换器,因为交换的文本行是 JSON;
- 第 82–83 行:设置通信超时;
- 第 28–70 行:定义 JSON 过滤器。这些与第 17.3.2.1 节中介绍的服务器端过滤器相同;
- 第 29–32 行:[jsonMapper] Bean 是我们关联到 [RestTemplate] 类的 [MappingJackson2HttpMessageConverter] 的 JSON 映射器。我们在 JSON 过滤器的定义中需要它;
- 第 34–41 行:定义 JSON 过滤器 [category without its products] 的 Bean。[jsonMapperShortCategory] 方法将第 73 行定义的 [RestTemplate] Bean 作为参数;
- 第 37 行:我们调用第 30 行中的 [jsonMapper] 方法以获取 JSON 映射器;
- 第 38–39 行:我们将过滤器设置为返回不包含其产品的类别;
- 第 40 行:按配置返回 JSON 映射器;
- 第 42–51 行:用于检索包含其产品的类别的 JSON 过滤器;
- 第 53–60 行:用于检索不包含其所属类别的产品的 JSON 过滤器;
- 第 62–70 行:用于检索包含其所属类别的产品的 JSON 过滤器;
所有这些 Bean 都将可供 [DAO] 层代码以及 JUnit 测试使用。
18.4. HTTP 客户端的实现
![]() |
上文展示的是与我们刚刚构建的 Web 服务进行通信的 [HTTP 客户端] 层。接下来我们将对其进行详细探讨。
![]() |
[Client] 类负责处理与 Web 服务/JSON 的通信。它实现了以下 [IClient] 接口:
package spring.webjson.client.dao;
import org.springframework.http.HttpMethod;
public interface IClient {
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}
该接口仅有一个方法 [getResponse]:
- 第 6 行:[getResponse] 方法是一个泛型方法,由两个类型作为参数:
- [T1]:表示预期从服务器接收的响应类型,形式为 [Response<T1>],例如 [List<Category>],
- [T2]:表示通过 POST 操作提交的 JSON 参数的类型,例如 [List<Product>];
- 第 6 行:[getResponse] 方法返回类型为 T1 的结果,例如 [List<Category>];
- 第 6 行:[getResponse] 的参数如下:
- [String url]:要查询的 URL;
- [HttpMethod method]:请求的 HTTP 方法,根据情况选择 GET 或 POST,
- [int errStatus]:若与服务器通信时发生错误,将在 [DaoException] 类中使用的错误代码;
- [T2 body]:若发起 POST 请求时需提交的值;
[Client] 类实现 [IClient] 接口如下:
package spring.webjson.client.dao;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import spring.webjson.client.infrastructure.DaoException;
@Component
public class Client implements IClient {
// injections
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// local
private String simpleClassName = getClass().getSimpleName();
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
}
// list of exception error messages
protected List<String> getMessagesForException(Exception exception) {
...
}
}
- 第 18 行:[Client] 类是 Spring 组件,因此可以注入到其他 Spring 组件中;
- 第 22–23 行:注入在 [AppConfig] 中定义的 [RestTemplate] Bean(参见第 18.3 节),该 Bean 负责与服务器的通信;
- 第 24–25 行:注入 [AppConfig] 中定义的 Web 服务 URL / JSON(参见第 18.3 节);
- 第 37–39 行:私有方法 [getMessagesForException] 是一个辅助方法,用于检索异常中包含的错误消息列表。我们已经多次遇到过它;
让我们继续:
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
// the server response
ResponseEntity<Response<T1>> response;
try {
// prepare the query
RequestEntity<?> request = null;
if (method == HttpMethod.GET) {
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
if (method == HttpMethod.POST) {
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
}
// execute the query
response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
});
} catch (Exception e) {
// encapsulate the exception
throw new DaoException(errStatus, e, simpleClassName);
}
...
}
- 第 18 行:该语句负责向服务器发送请求并接收其响应。[RestTemplate] 组件提供了大量用于与服务器交互的方法,但只有 [exchange] 方法支持泛型参数。这就是选择它的原因。第二个参数指定了预期响应的类型。第一个参数是 [RequestEntity] 请求(第 8 行)。 [exchange] 方法的返回类型为 [ResponseEntity<Response<T1>>](第 5 行)。[ResponseEntity] 类型封装了服务器的完整响应,包括 HTTP 头部和服务器发送的文档。同样,[RequestEntity] 类型封装了客户端的完整请求,包括 HTTP 头部和任何提交的数据;
- 第 8–16 行:我们需要构建 [RequestEntity] 请求。具体实现取决于我们使用的是 GET 请求还是 POST 请求;
- 第 10 行:GET 请求。 [RequestEntity] 类提供了静态方法,用于创建 GET、POST、HEAD 及其他类型的请求。通过链式调用 [RequestEntity.get] 方法及其构建请求的各个子方法,可以创建一个 GET 请求:
- [RequestEntity.get] 方法将目标 URL 作为 URI 实例形式的参数传入,
- [accept] 方法允许您定义 [Accept] HTTP 头中的元素。在此,我们指定接受服务器将发送的 [application/json] 类型;
- [build] 方法利用这些信息构建请求的 [RequestEntity] 类型;
- 第 14 行:POST 请求。[RequestEntity.post] 方法允许您通过串联构建请求的各种方法来创建 POST 请求:
- [RequestEntity.post] 方法将目标 URL 作为 URI 实例形式的参数接收,
- [header] 方法用于定义 HTTP 头部。在此,我们向服务器发送 [Content-Type: application/json] 头部,以表明所提交的数据将以 JSON 字符串的形式到达;
- [accept] 方法允许我们表明接受服务器将发送的 [application/json] 类型;
- [body] 方法设置提交的值。这是泛型 [getResponse] 方法的第四个参数(第 1 行);
- 第 20–23 行:如果与服务器发生通信错误,将抛出 [DaoException],其错误代码设置为作为泛型 [getResponse] 方法(第 3 行)第三个参数传递的 [errStatus] 参数;
[getResponse] 方法后续实现如下:
// generic request
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
// retrieve the body of the reply
Response<T1> entity = response.getBody();
int status = entity.getStatus();
// server-side errors?
if (status != 0) {
// create an exception
throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
} else {
// it's good
return entity.getBody();
}
}
- 第 4 行:我们已收到来自服务器的响应。其类型为 [ResponseEntity<Response<T1>>](前一个代码示例的第 5 行),其中 [Response] 类是服务器端已使用的类:
package spring.webjson.client.dao;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// the possible exception
private String exception;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, String exception, T body) {
this.status = status;
this.exception = exception;
this.body = body;
}
// getters and setters
...
}
让我们回到 [getResponse] 方法:
- 第 6 行:我们获取封装在 response 中的 [Response<T1>] 对象。该类型包含 [int status, String exception, T1 body] 字段;
- 第 7 行:我们获取响应的 [status],这是一个错误代码;
- 第 9–12 行:如果发生错误,我们会抛出一个异常,其中包含来自服务器响应的两项信息 [status, exception];
- 第 14 行:否则,返回 [Response<T1>] 响应中包含的 [T1] 类型;
[Client] 类是泛型类。它可用于任何 Web/JSON 客户端。
18.5. [Dao] 层的实现
![]() |
![]() |
18.5.1. [AbstractDao] 类
客户端 [DAO] 层与服务器端 [DAO] 层具有相同的接口(参见第 4.7 节):
package spring.webjson.client.dao;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// list of all T entities
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// special entities - short version
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// special entities - long version
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// update of several entities
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// delete all entities
public void deleteAllEntities();
// deletion of multiple entities
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
[AbstractDao] 类实现了 [IDao] 接口。它与服务器端同名的类(参见第 4.8 节)功能相似。它作为 [DaoCategorie] 和 [DaoProduit] 类的父类。两者不完全相同,原因有二:
- 在服务器端,[AbstractDao] 类管理一项信息:
// injections
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
这里我们不需要它。
- 在服务器端,[AbstractDao] 类使用 [@Transactional] 注解将每个方法封装在事务中。而在客户端,无需管理数据库,因此该注解不再出现;
[AbstractDao] 类在将调用委托给子类之前,仅需验证 [IDao] 接口方法的调用参数是否有效:
package spring.webjson.client.dao;
import java.util.ArrayList;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
// local
protected String simpleClassName = getClass().getSimpleName();
@Override
public List<T1> getShortEntitiesById(Iterable<Long> ids) {
// argument validity
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// result
return getShortEntitiesById(Lists.newArrayList(ids));
}
@Override
public List<T1> getShortEntitiesById(Long... ids) {
// argument validity
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// result
return getShortEntitiesById(Lists.newArrayList(ids));
}
...
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
...
}
// méthodes privées ----------------------------------------------
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
// elements null ?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
simpleClassName);
}
// empty elements?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// default result
return null;
}
@SuppressWarnings("unchecked")
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
// elements null ?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
}
// empty elements?
if (elements.length == 0) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// default result
return null;
}
// méthodes protégées ----------------------------------------------
abstract protected List<T1> getShortEntitiesById(List<Long> ids);
abstract protected List<T1> getShortEntitiesByName(List<String> names);
abstract protected List<T1> getLongEntitiesById(List<Long> ids);
abstract protected List<T1> getLongEntitiesByName(List<String> names);
abstract protected List<T1> saveEntities(List<T1> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
18.5.2. [DaoCategorie] 类
![]() |
[DaoCategorie] 类如下所示:
package spring.webjson.client.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ApplicationContext context;
@Autowired
private IClient client;
...
}
- 第 19 行:[DaoClient] 类是一个 Spring 组件,其他 Spring 组件可以注入其中;
- 第 20 行:[DaoClient] 类继承了我们刚才看到的 [AbstractDao<Category>] 类,因此实现了 [IDao<Category>] 接口;
- 第 22–23 行:我们注入 Spring 上下文以访问其中的 Bean;
- 第 24–25 行:我们注入了刚才构建的 HTTP 客户端;
[DaoCategory] 接口中各个方法的实现都遵循相同的模式。我们将介绍三个方法,其中一个基于 [GET] 操作,另外两个基于 [POST] 操作。
18.5.2.1. [getAllLongEntities] 方法
[getAllLongEntities] 方法返回数据库中所有类别的长版本:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// redo the product --> category link
return linkCategorieWithProduits(categories);
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(233, e2, simpleClassName);
}
}
- 第 2 行:该方法返回类别列表的完整名称;
- 第 5 行:JSON 映射器,用于序列化提交的值(此处未提供)并反序列化 [Client] 类返回的响应(即长格式的类别);
- 第 7 行:我们调用 [Client] 类的 [getResponse] 方法。该方法负责处理与 Web 服务 / JSON 的通信。其参数如下:
- 要查询的服务 URL [/getAllLongCategories];
- 要使用的 [GET] 方法;
- 发生错误时使用的错误代码(232);
- 要提交的值。此处无值;
- 第 7 行:在表达式 [client.<List<Category>, Void>] 中,我们为 [getResponse] 方法指定了泛型类型 [T1, T2] 的实际参数。回顾一下,[T1] 是预期响应的类型,而 [T2] 是提交值的类型。此处,我们期望结果类型为 [List<Category>],且没有提交值 [Void];
- 第 7 行:[getResponse] 方法返回的结果存储在一个 [Object] 类型的对象中。这有些奇怪,因为我们期望的类型是 [List<Category>]。这是因为 [getResponse] 方法虽然使用泛型类型 [T1, T2],但总是返回 [java.util.LinkedHashMap] 类型,因此必须对其进行处理才能返回正确的类型;
- 第 9 行:我们返回类别列表。为此,我们将 [map] 对象 [mapper.writeValueAsString(map)] 序列化为 JSON 字符串,然后将其反序列化回 [List<Category>] 类型;
- 第 13 行:我们已收到一个类别列表,其中部分类别可能包含产品。我们接收的是这些产品的简略版本。因此,在反序列化时,生成的 [Product] 对象的 [category] 字段会被设置为 null。[linkCategoryWithProducts] 方法会重新建立 [Product] 与其 [Category] 之间的关联;
- 第 14–15 行:我们捕获 [getResponse] 方法可能抛出的 [DaoException],并立即将其重新抛出。这种非典型行为是因为,如果不这样做,[DaoException] 将会被第 16–18 行捕获,而我们不希望出现这种情况;
- 第 16–18 行:我们捕获所有其他异常,并将它们封装在 [DaoException] 类型中。请记住,[DAO] 层只能抛出此类异常;
用于重建 [Product] 实体与 [Category] 实体之间关联的 [linkCategorieWithProduits] 方法如下:
private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
for (Categorie categorie : categories) {
List<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : produits) {
produit.setCategorie(categorie);
}
}
}
return categories;
}
18.5.2.2. 管理 JSON 过滤器
让我们回顾一下前一个 [getAllLongEntities] 方法中的 JSON 过滤器处理:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
- 第 5 行:我们从 Spring 上下文中获取一个 JSON 映射器,该映射器能够处理长格式的类别名称。让我们回顾一下 Spring 配置 [AppConfig] 中对该映射器的定义:
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
...
}
- 由 [getAlllongEntities] 方法请求的 [jsonMapperLongCategory] Bean 即第 7–15 行中的 Bean;
- 第 10 行:该映射器由第 2–5 行中的 [jsonMapper] 方法提供。我们可以看到,此 JSON 映射器属于 [RestTemplate] 对象,该对象负责管理客户端与服务器之间的 HTTP 交互。默认情况下,该映射器用于:
- 序列化发送到服务器的值;
- 反序列化服务器返回的响应;
让我们回到 [getAllLongEntities] 的代码:
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// the List<Categorie> category list
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// redo the product --> category link
return linkCategorieWithProduits(categories);
- 第 2 行:从 Spring 上下文中获取 [jsonMapperLongCategorie] 映射器;
- 第 4 行:执行 [getResponse] 方法。这包括:
- 对提交值的自动序列化(此处无提交值);
- 对接收到的响应进行自动反序列化,此处为 List<Category> 类型。这是因为 [Category] 实体有一个 JSON 过滤器 [jsonFilterCategory],需要进行处理。这就是第 2 行代码的原因;
- 第 6 行:结果使用同一映射器进行第二次序列化/反序列化,以获取 List<Category> 类型。第 4 行:[getResponse] 返回的类型为 [Object] 类型;
在以下方法中,请注意,从 Spring 上下文中请求的 JSON 映射器既用于提交值(序列化),也用于接收值(反序列化)。如果其中一个或两个值都带有 JSON 过滤器,则必须对其进行配置。因此,该映射器最多可配置两个过滤器。但在以下示例中,这种情况从未发生。 要么是提交值没有过滤器(List<Long>、List<String>),要么是接收值没有(List<CoreCategory>、List<CoreProduct>)。带有 JSON 过滤器的实体仅为 [Category] 和 [Product]。
18.5.2.3. [getShortEntitiesById] 方法
[getShortEntitiesById] 方法返回其作为参数接收的主键所对应的类别的简短版本:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// get a category without its products
Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
// the category
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(223, e2, simpleClassName);
}
}
- 第 5 行:该 JSON 映射器将对提交的值(主键列表)进行序列化,并对 [Client] 类返回的响应(类别短名称)进行反序列化。由于提交列表中的元素没有过滤器,因此所选的过滤器对提交的值没有影响;
- 第 7 行:我们调用父类的 [getResponse] 方法。该方法负责处理与 Web 服务 / JSON 的通信。其参数如下:
- 要查询的服务 URL [/getShortCategoriesById];
- 要使用的 [POST] 方法;
- 发生错误时使用的错误代码(204);
- 提交的值。此处为主键列表;
- 第 7 行:在表达式 [client.<List<Category>, List<Long>>] 中,我们为 [getResponse] 方法指定了泛型类型 [T1, T2] 的实际参数。 回顾一下,[T1] 是预期响应的类型,而 [T2] 是提交值的类型。在此,我们期望返回 [List<Category>] 类型的结果,而提交值是一个 [List<Long>] 类型的主键列表;
- 第 7 行:[getResponse] 方法返回的结果存储在 [Object] 类型中;
- 第 9 行:返回类别列表。为此,将 [map] 对象 [mapper.writeValueAsString(map)] 序列化为 JSON 字符串,然后将其反序列化为 [List<Category>] 类型;
18.5.2.4. [saveEntities] 方法
[saveEntities] 方法将类别持久化到数据库中。其代码如下:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// add categories
Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
entities);
// list of added core categories
List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<CoreCategorie>>() {
});
// categories are updated with the information received
for (int i = 0; i < entities.size(); i++) {
Categorie categorie = entities.get(i);
CoreCategorie coreCategorie = coreCategories.get(i);
categorie.setId(coreCategorie.getId());
List<Produit> produits = categorie.getProduits();
if (produits != null) {
List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
for (int j = 0; j < produits.size(); j++) {
Produit produit = produits.get(j);
produit.setId(coreProduits.get(j).getId());
produit.setIdCategorie(categorie.getId());
produit.setCategorie(categorie);
}
}
}
return entities;
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(220, e2, simpleClassName);
}
}
- 第 2 行:使用 [saveEntities] 方法将作为参数传递的类别持久化到数据库中。该方法返回这些类别,并为其添加主键。如果类别与产品一起传递,产品也会被持久化;
- 第 5 行:该 JSON 映射器将负责序列化提交的值(即类别列表的长格式版本),并反序列化 [Client] 类返回的响应([CoreCategory] 对象)。所选的过滤器不会影响结果,因为作为响应接收到的列表中的元素并未经过过滤;
- 第 7 行:调用父类的 [getResponse] 方法来处理与 Web 服务 / JSON 的通信;
- 第一个参数是 URL [/saveCategories];
- 第二个参数是要使用的 HTTP 方法,本例中为 [POST];
- 第三个参数是发生错误时使用的错误代码(200);
- 最后一个参数是提交的值,此处为待持久化的类别列表;
- 第 7 行:[getResponse] 方法的泛型参数 [T1, T2] 在此处为 [List<CoreCategory>, List<Category>]。第一个类型是预期响应的类型,第二个是提交值的类型;
- 第 7 行:我们将获取的响应存储在 [Object] 类型中;
- 第 9 行:我们重建类型为 [List<CoreCategory>] 的响应。待返回的响应类型应为 [List<Category>](第 2 行),而非 [List<CoreCategory>]。收到的响应是已持久化类别和产品的主键列表;
- 第 14–28 行:将接收的主键分配给类别和产品(第 17、23、24 行)。此外,还重建了 [Product] → [Category] 的关系(第 24–25 行);
所有其他方法均遵循相同模式。
18.6. JUnit 测试
让我们回到当前正在开发的客户端/服务器架构:
![]() |
我们构建了一个与 [DAO] 层 [4] 具有相同接口的 [DAO] 层 [2]。因此,为了测试 [DAO] 层 [2],我们可以使用之前用于测试 [DAO] 层 [4] 的 JUnit 测试:
![]() |
这三项测试采用以下测试配置进行:
![]() | ![]() |
![]() |
三项测试的结果如下:
![]() |
![]() |
- 在 [1] 中,[JUnitTestCheckArguments] 测试;
- 在 [2] 中,[JUnitTestDao] 测试;
- 在 [3] 中,客户端执行的 [JUnitTestPushTheLimits] 测试(项目 [spring-webjson-client-generic]);
- 在 [3] 中,在服务器端执行的 [JUnitTestPushTheLimits] 测试(项目 [spring-jdbc-generic-04])。我们观察到,与访问 DBMS 造成的延迟相比,网络层造成的延迟非常小;
18.7. Web 服务 / JSON / JPA / Hibernate 实现
接下来我们将考察以下架构:
![]() |
修改内容见[1]。服务器的[DAO]层依赖于一个JPA实现。我们将首先使用一个JPA/Hibernate实现。
18.7.1. Eclipse 项目
目前,已加载到Eclipse中的项目如下:
![]() |
[spring-webjson-server-jdbc-generic] 项目基于 [spring-jdbc-generic-04] 项目,该项目配置了用于访问 MySQL 数据库管理系统 (DBMS) 的 DAO/JDBC 层。 我们将创建一个新项目 [spring-webjson-server-jpa-generic],该项目将依赖于 [spring-jpa-generic] 项目,后者配置了用于访问 MySQL 数据库管理系统 (DBMS) 的 DAO/JPA/JDBC 层。我们知道,在这两种情况下,[DAO] 层都实现了相同的 [IDao] 接口。因此,[web] 层的代码保持不变。
我们可以从 [spring-webjson-server-jdbc-generic] 项目中复制并粘贴内容来创建 [spring-webjson-server-jpa-generic] 项目:
![]() |
- 在 [1] 中,指定一个专门为新项目创建的文件夹;
![]() |
需要进行三类修改。第一类修改位于项目的 Maven 配置文件 [pom.xml] 中:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-webjson-server-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-webjson-server-jpa-generic</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- web layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- layer [DAO] -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 第 5 行:更改 Maven 工件的名称;
- 第 24–28 行:依赖项现在指向 [spring-jpa-generic] 项目,不再指向 [spring-jdbc-generic-04];
最终,依赖关系如下:
![]() |
完成此操作后,我们将解决各个类中出现的所有导入问题。例如,[Product、Category] 实体不再位于 [spring-jdbc-generic-04] 项目中,而是位于 [spring-jpa-generic] 项目中。在类代码中按下 [Ctrl-Shift-O] 即可重新生成导入语句。
最后需要在 [AppConfig] 配置文件中进行修改:
package spring.webjson.server.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {
}
- 第 9 行:现在我们从 [spring-jpa-generic] 项目导入配置,而不是从 [spring-jdbc-generic-04] 项目导入;
就这样——我们准备就绪。我们使用 [spring-webjson-server-jpa-generic-hibernate-eclipselink] 配置启动 Web 服务:
![]() | ![]() |
然后,我们运行通用客户端 [spring-webjson-client-generic] 的三个测试:
![]() |
![]() |
- 在 [1] 中,[JUnitTestCheckArguments] 测试(运行配置 [spring-webjson-client-generic-JUnitTestCheckArguments]);
- 在 [2] 中,[JUnitTestDao] 测试(执行配置 [spring-webjson-client-generic-JUnitTestDao]);
- 在 [3] 中,客户端运行的 [JUnitTestPushTheLimits] 测试(运行配置 [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- 在 [4] 中,在服务器端执行的 [JUnitTestPushTheLimits] 测试(运行配置 [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.7.2. 为什么它能正常工作?
虽然它确实能运行,但仔细查看代码后,你会惊讶于它竟然能运行。 虽然由 [spring-jdbc-generic-04] 和 [spring-jpa-generic] 项目实现的 [DAO] 层确实提供了相同的接口,但它们操作的 [Category] 和 [Product] 实体并不相同:在 [spring-jpa-generic] 项目中,这些实体有一个额外的字段 [EntityType entityType],该字段可能取两个值:
- EntityType.POJO:该实体是一个普通对象,其字段可自由使用;
- EntityType.PROXY:该实体是由 [JPA] 层渲染的 PROXY 对象。在此情况下,某些字段(实际上是这些字段的 getter 方法)的行为与通常不同,并已制定以下规则:
- 如果 [Category.entityType == EntityType.PROXY],则不得使用 [getProducts] 方法;
- 如果 [Product.entityType == EntityType.PROXY],则不得使用 [getCategory] 方法;
然而,我们刚刚将 [spring-webjson-server-jdbc-generic] 项目迁移到了 [spring-webjson-server-jpa-generic],且未修改任何代码。这是如何做到的?
让我们来查看 [saveCategories] 方法的代码:
@RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
// we deserialize it
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// we persist categories
categories = daoCategorie.saveEntities(categories);
...
}
- 第 8 行:从一个 JSON 字符串创建了一个 List<Category> 对象:
- 在提交的数据中,产品(Product)不包含 [category] 字段。确实没有必要提交该字段。如果我们提交了该字段,反序列化会构建一个 [Product] 对象,其 [category] 字段( )将指向一个新创建的 [Category] 对象。对于 n 个产品,这将导致创建 n 个 [Category] 对象,而实际上只需一个。 此外,产品的 [category] 字段将不会指向正确的 [Category] 对象,即它们所属的那个对象。因此,在此处产品的 [category] 字段为 [category==null];
- 在 [Category] 和 [Product] 类中,[EntityType entityType] 字段定义如下:
protected EntityType entityType = EntityType.POJO;
因此,通过序列化生成的 [Category] 和 [Product] 实体均为 POJO 类型。
- 第 11 行:我们将类别进行持久化。这应该无法正常工作。事实上,虽然在 JDBC 实现中 [Product.category] 字段并非持久化所必需(而是使用 [categoryId] 字段),但在 JPA 实现中它却是绝对必要的。该字段必须指向一个 [Category] 实体,但此处它为 null。
让我们检查 [DAO / JPA] 层中 [DaoCategorie.saveEntities] 方法的代码:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// on note les produits qui vont être insérés
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories;
}
- 第 13–14 行:我们可以看到,对于 POJO 实体(第 8 行),[Product] → [Category] 的关系已被重新建立,本例中正是如此。这解释了为何类别的持久化能够正常工作。这种方法在其他情况下也很有用:你永远无法确定用户是否已将产品正确地关联到了类别。因此,我们代为完成这一步;
现在让我们来分析负责持久化产品的 [ProductController.saveProducts] 方法:
@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
...
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
// we deserialize it
ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
});
// we persist products
produits = daoProduit.saveEntities(produits);
List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
for (Produit produit : produits) {
coreProduits.add(new CoreProduit(produit.getId()));
}
// we return the answer
return new Response<List<CoreProduit>>(0, null, coreProduits);
...
}
- 第 8 行:根据提交的值重建一个 List<Product> 对象。基于前文所述的原因,每个 [Product] 对象将包含一个字段:
- [EntityType entityType] 等于 [EntityType.POJO];
- [Category category] 等于 null;
- 第 11 行:产品的持久化操作应会失败。事实上,在 JPA 中,只有当产品的 [category] 字段指向一个 [Category] 实体时,才可能对其进行持久化;
让我们看看 [DAO / JPA] 层中 [DaoProduit.saveEntities] 方法的代码:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// on rétablit (si besoin est) le lien entre un produits et sa catégorie
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// on persiste les produits
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
- 第 3–8 行:对于每个 POJO 类型的 [Product],会创建一个指向 [Category] 对象的链接,该链接具有正确的主键且版本不为空。这足以让 JPA 层正确地持久化该产品;
最后再看一点。[Category] 和 [Product] 对象有一个额外的字段 [EntityType entityType],当这些对象发送给客户端时,该字段会被序列化为 JSON。我们可以使用 [Advanced Rest Client] 进行验证:
![]() |
在客户端,[Category] 和 [Product] 实体在定义时未包含 [EntityType entityType] 字段。这是正常的,因为 [Category] 和 [Product] 对象在序列化时不包含其 PROXY 部分 [Category.products] 和 [Product.category]。因此,在客户端并不存在 PROXY 实体的概念,只有普通的对象。
在客户端,JSON字符串 [1] 由以下 [DaoCategorie.getAllShortEntities] 方法接收:
@Override
public List<Categorie> getAllShortEntities() {
...
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// get all categories
Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
// the List<Categorie> category list
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
...
}
- 第 5 行:我们配置 [RestTemplate] 对象的 JSON 映射器,以处理 [Category] 对象的 JSON 过滤器 [jsonFilterCategory] 以及 [Product] 对象的过滤器 [jsonFilterProduct];
- 第 7 行:使用此映射器对提交的值(此处没有)和接收的值(List<Category>)进行序列化/反序列化。 请注意,即使 [Category] 和 [Product] 实体在客户端不存在 [entityType] 字段,但接收到的 JSON 字符串中出现该字段也不会引发错误。该字段会被忽略。如果它确实引发了错误,我们需要修改客户端过滤器以忽略该字段。
18.8. Web 服务 / JSON / JPA / EclipseLink 实现
要实现 Web 服务 / JSON / JPA / EclipseLink,只需修改 JPA 实现:
![]() |
注意:按下 Alt-F5,然后重新生成所有 Maven 项目。
我们将使用已用于 Hibernate 的 [spring-webjson-server-jpa-generic-hibernate-eclipselink] 运行时配置来启动 Web 服务。完成后,请运行通用客户端 [spring-webjson-client-generic] 的三个测试:
![]() |
![]() |
- 在 [1] 中,[JUnitTestCheckArguments] 测试;
- 在 [2] 中,[JUnitTestDao] 测试;
- 在 [3] 中,客户端运行的 [JUnitTestPushTheLimits] 测试(项目 [spring-webjson-client-generic]);
- 在 [4] 中,服务器端运行的 [JUnitTestPushTheLimits] 测试(运行配置 [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.9. Web 服务 / JSON / JPA / OpenJPA 实现
要实现 Web 服务 / JSON / JPA / OpenJPA,只需更改 JPA 实现:
![]() |
注意:按下 Alt-F5,然后重新生成所有 Maven 项目。
我们将使用运行时配置 [spring-webjson-server-jpa-generic-openpa] 启动 Web 服务:
![]() | ![]() |
完成上述操作后,请运行通用客户端 [spring-webjson-client-generic] 的三个测试:
![]() |
![]() |
- 在 [1] 中,[JUnitTestCheckArguments] 测试(运行配置 [spring-webjson-client-generic-JUnitTestCheckArguments]);
- 在 [2] 中,[JUnitTestDao] 测试(运行配置 [spring-webjson-client-generic-JUnitTestDao]);
- 在 [3] 中,客户端运行的 [JUnitTestPushTheLimits] 测试(运行配置 [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- 在 [4] 中,在服务器端执行的 [JUnitTestPushTheLimits] 测试(执行配置 [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);
为了使测试正常运行,必须对 DAO/JPA 层进行修改。事实上,由于某种无法解释的原因,在填充数据库时,方法 [DaoCategorie.saveEntities] 和 [DaoProduit.saveEntities] 均告失败,这表明无法持久化脱离的实体。脱离的实体是指具有以下任一特征的实体:
- 主键不为空;
- 版本字段不为空;
上述两种情况均未被验证。由于不知从何处着手排查,我将待持久化的实体复制到一个全新的列表中,测试随即通过。这一修改本应:
- 在 [DAO / JPA] 层;
- 在创建待持久化实体的 [Web] 层中;
我选择在 [DAO / JPA] 层进行修改。当然,这会带来性能损失,但与数据库管理系统(DBMS)的响应时间相比,这种损失完全可以忽略不计。具体修改如下:
在 [spring-jpa-generic] 项目的 [DaoCategorie] 类中:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ***************************************************************************************
// on clone la liste des catégories -- nécessaire parfois pour OpenJpa -- bug non compris
// ***************************************************************************************
List<Categorie> categories2 = new ArrayList<Categorie>();
for (Categorie categorie : categories) {
// catégorie
Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
EntityType categorieType = categorie.getEntityType();
categorie2.setEntityType(categorieType);
categories2.add(categorie2);
// produits
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : produits) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
categorie2.setProduits(produits2);
}
}
// on note les produits qui vont être insérés
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories2) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories2);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories2;
}
- 第 3–25 行:作为参数接收的 [categories] 列表(第 2 行)在 [categories2] 列表中被复制(第 6 行)。正是这个列表被持久化并返回给调用方(第 52 行)。这带来一个重要后果:返回的列表与作为参数传递的列表不同,因此我们之前可以这样写:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// exploitation de [categories]
现在我们需要编写:
List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// exploitation de [categories]
在 [spring-jpa-generic] 项目的 [DaoProduct] 类中,[saveEntities] 方法也进行了类似的修改:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ***************************************************************************************
// on clone la liste des produits -- nécessaire parfois pour OpenJpa -- bug non compris
// ***************************************************************************************
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : entities) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
// on rétablit (si besoin est) le lien entre un produits et sa catégorie
for (Produit produit : produits2) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// on persiste les produits
try {
return Lists.newArrayList(produitsRepository.save(produits2));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
18.10. Web 服务实现 / JSON / JPA / EclipseLink / PostgreSQL
要实现 Web 服务 / JSON / JPA / EclipseLink / PostgreSQL,您必须安装:
- 用于配置 PostgreSQL JDBC 层的 [postgresql-config-jdbc] 项目;
- 用于配置 PostgreSQL JPA 层的 [postgresql-config-jpa-eclipselink] 项目;
- 按 Alt-F5 并重新生成所有 Maven 项目;
![]() |
启动 PostgreSQL 数据库管理系统,并使用之前使用的 [spring-webjson-server-jpa-generic-hibernate-eclipselink] 运行时配置启动 Web 服务。完成后,运行通用客户端 [spring-webjson-client-generic] 的三个测试:
![]() |
![]() |
- 在 [1] 中,[JUnitTestCheckArguments] 测试(运行配置 [spring-webjson-client-generic-JUnitTestCheckArguments]);
- 在 [2] 中,[JUnitTestDao] 测试(运行配置 [spring-webjson-client-generic-JUnitTestDao]);
- 在 [3] 中,客户端执行的 [JUnitTestPushTheLimits] 测试(运行配置 [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- 在 [4] 中,在服务器端执行的 [JUnitTestPushTheLimits] 测试(执行配置 [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);







































