Skip to content

14. 3 katmanlı mimaride MVC web uygulaması – Örnek 1

14.1. Présentation

Şimdiye kadar, sadece eğitim amaçlı örneklerle yetindik. Bu nedenle, bu örneklerin basit olması gerekiyordu. Şimdi, temel düzeyde ancak şimdiye kadar sunulan tüm örneklerden daha zengin bir uygulama sunuyoruz. Bu uygulamanın özelliği, 3 katmanlı mimarinin üç katmanını da kullanması olacaktır:

Image

Okuyucunun, 3 katmanlı bir mimaride bir web uygulamasının MVC ilkelerini unutmuşsa, 4. paragrafta bunları tekrar gözden geçirmesi önerilir.

Yazacağımız web uygulaması, dört işlemle bir grup kişiyi yönetmeye olanak sağlayacaktır:

  • grup üyelerinin listesi
  • gruba kişi ekleme
  • gruptaki bir kişinin bilgilerinin değiştirilmesi
  • gruptan bir kişinin silinmesi

Bu dört temel işlemi, bir veritabanı tablosundaki işlemlerle özdeşleştirebiliriz. Bu uygulamanın iki sürümünü yazacağız:

  • 1. sürümde, [dao] katmanı bir veritabanı kullanmayacaktır. Gruptaki kişiler, [dao] katmanı tarafından dahili olarak yönetilen basit bir [ArrayList] nesnesinde depolanacaktır. Bu, okuyucunun veritabanı kısıtlaması olmadan uygulamayı test etmesini sağlayacaktır.
  • Sürüm 2'de, kişi grubunu bir veritabanı tablosuna yerleştireceğiz. Bunun, sürüm 1'in web katmanını etkilemeden gerçekleştirileceğini ve bu katmanın değişmeden kalacağını göstereceğiz.

Aşağıdaki ekran görüntüleri , uygulamanın kullanıcıyla karşılıklı olarak görüntülediği sayfaları göstermektedir.

Image

Image

Image

 

14.2. Eclipse Projesi

Uygulama projesinin adı [personnes-01]'tir:

Image

Bu proje, uygulamanın 3 katmanlı mimarisinin üç katmanını da kapsamaktadır:

  • [dao] katmanı, [istia.st.mvc.personnes.dao] paketinde yer almaktadır
  • [metier] veya [service] katmanı, [istia.st.mvc.personnes.service] paketinde yer almaktadır
  • [web] veya [ui] katmanı, [istia.st.mvc.personnes.web] paketinde yer almaktadır
  • [istia.st.mvc.personnes.entites] paketi, farklı katmanlar arasında paylaşılan nesneleri içerir
  • [istia.st.mvc.personnes.tests] paketi, [dao] ve [service] katmanlarının Junit testlerini içerir

[dao], [service] ve [web] katmanlarını sırasıyla inceleyeceğiz. Yazması çok uzun sürer ve okuması da sıkıcı olabilir, bu nedenle sunulan içerik yeni olmadıkça açıklamaları bazen biraz hızlı geçebiliriz.

14.3. Bir kişinin temsil edilmesi

Uygulama, bir grup kişiyi yönetir. 14.1 numaralı paragrafta yer alan ekran görüntüleri, bir kişinin bazı özelliklerini göstermiştir. Biçimsel olarak, bunlar [Personne] sınıfı ile temsil edilir:

Image

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

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

     // kişinin benzersiz kimliği
    private int id;
     // güncel sürüm
    private long version;
     // soyadı
    private String nom;
     // ad
    private String prenom;
     // doğum tarihi
    private Date dateNaissance;
     // medeni durum
    private boolean marie = false;
     // çocuk sayısı
    private int nbEnfants;

     // getter - setter
...

     // varsayılan oluşturucu
    public Personne() {

    }

     // kişinin alanlarını başlatarak oluşturucu
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // başka bir kişiden kopyalanarak oluşturulan kişi oluşturucu
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • Bir kişi aşağıdaki bilgilerle tanımlanır:
    • id: bir kişiyi benzersiz bir şekilde tanımlayan bir numara
    • soyadı: kişinin soyadı
    • ad: kişinin adı
    • dateNaissance: doğum tarihi
    • evli: evli olup olmadığı
    • nbEnfants: çocuk sayısı
  • [version] özniteliği, uygulamanın ihtiyaçları doğrultusunda yapay olarak eklenmiş bir özniteliktir. Nesne yönünden bakıldığında, bu özniteliği [Personne]'ten türetilen bir sınıfa eklemek şüphesiz daha uygun olurdu. Bu özniteliğe duyulan ihtiyaç, web uygulamasının kullanım senaryoları üzerinde çalışırken ortaya çıkmaktadır. Bunlardan biri şöyledir:

T1 zamanında, bir U1 kullanıcısı, P adlı bir kişinin düzenleme ekranına girer. Bu anda çocuk sayısı 0’dır. Bu sayıyı 1’e değiştirir ancak değişikliğini onaylamadan önce, U2 adlı bir kullanıcı aynı kişi P’nin düzenleme sayfasına girer. U1 henüz değişikliğini onaylamadığından, U2 çocuk sayısını 0 olarak görür. U2, P adlı kişinin adını büyük harfe çevirir. Ardından U1 ve U2, değişikliklerini bu sırayla onaylar. U2'in yaptığı değişiklik geçerli olacaktır: isim büyük harfe dönüştürülecek ve çocuk sayısı sıfır olarak kalacaktır; oysa U1, bu sayıyı 1 olarak değiştirdiğini sanmaktadır.

Kişi sürümü kavramı bu sorunu çözmemize yardımcı olur. Aynı kullanım örneğini ele alalım:

T1 zamanında, U1 adlı bir kullanıcı, P adlı kişinin düzenleme moduna girer. Bu anda çocuk sayısı 0’dır ve sürüm V1’tir. Çocuk sayısını 1 olarak değiştirir ancak değişikliğini onaylamadan önce, U2 adlı bir kullanıcı aynı kişi P'nin düzenleme sayfasına girer. U1 henüz değişikliğini onaylamadığından, U2, çocuk sayısını 0 ve sürümü V1 olarak görür. U2, P adlı kişinin adını büyük harflerle yazar. Ardından U1 ve U2, değişikliklerini bu sırayla onaylar. Bir değişikliği onaylamadan önce, P kişisini değiştiren kişinin, halihazırda kayıtlı olan P kişisiyle aynı sürüme sahip olup olmadığı kontrol edilir. Bu durum, U1 kullanıcısı için geçerlidir. Dolayısıyla, yaptığı değişiklik kabul edilir ve değiştirilen kişinin sürümü, kişide bir değişiklik yapıldığına işaret etmek amacıyla V1'ten V2'e değiştirilir. U2'in değişikliği onaylandığında, bu kullanıcının kişi P'nin V1 sürümüne sahip olduğu, oysa kişi P'nin mevcut sürümünün V2 olduğu fark edilecektir. Böylece U2 kullanıcıya, kendisinden önce birinin bu işlemi gerçekleştirdiği ve P kişisinin yeni sürümünden başlaması gerektiği söylenebilir. Kullanıcı bunu yapacak, artık bir çocuğu olan V2 sürümündeki P kişisini alacak, ismi büyük harflerle yazacak ve onaylayacaktır. Kayıtlı P kişisinin sürümü hâlâ V2 ise, yaptığı değişiklik kabul edilecektir. Sonuç olarak, U1 ve U2 tarafından yapılan değişiklikler dikkate alınacaktır; oysa sürüm kullanılmayan senaryoda, değişikliklerden biri kaybolurdu.

  • 32-40. satırlar: Bir kişinin alanlarını başlatabilen bir oluşturucu. [version] alanı atlanır.
  • 43-51. satırlar: Parametre olarak verilen kişinin bir kopyasını oluşturan bir oluşturucu. Böylece, içeriği aynı olan ancak iki farklı işaretçi tarafından referans verilen iki nesne elde edilir.
  • 55. satır: [toString] yöntemi, kişinin durumunu temsil eden bir karakter dizesi döndürmek üzere yeniden tanımlanmıştır

14.4. [dao] katmanı

[dao] katmanı aşağıdaki sınıf ve arayüzlerden oluşur:

Image

  • [IDao], [dao] katmanı tarafından sunulan arayüzdür
  • [DaoImpl], kişi grubunun bir [ArrayList] nesnesi içinde kapsüllendiği bu arayüzün bir uygulamasıdır
  • [DaoException], [dao] katmanı tarafından tetiklenen denetlenmemiş (unchecked) istisna türüdür

[IDao] arayüzü şu şekildedir:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // tüm kişilerin listesi
    Collection getAll();
     // belirli bir kişiyi bulma
    Personne getOne(int id);
     // bir kişiyi eklemek/düzenlemek
    void saveOne(Personne personne);
     // bir kişiyi sil
    void deleteOne(int id);
}
  • Arayüz, kişi grubu üzerinde gerçekleştirilmek istenen dört işlem için dört yönteme sahiptir:
    • getAll: bir kişi koleksiyonunu almak için
    • getOne: belirli bir id değerine sahip bir kişiyi almak için
    • saveOne: bir kişi eklemek (id=-1) veya mevcut bir kişiyi düzenlemek (id <> -1)
    • deleteOne: belirli bir id değerine sahip bir kişiyi silmek için

[dao] katmanı istisnalar oluşturabilir. Bu istisnalar [DaoException] türünde olacaktır :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // hata kodu
    private int code;

    public int getCode() {
        return code;
    }

// oluşturucu
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • 3. satır: [RuntimeException]'ten türetilen [DaoException] sınıfı, kontrol edilemeyen bir istisna türüdür: derleyici,
    • bu tür istisnaları, istisna atabilecek bir yöntemi çağırdığımızda try / catch ile yönetmemizi
    • istisnayı tetikleyebilecek bir yöntemin imzasına "throws DaoException" işaretçisini eklemek

Bu teknik, [IDao] arayüzündeki yöntemleri belirli bir türdeki istisnalarla imzalamak zorunda kalmamızı önler. Böylece, kontrolsüz istisnalar atan tüm uygulamalar kabul edilebilir hale gelir ve bu da mimariye esneklik kazandırır.

  • 6. satır: bir hata kodu. [dao] katmanı, farklı hata kodlarıyla tanımlanacak çeşitli istisnalar atacaktır. Bu, istisnayı yönetmeye karar verecek katmanın hatanın tam kaynağını bilmesini ve böylece uygun önlemleri almasını sağlayacaktır. Aynı sonuca ulaşmanın başka yolları da vardır. Bunlardan biri, olası her hata türü için bir istisna türü oluşturmaktır; örneğin NomManquantException, PrenomManquantException, AgeIncorrectException, ...
  • 13-16. satırlar: Bir hata kodu ve bir hata mesajıyla tanımlanan bir istisna oluşturmaya yarayan oluşturucu.
  • satır 8-10: istisna işleme kodunun hata kodunu almasını sağlayacak yöntem.

[DaoImpl] sınıfı , [IDao] arayüzünü uygular:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // kişi listesi
    private ArrayList personnes = new ArrayList();

     // sonraki kişinin numarası
    private int id = 0;

     // başlatma işlemleri
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // kişi listesi
    public Collection getAll() {
        return personnes;
    }

     // belirli bir kişiyi bulma
    public Personne getOne(int id) {
         // aranan kişi
        int i = getPosition(id);
         // bulundu mu?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // kişi ekleme veya değiştirme
    public void saveOne(Personne personne) {
         // "kişi" parametresi geçerli mi?
        check(personne);
         // Ekleme mi, düzenleme mi?
        if (personne.getId() == -1) {
             // ekleme
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // değişiklik - kişi aranıyor
        int i = getPosition(personne.getId());
         // bulundu mu?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         // Orijinalin doğru sürümü var mı?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         // 10 ms bekleniyor
         //wait(10);
         // tamam - değişikliği yapıyoruz
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // bir kişinin silinmesi
    public void deleteOne(int id) {
         // kişiyi arıyoruz
        int i = getPosition(id);
         // bulundumu?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // kişiyi siliyoruz
            personnes.remove(i);
        }
    }

     // kimlik oluşturucu
    private int getNextId() {
        id++;
        return id;
    }

     // bir kişiyi arama
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         // kişi listesini gözden geçiriyoruz
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         // sonuç?
        return trouvé ? i : -1;
    }

     // bir kişinin bilgilerini kontrol etme
    private void check(Personne p) {
         // kişi p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // kimlik
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // doğum tarihi
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // çocuk sayısı
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // soyadı
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // ad
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // beklenen
    private void wait(int N) {
         // N ms bekleniyor
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // istisna izini görüntüler
            e.printStackTrace();
            return;
        }
    }
}

Bu kodun sadece ana hatlarını vereceğiz. Ancak en hassas kısımlar üzerinde biraz duracağız.

  • 13. satır: Kişi grubunu içerecek olan [ArrayList] nesnesi
  • 16. satır: En son eklenen kişinin kimliği. Her yeni eklemeyle bu kimlik 1 artırılacaktır.

[DaoImpl] sınıfından tek bir örnek oluşturulacaktır. Buna singleton denir. Bir web uygulaması, kullanıcılarına eşzamanlı olarak hizmet verir. Belirli bir anda, web sunucusu tarafından yürütülen birden fazla iş parçacığı bulunur. Bu iş parçacıkları, singletonları paylaşır:

  • [dao] katmanına ait olan
  • [service] katmanına ait olan
  • web katmanındaki çeşitli denetleyiciler, veri doğrulayıcılar vb.

Bir singletonun özel alanları varsa, hemen neden böyle alanlara sahip olduğu sorulmalıdır. Bunlar haklı mı? Zira bunlar farklı iş parçacıkları arasında paylaşılacaktır. Eğer salt okunur iseler, yalnızca bir iş parçacığının aktif olduğundan emin olduğumuz bir anda başlatılabilmeleri durumunda sorun teşkil etmez. Genellikle bu anı belirlemek mümkündür. Bu, web uygulamasının henüz müşterilere hizmet vermeye başlamadığı başlangıç anıdır. Eğer okuma/yazma erişimine açıklarsa, alanlara erişim için bir senkronizasyon mekanizması kurulmalıdır; aksi takdirde felakete davetiye çıkarılır. Bu sorunu, [dao] katmanını test ederken örnekleyeceğiz.

  • [DaoImpl] sınıfının oluşturucusu yoktur. Dolayısıyla varsayılan oluşturucusu kullanılacaktır.
  • 19-38. satırlar: [init] yöntemi, [dao] katmanının singleton örneğinin oluşturulduğu sırada çağrılacaktır. Bu yöntem, üç kişiden oluşan bir liste oluşturur.
  • 41-43. satırlar: [IDao] arayüzünün [getAll] yöntemini uygular. Kişi listesine bir referans döndürür.
  • 46-55. satırlar: [IDao] arayüzünün [getOne] yöntemini uygular. Parametresi, aranan kişinin kimliğidir.

Bu kimliği almak için 113-126. satırlardaki özel [getPosition] yöntemine başvurulur. Bu yöntem, aranan kişinin listede bulunduğu konumu döndürür; kişi bulunamazsa -1 değerini döndürür.

Kişi bulunmuşsa, [getOne] yöntemi, kişinin kendisine değil, bu kişinin bir kopyasına ilişkin bir referans (51. satır) döndürür. Nitekim, bir kullanıcı bir kişiyi düzenlemek istediğinde, bu kişiye ait bilgiler [dao] katmanından talep edilir ve bir [Personne] nesnesine yapılan referans şeklinde, düzenleme amacıyla [web] katmanına iletilir. Bu referans, değiştirme formunda giriş kutusu olarak işlev görecektir. Web katmanında kullanıcı değişikliklerini gönderdiğinde, giriş kutusunun içeriği değiştirilecektir. Eğer giriş kutusu, [dao] katmanındaki [ArrayList] kişisine ait bir referansa karşılık geliyorsa, o kişi de değiştirilmiş olur; ancak bu değişiklikler [service] ve [dao] katmanlarına yansıtılmamıştır. Kişi listesini yönetme yetkisi yalnızca sonuncu katmana aittir. Bu nedenle, web katmanının değiştirilecek kişinin bir kopyası üzerinde çalışması gerekir. Burada [dao] katmanı bu kopyayı sağlar.

Aranan kişi bulunamazsa, hata kodu 2 ile [DaoException] türünde bir istisna atılır (53. satır).

  • 94-104. satırlar: [IDao] arayüzünün [deleteOne] yöntemini uygular. Bu yöntemin parametresi, silinecek kişinin kimliğidir. Silinecek kişi mevcut değilse, hata kodu 2 ile [DaoException] türünde bir istisna atılır.
  • 58-91. satırlar: [IDao] arayüzünün [saveOne] yöntemini uygular. Bu yöntemin parametresi bir [Personne] nesnesidir. Bu nesnenin id değeri -1 ise, bu durum bir kişinin eklenmesi anlamına gelir. Aksi takdirde, listedeki bu id'ye sahip kişinin parametre değerleriyle güncellenmesi söz konusudur.
    • 60. satır: [Personne] parametresinin geçerliliği, 129-155. satırlarda tanımlanan özel bir yöntem olan [check] ile kontrol edilir. Bu yöntem, [Personne] nesnesinin çeşitli alanlarının değerleri üzerinde temel kontroller yapar. Herhangi bir anormallik tespit edildiğinde, belirli bir hata koduna sahip bir [DaoException] istisnası tetiklenir. [saveOne] yöntemi bu istisnayı yönetmediğinden, istisna çağıran yönteme iletilir.
    • 62. satır: [Personne] parametresinin kimliği -1 ise, bu bir ekleme işlemidir. [Personne] nesnesi, mevcut ilk kimlik numarasıyla (satır 64) ve 1'e eşit bir sürüm numarasıyla (satır 65) dahili kişi listesine eklenir (satır 66).
    • [Personne] parametresinin [id] değeri -1'den farklıysa, bu durumda iç listede bu [id] değerine sahip kişinin bilgilerinin değiştirilmesi söz konusudur. Öncelikle, değiştirilecek kişinin var olup olmadığı kontrol edilir (satır 70-75). Eğer yoksa, hata kodu 2 ile [DaoException] türünde bir istisna atılır.
    • Kişi gerçekten mevcutsa, mevcut sürümünün, orijinalde yapılacak değişiklikleri içeren [Personne] parametresindeki sürümle aynı olup olmadığı kontrol edilir. Aksi takdirde, bu, kişide değişiklik yapmak isteyen kişinin en son sürüme sahip olmadığı anlamına gelir. Bunu, hata kodu 3 olan [DaoException] türünde bir istisna oluşturarak bildiririz (satır 79-80).
    • Her şey yolunda giderse, değişiklikler kişinin orijinal kaydına yansıtılır (satır 85-90).

Bu yöntemin senkronize edilmesi gerektiği açıktır. Örneğin, değiştirilecek kişinin mevcut olup olmadığının kontrol edildiği an ile değişikliğin yapılacağı an arasında, bu kişi başka biri tarafından listeden silinmiş olabilir. Bu nedenle, yöntemin aynı anda yalnızca tek bir iş parçacığı tarafından yürütülmesini sağlamak için [synchronized] olarak tanımlanması gerekir. Aynı durum, [IDao] arayüzündeki diğer yöntemler için de geçerlidir. Ancak biz bunu yapmıyoruz; bu senkronizasyonu [service] katmanına taşımayı tercih ediyoruz. Senkronizasyon sorunlarını ortaya çıkarmak için, [dao] katmanının testleri sırasında, değişikliği yapabileceğimizi bildiğimiz an ile değişikliği fiilen yaptığımız an arasında [saveOne]'in yürütülmesini 10 ms süreyle durduracağız (83. satır). Böylece, [saveOne]'i çalıştıran iş parçacığı, işlemciyi başka bir iş parçacığına devredecektir. Bu sayede, kişi listesine erişim çakışmalarının ortaya çıkma olasılığını artırmış olacağız.

14.5. [dao] katmanının testleri

[dao] katmanı için bir JUnit testi yazılmıştır:

[TestDao], JUnit testidir. Kişi listesine eşzamanlı erişim sorunlarını ortaya çıkarmak için [ThreadDaoMajEnfants] türünde iş parçacıkları oluşturulur. Bu iş parçacıkları, belirli bir kişinin çocuk sayısını 1 artırmakla görevlidir.

[TestDao], [test1]'ten [test5]'e kadar beş teste sahiptir. Bunlardan sadece ikisini sunuyoruz; okuyucunun diğerlerini bu makaleye ait kaynak kodda keşfetmesi önerilir.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // katman [dao]
    private DaoImpl dao;

     // yapıcı
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // kişi listesi
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // mevcut olmayan bir öğenin değiştirilmesi-silinmesi
    public void test2() throws ParseException {
...
    }

     // kişi sürümlerinin yönetimi
    public void test3() throws ParseException, InterruptedException {
...
    }

     // iyimser kilitleme - çoklu iş parçacığı erişimi
    public void test4() throws Exception {
...
    }

     // saveOne geçerlilik testleri
    public void test5() throws ParseException {
    ...
}
  • 9. satır: Test edilen [dao] katmanının uygulamasına yapılan referans
  • 12-15. satırlar: JUnit test oluşturucusu. Test edilecek [dao] katmanından [DaoImpl] türünde bir örnek oluşturur ve bunu başlatır.

[test1] yöntemi, [IDao] arayüzünün dört yöntemini şu şekilde test eder:

    public void test1() throws ParseException {
         // güncel liste
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         // görüntüleme
        doListe(personnes);
         // kişi ekleme
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // doğrulama - kişi bulunamazsa sistem çökecektir
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // değişiklik
        p1.setNom("Y");
        dao.saveOne(p1);
         // doğrulama - kişi bulunamazsa sistem çökecektir
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // silme
        dao.deleteOne(id1);
         // doğrulama
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // kod 2 hatası almalıyız
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // kişi listesi
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • 3. satır: kişi listesi istenir
  • 6. satır: Liste görüntülenir
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

Ardından test, bir kişi ekler, onu değiştirir ve siler. Böylece [IDao] arayüzünün dört yöntemi de kullanılmış olur.

  • 8-10. satırlar: Yeni bir kişi eklenir (id=-1).
  • 11. satır: Eklenen kişinin kimliği alınır, çünkü ekleme işlemi ona bir kimlik atamıştır. Daha önce bir kimliği yoktu.
  • 13-14. satırlar: [dao] katmanından, az önce eklenen kişinin bir kopyası istenir. Unutulmamalıdır ki, istenen kişi bulunamazsa, [dao] katmanı bir istisna oluşturur. Bu durumda 13. satırda bir çökme meydana gelir. Bu durumu daha düzgün bir şekilde yönetebilirdik. 14. satırda, bulunan kişinin adı kontrol edilir.
  • 16-17. satırlar: Bu adı değiştiriyoruz ve [dao] katmanından değişiklikleri kaydetmesini istiyoruz.
  • 19-20. satırlar: [dao] katmanından az önce eklenen kişinin bir kopyasını isteriz ve yeni adını doğrularız.
  • 22. satır: Testin başında eklenen kişi silinir.
  • 23-34. satırlar: [dao] katmanından az önce silinen kişinin bir kopyası istenir. Kod 2 olan bir [DaoException] elde edilmelidir.
  • 36-37. satırlar: Kişi listesi yeniden istenir. Testin başlangıcındakiyle aynı liste elde edilmelidir.

[test4] yöntemi, [dao] katmanındaki yöntemlere eşzamanlı erişim sorunlarını ortaya çıkarmayı amaçlamaktadır. Bu yöntemlerin senkronize edilmediğini hatırlayalım. Test kodu şöyledir:

    public void test4() throws Exception {
         // kişi ekleme
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // çocuk sayısını güncellemek için N sayıda iş parçacığı oluşturma
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // iş parçacıklarının bitmesini bekliyoruz
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // kişiyi alıyoruz
        p1 = dao.getOne(id1);
         // bu kişinin N çocuğu olması gerekiyor
        assertEquals(N, p1.getNbEnfants());
         // p1 kişisinin silinmesi
        dao.deleteOne(p1.getId());
         // doğrulama
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // kod 2 hatası olmalı
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • 3-6. satırlar: Listeye çocuğu olmayan P adlı bir kişi eklenir. Bu kişinin [id] değeri kaydedilir (6. satır).
  • 7-13. satırlar: N adet iş parçacığı başlatılır. Her biri, P kişisinin çocuk sayısını 1 birim artıracaktır. Sonuç olarak, P kişisinin N çocuğu olması gerekir.
  • satır 15-17: N iş parçacığını başlatan [test4] yöntemi, bu iş parçacıklarının işlerini tamamlamasını bekledikten sonra P kişisinin yeni çocuk sayısını kontrol eder.
  • 18-21. satırlar: P kişisi alınır ve çocuk sayısının N olduğu kontrol edilir.
  • 22-35. satırlar: P kişisi silinir ve ardından listede artık bulunmadığı kontrol edilir.
  1. satırda, iş parçacıklarının [ThreadDaoMajEnfants] türünde olduğu görülüyor. Bu türün oluşturucusunun üç parametresi vardır:
  1. İş parçacığına verilen ad; bu, günlükler aracılığıyla iş parçacığını takip etmek içindir
  2. [dao] katmanında bir referans; böylece iş parçacığı bu katmana erişebilir
  3. İş parçacığının üzerinde çalışacağı kişinin kimliği

[ThreadDaoMajEnfants] türü şu şekildedir:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // iş parçacığının adı
    private String name;
     // [dao] katmanında referans
    private IDao dao;
     // üzerinde çalışılacak kişinin kimliği
    private int idPersonne;

     // oluşturucu
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // konu özü
    public void run() {
         // izleme
        suivi("lancé");
         // 1 artırmayı başaramayana kadar döngü devam eder
         // idPersonne kişisinin çocuk sayısı
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // idPersonne'ten kişinin bir kopyası alınır
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // takip
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // işlemciyi serbest bırakmak için 10 ms bekleme
            try {
                 // takip
                suivi("début attente");
                 // işlemciyi serbest bırakmak için duraklama
                Thread.sleep(10);
                 // takip
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // bekleme sona erdi - kopyayı onaylamaya çalışılıyor
             // bu arada diğer iş parçacıkları orijinali değiştirmiş olabilir
            int codeErreur = 0;
            try {
                 // bu kopyanın alt öğe sayısını 1 artır
                personne.setNbEnfants(nbEnfants + 1);
                 // orijinali değiştirmeye çalışılıyor
                dao.saveOne(personne);
                 // işlem tamamlandı - orijinal değiştirildi
                fini = true;
            } catch (DaoException ex) {
                 // hata kodu alınır
                codeErreur = ex.getCode();
                 // Bu, sürüm 3 hatası olmalı - aksi takdirde yeniden başlatılır
                 // istisna
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     // takip
                    suivi(ex.getMessage());
                }
                 // orijinal değişti - her şeye baştan başlanır
            }
        }
         // takip
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // takip
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • 9. satır: [ThreadDaoMajEnfants], bir iş parçacığıdır
  • 18-22. satırlar: İş parçacığını üç bilgiyle başlatan oluşturucu
    1. iş parçacığına verilen [name] adı
    2. [dao] katmanına ait [dao] referansı. Burada da yine, [DaoImpl] uygulamasının türüyle değil, [IDao] arayüzünün türüyle çalıştığımızı belirtelim.
    3. İş parçacığının üzerinde çalışması gereken kişinin [id] kimliği

[test4], [ThreadDaoMajEnfants] iş parçacığını başlattığında (test4'ün 12. satırı), bu iş parçacığının [run] yöntemi (25. satır) yürütülür:

  • 78-81. satırlar: Özel [suivi] yöntemi, ekran günlükleri oluşturulmasına olanak tanır. [run] yöntemi, iş parçacığının yürütülmesini takip edebilmek için bunu kullanır.
  • iş parçacığı, [id] kimlik numaralı P kişisinin çocuk sayısını 1 artırmaya çalışır. Bu güncelleme birkaç deneme gerektirebilir. [TH1] ve [TH2] adlı iki iş parçacığını ele alalım. [TH1], [dao] katmanından P kişisinin bir kopyasını ister. Kopyasını alır ve bunun V1 sürümüne sahip olduğunu görür. [TH1] kesintiye uğrar. Onu takip eden [TH2] de aynı işlemi gerçekleştirir ve P kişisinin aynı V1 sürümünü alır. [TH2] kesintiye uğrar. [TH2] kontrolü geri alır, P’nin çocuk sayısını artırır ve değişikliklerini kaydeder. Bu noktada, değişikliklerin kaydedildiğini ve P’nin sürümünün V2’e geçeceğini biliyoruz. [TH1] işini tamamlamıştır. [TH2] kontrolü devralır ve aynı işlemi yapar. P’ye yaptığı güncelleme reddedilecektir, çünkü elinde V1 sürümündeki P’nin bir kopyası varken, orijinal P artık V2 sürümündedir. [TH2], bu durumda [lecture -> mise à jour -> sauvegarde] döngüsünün tamamını yeniden başlatmak zorundadır. Bu nedenle, 32-72. satırlarda bir döngüyle karşılaşıyoruz. Bu döngüde, iş parçacığı:
  • değiştirilecek P kişisinin bir kopyasını ister (34. satır)
  • 10 ms bekler (satır 43). Bu yapay bir işlemdir ve çakışma olasılığını artırmak amacıyla, P kişisinin okunması ile kişi listesindeki fiili güncellemesi arasında iş parçacığını kesintiye uğratmayı amaçlamaktadır.
  • P'nin çocuk sayısını artırır (satır 54) ve P'yi kaydeder (satır 56). İş parçacığı P'nin doğru sürümüne sahip değilse, [dao] katmanı tarafından bir istisna tetiklenir. Ardından istisna kodu alınır (satır 61) ve bunun gerçekten 3 kodu (P'nin yanlış sürümü) olup olmadığı kontrol edilir. Aksi takdirde, istisna çağıran yönteme, yani sonuçta [test4] test yöntemine yeniden atılır. Kod 3 istisnası alınırsa, [lecture -> mise à jour -> sauvegarde] döngüsüne yeniden başlanır. İstisna alınmazsa, güncelleme yapılmış demektir ve iş parçacığının işi tamamlanmış olur.

Testlerin sonuçları nedir?

Test edilen ilk yapılandırmada:

  • [DaoImpl] yöntemindeki [saveOne] yöntemindeki bekleme komutu yorumlanmıştır (satır 83, paragraf 14.4).
         // 10 ms bekleniyor
         //wait(10);
  • [test4] yöntemi 100 iş parçacığı oluşturur (satır 8, paragraf 14.5).
         // çocuk sayısını güncellemek için N sayıda iş parçacığı oluşturuluyor
        final int N = 100;

Aşağıdaki sonuçlar elde edilir:

Image

Beş test de başarıyla tamamlandı.

Test edilen ikinci yapılandırmada:

  • [DaoImpl] yöntemindeki [saveOne] komutundaki bekleme talimatının yorum satırı kaldırılır (satır 83, paragraf 14.4).
         // 10 ms bekleniyor
        wait(10);
  • [test4] yöntemi 2 iş parçacığı oluşturur (satır 8, paragraf 14.5).
         // çocuk sayısını güncellemek için N iş parçacığı oluşturuluyor
        final int N = 2;

Aşağıdaki sonuçlar elde edilir:

[test4] testi başarısız oldu. Başlangıçta 0 çocuğu olan P adlı kişinin çocuk sayısını 1 artırmakla görevli iki iş parçacığı oluşturuldu. Dolayısıyla, iki iş parçacığının çalışmasından sonra 2 çocuk olması bekleniyordu, ancak sadece bir çocuk var.

Neler olduğunu anlamak için [test4] iş parçacığının ekran günlüklerini inceleyelim:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • 1. satır: 0 numaralı iş parçacığı çalışmaya başlar
  • 2. satır: P kişisinin bir kopyasını aldı ve çocuk sayısının 0 olduğunu gördü
  • 3. satır: [run] yönteminden gelen [Thread.sleep(10)] ile karşılaşır ve bu nedenle [1145536368171] (ms) süresinde durur
  • 4. satır: 1 numaralı iş parçacığı işlemciyi devralır ve çalışmaya başlar
  • satır 5: Kişi P'nin bir kopyasını alır ve çocuk sayısının 0 olduğunu tespit eder
  • 6. satır: [run] yöntemindeki [Thread.sleep(10)] zamanına ulaşır ve bu nedenle durur
  • 7. satır: 0 numaralı iş parçacığı, [1145536368187] (ms) zamanında, yani c.a.d'ten 16 ms sonra işlemciyi geri alır.
  • 8. satır: 1 numaralı iş parçacığı için de durum aynıdır
  • 9. satır: 0 numaralı iş parçacığı güncellemesini yaptı ve alt iş parçacık sayısını 1'e çıkardı
  • 10. satır: 1 numaralı iş parçacığı da aynısını yaptı

Buradaki soru, normalde 0 numaralı iş parçacığı tarafından az önce güncellenen P kişisinin doğru sürümüne artık sahip olmamasına rağmen, 1 numaralı iş parçacığının neden güncellemesini yapabildiğidir.

Öncelikle, 7. ve 8. satırlar arasında bir tutarsızlık göze çarpmaktadır: Görünüşe göre 0 numaralı iş parçacığı, bu iki satır arasında işlemci kontrolünü 1 numaralı iş parçacığına kaptırmıştır. O sırada ne yapıyordu? [saveOne] katmanındaki [saveOne] yöntemini çalıştırıyordu. Bu yöntemin iskeleti şu şekildedir (bkz. paragraf 14.4):

    public void saveOne(Personne personne) {
...
         // değişiklik - kişi aranıyor
....
         // orijinalin doğru sürümü elimizde mi?
...
         // 10 ms bekleniyor
        wait(10);
         // tamam - değişiklik yapılıyor
    ...
}
  • 0 numaralı iş parçacığı, [saveOne]'i çalıştırdı ve 8. satıra kadar ilerledi; burada işlemciyi bırakmak zorunda kaldı. Bu sırada, P kişisinin sürümünü okudu ve bu değer 1'di, çünkü P kişisi henüz güncellenmemişti.
  • İşlemci boşaldığı için, 1 numaralı iş parçacığı işlemciyi devraldı. Bu iş parçacığı da [saveOne] programını çalıştırdı ve 8. satıra kadar ilerledi; burada işlemciyi bırakmak zorunda kaldı. Bu sırada, P kişisinin sürümünü okudu ve bu değer 1'di, çünkü P kişisi henüz güncellenmemişti.
  • İşlemci boşaldığı için, 0 numaralı iş parçacığı onu devraldı. 9. satırdan itibaren güncellemesini yaptı ve çocuk sayısını 1'e değiştirdi. Ardından, 0 numaralı iş parçacığının [run] yöntemi sona erdi ve iş parçacığı, çocuk sayısını 1'e değiştirdiğini belirten günlüğü görüntüledi (9. satır).
  • İşlemci boşaldığı için, iş parçacığı 1 bu iş parçacığını devraldı. 9. satırdan itibaren güncellemesini yaptı ve çocuk sayısını 1'e değiştirdi. Neden 1? Çünkü çocuk sayısı 0 olan P'nin bir kopyasına sahip. Bunu log (5. satır) gösteriyor. Ardından 1 numaralı iş parçacığının [run] yöntemi sona erdi ve iş parçacığı, çocuk sayısını 1'e değiştirdiğini belirten günlüğü görüntüledi (10. satır).

Sorun nereden kaynaklanıyor? Sorun, 0 numaralı iş parçacığının, 1 numaralı iş parçacığı P'nin durumunun değişip değişmediğini öğrenmek için bu sürümü okumaya çalışmadan önce, yaptığı değişikliği onaylayıp P'nin sürümünü değiştirecek zamana sahip olmamasından kaynaklanıyor. Bu senaryo pek olası olmasa da imkansız değildir. Sadece iki iş parçacığıyla bu durumun ortaya çıkmasını sağlamak için 0 numaralı iş parçacığının işlemciyi kaybetmesini zorlamak gerekti. Bu hile olmadan, önceki yapılandırma 100 iş parçacığıyla aynı durumu ortaya çıkaramamıştı. [test4] testi başarılı olmuştu.

Çözüm nedir? Muhtemelen birkaç çözüm vardır. Bunlardan biri, uygulaması kolay olan, [saveOne] yöntemini senkronize etmektir:


    public synchronized void saveOne(Personne personne)

[synchronized] anahtar sözcüğü, yöntemi aynı anda yalnızca bir iş parçacığının çalıştırabilmesini sağlar. Böylece 1 numaralı iş parçacığı, ancak 0 numaralı iş parçacığı [saveOne] yönteminden çıktığında bu yöntemi çalıştırabilecektir. Böylece, 1 numaralı iş parçacığı [saveOne]'e girdiğinde, P'nin sürümünün değiştirilmiş olacağından emin oluruz. Doğru P sürümüne sahip olmadığı için güncellemesi reddedilecektir.

Senkronize edilmesi gereken, [dao] katmanındaki bu dört yöntemdir. Ancak, bu katmanı açıklanan haliyle korumaya ve senkronizasyonu [service] katmanına ertelemeye karar veriyoruz. Bunun birkaç nedeni vardır:

  • [dao] katmanına erişimin her zaman bir [service] katmanı üzerinden gerçekleştiğini varsayıyoruz. Web uygulamamızda durum böyledir.
  • [dao] katmanındaki yöntemleri senkronize etmemize neden olan nedenlerin dışında, başka nedenlerle [service] katmanındaki yöntemlere erişimi de senkronize etmek gerekebilir. Bu durumda, [dao] katmanındaki yöntemleri senkronize etmeye gerek yoktur. Şu durumdan eminsek:
  • [dao] katmanına yapılan tüm erişimlerin [service] katmanından geçtiği
  • ve [service] katmanını aynı anda yalnızca tek bir iş parçacığı kullanıyorsa

o zaman [dao] katmanındaki yöntemlerin aynı anda iki iş parçacığı tarafından yürütülmeyeceğinden emin olabiliriz.

Şimdi [service] katmanını inceleyeceğiz.

14.6. [service] katmanı

[service] katmanı aşağıdaki sınıf ve arayüzlerden oluşur:

Image

  • [IService], [dao] katmanı tarafından sunulan arayüzdür
  • [ServiceImpl], bunun bir uygulamasıdır

[IService] arayüzü şöyledir:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // tüm kişilerin listesi
    Collection getAll();
     // belirli bir kişiyi bul
    Personne getOne(int id);
     // bir kişiyi ekle/değiştir
    void saveOne(Personne personne);
     // bir kişiyi sil
    void deleteOne(int id);
}

Bu arayüz, [IDao] arayüzüyle aynıdır.

[IService] arayüzünün [ServiceImpl] uygulaması şöyledir:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     // [dao] katmanı
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // kişi listesi
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     // belirli bir kişiyi bul
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // bir kişiyi ekleme veya düzenleme
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // bir kişiyi silme
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • 10-19. satırlar: [IDao dao] özniteliği, [dao] katmanına bir referanstır. Bu öznitelik, Spring tarafından IoC olarak başlatılacaktır.
  • 22-24. satırlar: [IService] arayüzünün [getAll] yönteminin uygulaması. Yöntem, isteği [dao] katmanına devretmekle yetinir.
  • 27-29. satırlar: [IService] arayüzünün [getOne] yönteminin uygulaması. Yöntem, isteği sadece [dao] katmanına devretmektedir.
  • satır 32-34: [IService] arayüzünün [saveOne] yönteminin uygulaması. Yöntem, isteği sadece [dao] katmanına devretmektedir.
  • 37-39. satırlar: [IService] arayüzünün [deleteOne] yönteminin uygulaması. Yöntem, isteği sadece [dao] katmanına devretmektedir.
  • Tüm yöntemler senkronize edilmiştir (synchronized anahtar sözcüğü), bu sayede [service] katmanını ve dolayısıyla [dao] katmanını aynı anda yalnızca bir iş parçacığı kullanabilir.

14.7. [service] katmanının testleri

[service] katmanı için bir JUnit testi yazılmıştır:

[TestService], JUnit testidir. Yapılan testler, [dao] katmanı için yapılanlarla tamamen aynıdır. [TestService]'in iskeleti şöyledir:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     // katman [service]
    private ServiceImpl service;

     // oluşturucu
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     // kişi listesi
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
         // güncel liste
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // görüntüleme
        doListe(personnes);
         // kişi ekleme
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // doğrulama - kişi bulunamazsa sistem çökecektir
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // mevcut olmayan bir öğenin değiştirilmesi-silinmesi
    public void test2() throws ParseException {
...
    }

     // kişi sürüm yönetimi
    public void test3() throws ParseException, InterruptedException {
...
    }

     // iyimser kilitleme - çoklu iş parçacığı erişimi
    public void test4() throws Exception {
         // bir kişinin eklenmesi
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // çocuk sayısını güncellemek için N iş parçacığı oluşturma
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     // saveOne'in geçerlilik testleri
    public void test5() throws ParseException {
    ...
    }
}
  • 9. satır: [service] katmanı, [ServiceImpl] türü olarak test edilmiştir.
  • 11-15. satırlar: JUnit test oluşturucusu, test edilecek [service] katmanının bir örneğini oluşturur (12. satır), [dao] katmanının bir örneğini oluşturur (satır 13) ve [service] katmanına bu [dao] katmanını kullanması gerektiğini belirtir (satır 14).

[test1] yöntemi, [IService] arayüzünün dört yöntemini, aynı adlı [dao] katmanının test yöntemiyle aynı şekilde test eder. Tek fark, [dao] katmanı yerine [service] katmanına (25., 32. ve 35. satırlar) erişilmesidir.

[test4] yöntemi, [service] katmanındaki yöntemlere eşzamanlı erişim sorunlarını ortaya çıkarmayı amaçlamaktadır. Bu yöntem de yine [dao] katmanındaki [test4] test yöntemiyle aynıdır. Ancak bazı ayrıntılarda farklılıklar vardır:

  • [dao] katmanı yerine [service] katmanına başvurulur (55. satır)
  • iş parçacıklarına [dao] katmanı yerine [service] katmanına bir referans aktarılır (61. satır)

[ThreadServiceMajEnfants] türü de [ThreadDaoMajEnfants] türüyle neredeyse aynıdır; tek farkı, [dao] katmanı yerine [service] katmanıyla çalışmasıdır:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // iş parçacığının adı
    private String name;
     // [service] katmanına ilişkin referans
    private IService service;
     // üzerinde çalışılacak kişinin kimliği
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // takip
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • 12. satır: iş parçacığı [service] katmanıyla çalışıyor

Testleri, [dao] katmanında sorun yaratan yapılandırma ile gerçekleştiriyoruz:

  • [DaoImpl]'teki [saveOne] yöntemindeki bekleme komutunun yorum satırını kaldırıyoruz (satır 83, paragraf 14.4).
         // 10 ms bekleniyor
        wait(10);
  • [test4] yöntemi 100 iş parçacığı oluşturur (satır 65, paragraf 14.7).
         // çocuk sayısını güncellemek için N adet iş parçacığı oluşturuluyor
        final int N = 100;

Elde edilen sonuçlar şunlardır:

[service] katmanındaki yöntemlerin senkronizasyonu, [test4] testinin başarıyla sonuçlanmasını sağlamıştır.

14.8. [web] katmanı

Uygulamamızın 3 katmanlı mimarisini hatırlayalım:

[web] katmanı, kullanıcının kişi grubunu yönetebilmesi için ekranlar sunacaktır:

  • grup üyelerinin listesi
  • gruba kişi ekleme
  • gruptaki bir kişiyi düzenleme
  • gruptan bir kişinin silinmesi

Bunun için, [service] katmanını kullanacak ve bu katman da [dao] katmanını kullanacaktır. [web] katmanı tarafından yönetilen ekranları daha önce tanıtmıştık (14.1. paragraf). Web katmanını açıklamak için sırasıyla şunları ele alacağız:

  • yapılandırmasını
  • görünümleri
  • denetleyicisi
  • bazı testler

14.8.1. Web uygulamasının yapılandırması

Uygulamanın Eclipse projesi şu şekildedir:

Image

  • [istia.st.mvc.personnes.web] paketinde, [Application] denetleyicisi bulunur.
  • JSP / JSTL sayfaları, [WEB-INF/vues] içinde yer almaktadır.
  • [lib] klasörü, uygulama için gerekli olan üçüncü taraf arşivlerini içerir. Bunlar, [Web App Libraries] klasöründe görülebilir.

[web.xml]


[web.xml] dosyası, web sunucusu tarafından uygulamayı yüklemek için kullanılan dosyadır. İçeriği şöyledir:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personnes-01</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>
            istia.st.mvc.personnes.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Eşleme ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  ana sayfalar -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Beklenmedik hata sayfası -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • 27-30. satırlar: [/do/*] URL'leri, [personnes] servleti tarafından işlenecektir
  • 9-12. satırlar: [personnes] servleti, [Application] sınıfının bir örneğidir; bu sınıfı daha sonra oluşturacağız.
  • 13-24. satırlar: JSP sayfalarının URL'lerini tanımlayan üç [urlList, urlEdit, urlErreurs] parametresini tanımlar; bu sayfalar, [list, edit, erreurs] görünümlerine aittir.
  • 32-34. satırlar: Uygulamanın, web uygulaması klasörünün kök dizininde bulunan varsayılan bir giriş sayfası ([index.jsp]) vardır.
  • 36-39. satırlar: Uygulamanın, web sunucusu tarafından uygulama tarafından yönetilmeyen bir istisna algılandığında görüntülenen varsayılan bir hata sayfası vardır.
    • 37. satır: <exception-type> etiketi, <error-page> yönergesi tarafından yönetilen istisna türünü belirtir; burada [java.lang.Exception] türü ve türevleri, yani tüm istisnalar belirtilmiştir.
    • 38. satır: <location> etiketi, <exception-type> ile tanımlanan türde bir istisna oluştuğunda görüntülenecek JSP sayfasını belirtir. Sayfada şu yönerge varsa, oluşan istisna bu sayfada exception adlı bir nesne içinde bulunur:

<%@ page isErrorPage="true" %>
  • (devam)
    • <exception-type> etiketinde T1 türü belirtilmişse ve T1'ten türetilmemiş T2 türünde bir istisna web sunucusuna ulaşırsa, sunucu müşteriye genellikle kullanıcı dostu olmayan özel bir istisna sayfası gönderir. İşte bu nedenle [web.xml] dosyasındaki <error-page> etiketinin önemi ortaya çıkmaktadır.

[index.jsp]


Bu sayfa, bir kullanıcı URL belirtmeden doğrudan uygulama bağlamını talep ettiğinde görüntülenir, c.a.d. Burada [/personnes-01]. İçeriği şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp], istemciyi [/do/list] URL'sine yönlendirir. Bu URL, gruptaki kişilerin listesini gösterir.

14.8.2. Uygulamadaki JSP / JSTL sayfaları


[list.jsp] görünümü


Bu sayfa, kişi listesini görüntülemek için kullanılır:

Image

Kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbEnfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

  • Bu görünüm, modelinde bir öğe alır:
  • [personnes] öğesi, [ArrayList] türündeki bir nesneye ve [Personne] türündeki nesnelere bağlıdır
  • 22-34. satırlar: ${personnes} listesi taranarak, gruptaki kişileri içeren bir HTML tablosu görüntülenir.
  • 31. satır: [Modifier] bağlantısının işaret ettiği URL, mevcut kişinin [id] alanı tarafından ayarlanır; böylece [/do/edit] URL'siyle ilişkili denetleyici, hangi kişinin değiştirileceğini bilir.
  • 32. satır: Aynı işlem [Supprimer] bağlantısı için de yapılır.
  • 28. satır: Kişinin doğum tarihini JJ/MM/AAAA biçiminde görüntülemek için, Apache [Jakarta Taglibs] projesinin [DateTime] etiket kütüphanesindeki <dt> etiketi kullanılır:

Image

Bu etiket kütüphanesinin açıklama dosyası 3. satırda tanımlanmıştır.

  • 37. satır: Yeni bir kişi ekleme bağlantısı olan [Ajout], 31. satırdaki [Modifier] bağlantısı gibi [/do/edit] URL'sini hedeflemektedir. [id] parametresinin -1 değeri, burada bir düzenleme değil, bir ekleme işlemi yapıldığını gösterir.

[edit.jsp] görünümü


Bu görünüm, yeni bir kişinin eklenmesi veya mevcut bir kişinin değiştirilmesi için kullanılan formu görüntülemek amacıyla kullanılır:

[edit.jsp] görünümünün kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit != ''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Bu görünüm, yeni bir kişi ekleme veya mevcut bir kişiyi güncelleme formunu gösterir. Bundan sonra, yazımı basitleştirmek amacıyla tek bir terim olan [mise à jour]'i kullanacağız. [Valider] düğmesi (73. satır), formun POST görünümünü [/do/validate] URL'sinde (16. satır) tetikler. POST işlemi başarısız olursa, [edit.jsp] görünümü, meydana gelen hata veya hatalarla birlikte yeniden görüntülenir; aksi takdirde [list.jsp] görünümü görüntülenir.

  • Hem GET hem de başarısız olan POST üzerinde görüntülenen [edit.jsp] görünümü, şablonunda aşağıdaki öğeleri alır:
öznitelik
GET
POST
id
güncellenen kişinin kimlik numarası
aynı
version
sürümü
aynı
prenom
ad
girilen adı
nom
soyadı
girdilen soyadı
dateNaissance
doğum tarihi
girdilen doğum tarihi
marie
medeni durumu
girilen medeni durumu
nbEnfants
çocuk sayısı
girilen çocuk sayısı
erreurEdit
boş
POST sırasında, [Envoyer] düğmesi tarafından tetiklenen ekleme veya değiştirme işleminin başarısız olduğunu belirten bir hata mesajı. Hata yoksa boş bırakılır.
erreurPrenom
boş
hatalı bir ad bildirir – aksi takdirde boştur
erreurNom
boş
hatalı soyad belirtir – aksi takdirde boş
erreurDateNaissance
boş
hatalı doğum tarihi bildirir – aksi takdirde boş bırakılır
erreurNbEnfants
boş
yanlış çocuk sayısı bildirildi – aksi takdirde boş bırakılır
  • 11-15. satırlar: Formdaki POST işlemi başarısız olursa, [erreurEdit!=''] değeri alınır ve bir hata mesajı görüntülenir.
  • satır 16: form, [/do/validate] URL'sine gönderilecektir
  • 20. satır: Şablondaki [id] öğesi görüntülenir
  • 24. satır: Şablondaki [version] öğesi görüntülenir
  • 26-32. satırlar: Kişinin adının girilmesi:
    • formun ilk görüntülenişinde (GET), ${prenom}, güncellenen [Personne] nesnesinin [prenom] alanının geçerli değerini gösterir ve ${erreurPrenom} boştur.
    • POST'ten sonra bir hata oluşması durumunda, girilen ${prenom} değeri ile olası hata mesajı ${erreurPrenom} yeniden görüntülenir
  • 33-39. satırlar: kişinin soyadının girilmesi
  • satır 40-46: kişinin doğum tarihinin girilmesi
  • satır 47-61: Kişinin medeni durumunun bir radyo düğmesi ile girilmesi. Hangi radyo düğmesinin işaretlenmesi gerektiğini belirlemek için [Personne] nesnesinin [marie] alanındaki değer kullanılır.
  • satır 62-68: kişinin çocuk sayısının girilmesi
  • satır 71: HTML adlı gizli bir alan; adı [id] olup, değeri güncellenmekte olan kişinin [id] alanının değeridir; ekleme için -1, değişiklik için başka bir değer alır.
  • satır 72: [version] adlı gizli bir HTML alanı; değeri, güncellenmekte olan kişinin [id] alanının değeridir.
  • 73. satır: Formdaki [Submit] türündeki [Valider] düğmesi
  • 74. satır: Kişi listesine geri dönmeyi sağlayan bir bağlantı. Formu onaylamadan çıkmaya olanak tanıdığı için [Annuler] olarak adlandırılmıştır.

[exception.jsp] görünümü


Bu görünüm, uygulama tarafından yönetilmeyen ve web sunucusuna iletilen bir istisna oluştuğunu bildiren bir sayfayı görüntülemek için kullanılır.

Örneğin, grupta bulunmayan bir kişiyi silelim:

[exception.jsp] görünümünün kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>

  • Bu görünüm, modelinde [exception] öğesini içeren bir anahtar alır; bu, web sunucusu tarafından yakalanan istisnadır. Bu öğenin web sunucusu tarafından JSP sayfasının modeline dahil edilebilmesi için, sayfanın 3. satırdaki etiketini tanımlamış olması gerekir.
  • 6. satır: Yanıtın durum kodu HTTP, 200 olarak belirlenir. Bu, yanıtın ilk başlığı HTTP'tir. 200 kodu, istemciye isteğinin yerine getirildiğini bildirir. Genellikle, sunucunun yanıtına bir HTML belgesi eklenir. Burada da durum böyledir. Yanıtın HTTP durum kodu 200 olarak ayarlanmazsa, burada 500 değerini alır ve bu da bir hata oluştuğunu gösterir. Nitekim, yönetilmeyen bir istisna yakalayan web sunucusu bu durumu anormal olarak değerlendirir ve 500 koduyla bildirir. HTTP 500 koduna verilen tepki, tarayıcılara göre farklılık gösterir: Firefox, bu yanıtla birlikte gelebilecek HTML belgesini görüntülerken, IE bu belgeyi yok sayar ve kendi sayfasını görüntüler. Bu nedenle 500 kodunu 200 koduyla değiştirdik.
  • 16. satır: istisna metni görüntülenir
  • 18. satır: Kullanıcıya kişi listesine geri dönmesi için bir bağlantı sunulur

[erreurs.jsp] görünümü


Bu görünüm, uygulamanın başlatılmasında meydana gelen hataları bildiren bir sayfayı (c.a.d) ve denetleyici servletinin [init] yönteminin yürütülmesi sırasında tespit edilen hataları görüntülemek için kullanılır. Bu, örneğin aşağıdaki örnekte gösterildiği gibi [web.xml] dosyasında bir parametrenin eksik olması olabilir:

Image

[erreurs.jsp] sayfasının kodu şöyledir:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

Sayfa, şablonunda [erreurs] öğesini alır; bu öğe, [ArrayList] türünde bir nesnedir ve [String] nesnelerinden oluşur; bu nesneler ise hata mesajlarıdır. Bunlar, 13-15. satırlardaki döngü ile görüntülenir.

14.8.3. Uygulama denetleyicisi

[Application] denetleyicisi, [istia.st.mvc.personnes.web] paketinde tanımlanmıştır:

Image


'in yapısı ve denetleyicinin başlatılması


[Application] denetleyicisinin iskeleti şu şekildedir:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     // örnek ayarları
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // hizmet
    ServiceImpl service=null;

     // başlatma
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         // servletin başlatma parametreleri alınır
        ServletConfig config = getServletConfig();
         // diğer başlatma parametreleri işleniyor
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // parametre değeri
            valeur = config.getInitParameter(paramètres[i]);
             // parametre mevcut mu?
            if (valeur == null) {
                 // hata kaydedilir
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 // parametrenin değeri kaydedilir
                params.put(paramètres[i], valeur);
            }
        }
         // [erreurs] görünümünün URL'si özel bir işleme tabi tutulur
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         // [dao] katmanının örneklenmesi
        DaoImpl dao = new DaoImpl();
        dao.init();
         // [service] katmanının örneklenmesi
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // kişi listesinin görüntülenmesi
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // kişi düzenleme / ekleme
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // kişi düzenleme / ekleme işleminin onaylanması
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // kişi düzenleme / ekleme işleminin onaylanması
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // önceden doldurulmuş formun görüntülenmesi
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     // gönderme
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         // işlem GET'e devrediliyor
        doGet(request, response);    }
}
  • 20-36. satırlar: [web.xml] dosyasında beklenen parametreler alınır.
  • 39-41. satırlar: [urlErreurs] parametresi, olası başlatma hatalarını görüntüleyebilen [erreurs] görünümünün URL'sini belirlediği için mutlaka mevcut olmalıdır. Bu parametre yoksa, [ServletException] çalıştırılarak uygulama durdurulur (satır 40). Bu istisna web sunucusuna iletilir ve [web.xml] dosyasındaki <error-page> etiketiyle yönetilir. Böylece [exception.jsp] görünümü görüntülenir:

Image

Yukarıdaki [Retour à la liste] bağlantısı çalışmaz. Uygulama değiştirilip yeniden yüklenene kadar bu bağlantı kullanıldığında aynı yanıt verilir. Daha önce gördüğümüz gibi, bu bağlantı diğer istisna türleri için de yararlıdır.

  • 43. satır: [dao] katmanını uygulayan bir [DaoImpl] örneği oluşturur
  • 44. satır: Bu örneği başlatır (üç kişiden oluşan bir başlangıç listesi oluşturur)
  • 46. satır: [service] katmanını uygulayan bir [ServiceImpl] örneği oluşturur
  • 47. satır: [service] katmanını, [dao] katmanına bir referans vererek başlatır

Denetleyicinin başlatılmasından sonra, denetleyicinin yöntemleri, [service] katmanına ait bir [service] referansına (satır 15) sahip olur ve bu referansı, kullanıcı tarafından talep edilen eylemleri yürütmek için kullanır. Bu eylemler, [doGet] yöntemi tarafından yakalanacak ve denetleyicinin özel bir yöntemi tarafından işlenecektir:

Url
HTTP yöntemi
denetleyici yöntemi
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

[doGet] yöntemi


Bu yöntemin amacı, kullanıcı tarafından talep edilen eylemlerin işlenmesini doğru yönteme yönlendirmektir. Kod şöyledir:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         // servletin başlatılmasının nasıl gerçekleştiği kontrol ediliyor
        if (erreursInitialisation.size() != 0) {
             // hata sayfasına yönlendiriliyor
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             // son
            return;
        }
         // isteği gönderen yöntemi alınıyor
        String méthode = request.getMethod().toLowerCase();
         // yürütülecek eylemi alıyoruz
        String action = request.getPathInfo();
         // eylem?
        if (action == null) {
            action = "/list";
        }
         // eylemin yürütülmesi
        if (méthode.equals("get") && action.equals("/list")) {
             // kişi listesi
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // bir kişinin silinmesi
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             // kişi ekleme/değiştirme formunun görüntülenmesi
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             // kişi ekleme/değiştirme formunun onaylanması
            doValidatePersonne(request, response);
            return;
        }
         // diğer durumlar
        doListPersonnes(request, response);
    }
  • 7-13. satırlar: Başlatma hataları listesinin boş olup olmadığı kontrol edilir. Boş değilse, hatayı veya hataları bildirecek olan [erreurs(erreurs)] görünümü görüntülenir.
  • 15. satır: Müşterinin isteğini yapmak için kullandığı [get] veya [post] yöntemini alırız.
  • 17. satır: İsteğin [action] parametresinin değeri alınır.
  • 23-27. satırlar: Kişi listesini isteyen [GET /do/list] isteğinin işlenmesi.
  • 28-32. satırlar: Bir kişinin silinmesini talep eden [GET /do/delete] isteğinin işlenmesi.
  • satır 33-37: Bir kişinin güncelleme formunu talep eden [GET /do/edit] isteğinin işlenmesi.
  • satır 38-42: Güncellenen kişinin onaylanmasını talep eden [POST /do/validate] isteğinin işlenmesi.
  • 44. satır: İstenen eylem önceki beş eylemden biri değilse, o zaman [GET /do/list] gibi işlem yapılır.

[doListPersonnes] yöntemi


Bu yöntem, kişi listesini talep eden [GET /do/list] isteğini işler:

Image

Kodu şu şekildedir:

1
2
3
4
5
6
7
8
9
     // kişi listesinin görüntülenmesi
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // [list] görünüm şablonu
        request.setAttribute("personnes", service.getAll());
         // [list] görünümünün görüntülenmesi
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • 5. satır: [service] katmanından grubun kişi listesi istenir ve bu liste, şablonda "kişiler" anahtarı altında yerleştirilir.
  • 7. satır: 14.8.2. paragrafında açıklanan [list.jsp] görünümünü görüntülenir.

[doDeletePersonne] yöntemi


Bu yöntem, id=XX olan kişinin silinmesini talep eden [GET /do/delete?id=XX] sorgusunu işler. [/do/delete?id=XX] URL'si, [list.jsp] görünümündeki [Supprimer] bağlantılarının URL'sidir:

Image

ve kodu şöyledir:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  1. satırda, [Supprimer] bağlantısının [/do/delete?id=XX] URL'si görülmektedir. Bu URL'yi işlemekle görevli [doDeletePersonne] yöntemi, id=XX olan kişiyi silmeli ve ardından grubun yeni kişi listesini görüntülemelidir. Kod şu şekildedir:
     // kişi düzenleme / ekleme onayı
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // kişinin kimliğini alıyoruz
        int id = Integer.parseInt(request.getParameter("id"));
         // kişiyi silme
        service.deleteOne(id);
         // kişi listesine yönlendirme
        response.sendRedirect("list");
    }
  • 5. satır: İşlenen URL, [/do/delete?id=XX] biçimindedir. [id] parametresinden [XX] değeri alınır.
  • 7. satır: [service] katmanından, elde edilen kimliğe sahip kişinin silinmesi istenir. Herhangi bir doğrulama yapmıyoruz. Silinmesi istenen kişi mevcut değilse, [dao] katmanı bir istisna oluşturur ve bu istisna [service] katmanı tarafından üst katmana iletilir. Bunu denetleyicide de yönetmiyoruz. Dolayısıyla istisna, yapılandırma gereği 14.8.2 numaralı paragrafta açıklanan [exception.jsp] sayfasını görüntüleyecek olan web sunucusuna iletilir:

Image

  • 9. satır: Silme işlemi gerçekleştiyse (istisna yok), istemciden [list] göreceli URL'sine yönlendirilmesi istenir. Az önce işlenen URL [/do/delete] olduğu için, yönlendirme URL'si [/do/list] olacaktır. Böylece tarayıcı, kişi listesinin görüntülenmesini sağlayacak olan [GET /do/list] adresine yönlendirilecektir.

[doEditPersonne] yöntemi


Bu yöntem, id=XX olan kişinin güncelleme formunu talep eden [GET /do/edit?id=XX] isteğini işler. [/do/edit?id=XX] URL'si, [Modifier] bağlantılarının ve [list.jsp] görünümündeki [Ajout] bağlantısının URL'sidir:

Image

kodu şu şekildedir:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  1. satırda, [Modifier] bağlantısının [/do/edit?id=XX] URL'si ve 17. satırda, [Ajout] bağlantısının [/do/edit?id=-1] URL'si görülmektedir. [doEditPersonne] yöntemi, id=XX olan kişinin düzenleme formunu görüntülemeli veya yeni bir giriş söz konusuysa boş bir form sunmalıdır.

[doEditPersonne] yönteminin kodu şöyledir:

     // kişinin değiştirilmesi / eklenmesi
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // kişinin kimliğini alır
        int id = Integer.parseInt(request.getParameter("id"));
         // Ekleme mi, düzenleme mi?
        Personne personne = null;
        if (id != -1) {
             // değişiklik - değiştirilecek kişi alınır
            personne = service.getOne(id);
        } else {
             // ekleme - boş bir kişi oluşturulur
            personne = new Personne();
            personne.setId(-1);
        }
         // [Personne] nesnesini [edit] görünüm şablonuna yerleştirilir
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         // [edit] görünümünün görüntülenmesi
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET, [/do/edit?id=XX] türünde bir URL'yi hedefler. 5. satırda, [id]'in değerini alıyoruz. Ardından iki durum söz konusudur:
  1. id, -1'den farklıysa, bu bir düzenleme işlemidir ve değiştirilecek kişinin bilgileriyle önceden doldurulmuş bir form görüntülenmelidir. 10. satırda, bu kişi [service] katmanından istenir.
  2. id değeri -1'e eşitse, bu bir ekleme işlemidir ve boş bir form görüntülenmelidir. Bunun için 13-14. satırlarda boş bir kişi oluşturulur.
  • Elde edilen [Personne] nesnesi, 14.8.2 paragrafında açıklanan [edit.jsp] sayfa şablonuna yerleştirilir. Bu şablon, [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants] öğelerini içerir. Bu öğeler, değeri boş dize olan [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] hariç olmak üzere 17-30. satırlarda başlatılır. Şu bilinmektedir ki, şablonda bulunmadıkları takdirde, JSTL kütüphanesi bunların değeri olarak boş bir dize görüntüleyecektir. [erreurEdit] öğesinin değeri de boş bir dize olsa da, [edit.jsp] sayfasında değeri üzerinde bir test yapıldığı için yine de başlatılır.
  • Şablon hazır hale geldiğinde, kontrol [edit.jsp] sayfasının 32-33. satırlarına geçer ve bu sayfa [edit] görünümünü oluşturur.

[doValidatePersonne] yöntemi


Bu yöntem, güncelleme formunu doğrulayan [POST /do/validate] isteğini işler. Bu POST, [Valider] düğmesi tarafından tetiklenir:

Image

Yukarıdaki görünümdeki HTML formundaki giriş alanlarını hatırlayalım:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

POST isteği, [prenom, nom, dateNaissance, marie, nbEnfants, id, version] parametrelerini içerir ve [/do/validate] URL'sine gönderilir (1. satır). Bu istek, aşağıdaki [doValidatePersonne] yöntemi tarafından işlenir:

// Kişi düzenleme/ekleme işlemi onaylanır
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // gönderilen verilerin alınması
        boolean formulaireErroné = false;
        boolean erreur;
         // ad
        String prenom = request.getParameter("prenom").trim();
         // ad geçerli mi?
        if (prenom.length() == 0) {
             // hatkayı not ediyoruz
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // soyadı
        String nom = request.getParameter("nom").trim();
         // ad geçerli mi?
        if (nom.length() == 0) {
             // hata kaydediliyor
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // doğum tarihi
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // hata kaydedildi
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // medeni durum
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // çocuk sayısı
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // hata not edilir
            erreur = true;
        }
         // çocuk sayısı yanlış mı?
        if (erreur) {
             // hata bildiriliyor
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         // kişi kimliği
        int id = Integer.parseInt(request.getParameter("id"));
         // sürüm
        long version = Long.parseLong(request.getParameter("version"));
         // Formda hata mı var?
        if (formulaireErroné) {
             // form, hata mesajlarıyla birlikte yeniden görüntüleniyor
            showFormulaire(request, response, "");
             // bitti
            return;
        }
         // form doğru - kişi kaydediliyor
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             // kayıt
            service.saveOne(personne);
        } catch (DaoException ex) {
             // form, meydana gelen hata mesajıyla birlikte yeniden görüntülenir
            showFormulaire(request, response, ex.getMessage());
             // bitti
            return;
        }
         // kişi listesine yönlendiriliyor
        response.sendRedirect("list");
    }

     // önceden doldurulmuş formun görüntülenmesi
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // [edit] görünüm şablonu hazırlanıyor
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         // [edit] görünümünün görüntülenmesi
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 8-14. satırlar: POST isteğinin [prenom] parametresi alınır ve geçerliliği kontrol edilir. Yanlış olduğu tespit edilirse, [erreurPrenom] öğesi bir hata mesajıyla başlatılır ve isteğin özniteliklerine eklenir.
  • 16-22. satırlar: [nom] parametresi için de benzer şekilde işlem yapılır
  • 24-32. satırlar: [dateNaissance] parametresi için de benzer şekilde işlem yapılır
  • 34. satır: [marie] parametresi alınır. Geçerliliği kontrol edilmez, çünkü ilk bakışta bir radyo düğmesinin değerinden geldiği varsayılır. Bununla birlikte, bir programın [POST /personnes-01/do/validate] parametresini, uydurma bir [marie] parametresiyle birlikte oluşturmasını engelleyen hiçbir şey yoktur. Bu nedenle bu parametrenin geçerliliğini test etmeliyiz. Burada, denetleyici bunları kendisi yönetemediği takdirde [exception.jsp] sayfasının görüntülenmesini sağlayan istisna yönetimine güveniyoruz. Dolayısıyla, 34. satırda [marie] parametresinin boole değerine dönüştürülmesi başarısız olursa, bir istisna ortaya çıkacak ve bu da [exception.jsp] sayfasının müşteriye gönderilmesine yol açacaktır. Bu işleyiş bizim için uygundur.
  • 34-54. satırlar: [nbEnfants] parametresini alır ve değerini kontrol ederiz.
  • 56. satır: [id] parametresini, değerini kontrol etmeden alıyoruz
  • 58. satır: [version] parametresi için de aynı işlem yapılır
  • 60-65. satırlar: Form hatalıysa, daha önce oluşturulan hata mesajlarıyla birlikte yeniden görüntülenir
  • 67-69. satırlar: Form geçerliyse, form öğeleriyle yeni bir [Personne] nesnesi oluşturulur
  • satır 70-78: Kişi kaydedilir. Kaydetme işlemi başarısız olabilir. Çoklu kullanıcı ortamında, değiştirilecek kişi silinmiş veya başka biri tarafından zaten değiştirilmiş olabilir. Bu durumda, [dao] katmanı bir istisna oluşturur ve bu istisna burada yönetilir.
  • 80. satır: İstisna oluşmamışsa, kullanıcıyı grubun yeni durumunu göstermek üzere [/do/list] URL’sine yönlendiririz.
  • 75. satır: Kaydetme sırasında bir istisna oluştuysa, istisnaya ait hata mesajını (3. parametre) ileterek ilk formun yeniden görüntülenmesini talep ederiz.

[showFormulaire] yöntemi (satır 84-101), girilen değerlerle (request.getParameter(" ... ")) [edit.jsp] sayfası için gerekli şablonu oluşturur. Hata mesajlarının daha önce [doValidatePersonne] yöntemi tarafından şablona yerleştirildiğini hatırlayalım. [edit.jsp] sayfası 99-100. satırlarda görüntülenir.

14.9. Web uygulamasının testleri

14.1. paragrafında bir dizi test sunulmuştur. Okuyucuyu bu testleri tekrar yapmaya davet ediyoruz. Burada, çoklu kullanıcı ortamında veri erişim çakışmaları durumlarını gösteren başka ekran görüntüleri sunuyoruz:

[Firefox], U1 kullanıcısının tarayıcısı olacaktır. Bu kullanıcı, [http://localhost:8080/personnes-01] URL’sini talep eder:

Image

[IE], U2 kullanıcısının tarayıcısı olacaktır. Bu kullanıcı aynı URL'yi ister:

Image

U1 kullanıcısı, [Lemarchand] kişisinin düzenleme sayfasına girer:

Image

U2 kullanıcısı da aynısını yapar:

Image

U1 kullanıcısı değişiklikleri yapar ve onaylar:

U2 kullanıcısı da aynısını yapıyor:

U2 kullanıcısı, formdaki [Annuler] bağlantısını kullanarak kişi listesine geri döner:

Image

[Lemarchand] adlı kişiyi, U1 tarafından değiştirilmiş haliyle bulur. Şimdi U2, [Lemarchand]'i siler:

U1 her zaman kendi listesine sahiptir ve [Lemarchand]'i yeniden değiştirmek ister:

U1, ne olduğunu görmek için [Retour à la liste] bağlantısını kullanır:

Image

Gerçekten de [Lemarchand]'in artık listede olmadığını fark eder...

14.10. Conclusion

Kişi listesi yönetimine ilişkin basit bir örnek üzerinde, 3 katmanlı bir mimari olan [web, metier, dao] içinde MVC mimarisini hayata geçirdik. Bu, önceki bölümlerde sunulan kavramları kullanmamızı sağladı. İncelenen sürümde, kişi listesi bellekte tutuluyordu. Yakında bu listenin bir veritabanı tablosunda tutulduğu sürümleri inceleyeceğiz.

Ancak öncelikle, bir ntier uygulamasının farklı katmanlarının entegrasyonunu kolaylaştıran Spring IoC adlı bir aracı tanıtacağız.