Skip to content

18. Web hizmeti / jSON için programlanmış bir istemci

Artık [dbproduitscategories] veritabanı web üzerinde mevcut olduğuna göre, bunu kullanan bir uygulama yazacağız. Böylece şu istemci/sunucu mimarisine sahip olacağız:

İstemci uygulaması üç katmandan oluşacaktır:

  • veritabanını sunan web uygulaması / jSON ile iletişim kurmak için bir [Client HTTP] [3] katmanı;
  • [DAO] [2] katmanı, [DAO] [4] katmanıyla aynı arayüzü sunacak;
  • istemci ve sunucunun düzgün çalıştığını doğrulamak için JUnit ve [1] test katmanları;

18.1. Eclipse Projesi

Müşterinin Eclipse projesi şu şekildedir:

 
  • [spring.webjson.client.config] paketi, [DAO] katmanının Spring yapılandırmasını içerir;
  • [spring.webjson.client.dao] paketi, [DAO] katmanının uygulamasını içerir;
  • [spring.webjson.client.entities] paketi, /jSON web hizmeti ile alışverişi yapılan nesneleri içerir. Bunların hepsini biliyoruz;
  • [spring.webjson.client.infrastructure] paketi, proje tarafından kullanılan istisna sınıflarını içerir. Bunların hepsini biliyoruz;

18.2. Projenin Maven Yapılandırması

Proje, aşağıdaki [pom.xml] dosyası ile yapılandırılmış bir Maven projesidir:


<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>
        <!-- Spring tarafından kullanılan jSON kütüphanesi -->
        <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 tarafından kullanılan bileşen 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>
        <!-- günlük kütüphanesi -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot Testi -->
        <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>
    <!-- eklentiler -->
    <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. satırlar: [spring-boot-starter-parent] adlı üst Maven projesi; bu sayede sürümleri belirtmeden bir dizi bağımlılık tanımlayabiliyoruz, çünkü sürümler üst projede tanımlanmıştır;
  • 24-27. satırlar: bir web uygulaması yazmıyor olsak da, bir web uygulamasıyla / jSON ile kolayca arayüz oluşturmamızı sağlayan [RestTemplate] sınıfını içeren [spring-web] bağımlılığına ihtiyacımız var;
  • 29-36. satırlar: bir jSON kütüphanesi;
  • satır 38-41: İstemcinin HTTP isteklerine bir timeout eklememizi sağlayacak bir bağımlılık. timeout, sunucudan yanıt alınması için maksimum bekleme süresidir. Bu süre aşıldığında, istemci bir istisna oluşturarak timeout hatasını bildirir;
  • 43-48. satırlar: Google Guava kütüphanesi;
  • 50-53. satırlar: günlük kütüphanesi;
  • 54-64. satırlar: JUnit test bağımlılığı. Bu bağımlılık, özellikle testler için gerekli olan JUnit 4 kütüphanesini içerir. Bu bağımlılıklar, yalnızca test aşaması için gerekli olduklarını belirten [<scope>test</scope>] özniteliğine sahiptir. Bunlar, projenin nihai arşivine dahil edilmez;

18.3. Spring Yapılandırması

  

[AppConfig] sınıfı, HTTP istemcisinin Spring yapılandırmasını gerçekleştirir. Kod şöyledir:


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 {

    // sabitler
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8081";

    // filtreler 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) {
        // bileşen oluşturma RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // dönüştürücü jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // iletişim zaman aşımı
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // sonuç
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • 20. satır: Sınıf, bir Spring yapılandırma sınıfıdır;
  • 21. satır: Diğer Spring bileşenleri [spring.webjson.client.dao] paketinde aranmalıdır;
  • 25. satır: Bir saniye (1000 ms) süreli bir timeout ayarlanır;
  • satır 88-91: bu değeri döndüren bean;
  • 26. satır: web hizmetinin URL'i / jSON;
  • satır 93-96: bu değeri döndüren bean;
  • satır 72-86: / jSON web hizmeti ile iletişimi sağlayan [RestTemplate] sınıfının yapılandırması. Yapılandırılması gerekmediğinde, kodda basit bir [new RestTemplate()] ile kullanılabilir. Burada, /jSON web hizmeti ile veri alışverişi için timeout değerini belirlemek istiyoruz. 89. satırdaki [timeout] bean'i, 73. satırdaki [restTemplate] yöntemine parametre olarak aktarılmıştır;
  • 75. satır: [HttpComponentsClientHttpRequestFactory] bileşeni, iletişimde timeout değerini belirlememizi sağlayan bileşendir (82-83. satırlar);
  • 76. satır: [RestTemplate] sınıfı bu bileşenle oluşturulmuştur. Bu sınıf, web hizmeti / jSON ile iletişim kurmak için bu bileşene dayandığından, veri alışverişleri timeout'e gönderilecektir;
  • 78-80. satırlar: [RestTemplate] sınıfına bir jSON dönüştürücü atanır. Web hizmetini incelerken bundan daha önce bahsetmiştik. İstemci ve sunucu arasında metin satırları alışverişi yapılır. Bir dönüştürücü, bir nesneyi metne serileştirir ve tersine, bir metni nesneye deserileştirir. [RestTemplate] sınıfına atanan birden fazla dönüştürücü olabilir ve belirli bir anda hangisinin seçileceği, sunucu tarafından gönderilen HTTP başlıklarına bağlıdır. Burada, değiş tokuş edilen metin satırları jSON formatında olduğu için sadece bir jSON dönüştürücümüz vardır;
  • 82-83. satırlar: veri alışverişi için timeout değerleri belirlenir;
  • 28-70. satırlar: jSON filtrelerini tanımlar. Bunlar, 17.3.2.1 numaralı paragrafta sunulan sunucudakilerle aynıdır;
  • 29-32. satırlar: [jsonMapper] bean'i, [MappingJackson2HttpMessageConverter] dönüştürücüsünün jSON eşleştiricisidir; bu dönüştürücüyü [RestTemplate] sınıfıyla ilişkilendirdik. Buna, jSON filtrelerinin tanımında ihtiyacımız var;
  • 34-41. satırlar: jSON ve [catégorie sans ses produits] filtrelerini tanımlayan bir bean. [jsonMapperShortCategorie] yöntemi, 73. satırda tanımlanan [restTemplate] bean'ini parametre olarak alır;
  • satır 37: jSON eşleştiricisini almak için satır 30'daki [jsonMapper] yöntemine çağrı yapılır;
  • 38-39. satırlar: Ürünleri hariç bir kategori elde etmek için filtre ayarlanır;
  • 40. satır: jSON eşleştiricisi bu şekilde yapılandırılır;
  • satır 42-51: bir kategoriyi ürünleriyle birlikte elde etmek için jSON filtresi;
  • satır 53-60: kategorisi olmayan bir ürün elde etmek için jSON filtresi;
  • 62-70. satırlar: bir ürünü kendi kategorisiyle birlikte görüntülemek için jSON filtresi;

Tüm bu bean'ler, [DAO] katmanındaki kodlarda ve JUnit testlerinde kullanılabilir olacaktır.

18.4. HTTP istemcisinin uygulanması

Yukarıda, az önce oluşturduğumuz web hizmeti ile iletişim kuran [Client HTTP] katmanı görülmektedir. Şimdi bunu inceleyeceğiz.

  

[Client] sınıfı, /jSON web hizmeti ile veri alışverişini gerçekleştirir. Aşağıdaki [IClient] arayüzünü uygular:


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);
}

Arayüzde yalnızca [getResponse] adlı bir yöntem bulunmaktadır:

  • 6. satır: [getResponse] yöntemi, iki türle parametrelendirilen genel bir yöntemdir:
    • [T1]: [Response<T1>]'te sunucudan beklenen yanıt türüdür; örneğin [List<Categorie>],
    • [T2]: POST işlemleri tarafından gönderilen jSON parametresinin türüdür; örneğin [List<Produit>];
  • 6. satır: [getResponse] yöntemi, T1 türünde bir sonuç döndürür; örneğin [List<Categorie>];
  • 6. satır: [getResponse]'in parametreleri şunlardır:
    • [String url]: sorgulanacak URL;
    • [HttpMethod method]: sorgunun HTTP yöntemi; duruma göre GET veya POST,
    • [int errStatus]: Sunucu ile iletişim sırasında hata oluşması durumunda [DaoException] sınıfında kullanılacak hata kodu,
    • [T2 body]: POST mevcut ise gönderilecek değer;

[Client] sınıfı, [IClient] arayüzünü aşağıdaki şekilde uygular:


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 {

    // enjeksiyonlar
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // yerel
    private String simpleClassName = getClass().getSimpleName();

    // genel istek
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }

    // bir istisnaya ait hata mesajları listesi
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • 18. satır: [Client] sınıfı bir Spring bileşenidir ve bu nedenle diğer Spring bileşenlerine enjekte edilebilir;
  • 22-23. satırlar: Sunucu ile iletişimi sağlayan ve [AppConfig]'te tanımlanan [RestTemplate] bean'inin enjeksiyonu (bkz. 18.3. paragraf);
  • 24-25. satırlar: [AppConfig] içinde tanımlanan web hizmeti / jSON'in URL'e enjeksiyonu (bkz. paragraf 18.3);
  • satır 37-39: Özel yöntem [getMessagesForException], bir istisnada bulunan hata mesajlarının listesini elde etmeyi sağlayan bir yardımcı yöntemdir. Bu yönteme daha önce birkaç kez rastladık;

Devam edelim:


    // genel istek
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // sunucu yanıtı
        ResponseEntity<Response<T1>> response;
        try {
            // istek hazırlanıyor
            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);
            }
            // istek yürütülüyor
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // istisna kapsülleniyor
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • 18. satır: Sunucuya istekte bulunan ve yanıtını alan komut. [RestTemplate] bileşeni, sunucuyla iletişim kurmak için çok sayıda yöntem sunar; ancak yalnızca [exchange] yöntemi genel parametreleri kabul eder. Bu nedenle bu yöntem seçilmiştir. İkinci parametre, beklenen yanıtın türünü belirler. İlk parametre, [RequestEntity] türündeki istektir (satır 8). [exchange] yönteminin sonucu, [ResponseEntity<Response<T1>>] türündedir (satır 5). [ResponseEntity] türü, sunucunun gönderdiği HTTP başlıklarını ve belgeyi içeren tam yanıtı kapsar. Aynı şekilde, [RequestEntity] türü, HTTP başlıklarını ve varsa gönderilen değeri de içeren istemcinin tüm isteğini kapsar;
  • satır 8-16: [RequestEntity] türündeki isteği oluşturmamız gerekiyor. Bu istek, isteği oluşturmak için GET mi yoksa POST mi kullanıldığına göre farklılık gösterir;
  • 10. satır: GET için sorgu. [RequestEntity] sınıfı, GET, POST, HEAD... sorgularını oluşturmak için statik yöntemler sunar. [RequestEntity.get] yöntemi, bunu oluşturan çeşitli yöntemleri zincirleyerek bir GET sorgusu oluşturulmasına olanak tanır:
    • [RequestEntity.get] yöntemi, hedef URL'i bir URI örneği biçiminde parametre olarak kabul eder,
    • [accept] yöntemi, HTTP ve [Accept] başlık öğelerini tanımlamaya olanak tanır. Burada, sunucunun göndereceği [application/json] türünü kabul ettiğimizi belirtiriz;
    • [build] yöntemi, bu farklı bilgileri kullanarak isteğin [RequestEntity] türünü oluşturur;
  • 14. satır: POST için istek. [RequestEntity.post] yöntemi, bunu oluşturan çeşitli yöntemleri zincirleyerek bir POST isteği oluşturulmasına olanak tanır:
    • [RequestEntity.post] yöntemi, hedef URL'i bir URI örneği biçiminde parametre olarak kabul eder,
    • [header] yöntemi, bir HTTP başlığını tanımlar. Burada, sunucuya [Content-Type: application/json] başlığını göndererek, gönderilen değerin jSON dizesi biçiminde ulaşacağını bildiririz;
    • [accept] yöntemi, sunucunun göndereceği [application/json] türünü kabul ettiğimizi belirtmemizi sağlar;
    • [body] yöntemi, gönderilen değeri belirler. Bu değer, genel [getResponse] yönteminin (1. satır) 4. parametresidir;
  • 20-23. satırlar: sunucuyla iletişim hatası meydana gelirse, [DaoException] türünde bir istisna atılır; hata kodu olarak, genel [getResponse] yönteminin (satır 3) 3. parametresi olarak geçirilen [errStatus] parametresi kullanılır;

[getResponse] yöntemi şu şekilde devam eder:


// genel istek
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // yanıt gövdesi alınıyor
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // sunucu tarafında hata mı var?
        if (status != 0) {
            // istisna oluşturuluyor
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // tamam
            return entity.getBody();
        }
    }
  • 4. satır: Sunucudan yanıt aldık. Bu yanıt, [ResponseEntity<Response<T1>>] türündedir (önceki kodda incelenen 5. satır); burada [Response] sınıfı, sunucu tarafında zaten kullanılan sınıftır:

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- özellikler
    // işlemin durumu
    private int status;
    // olası istisna
    private String exception;
    // yanıt gövdesi
    private T body;

    // yapıcılar
    public Response() {

    }

    public Response(int status, String exception, T body) {
        this.status = status;
        this.exception = exception;
        this.body = body;
    }

    // alıcı ve ayarlayıcılar
...
}

[getResponse] yöntemine geri dönelim:

  • 6. satır: Yanıtta kapsüllenmiş [Response<T1>] türündeki belgeyi alıyoruz. Bu türün [int status, String exception, T1 body] alanları vardır;
  • 7. satır: Yanıtta bulunan ve bir hata kodu olan [status]'i alıyoruz;
  • 9-12. satırlar: Hata varsa, sunucu yanıtındaki [status, exception] bilgilerini içeren bir istisna tetiklenir;
  • 14. satır: aksi takdirde, [Response<T1>] türündeki yanıtta bulunan [T1] türünü döndürürüz;

[Client] sınıfı geneldir. Herhangi bir web istemcisi / jSON için kullanılabilir.

18.5. [Dao] katmanının uygulanması

  

18.5.1. [AbstractDao] sınıfı

İstemci tarafındaki [DAO] katmanı, sunucu tarafındaki [DAO] katmanıyla aynı arayüze sahiptir (bkz. paragraf 4.7):


package spring.webjson.client.dao;

import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // T varlıklarının tam listesi
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // belirli varlıkların listesi - kısa sürüm
    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);

    // belirli varlıklar - uzun sürüm
    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);

    // birden fazla varlığın güncellenmesi
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // tüm varlıkların silinmesi
    public void deleteAllEntities();

    // birden fazla öğenin silinmesi
    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] sınıfı, [IDao] arayüzünü uygular. Bu sınıf, sunucu tarafındaki aynı isimli sınıfa benzerdir (bkz. paragraf 4.8). [DaoCategorie] ve [DaoProduit] sınıfları için üst sınıf görevi görür. İki nedenden dolayı aynı değildir:

  • sunucu tarafında, [AbstractDao] sınıfı bir bilgiyi yönetir:

    // eklemeler
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

Bunlara burada ihtiyacımız yok.

  • Sunucu tarafında, [AbstractDao] sınıfı, her yöntemi bir işlem içine sarmalamak için [@Transactional] anotasyonlarını kullanır. İstemci tarafında ise yönetilecek bir veritabanı yoktur. Dolayısıyla bu anotasyon ortadan kalkar;

[AbstractDao] sınıfı, çağrıyı alt sınıflara devretmeden önce [IDao] arayüzündeki yöntemlerin çağrı parametrelerinin geçerliliğini kontrol etmekle yetinir:


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> {

    // yerel
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // argümanın geçerliliği
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // sonuç
        return getShortEntitiesById(Lists.newArrayList(ids));
    }

    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // argümanın geçerliliği
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // sonuç
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }

    // özel yöntemler ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // null öğeler mi?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // boş öğeler mi?
        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>();
            }
        }
        // varsayılan sonuç
        return null;
    }

    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // null öğeler mi?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // boş öğeler mi?
        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>();
            }
        }
        // varsayılan sonuç
        return null;
    }

    // korumalı yöntemler ----------------------------------------------
    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] sınıfı

  

[DaoCategorie] sınıfı şu şekildedir:


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. satır: [DaoClient] sınıfı, içine diğer Spring bileşenlerinin enjekte edilebildiği bir Spring bileşenidir;
  • 20. satır: [DaoClient] sınıfı, az önce incelediğimiz [AbstractDao<Categorie>] sınıfını genişletir ve dolayısıyla [IDao<Categorie>] arayüzünü uygular;
  • 22-23. satırlar: Bean'lere erişebilmek için Spring bağlamı enjekte ediliyor;
  • 24-25. satırlar: Az önce oluşturduğumuz HTTP istemcisini enjekte ediyoruz;

[DaoCategorie] arayüzünün çeşitli yöntemlerinin uygulamaları, hepsi aynı şemayı takip eder. Üç yöntem sunacağız; bunlardan biri [GET] işlemine, diğer ikisi ise [POST] işlemine dayanmaktadır.

18.5.2.1. [getAllLongEntities] yöntemi

[getAllLongEntities] yöntemi, veritabanındaki tüm kategorilerin uzun versiyonlarını oluşturur:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // tüm kategorileri al
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // kategori listesi List<Kategori>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ürün --> kategori bağlantısını yeniden oluştur
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • 2. satır: Yöntem, kategorilerin listesini uzun versiyonlarıyla döndürür;
  • 5. satır: jSON eşleştiricisi, gönderilen değeri (yoktur) serileştirmeye ve [Client] sınıfı tarafından döndürülen yanıtı (kategorilerin uzun versiyonları) deserileştirmeye olanak tanır;
  • 7. satır: [Client] sınıfının [getResponse] yöntemi çağrılır. Web hizmeti / jSON ile veri alışverişini sağlayan bu yöntemdir. Parametreleri şunlardır:
    • sorgulanan hizmetin URL numarası [/getAllLongCategories];
    • kullanılacak yöntem: [GET];
    • hata olması durumunda kullanılacak hata kodu (232);
    • gönderilen değer. Burada böyle bir değer yoktur;
  • 7. satır: [client.<List<Categorie>, Void>] ifadesinde, [getResponse] yönteminin [T1, T2] jenerik türlerinin etkin parametreleri belirtilir. Hatırlatmak gerekirse, [T1] beklenen yanıtın türü, [T2] ise gönderilen değerin türüdür. Burada [List<Categorie>] türünde bir sonuç beklenmektedir ve [Void] türünde gönderilen bir değer yoktur;
  • 7. satır: [getResponse] yöntemi tarafından döndürülen sonuç, [Object] türünde bir nesneye atanır. Oysa [List<Categorie>] türü bekleniyor olması biraz tuhaf. Bunun nedeni, [getResponse] yönteminin [T1, T2] jenerik türleriyle çalışması ve sistematik olarak [java.util.LinkedHashMap] türünde bir sonuç döndürmesidir; doğru türü elde etmek için bu sonucu kullanmak gerekir;
  • 9. satır: Kategori listesi döndürülür. Bunun için, [map] [mapper.writeValueAsString(map)] nesnesini bir jSON dizesine serileştirip, bunu bir [List<Categorie>] türüne yeniden serileştiriyoruz;
  • 13. satır: Kategori listesi alınır; bu kategorilerin bazıları ürün içerebilir. Bu ürünlerin kısa versiyonları alınır. Dolayısıyla, deserialize edildiklerinde oluşturulan [Produit] nesneleri, [categorie==null] alanına sahiptir. [linkCategorieWithProduits] yöntemi, bir [Produit] ile onun [Categorie]'i arasındaki bağlantıyı yeniden oluşturur;
  • 14-15. satırlar: [getResponse] yöntemi tarafından tetiklenebilecek [DaoException] türündeki istisnayı durdurup hemen yeniden tetikliyor. Bu tuhaf davranış, bunu yapmazsak [DaoException] türündeki istisnanın 16-18. satırlar tarafından durdurulacak olmasından kaynaklanmaktadır ve bunu istemiyoruz;
  • 16-18. satırlar: Diğer tüm istisnaları durdurarak bunları [DaoException] türünde kapsüllemek. [DAO] katmanının yalnızca bu tür istisnaları tetiklemesi gerektiğini hatırlatırız;

[Produit] varlıklarıyla [Categorie] varlıkları arasındaki bağlantıları yeniden oluşturan [linkCategorieWithProduits] yöntemi şöyledir:


    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 filtre yönetimi

Önceki [getAllLongEntities] yöntemindeki jSON filtre yönetimine geri dönelim:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // tüm kategorileri al
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // kategori listesi List<Kategori>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });

  • 5. satır: Spring bağlamından, kategorilerin uzun sürümlerini yönetebilen bir jSON eşleştiricisi alınır. Spring yapılandırmasında [AppConfig] bu eşleştiricinin tanımına geri dönelim:

// filtreler 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] yöntemi tarafından istenen [jsonMapperLongCategorie] bean'i, 7-15. satırlardaki bean'dir;
  • 10. satır: eşleştirici, 2-5. satırlardaki [jsonMapper] yöntemi tarafından sağlanır. Görüldüğü üzere, bu jSON eşleştiricisi, istemci ile sunucu arasındaki HTTP veri alışverişini yöneten [RestTemplate] nesnesine aittir. Varsayılan olarak aşağıdakiler için bu eşleştirici kullanılır:
    • sunucuya gönderilen değeri serileştirmek;
    • sunucu tarafından geri gönderilen yanıtı deserialize etmek;

[getAllLongEntities] koduna geri dönelim:


            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // tüm kategorileri al
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // kategori listesi List<Kategori>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ürün --> kategori bağlantısını yeniden oluştur
return linkCategorieWithProduits(categories);
  • 2. satır: Spring bağlamından [jsonMapperLongCategorie] eşleştiricisi alınır;
  • 4. satır: [getResponse] yöntemi yürütülür. Bu aşamada şunlar gerçekleşir:
    • gönderilen değerin otomatik serileştirilmesi (burada böyle bir değer yok);
    • alınan yanıtın burada otomatik olarak deserializasyonu, burada bir List<Categorie> türü. Bunun nedeni, [Categorie] varlığının bir jSON [jsonFilterCategorie] filtresine sahip olması ve bunun yönetilmesi gerekmesidir. 2. satırın nedeni budur;
  • 6. satır: Sonuç, List<Categorie> türünü elde etmek için aynı eşleştiriciyle ikinci bir serileştirme/deserileştirme işleminden geçirilir. 4. satırda, [getResponse] tarafından döndürülen tür, [Object] türüdür;

Aşağıdaki yöntemlerde, Spring bağlamından istenen jSON eşleştiricisinin hem gönderilen değer (serileştirme) hem de alınan değer (deserileştirme) için kullanıldığı unutulmamalıdır. Değerlerden biri veya her ikisi de jSON filtresine sahipse, bunların yapılandırılması gerekir. Dolayısıyla eşleştirici, en fazla iki filtreye sahip olabilir. Aşağıdaki örnekte bu durum hiç gerçekleşmez. Ya gönderilen değerin bir filtresi yoktur (List<Long>, List<String>), ya da alınan değerin bir filtresi yoktur (List<CoreCategorie>, List<CoreProduit>). jSON filtresine sahip varlıklar yalnızca [Categorie] ve [Produit]'tir.

18.5.2.3. [getShortEntitiesById] yöntemi

[getShortEntitiesById] yöntemi, parametre olarak aldığı birincil anahtarlara karşılık gelen kategorilerin kısa sürümlerini döndürür:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // ürünleri hariç bir kategoriyi al
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // kategori
            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. satır: Gönderilen değeri (birincil anahtar listesi) serileştirmeye ve [Client] sınıfı tarafından döndürülen yanıtı (kategorilerin kısa versiyonları) deserileştirmeye olanak tanıyan jSON eşleştiricisi. Seçilen filtre, gönderilen değer üzerinde hiçbir etkiye sahip olmayacaktır; çünkü gönderilen listenin öğeleri için herhangi bir filtre bulunmamaktadır;
  • 7. satır: üst sınıfın [getResponse] yöntemini çağırıyoruz. Web hizmeti / jSON ile iletişimi sağlayan bu yöntemdir. Parametreleri şunlardır:
    • sorgulanan hizmetin URL değeri ([/getShortCategoriesById]);
    • kullanılacak [POST] yöntemi;
    • hata olması durumunda kullanılacak hata kodu (204);
    • gönderilen değer. Burada birincil anahtarların listesi yer almaktadır;
  • 7. satır: [client.<List<Categorie>, List<Long>>] ifadesinde, [getResponse] yönteminin [T1, T2] jenerik türlerinin etkin parametreleri belirtilir. Hatırlatmak gerekirse, [T1] beklenen yanıtın türü, [T2] ise gönderilen değerin türüdür. Burada [List<Categorie>] türünde bir sonuç beklenmektedir ve gönderilen değer, [List<Long>] türünde birincil anahtar listesidir;
  • 7. satır: [getResponse] yöntemi tarafından döndürülen sonuç, [Object] türünde bir nesneye yerleştirilir;
  • 9. satır: Kategori listesi döndürülür. Bunun için, [map] [mapper.writeValueAsString(map)] nesnesi, jSON dizesine serileştirilir ve bu dizi, [List<Categorie>] türüne yeniden serileştirilir;

18.5.2.4. [saveEntities] yöntemi

[saveEntities] yöntemi, kategorileri veritabanında kalıcı hale getirir. Kod şöyledir:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // kategoriler ekle
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // eklenen ana kategorilerin listesi
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // Alınan bilgilerle kategoriler güncelleniyor
            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. satır: [saveEntities] yöntemi, parametre olarak geçirilen kategorileri veritabanına kaydetmek için kullanılır. Bu yöntem, söz konusu kategorileri birincil anahtarlarıyla birlikte veritabanına kaydeder. Kategoriler ürünlerle birlikte geçirilirse, bu ürünler de veritabanına kaydedilir;
  • 5. satır: Gönderilen değeri (uzun biçimdeki kategori listesi) serileştirmeye ve [Client] sınıfı tarafından döndürülen yanıtı ([CoreCategorie] nesneleri) deserileştirmeye olanak sağlayacak jSON eşleştiricisi. Seçilen filtre, yanıt olarak alınan listenin öğeleri filtrelenmediği için sonuç üzerinde hiçbir etkiye sahip olmayacaktır;
  • 7. satır: Web hizmeti / jSON ile veri alışverişi yapmak için üst sınıfın [getResponse] yöntemini çağırıyoruz;
    • 1. parametre URL [/saveCategories]'tir;
    • ikinci parametre, kullanılacak HTTP yöntemidir; burada [POST] kullanılmaktadır;
    • üçüncü parametre, hata olması durumunda kullanılacak hata kodudur (200);
    • son parametre ise gönderilen değerdir; burada, kalıcı hale getirilecek kategoriler listesi yer almaktadır;
  • 7. satır: [getResponse] yönteminin genel parametreleri [T1, T2], burada [List<CoreCategorie>, List<Categorie>] olarak belirtilmiştir. İlk tür beklenen yanıtın türüdür, ikincisi ise gönderilen değerin türüdür;
  • 7. satır: Alınan yanıt, [Object] türüne atanır;
  • 9. satır: [List<CoreCategorie>] türündeki yanıt yeniden oluşturulur. Verilecek yanıt, [List<Categorie>] türündedir (2. satır) ve [List<CoreCategorie>] değildir. Alınan yanıt, kalıcı kategorilerin ve ürünlerin birincil anahtarlarının listesidir;
  • satır 14-28: Alınan birincil anahtarlar, kategorilere ve ürünlere atanır (satır 17, 23, 24). Ayrıca, [Produit] --> [Categorie] bağlantıları yeniden oluşturulur (satır 24-25);

Diğer tüm yöntemler de aynı şablonu izler.

18.6. JUnit testi

Şu anda oluşturulmakta olan istemci/sunucu mimarisine geri dönelim:

[DAO] [2] katmanını, [DAO] [4] katmanıyla aynı arayüze sahip olacak şekilde oluşturduk. Dolayısıyla, [DAO] ve [2] katmanlarını test etmek için, [DAO] ve [4] katmanlarını test etmekte kullanılan JUnit testlerini kullanabiliriz:

  

Bu üç test, aşağıdaki yürütme yapılandırmalarıyla çalıştırılır:

 

Üç testin sonuçları şunlardır:

  • [1]'te, [JUnitTestCheckArguments] testi;
  • [2]'te, [JUnitTestDao] testi;
  • [3]'te, istemci tarafında yürütülen [JUnitTestPushTheLimits] testi ([spring-webjson-client-generic] projesi);
  • [3]'te, sunucu tarafında yürütülen [JUnitTestPushTheLimits] testi ([spring-jdbc-generic-04] projesi). Ağ katmanının, SGBD'e erişimden kaynaklanan yavaşlamaya kıyasla çok az bir yavaşlamaya neden olduğu görülmektedir;

18.7. Web hizmeti uygulaması / jSON / JPA / Hibernate

Şimdi şu mimariye odaklanacağız:

Değişiklik [1]'te yer almaktadır. Sunucunun [DAO] katmanı, JPA uygulamasına dayanmaktadır. Öncelikle bir JPA / Hibernate uygulaması kullanacağız.

18.7.1. Eclipse projesi

Şu anda Eclipse'e yüklenen projeler şunlardır:

  

[spring-webjson-server-jdbc-generic] projesi, [spring-jdbc-generic-04] projesine dayanıyordu; bu proje, DAO / JDBC katmanını yapılandıran SGBD ve MySQL'e erişim katmanını yapılandıran projeye dayanıyordu. Şimdi, DAO / JPA / JDBC katmanını yapılandıran [spring-jpa-generic] projesine dayanacak. Her iki durumda da [DAO] katmanının aynı [IDao] arayüzünü uyguladığını biliyoruz. Dolayısıyla [web] katmanının kodu değişmez.

[spring-webjson-server-jpa-generic] projesini, [spring-webjson-server-jdbc-generic] projesini kopyalayıp yapıştırarak oluşturabiliriz:

  • adını [1] olarak değiştirip, yeni proje için özel olarak oluşturulmuş bir klasörü belirleyebiliriz;
  

Yapılması gereken üç tür değişiklik vardır. İlki, projenin Maven yapılandırma dosyası olan [pom.xml] dosyasındadır:


<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 katmanı -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- katman [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- eklentiler -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • 5. satır: Maven artefaktının adı değiştirilir;
  • 24-28. satırlar: Bağımlılık artık [spring-jpa-generic] projesine yöneliktir, [spring-jdbc-generic-04] projesine değil;

Sonuç olarak, bağımlılıklar şu şekildedir:

  

Bu işlem tamamlandıktan sonra, çeşitli sınıflarda ortaya çıkan tüm içe aktarma sorunları çözülür. Örneğin, [Produit, Categorie] varlıklarını artık [spring-jdbc-generic-04] projesinde değil, [spring-jpa-generic] projesinde aramak gerekir. Bir sınıfın kodunda [Ctrl-Maj-O] yazmak, içe aktarmaları yeniden oluşturmak için yeterlidir.

Son değişiklik, [AppConfig] yapılandırma dosyasında yapılmalıdır:


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. satır: Artık [spring-jpa-generic] projesinin yapılandırmasını içe aktarıyoruz, [spring-jdbc-generic-04] projesininkini değil;

Bu değişiklik yapıldıktan sonra hazırız. [spring-webjson-server-jpa-generic-hibernate-eclipselink] yapılandırmasıyla web hizmetini başlatıyoruz:

Ardından, genel istemci [spring-webjson-client-generic]'in üç testini çalıştırıyoruz:

  • [1]'te, [JUnitTestCheckArguments] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • [2]'te, [JUnitTestDao] testi ([spring-webjson-client-generic-JUnitTestDao] yürütme yapılandırması);
  • [3]'e, istemci tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • [4]'te, sunucu tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Neden çalışıyor?

Çalışıyor, ancak koda dikkatlice bakıldığında bunun çalışması şaşırtıcı. [spring-jdbc-generic-04] ve [spring-jpa-generic] projeleri tarafından uygulanan [DAO] katmanları aynı arayüze sahip olsa da, aynı [Categorie] ve [Produit] varlıklarını işlemiyorlar: [spring-jpa-generic] projesinde, bu varlıkların [EntityType entityType] adlı ek bir alanı var ve bu alanın iki olası değeri bulunuyor:

  • EntityType.POJO: varlık, tüm alanları serbestçe kullanılabilen normal bir nesnedir;
  • EntityType.PROXY: Varlık, [JPA] katmanı tarafından oluşturulan bir PROXY nesnesidir. Bu durumda, bazı alanlar (aslında bu alanların getter'ları) normal davranış göstermez ve aşağıdaki kurallar belirlenmiştir:
    • eğer [Categorie.entityType==EntityType.PROXY] ise, o zaman [getProduits] yöntemini kullanmamalısınız;
    • eğer [Produit.entityType==EntityType.PROXY] ise, o zaman [getCategorie] yöntemi kullanılmamalıdır;

Oysa az önce [spring-webjson-server-jdbc-generic] projesini, kodda herhangi bir değişiklik yapmadan [spring-webjson-server-jpa-generic]'e taşıdık. Bu nasıl mümkün olabilir?

[saveCategories] yönteminin kodunu inceleyelim:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // gönderilen değer alınır
            String body = CharStreams.toString(request.getReader());
            // seri halinden dönüştürülür
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // kategorileri kalıcı hale getiriyoruz
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • 8. satır: Bir jSON dizesi kullanılarak bir List<Categorie> nesnesi oluşturulur:
    • gönderilen değerde, ürünlerin [categorie] alanı yoktur. Aslında bu alanı göndermek gereksizdir. Eğer gönderilseydi, deserializasyon işlemi, yeni oluşturulmuş bir [Categorie] nesnesine işaret eden bir [categorie] alanına sahip bir [Produit] nesnesi oluştururdu. n adet ürün için, tek bir nesneye ihtiyaç varken n adet [Categorie] nesnesi oluşturulmuş olur. Ayrıca, ürünlerin [categorie] alanı, ürünlerin ait olduğu doğru [Categorie] nesnesine işaret etmez. Dolayısıyla burada ürünlerin [categorie==null] alanı vardır;
    • [Categorie] ve [Produit] sınıflarında ise [EntityType entityType] alanı şu şekilde tanımlanmıştır:

    protected EntityType entityType = EntityType.POJO;

Dolayısıyla, serileştirme işlemiyle oluşturulan [Categorie] ve [Produit] varlıklarının tümü POJO türündedir.

  • 11. satır: kategoriler kalıcı hale getiriliyor. Burada bu işlem işe yaramamalıdır. Nitekim, JDBC uygulamasında, [Produit.categorie] alanı kalıcılık için gerekli değilse (burada [idCategorie] alanı kullanılır), JPA uygulaması için bu alan kesinlikle gereklidir. Bu alan bir [Categorie] varlığına işaret etmelidir, ancak burada değeri null'tir.

[DAO / JPA] katmanındaki [DaoCategorie.saveEntities] yönteminin kodunu inceleyelim:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // eklenecek ürünleri not ediyoruz
        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);
                    }
                    // bu fırsatı değerlendirerek (gerekirse) ürün --> kategori ilişkisini yeniden kurulur
                    produit.setCategorie(categorie);
                }
            }
        }
        // kategoriler / ürünler kalıcı hale getirilir
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // eklenen ürünlerin [idCategorie] alanını güncellenir
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // sonuç
        return categories;
    }
  • 13-14. satırlar: POJO varlıklarında (8. satır) [Produit] --> [Categorie] bağlantısının yeniden kurulduğunu görüyoruz; bu durum burada da geçerlidir. Bu da kategorilerin kalıcılığının neden işe yaradığını açıklıyor. Bu durum başka koşullarda da yararlıdır: Kullanıcının ürünleri kategorilere doğru bir şekilde bağladığından asla emin olamayız. Bu yüzden bunu onun yerine biz yapıyoruz;

Şimdi, ürünleri kalıcı hale getiren [ProduitController.saveProduits] yöntemini inceleyelim:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // gönderilen değer alınır
            String body = CharStreams.toString(request.getReader());
            // de-serialize ediliyor
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // ürünler korunuyor
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // yanıt verilir
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • 8. satır: Gönderilen değerden bir List<Ürün> nesnesi oluşturulur. Daha önce açıklanan nedenlerden dolayı, her [Produit] nesnesinde şu alan bulunur:
    • [EntityType entityType], [EntityType.POJO]'e eşit;
    • [Categorie categorie], null'e eşittir;
  • 11. satır: Ürünlerin kalıcılığı başarısız olmalıdır. Nitekim, JPA ile bir ürünün kalıcılığı, ancak [categorie] alanı bir [Categorie] varlığına işaret ediyorsa mümkündür;

[DAO / JPA] katmanındaki [DaoProduit.saveEntities] yönteminin koduna bakalım:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // (gerekirse) bir ürün ile kategorisi arasındaki bağlantıyı yeniden kurulur
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // ürünleri kalıcı hale getirilir
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • 3-8. satırlar: POJO türündeki her [Produit] için, doğru birincil anahtara sahip ve null olmayan bir sürüme sahip bir [Categorie] nesnesine bağlantı oluşturulur. Bu, JPA katmanının ürünü doğru şekilde kalıcı hale getirmesi için yeterlidir;

Son bir noktaya bakalım. [Categorie] ve [Produit] nesneleri, bu nesneler müşteriye gönderildiğinde jSON olarak serileştirilecek olan [EntityType entityType] adlı ek bir alana sahiptir. Bunu [Advanced Rest Client] ile doğrulayabiliriz:

İstemci tarafında ise [Categorie] ve [Produit] nesneleri, [EntityType entityType] alanı olmadan tanımlanmıştır. Bu normaldir, çünkü [Categorie] ve [Produit] nesneleri, PROXY, [Categorie.produits] ve [Produit.categorie] kısımları olmadan serileştirilir. Dolayısıyla istemci tarafında PROXY adlı bir varlık kavramı yoktur. Sadece normal nesneler vardır.

İstemci tarafında, jSON [1] dizesi, aşağıdaki [DaoCategorie.getAllShortEntities] yöntemi tarafından alınır:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filtreler jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // tüm kategorileri alın
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // kategori listesi List<Kategori>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • 5. satır: [RestTemplate] nesnesinin jSON eşleştiricisi,[Categorie] nesnesine ait jSON ve [jsonFilterCategorie] filtrelerini ve [Produit] nesnesine ait [jsonFilterProduit] filtresini yönetecek şekilde yapılandırılır;
  • 7. satır: gönderilen değer (burada yok) ve alınan değer (List<Categorie>) bu eşleştirici ile serileştirilir/serileştirme işlemi kaldırılır. jSON dizisinde [entityType] alanının bulunduğu, ancak bu alanın istemci tarafındaki [Categorie] ve [Produit] varlıklarında mevcut olmadığı görülmektedir; bir hata oluşturmadığı görülmektedir. Bu alan göz ardı edilmektedir. Eğer bir hata oluşturmuş olsaydı, göz ardı edilmesi için istemci tarafındaki filtreler değiştirilirdi.

/ jSON / JPA / EclipseLink web hizmetini uygulamak için, JPA uygulamasını değiştirmeniz yeterlidir:

  

Not: Alt+F5 tuşlarına basın ve ardından tüm Maven projelerini yeniden oluşturun.

Web hizmetini, Hibernate için daha önce kullanılmış olan [spring-webjson-server-jpa-generic-hibernate-eclipselink] yürütme yapılandırmasıyla başlatacağız. Bu işlem tamamlandıktan sonra, genel istemci [spring-webjson-client-generic]'in üç testini çalıştırın:

  • [1]'te [JUnitTestCheckArguments] testi;
  • [2]'te, [JUnitTestDao] testini;
  • [3]'te, istemci tarafında yürütülen [JUnitTestPushTheLimits] testi ([spring-webjson-client-generic] projesi);
  • [4]'te, sunucu tarafında yürütülen [JUnitTestPushTheLimits] testi ([spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink] yürütme yapılandırması);

18.9. Web hizmetinin uygulanması / jSON / JPA / OpenJpa

Web hizmetini / jSON / JPA / OpenJpa uygulamak için, JPA uygulamasını değiştirmeniz yeterlidir:

  

Not: Alt+F5 tuşlarına basın ve ardından tüm Maven projelerini yeniden oluşturun.

Web hizmetini [spring-webjson-server-jpa-generic-openpa] çalıştırma yapılandırmasıyla başlatacağız:

Bunu yaptıktan sonra, [spring-webjson-client-generic] genel istemcisinin üç testini çalıştırın:

  • [1]'te, [JUnitTestCheckArguments] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • [2]'te, [JUnitTestDao] testi ([spring-webjson-client-generic-JUnitTestDao] yürütme yapılandırması);
  • [3]'e, istemci tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • [4]'te, sunucu tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Testlerin çalışması için DAO / JPA katmanında değişiklikler yapılması gerekti. Nitekim, anlaşılmaz bir şekilde [DaoCategorie.saveEntities] ve [DaoProduit.saveEntities] yöntemleri, veritabanının doldurulması sırasında, ayrılmış öğelerin kalıcı hale getirilemediğini belirterek hata verdi. Ayrılmış öğe, aşağıdakilerden birine sahip olan öğedir:

  • null olmayan bir birincil anahtara sahip;
  • null olmayan bir sürüme sahip olan;

Her iki durum da kontrol edilmiyordu. Nerede aramam gerektiğini bilmediğim için, kalıcı hale getirilecek varlıkları yepyeni bir listeye kopyaladım ve testler bu şekilde başarılı oldu. Bu değişiklik şu şekilde yapılabilirdi:

  • [DAO / JPA] katmanında;
  • kalıcı varlıkların oluşturulduğu [web] katmanında;

Ben bunu [DAO / JPA] katmanında yapmayı tercih ettim. Elbette bir performans kaybı var, ancak bu, SGBD'in yanıt süreleriyle karşılaştırıldığında tamamen ihmal edilebilir düzeyde. Değişiklikler şunlardır:

[spring-jpa-generic] projesindeki [DaoCategorie] sınıfında:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // kategori listesini kopyalıyoruz -- bazen OpenJpa için gereklidir -- hata anlaşılmadı
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // kategori
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // ürünler
            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);
            }
        }
        // eklenecek ürünler not edilir
        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);
                    }
                    // bu fırsatı değerlendirerek (gerekirse) ürün --> kategori ilişkisini yeniden kurulur
                    produit.setCategorie(categorie);
                }
            }
        }
        // kategorileri / ürünleri kalıcı hale getirilir
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // eklenen ürünlerin [idCategorie] alanını güncelliyoruz
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // sonuç
        return categories2;
    }
  • 3-25. satırlar: parametre olarak alınan [categories] listesi (2. satır), [categories2] listesinde (6. satır) kopyalanır. Kalıcı hale getirilen ve çağırana döndürülen liste budur (satır 52). Bunun önemli bir sonucu vardır: parametre olarak geçirilen listeden farklı bir liste döndürülür ve bu nedenle daha önce şöyle yazılabilen yerlerde:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// [categories]'in işlenmesi

Artık şöyle yazmak gerekir:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// [categories]'in işlenmesi

[spring-jpa-generic] projesindeki [DaoProduit] sınıfında, [saveEntities] yöntemi de benzer şekilde değiştirilir:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // ürün listesi kopyalanıyor -- bazen OpenJpa için gereklidir -- hata dahil değildir
        // ***************************************************************************************
        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);
        }

        // (gerekirse) bir ürün ile kategorisi arasındaki bağlantıyı yeniden kurulur
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // ürünleri kalıcı hale getirilir
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

/ jSON / JPA / EclipseLink / PostgresQL web hizmetini uygulamak için şunları yüklemeniz gerekir:

  • PostgreSQL katmanının JDBC yapılandırma projesini;
  • PostgreSQL'in JPA katmanının yapılandırma projesi olan [postresql-config-jpa-eclipselink];
  • Alt-F5 tuşlarına basın ve tüm Maven projelerini yeniden oluşturun;
  

SGBD ve PostgreSQL'i başlatın ve daha önce kullanılmış olan [spring-webjson-server-jpa-generic-hibernate-eclipselink] çalıştırma yapılandırmasıyla web hizmetini başlatın. Bunu yaptıktan sonra, genel istemci [spring-webjson-client-generic]'in üç testini çalıştırın:

  • [1]'te, [JUnitTestCheckArguments] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • [2]'te, [JUnitTestDao] testi ([spring-webjson-client-generic-JUnitTestDao] yürütme yapılandırması);
  • [3]'e, istemci tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • [4]'te, sunucu tarafında yürütülen [JUnitTestPushTheLimits] testi (çalıştırma yapılandırması [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);