18. یک برنامهٔ کلاینت برای سرویس وب / jSON
اکنون که پایگاه داده [dbproduitscategories] در وب در دسترس است، ما برنامهای خواهیم نوشت که از آن استفاده میکند. سپس معماری کلاینت/سرور زیر را خواهیم داشت:
![]() |
برنامهٔ کلاینت سه لایه خواهد داشت:
- یک لایه [Client HTTP] [3] برای ارتباط با برنامه وب / jSON که پایگاه داده را در معرض دید قرار میدهد؛
- یک لایه [DAO] [2]، که همان رابط کاربری لایه [DAO] [4] را ارائه خواهد داد؛
- یک لایه آزمایشی (JUnit و [1]) برای تأیید اینکه کلاینت و سرور به درستی کار میکنند؛
18.1. پروژه اکلیپس
پروژه Eclipse مشتری به شرح زیر است:
![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
- پکیج [spring.webjson.client.config] شامل پیکربندی Spring برای لایه [DAO] است؛
- پکیج [spring.webjson.client.dao] شامل پیادهسازی لایه [DAO] است؛
- پکیج [spring.webjson.client.entities] حاوی اشیایی است که با سرویس وب / jSON مبادله میشوند. ما با همه آنها آشنا هستیم؛
- پکیج [spring.webjson.client.infrastructure] شامل کلاسهای استثنا (exception) مورد استفاده در پروژه است. ما با همهٔ آنها آشنا هستیم؛
18.2. پیکربندی Maven پروژه
این پروژه یک پروژه 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-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>
<!-- اسپرینگ -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!--jSON کتابخانهای است که توسط 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>
<!-- کامپوننتی که توسط Spring RestTemplate استفاده میشود -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- گوگل گوآوا -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!--کتابخانهٔ لاگگیری -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- آزمون Spring Boot -->
<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>
- خطوط ۱۶–۲۰: پروژهٔ والد Maven با شناسهٔ [spring-boot-starter-parent]، که به ما امکان میدهد تعدادی وابستگی را بدون مشخص کردن نسخههایشان تعریف کنیم، زیرا این نسخهها در پروژهٔ والد تعریف شدهاند؛
- خطوط ۲۴–۲۷: اگرچه ما در حال نوشتن یک برنامه وب نیستیم، اما به وابستگی [spring-web] نیاز داریم که شامل کلاس [RestTemplate] است و به ما امکان میدهد به راحتی با یک برنامه وب / jSON تعامل داشته باشیم؛
- خطوط ۲۹–۳۶: یک کتابخانه به نام jSON;
- خطوط ۳۸–۴۱: وابستگیای که به ما امکان میدهد یک timeout را به درخواستهای HTTP کلاینت متصل کنیم. timeout حداکثر زمانی است که برای پاسخدادن سرور مجاز است. اگر این زمان از حد عبور کند، کلاینت با پرتاب یک استثنا، خطای timeout را گزارش میکند؛
- خطوط ۴۳–۴۸: کتابخانه Google Guava؛
- خطوط ۵۰–۵۳: کتابخانهٔ لاگگیری؛
- خطوط ۵۴–۶۴: وابستگی تست JUnit. بهویژه، این شامل کتابخانه JUnit 4 مورد نیاز برای تست میشود. این وابستگیها دارای ویژگی [<scope>test</scope>] هستند که نشان میدهد آنها فقط برای مرحله تست مورد نیازند. آنها در آرشیو نهایی پروژه گنجانده نمیشوند؛
18.3. پیکربندی بهار
![]() |
کلاس [AppConfig]، اسپرینگ را برای کلاینت HTTP پیکربندی میکند. کد آن به شرح زیر است:
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 {
// ثوابت
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8081";
// فیلترها 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) {
// ایجاد مؤلفه RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// مبدل jSON
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
// اتمام زمان ارتباط
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// نتیجه
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
}
- خط ۲۰: این کلاس یک کلاس پیکربندی Spring است؛
- خط ۲۱: سایر اجزای Spring در پکیج [spring.webjson.client.dao] قرار دارند؛
- خط ۲۵: یک timeout یک ثانیهای (۱۰۰۰ میلیثانیه) تنظیم شده است؛
- خطوط ۸۸–۹۱: بین (bean) که این مقدار را برمیگرداند؛
- خط 26: URL برای سرویس وب / jSON;
- خطوط ۹۳–۹۶: بین (bean) که این مقدار را برمیگرداند؛
- خطوط ۷۲–۸۶: پیکربندی کلاس [RestTemplate]، که ارتباط با سرویس وب / jSON را مدیریت میکند. هنگامی که نیازی به پیکربندی آن نباشد، میتوان به سادگی در کد به آن به عنوان [new RestTemplate()] ارجاع داد. در اینجا، میخواهیم timeout را برای ارتباط با سرویس وب / jSON تنظیم کنیم. بیان [timeout] در خط 89 به عنوان پارامتر به متد [restTemplate] در خط 73 ارسال میشود؛
- خط ۷۵: کامپوننت [HttpComponentsClientHttpRequestFactory] کامپوننتی است که به ما امکان میدهد timeout را برای مبادلات (خطوط ۸۲–۸۳) تنظیم کنیم؛
- خط ۷۶: کلاس [RestTemplate] با استفاده از این کامپوننت ساخته میشود. از آنجایی که این کلاس برای ارتباط با سرویس وب / jSON به این کامپوننت متکی است، مبادلات در واقع تابع timeout خواهند بود؛
- خطوط ۷۸–۸۰: یک مبدل jSON با کلاس [RestTemplate] مرتبط است. ما قبلاً هنگام بررسی سرویس وب در این مورد بحث کردهایم. کلاینت و سرور خطوط متنی را مبادله میکنند. یک مبدل مسئول سریالسازی یک شیء به متن و بالعکس، دسریالسازی متن به یک شیء است. ممکن است چندین مبدل با کلاس [RestTemplate] مرتبط باشند و مبدلی که در هر لحظه انتخاب میشود، به هدرهای HTTP ارسالشده توسط سرور بستگی دارد. در اینجا، ما تنها یک مبدل، jSON، داریم، زیرا خطوط متنی که مبادله میشوند از نوع jSON هستند؛
- خطوط ۸۲–۸۳: فیلترهای timeout برای مبادلات تنظیم میشوند؛
- خطوط ۲۸–۷۰: فیلترهای jSON را تعریف میکنند. این فیلترها همانهایی هستند که در سرور وجود دارند، همانطور که در بخش 17.3.2.1 توضیح داده شده است؛
- خطوط 29–32: bean [jsonMapper]، mapper jSON برای converter [MappingJackson2HttpMessageConverter] است که ما آن را با کلاس [RestTemplate] مرتبط کردهایم. ما به این در تعریف فیلترهای jSON نیاز داریم؛
- خطوط ۳۴–۴۱: یک بین که فیلترهای jSON و [catégorie sans ses produits] را تعریف میکند. متد [jsonMapperShortCategorie]، بین [restTemplate] را که در خط ۷۳ تعریف شده است، به عنوان پارامتر میپذیرد؛
- خط ۳۷: متد [jsonMapper] از خط ۳۰ برای بازیابی مپر jSON فراخوانی میشود؛
- خطوط ۳۸–۳۹: فیلتر طوری تنظیم شده است که یک دستهبندی را بدون محصولات آن بازگرداند؛
- خط ۴۰: مپر jSON با این پیکربندی رندر میشود؛
- خطوط ۴۲–۵۱: فیلتر jSON برای بازیابی یک دستهبندی به همراه محصولاتش؛
- خطوط ۵۳–۶۰: فیلتر jSON برای نمایش یک محصول بدون دستهبندی آن؛
- خطوط ۶۲–۷۰: فیلتر jSON برای نمایش یک محصول همراه با دستهبندی آن؛
تمام این بیینها در لایه [DAO] و همچنین در تستهای JUnit در دسترس کدها قرار خواهند گرفت.
18.4. پیادهسازی کلاینت HTTP
![]() |
در بالا، لایه [Client HTTP] است که با سرویس وبی که همین حالا ساختهایم ارتباط برقرار میکند. اکنون آن را بررسی خواهیم کرد.
![]() |
کلاس [Client] ارتباط با سرویس وب / 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]:
- خط ۶: متد [getResponse] یک متد عمومی است که با دو نوع پارامترگذاری میشود:
- [T1]: نوع پاسخی است که از سرور در [Response<T1>] انتظار میرود، برای مثال [List<Categorie>]،
- [T2]: نوع پارامتر jSON است که توسط عملیات POST ارسال میشود، برای مثال [List<Produit>];
- خط ۶: متد [getResponse] نتیجهای از نوع T1 برمیگرداند، برای مثال [List<Categorie>];
- خط ۶: پارامترهای [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 {
//تزریقها
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// محلی
private String simpleClassName = getClass().getSimpleName();
// درخواست عمومی
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
}
// فهرست پیامهای خطا برای یک استثنا
protected List<String> getMessagesForException(Exception exception) {
...
}
}
- خط ۱۸: کلاس [Client] یک کامپوننت Spring است و بنابراین میتواند به سایر کامپوننتهای Spring تزریق شود؛
- خطوط ۲۲–۲۳: تزریق bean با شناسه [RestTemplate] که در [AppConfig] تعریف شده است (به بخش ۱۸.۳ مراجعه کنید)، که مسئول مدیریت ارتباط با سرور است؛
- خطوط ۲۴–۲۵: تزریق سرویس وب URL / jSON که در [AppConfig] تعریف شده است (به بخش ۱۸.۳ مراجعه کنید)؛
- خطوط ۳۷–۳۹: متد خصوصی [getMessagesForException] یک متد ابزارکی است که برای بازیابی فهرست پیامهای خطا موجود در یک استثنا استفاده میشود. ما چندین بار با آن مواجه شدهایم؛
ادامه میدهیم:
// درخواست عمومی
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
// پاسخ سرور
ResponseEntity<Response<T1>> response;
try {
// آمادهسازی درخواست
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);
}
// اجرای درخواست
response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
});
} catch (Exception e) {
// دربرگرفتن استثنا
throw new DaoException(errStatus, e, simpleClassName);
}
...
}
- خط ۱۸: دستوری که درخواست را به سرور ارسال میکند و پاسخ آن را دریافت میکند. کامپوننت [RestTemplate] مجموعهای گسترده از روشها برای ارتباط با سرور ارائه میدهد، اما تنها روش [exchange] پارامترهای عمومی را میپذیرد. به همین دلیل این روش انتخاب شد. پارامتر دوم نوع پاسخ مورد انتظار را مشخص میکند. پارامتر اول درخواست از نوع [RequestEntity] (خط ۸) است. نتیجه متد [exchange] از نوع [ResponseEntity<Response<T1>>] (خط ۵) است. نوع [ResponseEntity] پاسخ کامل سرور را در بر میگیرد که شامل سربرگهای HTTP و سند ارسالشده توسط سرور است. به همین ترتیب، نوع [RequestEntity] کل درخواست کلاینت را در بر میگیرد، شامل سربرگهای HTTP و هرگونه دادهی ارسالشده با POST؛
- خطوط ۸–۱۶: ما باید درخواست از نوع [RequestEntity] را بسازیم. این امر بسته به اینکه از GET یا POST برای ارسال درخواست استفاده شود، متفاوت است؛
- خط ۱۰: درخواست برای GET. کلاس [RequestEntity] متدهای ایستا برای ایجاد پرسوجوهای GET، POST، HEAD، ... را فراهم میکند. روش [RequestEntity.get] به شما امکان میدهد تا با زنجیرهسازی روشهای مختلف سازنده، یک پرسوجوی GET ایجاد کنید:
- متد [RequestEntity.get] هدف URL را به صورت یک نمونه از URI به عنوان پارامتر میپذیرد،
- روش [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) است؛
- خطوط ۲۰–۲۳: اگر خطای ارتباطی با سرور رخ دهد، یک استثنا از نوع [DaoException] پرتاب میشود، که در آن کد خطا، پارامتر [errStatus] است که به عنوان سومین پارامتر متد عمومی [getResponse] (خط ۳) ارسال میشود؛
متد [getResponse] به شرح زیر ادامه مییابد:
// درخواست عمومی
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
//بازیابی بدنه پاسخ
Response<T1> entity = response.getBody();
int status = entity.getStatus();
// آیا خطاهای سمت سرور وجود دارد؟
if (status != 0) {
//یک استثنا پرتاب میشود
throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
} else {
// بسیار خب
return entity.getBody();
}
}
- خط ۴: پاسخ از سرور دریافت شد. این پاسخ از نوع [ResponseEntity<Response<T1>>] است (خط ۵ کد قبلی بررسیشده)، که در آن کلاس [Response] کلاسی است که پیشتر در سمت سرور استفاده شده است:
package spring.webjson.client.dao;
public class Response<T> {
// ----------------- ویژگیها
//وضعیت عملیات
private int status;
// هر استثنا
private String exception;
// بدنه پاسخ
private T body;
// سازندهها
public Response() {
}
public Response(int status, String exception, T body) {
this.status = status;
this.exception = exception;
this.body = body;
}
// گیرندهها و تنظیمکنندهها
...
}
بیایید به متد [getResponse] بازگردیم:
- خط ۶: ما سند از نوع [Response<T1>] را که در پاسخ جای گرفته است، بازیابی میکنیم. این نوع دارای فیلدهای [int status, String exception, T1 body] است؛
- خط ۷: ما [status] را از پاسخ بازیابی میکنیم که یک کد خطا است؛
- خطوط ۹–۱۲: اگر خطایی وجود داشته باشد، یک استثنا (exception) پرتاب میکنیم که حاوی دو مورد اطلاعات ([status, exception]) از پاسخ سرور است؛
- خط ۱۴: در غیر این صورت، نوع [T1] موجود در پاسخ [Response<T1>] را برمیگردانیم؛
کلاس [Client] عمومی است. میتوان از آن برای هر مشتری وب / 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> {
// فهرست تمام موجودیتهای T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// واحدهای مشخص – نسخهٔ کوتاه
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);
// واحدهای خاص – نسخه طولانی
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);
// بهروزرسانی چندین انتیت
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// حذف تمام انتیتها
public void deleteAllEntities();
// حذف چندین انتیت
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] یک قطعه اطلاعات را مدیریت میکند:
// افزودنها
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
که در اینجا به آن نیازی نداریم.
- در سمت سرور، کلاس [AbstractDao] از anotationهای [@Transactional] برای قرار دادن هر متد درون یک تراکنش استفاده میکند. در سمت کلاینت، هیچ پایگاهداده برای مدیریت وجود ندارد. بنابراین این anotation ناپدید میشود؛
کلاس [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> {
// محلی
protected String simpleClassName = getClass().getSimpleName();
@Override
public List<T1> getShortEntitiesById(Iterable<Long> ids) {
// اعتبار آرگومان
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// نتیجه
return getShortEntitiesById(Lists.newArrayList(ids));
}
@Override
public List<T1> getShortEntitiesById(Long... ids) {
// اعتبار استدلال
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// نتیجه
return getShortEntitiesById(Lists.newArrayList(ids));
}
...
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
...
}
// متدهای خصوصی ----------------------------------------------
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
// عناصر null؟
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
simpleClassName);
}
// عناصر خالی؟
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>();
}
}
// نتیجهٔ پیشفرض
return null;
}
@SuppressWarnings("unchecked")
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
// عناصر null؟
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
}
// عناصر خالی؟
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>();
}
}
// نتیجه پیشفرض
return null;
}
// روشهای محافظتشده ----------------------------------------------
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;
...
}
- خط ۱۹: کلاس [DaoClient] یک کامپوننت Spring است که بنابراین میتوان کامپوننتهای دیگر Spring را در آن تزریق کرد؛
- خط ۲۰: کلاس [DaoClient] از کلاس [AbstractDao<Categorie>] که همیناکنون دیدیم، ارث میبرد و بنابراین رابط [IDao<Categorie>] را پیادهسازی میکند؛
- خطوط 22–23: ما برای دسترسی به بینهای آن، کانکست Spring را تزریق میکنیم؛
- خطوط ۲۴–۲۵: ما کلاینت HTTP را که همین حالا ایجاد کردهایم، تزریق میکنیم؛
پیادهسازیهای متدهای مختلف رابط [DaoCategorie] همگی از الگوی یکسانی پیروی میکنند. ما به سه متد نگاه خواهیم کرد: یکی مبتنی بر یک عملیات [GET]، و دو متد دیگر مبتنی بر یک عملیات [POST].
18.5.2.1. روش [getAllLongEntities]
روش [getAllLongEntities] نسخه طولانی تمام دستهبندیها را در پایگاه داده بازمیگرداند:
@Override
public List<Categorie> getAllLongEntities() {
try {
// فیلترها jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// دریافت همهٔ دستهبندیها
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// فهرست دستهبندیها List<Category>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// برقراری مجدد لینک محصول–>دستهبندی
return linkCategorieWithProduits(categories);
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(233, e2, simpleClassName);
}
}
- خط ۲: این متد لیست دستهبندیها را در شکل کامل آنها بازمیگرداند؛
- خط ۵: مپکننده jSON که مقدار ارسالشده (که وجود ندارد) را سریالیزه کرده و پاسخ بازگشتی از کلاس [Client] (دستهبندیها در شکل کاملشان) را دسریالیزه میکند؛
- خط ۷: متد [getResponse] از کلاس [Client] فراخوانی میشود. این متد مسئول برقراری ارتباط با سرویس وب /jSON است. پارامترهای آن به شرح زیر هستند:
- URL سرویس مورد پرسوجو، [/getAllLongCategories]؛
- متد [GET] که باید استفاده شود؛
- کد خطا برای استفاده در صورت بروز خطا (232);
- مقدار ارسالشده. در این مورد، هیچ مقداری وجود ندارد؛
- خط ۷: در عبارت [client.<List<Categorie>, Void>]، پارامترهای واقعی انواع عمومی [T1, T2] برای متد [getResponse] مشخص شدهاند. توجه داشته باشید که [T1] نوع پاسخ مورد انتظار و [T2] نوع مقدار ارسالشده است. در اینجا، نتیجه از نوع [List<Categorie>] انتظار میرود و هیچ مقدار ارسالشدهای از نوع [Void] وجود ندارد؛
- خط ۷: نتیجهای که توسط متد [getResponse] بازگردانده میشود، در یک شیء از نوع [Object] ذخیره میشود. این کمی عجیب است، با توجه به اینکه انتظار میرود نوع [List<Categorie>] باشد. این به این دلیل است که متد [getResponse]، که با انواع عمومی [T1, T2] کار میکند، همیشه یک نوع [java.util.LinkedHashMap] را بازمیگرداند، که سپس باید برای تولید نوع صحیح پردازش شود؛
- خط ۹: فهرست دستهبندیها بازگردانده میشود. برای این کار، ما شیء [map] [mapper.writeValueAsString(map)] را به یک رشته jSON سریالیزه میکنیم، که سپس آن را دوباره به یک نوع [List<Categorie>] دسریالیزه میکنیم؛
- خط ۱۳: ما یک فهرست دستهبندی دریافت کردهایم که برخی از آنها ممکن است محصول داشته باشند. ما نسخهٔ کوتاه این محصولات را دریافت میکنیم. بنابراین، هنگام سریالسازی معکوس، اشیاء [Produit] ایجادشده دارای فیلد [categorie==null] هستند. متد [linkCategorieWithProduits] پیوند بین یک [Produit] و [Categorie] آن را دوباره برقرار میکند؛
- خطوط 14–15: استثنای [DaoException] که ممکن است توسط متد [getResponse] پرتاب شده باشد، گرفته میشود، تنها برای اینکه بلافاصله دوباره پرتاب شود. این رفتار عجیب به این دلیل است که اگر این کار انجام نشود، استثنای نوع [DaoException] توسط خطوط 16–18 گرفته خواهد شد، و این چیزی نیست که ما بخواهیم؛
- خطوط ۱۶–۱۸: ما سایر استثناها را میگیریم تا آنها را در نوع [DaoException] قرار دهیم. به یاد داشته باشید که لایه [DAO] باید فقط این نوع استثنا را پرتاب کند؛
متد [linkCategorieWithProduits] که پیوندها بین اشیاء [Produit] و [Categorie] را دوباره برقرار میکند، به شرح زیر است:
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
بیایید به مدیریت فیلترها jSON در روش قبلی [getAllLongEntities] بازگردیم:
@Override
public List<Categorie> getAllLongEntities() {
try {
// فیلترها jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// دریافت همهٔ دستهبندیها
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// فهرست دستهبندیها List<Category>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
- خط ۵: ما یک مپر jSON را از کانکست Spring بازیابی میکنیم که قادر به مدیریت نسخههای طولانی دستهبندیها است. بیایید دوباره به تعریف این مپر در پیکربندی Spring [AppConfig] نگاه کنیم:
// فیلترها 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) {
...
}
- بیان [jsonMapperLongCategorie] که توسط متد [getAlllongEntities] درخواست شده، بیانی است که در خطوط ۷ تا ۱۵ تعریف شده است؛
- خط ۱۰: مپر توسط متد [jsonMapper] در خطوط ۲–۵ فراهم شده است. میتوانیم ببینیم که این مپر، jSON، متعلق به شیء [RestTemplate] است که مبادلات HTTP بین کلاینت و سرور را مدیریت میکند. این مپر است که بهصورت پیشفرض برای:
- سریالسازی مقدار ارسالشده به سرور؛
- دسریالیز کردن پاسخ بازگرداندهشده توسط سرور؛
بیایید به کد مربوط به [getAllLongEntities] بازگردیم:
// فیلترها jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// دریافت تمام دستهبندیها
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// فهرست دستهبندیها List<Category>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// برقراری مجدد ارتباط محصول به دستهبندی
return linkCategorieWithProduits(categories);
- خط ۲: مپر [jsonMapperLongCategorie] از کانکست Spring بازیابی میشود؛
- خط ۴: متد [getResponse] اجرا میشود. این شامل موارد زیر است:
- سریالسازی خودکار مقدار ارسالشده (در اینجا وجود ندارد)؛
- سریالسازی خودکار پاسخ دریافتی، در این مورد یک List<Categorie>. این به این دلیل است که انتیت [Categorie] دارای فیلتر jSON [jsonFilterCategorie] است که باید مدیریت میشد. این دلیل خط ۲ است؛
- خط ۶: نتیجه تحت یک سریالیسازی/دسریالیسازی دوم با استفاده از همین مپر قرار میگیرد تا نوع `List<Categorie>` را بازیابی کند. در خط ۴، نوع بازگشتی توسط `[getResponse]` یک نوع `[Object]` است؛
در متدهای زیر، باید توجه داشت که مپر jSON که از کانکست Spring درخواست شده است، هم برای مقدار ارسالشده (سریالیزاسیون) و هم برای مقدار دریافتشده (دسریالیزاسیون) استفاده میشود. اگر یکی یا هر دو مقدار دارای فیلتر jSON باشند، این فیلترها باید پیکربندی شوند. بنابراین، مپر میتواند حداکثر دو فیلتر پیکربندیشده داشته باشد. در مثال زیر، این امر هرگز رخ نمیدهد. یا مقدار ارسالشده فاقد فیلتر است (List<Long>, List<String>)، یا مقدار دریافتشده فاقد فیلتر است (List<CoreCategorie>, List<CoreProduit>). اشیایی که دارای فیلتر jSON هستند، فقط [Categorie] و [Produit] هستند.
18.5.2.3. متد [getShortEntitiesById]
روش [getShortEntitiesById] نسخههای کوتاه دستهبندیهایی را که کلیدهای اصلی آنها را بهعنوان پارامتر دریافت میکند، بازمیگرداند:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
// فیلترها jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
//بازیابی یک دستهبندی بدون محصولات آن
Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
// دستهبندی
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(223, e2, simpleClassName);
}
}
- خط ۵: مپر jSON که مقدار ارسالشده (یک لیست از کلیدهای اصلی) را سریالیزه کرده و پاسخ بازگرداندهشده توسط کلاس [Client] (دستهبندیها در شکل کوتاهشان) را دسریالیزه میکند. فیلتر انتخابشده هیچ تأثیری بر مقدار ارسالشده نخواهد داشت، زیرا فیلتری برای عناصر موجود در لیست ارسالشده وجود ندارد؛
- خط ۷: متد [getResponse] کلاس والد فراخوانی میشود. این متد است که ارتباط با سرویس وب /jSON را مدیریت میکند. پارامترهای آن به شرح زیر است:
- URL سرویس مورد پرسوجو، [/getShortCategoriesById]؛
- متد [POST] که باید استفاده شود؛
- کد خطا برای استفاده در صورت بروز خطا (204);
- مقداری که ارسال میشود. در این مورد، این یک لیست از کلیدهای اصلی است؛
- خط ۷: در عبارت [client.<List<Categorie>, List<Long>>]، پارامترهای واقعی از انواع عمومی [T1, T2] برای متد [getResponse] مشخص شدهاند. توجه داشته باشید که [T1] نوع پاسخ مورد انتظار و [T2] نوع مقدار ارسالشده است. در اینجا، نتیجهای از نوع [List<Categorie>] انتظار میرود، و مقدار ارسالشده یک لیست از کلیدهای اصلی از نوع [List<Long>] است؛
- خط ۷: نتیجهای که توسط متد [getResponse] بازگردانده میشود، در یک شی از نوع [Object] ذخیره میشود؛
- خط ۹: لیست دستهبندیها بازگردانده میشود. برای این کار، شیء از نوع [map] [mapper.writeValueAsString(map)] به یک رشته از نوع jSON سریال میشود، که سپس دوباره به نوع [List<Categorie>] دیسریال میشود؛
18.5.2.4. متد [saveEntities]
متد [saveEntities] دستهبندیها را در پایگاه داده ذخیره میکند. کد آن به شرح زیر است:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// فیلترهای jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// افزودن دستهبندیها
Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
entities);
// فهرست دستهبندیهای اصلی اضافه شده
List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<CoreCategorie>>() {
});
// دستهبندیها با اطلاعات دریافتی بهروزرسانی میشوند
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);
}
}
- خط ۲: متد [saveEntities] برای ذخیره دائمی دستهبندیهایی که به عنوان پارامتر ارسال شدهاند در پایگاه داده استفاده میشود. این متد همان دستهبندیها را به همراه کلیدهای اصلیشان برمیگرداند. اگر دستهبندیها به همراه محصولات ارسال شوند، آنها نیز ذخیره میشوند؛
- خط ۵: مبدل jSON که مقدار ارسالشده (فهرستی از دستهبندیها به شکل کامل) را سریالیزه میکند و پاسخ بازگشتی از کلاس [Client] (ابجکتهای [CoreCategorie]) را دسریالیزه میکند. فیلتر انتخابشده هیچ تأثیری بر نتیجه نخواهد داشت، زیرا عناصر موجود در فهرست دریافتی در پاسخ فیلتر نمیشوند؛
- خط ۷: متد [getResponse] والد برای مدیریت ارتباط با سرویس وب / jSON فراخوانی میشود؛
- پارامتر اول URL [/saveCategories] است؛
- پارامتر دوم متد HTTP است که باید استفاده شود، در این مورد [POST]؛
- پارامتر سوم کد خطایی است که در صورت بروز خطا باید استفاده شود (200);
- پارامتر نهایی، مقدار ارسالشده است که در این مورد، فهرست دستهبندیهایی است که باید ذخیره شوند؛
- خط ۷: پارامترهای عمومی [T1, T2] برای متد [getResponse] در اینجا [List<CoreCategorie>, List<Categorie>] هستند. نوع اول مربوط به پاسخ مورد انتظار و نوع دوم مربوط به نوع مقدار ارسالشده است؛
- خط ۷: نوع پاسخ دریافتی به [Object] اختصاص داده میشود؛
- خط ۹: پاسخ از نوع [List<CoreCategorie>] بازسازی میشود. پاسخ بازگشتی از نوع [List<Categorie>] (خط ۲) است و نه [List<CoreCategorie>]. پاسخ دریافتی، لیست کلیدهای اصلی برای دستهبندیها و محصولات ذخیرهشده است؛
- خطوط 14–28: کلیدهای اصلی دریافتشده به دستهبندیها و محصولات اختصاص داده میشوند (خطوط 17، 23، 24). علاوه بر این، لینکهای [Produit] → [Categorie] بازسازی میشوند (خطوط 24–25);
تمام روشهای دیگر از الگوی مشابهی پیروی میکنند.
18.6. آزمون JUnit
بیایید به معماری کلاینت/سرور که در حال حاضر در حال توسعه است بازگردیم:
![]() |
ما یک لایه [DAO] [2] با همان رابط لایه [DAO] [4] ساختهایم. برای آزمایش لایههای [DAO] و [2]، بنابراین میتوانیم از تستهای JUnit که برای آزمایش لایههای [DAO] و [4] استفاده شدند، استفاده کنیم:
![]() |
این سه آزمون با استفاده از پیکربندیهای اجرای زیر اجرا میشوند:
![]() | ![]() |
![]() |
نتایج سه آزمون به شرح زیر است:
![]() |
![]() |
- در [1]، آزمون [JUnitTestCheckArguments];
- در [2]، تست [JUnitTestDao]؛
- در [3]، تست سمت کلاینت [JUnitTestPushTheLimits] (پروژه [spring-webjson-client-generic]);
- در [3]، اجرای تست [JUnitTestPushTheLimits] در سمت سرور (پروژه [spring-jdbc-generic-04]). مشاهده میشود که لایه شبکه در مقایسه با تأخیری که دسترسی به SGBD ایجاد میکند، باعث کاهش سرعت بسیار کمی میشود؛
18.7. پیادهسازی سرویس وب / jSON / JPA / Hibernate
اکنون به معماری زیر نگاه خواهیم کرد:
![]() |
تغییر در [1] است. لایه [DAO] سرور بر اساس پیادهسازی JPA است. ابتدا از پیادهسازی JPA / Hibernate استفاده خواهیم کرد.
18.7.1. پروژه اکلیپس
در حال حاضر، پروژههای زیر در اکلیپس بارگذاری شدهاند:
![]() |
پروژه [spring-webjson-server-jdbc-generic] بر اساس پروژه [spring-jdbc-generic-04] ساخته شده است، که DAO / را پیکربندی میکند لایه JDBC برای دسترسی به SGBD و MySQL. ما قصد داریم پروژه جدیدی به نام [spring-webjson-server-jpa-generic] ایجاد کنیم که به نوبه خود بر اساس پروژه [spring-jpa-generic] خواهد بود، که لایه DAO / را پیکربندی میکند. لایه JPA / JDBC برای دسترسی به SGBD و MySQL. ما میدانیم که در هر دو مورد، لایه [DAO] همان رابط [IDao] را پیادهسازی میکند. بنابراین کد لایه [web] بدون تغییر باقی میماند.
میتوانیم پروژه [spring-webjson-server-jpa-generic] را با کپی و پیست از پروژه [spring-webjson-server-jdbc-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>
<!--لایه وب -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--لایه [DAO] -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- افزونهها -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- خط ۵: نام آرتفکت Maven را تغییر دهید؛
- خطوط 24–28: وابستگی اکنون به پروژه [spring-jpa-generic] به جای [spring-jdbc-generic-04] است؛
در نهایت، وابستگیها به شرح زیر است:
![]() |
پس از انجام این کار، تمام مشکلات مربوط به import که در کلاسهای مختلف ایجاد شدهاند را برطرف میکنیم. به عنوان مثال، موجودیتهای [Produit, Categorie] دیگر در پروژه [spring-jdbc-generic-04] یافت نمیشوند، بلکه در پروژه [spring-jpa-generic] قرار دارند. فقط با افزودن [Ctrl-Maj-O] به کد یک کلاس، برای بازتولید مجدد imports کافی است.
تغییر نهایی باید در فایل پیکربندی [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 {
}
- خط ۹: اکنون پیکربندی را از پروژه [spring-jpa-generic] به جای پروژه [spring-jdbc-generic-04] وارد میکنیم؛
با انجام این کار، ما آمادهایم. سرویس وب را با پیکربندی [spring-webjson-server-jpa-generic-hibernate-eclipselink] راهاندازی میکنیم:
![]() | ![]() |
سپس سه تست را برای کلاینت عمومی [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. چرا این کار میکند؟
این کار میکند، و با این حال وقتی با دقت به کد نگاه میکنید، شگفتآور است که کار میکند. اگرچه لایههای [DAO] که توسط پروژههای [spring-jdbc-generic-04] و [spring-jpa-generic] پیادهسازی شدهاند، در واقع رابط یکسانی دارند، آنها با همان اشیاء [Categorie] و [Produit] سروکار ندارند؛ در پروژه [spring-jpa-generic] این اشیاء دارای یک فیلد اضافی به نام [EntityType entityType] هستند که دو مقدار ممکن دارد:
- EntityType.POJO: این انتیت یک شیء عادی است که تمام فیلدهای آن میتوانند آزادانه استفاده شوند؛
- EntityType.PROXY: این انتها یک شیء PROXY است که توسط لایه [JPA] رندر شده است. در این حالت، برخی فیلدها (به طور خاص، گترهای این فیلدها) طبق معمول رفتار نمیکنند و قوانین زیر وضع شده است:
- اگر [Categorie.entityType==EntityType.PROXY] باشد، آنگاه نباید از متد [getProduits] استفاده شود؛
- اگر [Produit.entityType==EntityType.PROXY] باشد، آنگاه نباید از متد [getCategorie] استفاده شود؛
با این حال، ما همیناکنون پروژه [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) {
...
// ارزش ارسالشده را بازیابی کنید
String body = CharStreams.toString(request.getReader());
// آن را از حالت سریالی خارج میکند
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// ردهبندیها را حفظ میکند
categories = daoCategorie.saveEntities(categories);
...
}
- خط ۸: یک شیء List<Category> از یک رشته jSON ایجاد میشود:
- در مقدار ارسالشده، محصولات فاقد فیلد [categorie] هستند. در واقع، نیازی به ارسال این فیلد نیست. اگر ارسال میشد، فرایند سریالبرداری یک شیء [Produit] را با یک فیلد [categorie] که به یک شیء [Categorie] تازهساخت اشاره میکند، ایجاد میکرد. برای n محصول، این امر منجر به ایجاد n شیء [Categorie] میشود، در حالی که تنها به یکی نیاز است. علاوه بر این، فیلد [categorie] محصولات به شیء [Categorie] صحیح، یعنی شئای که به آن تعلق دارند، اشاره نخواهد کرد. بنابراین در اینجا، محصولات دارای فیلد [categorie==null] هستند؛
- در کلاسهای [Categorie] و [Produit]، فیلد [EntityType entityType] به شرح زیر تعریف شده است:
protected EntityType entityType = EntityType.POJO;
بنابراین، اشیاء [Categorie] و [Produit] که توسط سریالیسازی ایجاد شدهاند، همگی از نوع POJO هستند.
- خط ۱۱: دستهبندیها پایدارسازی میشوند. این نباید کار کند. در واقع، در پیادهسازی JDBC، میدان [Produit.categorie] برای پایداری ضروری نیست (در عوض از میدان [idCategorie] استفاده میشود)، اما برای پیادهسازی JPA، کاملاً ضروری است. این فیلد باید به یک موجودیت [Categorie] اشاره کند، اما در اینجا روی null تنظیم شده است.
بیایید کد متد [DaoCategorie.saveEntities] را در لایه [DAO / JPA] بررسی کنیم:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ما محصولات قابل درج را یادداشت میکنیم
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);
}
// از این فرصت استفاده میکنیم تا (در صورت لزوم) رابطه محصول ← دستهبندی را مجدداً برقرار کنیم
produit.setCategorie(categorie);
}
}
}
//دستهبندیها و محصولات ذخیره میشوند
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// میدان [idCategorie] را برای محصولات درجشده بهروزرسانی کنید
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// نتیجه
return categories;
}
- ردههای ۱۳–۱۴: میبینیم که پیوند [Produit] --> [Categorie] برای موجودیتهای POJO (ردیف ۸) دوباره برقرار شده است، که در اینجا نیز همینطور است. این موضوع توضیح میدهد که چرا پایداری دستهبندی کار کرد. این سناریو در شرایط دیگر نیز مفید است: هرگز نمیتوان مطمئن بود که کاربر محصولات را به درستی به دستهبندیها پیوند داده است. بنابراین ما این کار را برای آنها انجام میدهیم؛
اکنون بیایید روش [ProduitController.saveProduits] را که محصولات را حفظ میکند، بررسی کنیم:
@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
...
// ارزش ارسالشده را بازیابی کنید
String body = CharStreams.toString(request.getReader());
// آن را از حالت سریال خارج میکند
ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
});
//محصولات نگه داشته میشوند
produits = daoProduit.saveEntities(produits);
List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
for (Produit produit : produits) {
coreProduits.add(new CoreProduit(produit.getId()));
}
// پاسخ بازگردانده میشود
return new Response<List<CoreProduit>>(0, null, coreProduits);
...
}
- خط ۸: یک شیء List<Product> از مقدار ارسالشده بازسازی میشود. به دلایل توضیحدادهشده در بالا، هر شیء [Produit] دارای یک فیلد خواهد بود:
- [EntityType entityType] برابر با [EntityType.POJO] است؛
- [Categorie categorie] برابر با null است؛
- خط ۱۱: پایداری محصول احتمالاً با شکست مواجه میشود. این به این دلیل است که با JPA، یک محصول تنها در صورتی میتواند پایدار شود که فیلد [categorie] آن به یک موجودیت [Categorie] اشاره کند؛
بیایید کد متد [DaoProduit.saveEntities] در لایه [DAO / JPA] را بررسی کنیم:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ما (در صورت لزوم) پیوند بین محصول و دستهبندی آن را دوباره برقرار میکنیم
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
//محصولات حفظ میشوند
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
- خطوط ۳–۸: برای هر [Produit] از نوع POJO، یک لینک به یک شیء [Categorie] با کلید اصلی صحیح و نسخهای غیر از null ایجاد میشود. این برای لایه JPA کافی است تا محصول را به درستی حفظ کند؛
بیایید به یک نکته پایانی بپردازیم. اشیاء [Categorie] و [Produit] یک فیلد اضافی به نام [EntityType entityType] دارند که هنگام ارسال این اشیاء به کلاینت، به صورت jSON سریالیزه خواهد شد. ما میتوانیم این را با [Advanced Rest Client] تأیید کنیم:
![]() |
در سمت کلاینت، اشیاء [Categorie] و [Produit] بدون فیلد [EntityType entityType] تعریف شدهاند. این امر طبیعی است، زیرا اشیاء [Categorie] و [Produit] بدون اجزای PROXY، [Categorie.produits] و [Produit.categorie] خود سریالسازی میشوند. بنابراین، در سمت کلاینت، هیچگونه موجودیت PROXY وجود ندارد. تنها اشیاء عادی وجود دارند.
در سمت کلاینت، رشته jSON [1] توسط متد زیر [DaoCategorie.getAllShortEntities] دریافت میشود:
@Override
public List<Categorie> getAllShortEntities() {
...
// فیلتر میکند jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// بازیابی همهٔ دستهبندیها
Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
// فهرست دستهبندیها List<Category>
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
...
}
- خط ۵: مپر jSON از شیء [RestTemplate] پیکربندی شده است تا فیلترهای jSON و [jsonFilterCategorie] را مدیریت کند.شیء [Categorie] و فیلتر [jsonFilterProduit] از شیء [Produit]؛
- خط ۷: مقدار ارسالشده (در اینجا هیچ مقداری وجود ندارد) و مقدار دریافتشده (List<Category>) با استفاده از این مپر سریالیزه/دسریالیزه میشوند. مشاهده میشود که وجود فیلد [entityType] در رشته دریافتی jSON، با وجود اینکه این فیلد در اشیاء سمت کلاینت [Categorie] و [Produit] وجود ندارد، موجب خطا نمیشود. نادیده گرفته میشود. اگر موجب خطا شده بود، فیلترهای سمت کلاینت برای نادیده گرفتن آن اصلاح میشدند.
18.8. پیادهسازی سرویس وب / jSON / JPA / EclipseLink
برای پیادهسازی سرویس وب / jSON / JPA / EclipseLink، کافی است پیادهسازی JPA را تغییر دهید:
![]() |
توجه: کلیدهای Alt+F5 را فشار دهید، سپس تمام پروژههای Maven را مجدداً تولید کنید.
ما سرویس وب را با استفاده از پیکربندی زمان اجرا [spring-webjson-server-jpa-generic-hibernate-eclipselink] که قبلاً برای Hibernate استفاده میشود، راهاندازی خواهیم کرد. پس از انجام این کار، سه تست برای کلاینت عمومی [spring-webjson-client-generic] را اجرا کنید:
![]() |
![]() |
- در [1]، تست [JUnitTestCheckArguments]؛
- در [2]، تست [JUnitTestDao]؛
- در [3]، تست سمت کلاینت [JUnitTestPushTheLimits] (پروژه [spring-webjson-client-generic]);
- در [4]، اجرای تست [JUnitTestPushTheLimits] در سمت سرور (پیکربندی اجرا [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.9. پیادهسازی سرویس وب / jSON / JPA / OpenJpa
برای پیادهسازی سرویس وب / jSON / JPA / OpenJpa، کافی است پیادهسازی را به JPA تغییر دهید:
![]() |
توجه: کلیدهای Alt+F5 را فشار دهید، سپس تمام پروژههای Maven را مجدداً تولید کنید.
ما سرویس وب را با استفاده از پیکربندی زمان اجرا [spring-webjson-server-jpa-generic-openpa] راهاندازی خواهیم کرد:
![]() | ![]() |
پس از انجام این کار، سه آزمون را برای مشتری عمومی [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] هنگام پر کردن پایگاه داده با خطا مواجه شدند که نشان میداد عناصر جداشده نمیتوانند ذخیره شوند. یک عنصر جداشده عنصری است که یا:
- یک کلید اصلی که null نیست؛
- نسخهای که null نیست؛
هیچیک از این موارد بررسی نشده بود. از آنجا که نمیدانستم به کجا نگاه کنم، اِنتیتِهایی را که باید ذخیره میشدند در یک لیست کاملاً جدید کپی کردم و سپس تستها کار کردند. این تغییر میتوانست به یکی از این دو روش انجام شود:
- در لایه [DAO / JPA]؛
- در لایه [web] که اِنتیتِیهای قابل ذخیرهسازی را ایجاد میکند؛
من تصمیم گرفتم این کار را در لایه [DAO / JPA] انجام دهم. البته کاهش عملکردی وجود دارد، اما این کاملاً ناچیز است در مقایسه با زمانهای پاسخ SGBD. تغییرات به شرح زیر است:
در کلاس [DaoCategorie] پروژه [spring-jpa-generic]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ***************************************************************************************
// کپی لیست دستهبندیها -- گاهی برای OpenJpa ضروری است -- باگ درک نشده
// ***************************************************************************************
List<Categorie> categories2 = new ArrayList<Categorie>();
for (Categorie categorie : categories) {
// دستهبندی
Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
EntityType categorieType = categorie.getEntityType();
categorie2.setEntityType(categorieType);
categories2.add(categorie2);
// محصولات
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);
}
}
// ما محصولات قابل درج را یادداشت میکنیم
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);
}
// از این فرصت استفاده میکنیم تا (در صورت لزوم) رابطهٔ محصول ← دستهبندی را مجدداً برقرار کنیم
produit.setCategorie(categorie);
}
}
}
// ذخیرهٔ دستهبندیها و محصولات
try {
categoriesRepository.save(categories2);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// بهروزرسانی فیلد [idCategorie] برای محصولات درجشده
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// نتیجه
return categories2;
}
- خطوط ۳–۲۵: لیست [categories] که بهعنوان پارامتر دریافت شده (خط ۲) در لیست [categories2] (خط ۶) کپی میشود. همین فهرست است که پایدارسازی شده و به فراخواننده بازگردانده میشود (خط ۵۲). این موضوع پیامد مهمی دارد: فهرستی متفاوت از آنچه بهعنوان پارامتر ارسال شده بازگردانده میشود، و بنابراین جایی که قبلاً میتوانست نوشت:
اکنون باید بنویسیم:
List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
//پردازش [categories]
در کلاس [DaoProduit] از پروژه [spring-jpa-generic]، متد [saveEntities] به شیوهای مشابه اصلاح شده است:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ***************************************************************************************
// کپیبرداری از فهرست محصولات -- گاهی برای OpenJpa ضروری است -- باگ در نظر گرفته نشده است
// ***************************************************************************************
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);
}
// احیای (در صورت لزوم) پیوند بین یک محصول و دستهبندی آن
for (Produit produit : produits2) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// محصولات را حفظ میکند
try {
return Lists.newArrayList(produitsRepository.save(produits2));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
18.10. پیادهسازی سرویس وب / jSON / JPA / EclipseLink / PostgreSQL
برای پیادهسازی سرویس وب / jSON / JPA / EclipseLink / PostgresQL، باید نصب کنید:
- پروژه [postgresql-config-jdbc]، که لایه JDBC از PostgreSQL را پیکربندی میکند؛
- پروژه [postresql-config-jpa-eclipselink]، که لایه JPA از PostgreSQL را پیکربندی میکند؛
- کلیدهای Alt-F5 را فشار دهید و همه پروژههای Maven را مجدداً تولید کنید؛
![]() |
SGBD و PostgreSQL را اجرا کنید و سرویس وب را با استفاده از پیکربندی زمان اجرای [spring-webjson-server-jpa-generic-hibernate-eclipselink] که قبلاً استفاده شده بود، راهاندازی کنید. پس از انجام این کار، سه تست را برای کلاینت عمومی [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]);







































