Skip to content

2. Spring 4 sunucusu

Yukarıdaki mimaride, şimdi Spring 4 çerçevesiyle oluşturulan / JSON web hizmetinin yapımını ele alacağız. Bunu birkaç aşamada yazacağız:

  • öncelikle [métier] ve [DAO] katmanları (Veri Erişim Nesnesi). Burada Spring Data'yı kullanacağız;
  • ardından kimlik doğrulaması içermeyen JSON web hizmeti. Burada Spring MVC kullanacağız;
  • ardından Spring Security ile kimlik doğrulama kısmını ekleyeceğiz.

Öncelikle, uygulamanın temelini oluşturan veritabanının yapısını açıklayalım.

2.1. Veritabanı

Bundan sonra [dbrdvmedecins] olarak anılacak olan veritabanı, aşağıdaki tabloları içeren bir MySQL5 veritabanıdır:

  

Randevular aşağıdaki tablolarla yönetilir:

  • [medecins]: muayenehanedeki doktorların listesini içerir;
  • [clients]: muayenehanedeki hastaların listesini içerir;
  • [creneaux]: her bir doktorun randevu saat aralıklarını içerir;
  • [rv]: doktorların randevu listesini içerir.

[roles], [users] ve [users_roles] tabloları kimlik doğrulama ile ilgili tablolardır. Şimdilik bunlarla ilgilenmeyeceğiz.

Randevuları yöneten tablolar arasındaki ilişkiler şunlardır:

 
  • bir zaman aralığı bir doktora aittir – bir doktorun 0 veya daha fazla zaman aralığı vardır;
  • bir randevu, bir hastayı ve bir doktoru, doktorun zaman aralığı aracılığıyla bir araya getirir;
  • bir müşterinin 0 veya daha fazla randevusu vardır;
  • bir zaman aralığına 0 veya daha fazla randevu (farklı günlerde) ilişkilidir.

2.1.1. [MEDECINS] tablosu

Bu tablo, [RdvMedecins] uygulaması tarafından yönetilen doktorlar hakkında bilgiler içerir.

  • ID: doktoru tanımlayan numara - tablonun birincil anahtarı
  • VERSION: Tablodaki satırın sürümünü tanımlayan numara. Bu sayı, satırda her değişiklik yapıldığında 1 artırılır.
  • NOM: doktorun soyadı
  • PRENOM: doktorun adı
  • TITRE: unvanı (Bayan, Bayan, Bay)

2.1.2. [CLIENTS] tablosu

Farklı doktorların hastaları [CLIENTS] tablosunda kaydedilir:

  • ID: müşteriyi tanımlayan numara - tablonun birincil anahtarı
  • VERSION: Tablodaki satırın sürümünü tanımlayan numara. Bu sayı, satırda her değişiklik yapıldığında 1 artırılır.
  • NOM: müşterinin adı
  • PRENOM: müşterinin adı
  • TITRE: unvanı (Bayan, Bayan, Bay)

2.1.3. [CRENEAUX] tablosu

Bu tablo, RV'lerin mümkün olduğu zaman aralıklarını listeler:

  • ID: zaman aralığını tanımlayan numara - tablonun birincil anahtarı (satır 8)
  • VERSION: Tablodaki satırın sürümünü tanımlayan numara. Bu sayı, satırda her değişiklik yapıldığında 1 artırılır.
  • ID_MEDECIN: Bu zaman dilimine ait olan doktoru tanımlayan numara – MEDECINS (ID) sütunundaki yabancı anahtar.
  • HDEBUT: zaman aralığının başlangıç saati
  • MDEBUT: zaman aralığının başlangıç dakikası
  • HFIN: zaman aralığının bitiş saati
  • MFIN: zaman aralığının bitiş dakikası

[CRENEAUX] tablosunun ikinci satırı (yukarıdaki [1]'e bakınız), örneğin, 2 numaralı zaman diliminin saat 08:20'de başlayıp 08:40'ta bittiğini ve 1 numaralı doktora ait olduğunu gösterir (Bayan Marie PELISSIER) ait olduğunu gösterir.

2.1.4. [RV] tablosu

Bu tablo, her doktor için ayrılan RV'leri listeler:

  • ID: RV'i benzersiz bir şekilde tanımlayan numara – birincil anahtar
  • JOUR: RV'in tarihi
  • ID_CRENEAU: RV'in zaman aralığı – [CRENEAUX] tablosundaki [ID] alanındaki yabancı anahtar – hem zaman aralığını hem de ilgili doktoru belirler.
  • ID_CLIENT: Rezervasyonun yapıldığı müşterinin numarası – [CLIENTS] tablosundaki [ID] alanına yabancı anahtar

Bu tablo, birleştirilen sütunların değerleri (JOUR, ID_CRENEAU) üzerinde tekillik kısıtlaması içeren bir 'e sahiptir:

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

[RV] tablosundaki bir satırın (JOUR1, ID_CRENEAU1) sütunları için (JOUR, ID_CRENEAU) için (JOUR1, ID_CRENEAU1) değerine sahipse, bu değer başka hiçbir yerde bulunamaz. Aksi takdirde, bu durum aynı hekim için aynı anda iki adet RV'in alınmış olduğu anlamına gelir. Java programlama açısından bakıldığında, veritabanındaki JDBC sürücüsü, bu durum meydana geldiğinde bir SQLException başlatır.

id satırındaki 3 değeri (yukarıdaki [1]'e bakınız), 23/08/2006 tarihinde 20 numaralı zaman dilimi ve 4 numaralı müşteri için bir RV'in rezerve edildiği anlamına gelir. [CRENEAUX] tablosundan, 20 numaralı randevu aralığının 16:20 - 16:40 saat aralığına karşılık geldiği ve 1 numaralı hekime (Bayan Marie PELISSIER) ait olduğu anlaşılmaktadır. [CLIENTS] tablosundan, 4 numaralı müşterinin Bayan Brigitte BISTROU olduğu anlaşılmaktadır.

2.2. Spring Data'ya Giriş

Projenin [DAO] katmanını, Spring ekosisteminin bir parçası olan Spring Data ile uygulayacağız.

Spring web sitesinde, Spring [http://spring.io/guides] ile çalışmaya başlamak için çok sayıda öğretici bulunmaktadır. Spring Data'yı tanıtmak için bunlardan birini kullanacağız. Bunun için Spring Tool Suite'i (STS) kullanacağız.

  • [1]'te, [spring.io/guides]'teki öğreticilerden birini içe aktarıyoruz;
  • [2]'te, Spring Data ile bir veritabanına nasıl erişileceğini gösteren [Accessing Data Jpa] öğreticisini seçiyoruz;
  • [3]'e dönüştürdükten sonra, Maven ile yapılandırılmış bir proje seçiyoruz;
  • [4]'te, öğretici iki şekilde sunulabilir: öğreticiyi takip ederek doldurulacak boş bir sürüm olan [initial] veya öğreticinin son hali olan [complete]. Biz ikincisini seçiyoruz;
  • [5]'te, öğreticiyi bir tarayıcıda görüntülemeyi seçebiliriz;
  • [6]'te ise nihai projeyi görebiliriz.

2.2.1. Projenin Maven yapılandırması

Projenin Maven bağımlılıkları [pom.xml] dosyasında yapılandırılmıştır:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- her şey için UTF-8'i kullan -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • 5-9. satırlar: bir üst Maven projesini tanımlar. Projenin bağımlılıklarının büyük kısmını bu proje tanımlar. Bu bağımlılıklar yeterli olabilir; bu durumda başka bağımlılık eklenmez, ya da yeterli olmayabilir; bu durumda eksik bağımlılıklar eklenir;
  • 12-15. satırlar: [spring-boot-starter-data-jpa]'e bir bağımlılık tanımlar. Bu artefakt, Spring Data sınıflarını içerir;
  • satır 16-19: bellek içi veritabanları oluşturmaya ve yönetmeye olanak tanıyan SGBD ve H2'e bir bağımlılık tanımlar.

Bu bağımlılıkların getirdiği sınıflara bir göz atalım:

Bunların sayısı oldukça fazla:

  • bazıları Spring ekosistemine aittir (spring ile başlayanlar);
  • diğerleri ise Hibernate ekosistemine aittir (hibernate, jboss); burada JPA uygulamasını kullanıyoruz;
  • bazıları test kütüphaneleridir (junit, hamcrest);
  • bazıları ise günlük kütüphaneleridir (log4j, logback, slf4j);

Hepsini saklayacağız. Üretim ortamındaki bir uygulama için ise sadece gerekli olanları saklamak gerekir.

[pom.xml] dosyasının 26. satırında şu satır yer almaktadır:


<start-class>hello.Application</start-class>

Bu satır, aşağıdaki satırlarla bağlantılıdır:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

6-9. satırlarda, [spring-boot-maven-plugin] eklentisi uygulamanın çalıştırılabilir jar dosyasını oluşturmaya olanak tanır. [pom.xml] dosyasının 26. satırı ise bu jar dosyasının çalıştırılabilir sınıfını belirtir.

2.2.2. [JPA] katmanı

Veritabanına erişim, [JPA] katmanı ve Java Persistence API aracılığıyla gerçekleştirilir:

  

Uygulama basittir ve [Customer] istemcilerini yönetir. [Customer] sınıfı, [JPA] katmanının bir parçasıdır ve şu şekildedir:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Bir müşterinin kimliği [id], adı [firstName] ve soyadı [lastName]'tir. Her [Customer] örneği, veritabanındaki bir tablonun bir satırını temsil eder.

    1. satır: JPA anotasyonu, [Customer] örneklerinin kalıcılığının (Oluşturma, Okuma, Güncelleme, Silme) bir JPA uygulaması tarafından yönetileceğini belirtir. Maven bağımlılıklarına göre, JPA / Hibernate uygulamasının kullanıldığı görülmektedir;
  • 11-12. satırlar: JPA anotasyonları, [id] alanını [Customer] tablosunun birincil anahtarıyla ilişkilendirir. 12. satır, JPA uygulamasının, kullanılan SGBD'e özgü birincil anahtar oluşturma yöntemini (burada H2) kullanacağını belirtir;

JPA ile ilgili başka bir açıklama yoktur. Bu durumda varsayılan değerler kullanılacaktır:

  • [Customer] tablosu, sınıfın adını taşıyacaktır, yani [Customer];
  • bu tablonun sütunları, sınıfın alanlarının adlarını taşıyacaktır: [id, firstName, lastName]; tablonun sütun adlarında büyük/küçük harf ayrımı yapılmadığını unutmayın;

Kullanılan JPA uygulamasının hiçbir aşamada adı geçmediğine dikkat edilmelidir.

2.2.3. [DAO] katmanı

  

[CustomerRepository] sınıfı, [DAO] katmanını uygular. Kod şöyledir:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Dolayısıyla bu bir arayüzdür, sınıf değildir (7. satır). Spring Data’ya ait [CrudRepository] arayüzünü genişletir (5. satır). Bu arayüz iki türle tanımlanmıştır: ilki, yönetilen öğelerin türü; burada [Customer] türüdür; ikincisi ise yönetilen öğelerin birincil anahtarının türü; burada [Long] türüdür. [CrudRepository] arayüzü şu şekildedir:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Bu arayüz, JPA T türü üzerinde gerçekleştirilebilecek CRUD işlemlerini (Oluştur – Oku – Güncelle – Sil) tanımlar:

    1. satır: save yöntemi, bir T varlığının veritabanında kalıcı hale getirilmesini sağlar. Bu yöntem, varlığı SGBD tarafından verilen birincil anahtarla kalıcı hale getirir. Ayrıca, birincil anahtarı id ile tanımlanan bir T varlığının güncellenmesini de sağlar. Hangi işlemin gerçekleştirileceği, birincil anahtar id'nin değerine göre belirlenir: bu değer null ise kalıcılık işlemi gerçekleştirilir, aksi takdirde güncelleme işlemi gerçekleştirilir;
    1. satır: Aynı işlem, ancak bu kez bir varlık listesi için;
  • satır 12: findOne yöntemi, birincil anahtarı id ile tanımlanan bir T varlığını bulmaya yarar;
    1. satır: delete yöntemi, birincil anahtar id ile tanımlanan bir T varlığını silmeye yarar;
  • satır 24-28: [delete] yönteminin varyantları;
  • satır 16: [findAll] yöntemi, kalıcı T varlıklarının tümünü bulmaya yarar;
  • satır 18: aynısı, ancak kimlik listesi aktarılan varlıklarla sınırlıdır;

[CustomerRepository] arayüzüne geri dönelim:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
    1. satır, [Customer]'i [lastName] adıyla bulmaya olanak tanır;

[DAO] katmanı ile ilgili bilgiler bu kadar. Önceki arayüzün bir uygulama sınıfı yoktur. Bu sınıf, yürütme sırasında [Spring Data] tarafından oluşturulur. [CrudRepository] arayüzünün yöntemleri otomatik olarak uygulanır. [CustomerRepository] arayüzüne eklenen yöntemler için ise durum değişebilir. [Customer] tanımına geri dönelim:


    private long id;
    private String firstName;
private String lastName;
  1. satırdaki yöntem, [Customer]'in [lastName] (3. satır) alanına referans verdiği için [Spring Data] tarafından otomatik olarak uygulanır. Spring Data, uygulanacak arayüzde bir [findBySomething] yöntemi ile karşılaştığında, bunu aşağıdaki JPQL (Java Persistence Query Language) sorgusu ile uygular:
select t from T t where t.something=:value

Bu nedenle, T türünün [something] adlı bir alanı olması gerekir. Böylece

List<Customer> findByLastName(String lastName);

yöntemi, aşağıdakine benzer bir kodla uygulanacaktır:

return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()

burada [em], JPA kalıcılık bağlamını ifade eder. Bu, ancak [Customer] sınıfında [lastName] adlı bir alan varsa mümkündür; ki bu durum söz konusudur.

Sonuç olarak, basit durumlarda Spring Data, [DAO] katmanını basit bir arayüzle uygulamamıza olanak tanır.

2.2.4. [console] katmanı

  

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


package hello;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // birkaç müşteriyi kaydet
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // tüm müşterileri getir
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();

        // ID ile tek bir müşteriyi getir
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // müşterileri soyadına göre getir
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }

        context.close();
    }

}
    1. satır: Sınıfın Spring'i yapılandırmak için kullanıldığını gösterir. Spring'in son sürümleri, XML yerine Java ile yapılandırılabilir. Her iki yöntem de aynı anda kullanılabilir. [Configuration] anotasyonuna sahip bir sınıfın kodunda normalde Spring bean'leri, yani örneklenecek sınıf tanımları bulunur. Burada ise hiçbir bean tanımlanmamıştır. Burada şunu hatırlatmak gerekir ki, bir SGBD ile çalışırken çeşitli Spring bean'lerinin tanımlanması gerekir:
    2. kullanılacak JPA uygulamasını tanımlayan bir [EntityManagerFactory],
    3. kullanılacak veri kaynağını tanımlayan bir [DataSource],
    4. kullanılacak işlem yöneticisini tanımlayan bir [TransactionManager];

Burada bu bean'lerin hiçbiri tanımlanmamıştır.

    1. satır: [EnableAutoConfiguration] anotasyonu, [Spring Boot] projesinden (5-6. satırlar) gelen bir anotasyondur. Bu anotasyon, [SpringApplication] sınıfı (16. satır) aracılığıyla Spring Boot'tan, Classpath'te bulunan kütüphanelere göre uygulamayı yapılandırmasını ister. Hibernate kütüphaneleri Classpath'te bulunduğundan, [entityManagerFactory] bean'i Hibernate ile uygulanacaktır. SGBD ve H2 kütüphaneleri Classpath'te bulunduğu için, [dataSource] bean'i H2 ile uygulanacaktır. [dataSource] bean'inde kullanıcı adı ve şifre de tanımlanmalıdır. Burada Spring Boot, H2'in varsayılan yöneticisini kullanacaktır; bu, şifresizdir. [spring-tx] kütüphanesi Classpath'te bulunduğu için Spring'in işlem yöneticisi kullanılacaktır.

Ayrıca, [Application] sınıfının bulunduğu klasör, Spring tarafından örtük olarak tanınan veya Spring anotasyonlarıyla açıkça tanımlanan bean'leri aramak üzere taranacaktır. Böylece, [Customer] ve [CustomerRepository] sınıfları incelenecektir. İlki [@Entity] anotasyonuna sahip olduğu için Hibernate tarafından yönetilecek bir varlık olarak sınıflandırılacaktır. İkincisi ise [CrudRepository] arayüzünü genişlettiği için bir Spring bean'i olarak kaydedilecektir.

Kodun 16-17. satırlarını inceleyelim:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
    1. satır: Spring Boot projesindeki [SpringApplication] sınıfının [run] statik yöntemi yürütülür. Bu yöntemin parametresi, [Configuration] veya [EnableAutoConfiguration] anotasyonuna sahip sınıftır. Daha önce açıklanan tüm süreç bu noktada gerçekleşir. Sonuç, bir Spring uygulama bağlamıdır, yani Spring tarafından yönetilen bir dizi bean;
    1. satır: Bu Spring bağlamından, [CustomerRepository] arayüzünü uygulayan bir bean talep edilir. Burada, bu arayüzü uygulamak için Spring Data tarafından oluşturulan sınıfı alırız.

Takip eden işlemler, yalnızca [CustomerRepository] arayüzünü uygulayan bean’in yöntemlerini kullanır. 50. satırda, bağlamın kapatıldığına dikkat edilmelidir. Konsol çıktıları şöyledir:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.0.2.RELEASE)

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : Gportpers3 üzerinde PID 11664 ile Uygulama başlatılıyor (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes, D:\Temp\wksSTS\gs-accessing-data-jpa-complete dizininde ST tarafından başlatıldı)
2014-06-05 16:23:13.936  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : QZXW2HTMLCb3JnLnNwcmluZ2ZyYW1ld29yay5jb250ZXh0LmFubm90YXRpb24uQW5ub3RhdGlvbkNvbmZpZ0FwcGxpY2F0aW9uQ29udGV4dAZQ yenileniyorX@331a8fa0: başlangıç tarihi [Thu Jun 05 16:23:13 CEST 2014]; bağlam hiyerarşisinin kökü
2014-06-05 16:23:15.424  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : 'default' kalıcılık birimi için JPA kapsayıcısı EntityManagerFactory oluşturuluyor
2014-06-05 16:23:15.518  INFO 11664 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: PersistenceUnitInfo işleniyor [
    name: default
    ...]
2014-06-05 16:23:15.690  INFO 11664 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-05 16:23:15.692  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties bulunamadı
2014-06-05 16:23:15.694  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bayt kodu sağlayıcı adı : javassist
2014-06-05 16:23:15.988  INFO 11664 --- [        main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-05 16:23:16.078  INFO 11664 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Kullanılan lehçe: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: ASTQueryTranslatorFactory kullanılarak
2014-06-05 16:23:16.613  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: hbm2ddl şema dışa aktarma işlemi devam ediyor
Hibernate: drop table customer if exists
Hibernate: create table customer (id bigint generated by default as identity, first_name varchar(255), last_name varchar(255), primary key (id))
2014-06-05 16:23:16.619  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Dışa aktarma şeması tamamlandı
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Başlangıçta JMX maruziyeti için bean'leri kaydediliyor
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Uygulama 3,906 saniye içinde başlatıldı (JVM 5,013 saniye boyunca çalışıyor)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Hibernate: select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name as last_nam3_0_0_ from customer customer0_ where customer0_.id=?
Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_ where customer0_.last_name=?
Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2014-06-05 16:23:17.330  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : Kapanış org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: başlangıç tarihi [Thu Jun 05 16:23:13 CEST 2014]; bağlam hiyerarşisinin kökü
2014-06-05 16:23:17.332  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Kapatma sırasında JMX'e açık bean'lerin kaydı siliniyor
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : 'default' kalıcılık birimi için JPA EntityManagerFactory kapatılıyor
2014-06-05 16:23:17.334  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: hbm2ddl şema dışa aktarma işlemi yürütülüyor
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Dışa aktarma şeması tamamlandı
  • 1-8. satırlar: Spring Boot projesinin logosu;
    1. satır: [hello.Application] sınıfı çalıştırılır;
    1. satır: [AnnotationConfigApplicationContext], Spring'in [ApplicationContext] arayüzünü uygulayan bir sınıftır. Bu, bir bean konteyneridir;
  • satır 11: [entityManagerFactory] bean'i, bir Spring sınıfı olan [LocalContainerEntityManagerFactory] sınıfı ile uygulanır;
    1. satır: [hibernate] ortaya çıkıyor. Seçilen, JPA adlı bu uygulamadır;
    1. satır: SGBD ile birlikte kullanılacak Hibernate lehçesi, SQL varyantıdır. Burada [H2Dialect] lehçesi, Hibernate'in SGBD ve H2 ile çalışacağını göstermektedir;
  • 22-24. satırlar: [CUSTOMER] tablosu oluşturulur. Bu, Hibernate’in JPA tanımlarından tablolar oluşturmak üzere yapılandırıldığı anlamına gelir; burada [Customer] sınıfının JPA tanımı söz konusudur;
  • 27-32. satırlar: [CUSTOMER] tablosuna satır eklemelerini gösteren Hibernate günlükleri. Bu, Hibernate’in günlük oluşturacak şekilde yapılandırıldığı anlamına gelir;
  • satır 35-39: eklenen beş müşteri;
  • satır 42-44: arayüzün [findOne] yönteminin sonucu;
  • 47-50. satırlar: [findByLastName] yönteminin sonuçları;
    1. satır ve sonrası: Spring bağlamının kapatılmasına ait günlükler.

2.2.5. Spring Data projesinin manuel yapılandırması

Önceki projeyi [gs-accessing-data-jpa-2] projesinde kopyalıyoruz:

  

Bu yeni projede, Spring Boot tarafından yapılan otomatik yapılandırmaya güvenmeyeceğiz. Yapılandırmayı manuel olarak gerçekleştireceğiz. Varsayılan yapılandırmalar bize uymuyorsa bu yararlı olabilir.

Öncelikle, [pom.xml] dosyasında gerekli bağımlılıkları açıkça belirteceğiz:


<dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring işlemleri -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.5.2.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Veritabanı -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
  • 3-17. satırlar: Spring'in temel kütüphaneleri;
  • 19-28. satırlar: veritabanı işlemlerini yönetmek için Spring kütüphaneleri;
  • 30-34. satırlar: veritabanına erişim için kullanılan Spring Data;
  • satır 36-40: Uygulamayı başlatmak için Spring Boot;
  • 48-52. satırlar: SGBD H2;
  • satır 54-63: Veritabanları genellikle, bağlantının tekrar tekrar açılıp kapanmasını önleyen açık bağlantı havuzlarıyla kullanılır. Burada kullanılan uygulama [commons-dbcp]'tir;

Yine [pom.xml] dosyasında, yürütülebilir sınıfın adı değiştirilir:


    <properties>
...
        <start-class>demo.console.Main</start-class>
</properties>

Yeni projede, [Customer] varlığı ve [CustomerRepository] arayüzü değişmez. [Application] sınıfını değiştireceğiz; bu sınıf iki sınıfa bölünecektir:

  • [Config], yapılandırma sınıfı olacak:
  • [Main], bu da yürütülebilir sınıf olacaktır;
  

Çalıştırılabilir sınıf [Main], yapılandırma açıklamaları hariç olmak üzere öncekiyle aynıdır:


package demo.console;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...

        context.close();
    }

}
    1. satır: [Main] sınıfında artık yapılandırma ek açıklamaları bulunmamaktadır;
    1. satır: Uygulama Spring Boot ile başlatılır. [Config.class] parametresi, projenin yeni yapılandırma sınıfıdır;

Projeyi yapılandıran [Config] sınıfı şöyledir:


package demo.config;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // veri kaynağı H2
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    // sağlayıcı JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // İşlem yöneticisi
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
    1. satır: [@Configuration] anotasyonu, [Config] sınıfını bir Spring yapılandırma sınıfı haline getirir;
    1. satır: [@EnableJpaRepositories] anotasyonu, Spring Data arayüzlerinin ([CrudRepository]) bulunduğu klasörleri belirtmeye olanak tanır. Bu arayüzler, Spring bileşenlerine dönüşecek ve Spring bağlamında kullanılabilir hale gelecektir;
    1. satır: [@EnableTransactionManagement] anotasyonu, [CrudRepository] arayüzlerinin yöntemlerinin bir işlem içinde gerçekleştirilmesi gerektiğini belirtir;
    1. satır: [@EntityScan] anotasyonu, JPA varlıklarının aranacağı klasörlerin adlarının belirtilmesini sağlar. Burada bu açıklama satırı yorum olarak işaretlenmiştir, çünkü bu bilgi 50. satırda açıkça verilmiştir. [@EnableAutoConfiguration] modu kullanılıyorsa ve JPA varlıklarının yapılandırma sınıfıyla aynı klasörde bulunmaması durumunda bu açıklama satırı mevcut olmalıdır;
    1. satır: [@ComponentScan] açıklaması, Spring bileşenlerinin aranacağı klasörlerin listelenmesini sağlar. Spring bileşenleri, @Service, @Component, @Controller gibi Spring anotasyonlarıyla etiketlenmiş sınıflardır. Burada, [Config] sınıfında tanımlananlar dışında başka bileşen bulunmadığından, anotasyon yorum satırına alınmıştır;
  • 25-33. satırlar: veri kaynağını, yani H2 veritabanını tanımlar. 25. satırdaki @Bean anotasyonu, bu yöntemle oluşturulan nesneyi Spring tarafından yönetilen bir bileşen haline getirir. Yöntemin adı burada herhangi bir şey olabilir. Ancak, 47. satırdaki EntityManagerFactory yoksa ve otomatik yapılandırma ile tanımlanmışsa, bu yöntemin adı [dataSource] olmalıdır;
    1. satır: Veritabanının adı [demo] olacak ve proje klasöründe oluşturulacaktır;
  • satır 36-43: Kullanılan JPA uygulamasını tanımlar; burada bir Hibernate uygulamasıdır. Yöntemin adı burada herhangi bir şey olabilir;
  • satır 39: SQL için günlük kaydı yok;
  • satır 30: veritabanı mevcut değilse oluşturulacaktır;
  • satır 46-54: JPA'in kalıcılığını yönetecek olan EntityManagerFactory'i tanımlar. Yöntemin adı mutlaka [entityManagerFactory] olmalıdır;
    1. satır: Yöntem, daha önce tanımlanan iki bean'in türünde iki parametre alır. Bu bean'ler daha sonra oluşturulur ve Spring tarafından yöntemin parametreleri olarak enjekte edilir;
    1. satır: Kullanılacak JPA uygulaması belirlenir;
  • satır 50: JPA varlıklarının bulunacağı klasörleri belirler;
  • satır 51: yönetilecek veri kaynağını belirler;
  • satır 57-62: işlem yöneticisi. Yöntemin adı mutlaka [transactionManager] olmalıdır. Bu yöntem, satır 46-54'teki bean'i parametre olarak alır;
    1. satır: işlem yöneticisi, EntityManagerFactory ile ilişkilendirilir;

Yukarıdaki yöntemler herhangi bir sırayla tanımlanabilir.

Projenin çalıştırılması aynı sonuçları verir. Proje klasöründe, H2 adlı yeni bir veritabanı dosyası belirir:

  

Son olarak, Spring Boot'u kullanmadan da bu işlemi gerçekleştirebiliriz. İkinci bir çalıştırılabilir sınıf olan [Main2]'i oluşturuyoruz:

  

[Main2] sınıfının kodu şöyledir:


package demo.console;

import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main2 {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....

        context.close();
    }

}
    1. satır: [Config] yapılandırma sınıfı artık [AnnotationConfigApplicationContext] Spring sınıfı tarafından kullanılıyor. 5. satırda görüldüğü gibi, artık Spring Boot'a bağlılık kalmamıştır.

Çalıştırıldığında önceki ile aynı sonuçlar elde edilir.

2.2.6. Çalıştırılabilir bir arşiv oluşturma

Projenin çalıştırılabilir bir arşivini oluşturmak için şu adımları uygulayabilirsiniz:

  • [1]'te: bir yürütme yapılandırması oluşturulur;
  • [2]'te: [Java Application] türü
  • [3]'te: yürütülecek projeyi belirtir (Browse düğmesini kullanın);
  • [4]'te: yürütülecek sınıfı belirtir;
  • [5]: çalıştırma yapılandırmasının adı – herhangi bir isim olabilir;
  • [6]'te: proje dışa aktarılır;
  • [7]'te: çalıştırılabilir bir arşiv biçiminde;
  • [8]: Oluşturulacak yürütülebilir dosyanın yolunu ve adını belirtir;
  • [9]'te: [5]'te oluşturulan çalıştırma yapılandırmasının adı;

Bu işlem tamamlandıktan sonra, yürütülebilir arşivin bulunduğu klasörde bir konsol açılır:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-2.jar

Arşiv şu şekilde çalıştırılır:


.....\dist>java -jar gs-accessing-data-jpa-2.jar

Konsolda elde edilen sonuçlar şunlardır:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder daha fazla ayrıntı için.
juin 12, 2014 9:48:38 AM org.hibernate.ejb.HibernatePersistence logDeprecation
WARN: HHH015016: Encountered a deprecated javax.persistence.spi.PersistenceProvider [org.hibernate.ejb.HibernatePersistence]; use [org.hibernate.jpa.HibernatePersistenceProvider] instead.
juin 12, 2014 9:48:38 AM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
        name: default
        ...]
juin 12, 2014 9:48:38 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.4.Final}
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 12, 2014 9:48:39 AM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
juin 12, 2014 9:48:39 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
juin 12, 2014 9:48:39 AM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

2.2.7. Yeni bir Spring Data projesi oluşturma

Bir Spring Data proje şablonu oluşturmak için aşağıdaki adımları uygulayabilirsiniz:

  • [1]'te yeni bir proje oluşturulur;
  • [2]'te: [Spring Starter Project] türünde;
  • oluşturulan proje bir Maven projesi olacaktır. [3]'te, proje grubunun adını belirtin;
  • [4]'te: projenin derlenmesi ile oluşturulacak artefaktın (burada bir jar dosyası) adını belirtin;
  • [5]'te: projede oluşturulacak çalıştırılabilir sınıfın paketi belirtilir;
  • [6]'te: projenin Eclipse adı – herhangi bir ad olabilir ([4] ile aynı olması gerekmez);
  • [7]'te: [JPA] katmanına sahip bir proje oluşturulacağı belirtilir. Böyle bir proje için gerekli bağımlılıklar daha sonra [pom.xml] dosyasına eklenecektir;
  • [8] dosyasında: oluşturulan proje;

[pom.xml] dosyası, JPA projesine gerekli bağımlılıkları içerir:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
        <relativePath/> <!-- depo içinden üst öğeyi ara -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • 9-12. satırlar: JPA için gerekli bağımlılıklar – [Spring Data]'i de içerecektir;
  • 13-17. satırlar: Spring ile entegre edilmiş JUnit testleri için gerekli bağımlılıklar;

Çalıştırılabilir sınıf [Application] hiçbir işlem yapmaz, ancak önceden yapılandırılmıştır:


package istia.st;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

[ApplicationTests] test sınıfı hiçbir işlem yapmaz, ancak önceden yapılandırılmıştır:


package istia.st;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
    1. satır: [@SpringApplicationConfiguration] anotasyonu, [Application] yapılandırma dosyasının kullanılmasını sağlar. Böylece test sınıfı, bu dosyada tanımlanacak tüm bean'lerden yararlanacaktır;
    1. satır: [@RunWith] anotasyonu, Spring'in JUnit ile entegrasyonunu sağlar: sınıf, bir JUnit testi olarak çalıştırılabilecektir. [@RunWith], JUnit (4. satır) anotasyonudur; [SpringJUnit4ClassRunner] sınıfı ise bir Spring sınıfıdır (6. satır);

Artık JPA adlı bir uygulama iskeletine sahip olduğumuza göre, bunu tamamlayarak randevu yönetimi uygulamamızın sunucu kalıcılık katmanı projesini yazabiliriz.

2.3. Sunucu için Eclipse projesi

  

Projenin ana unsurları şunlardır:

  • [pom.xml]: projenin Maven yapılandırma dosyası;
  • [rdvmedecins.entities]: JPA varlıklar;
  • [rdvmedecins.repositories]: JPA varlıklarına erişim için Spring Data arayüzleri;
  • [rdvmedecins.metier]: [métier] katmanı;
  • [rdvmedecins.domain]: Katman tarafından işlenen varlıklar [métier];
  • [rdvmdecins.config]: kalıcılık katmanının yapılandırma sınıfları;
  • [rdvmedecins.boot]: temel bir konsol uygulaması;

2.4. Maven yapılandırması

Projenin [pom.xml] dosyası şu şekildedir:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.rdvmedecins</groupId>
    <artifactId>rdvmedecins-metier-dao</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
    </dependencies>
    <properties>
        <!-- her şey için UTF-8 kullanın -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>istia.st.spring.data.main.Application</start-class>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
  • 8-12. satırlar: Proje, üst proje olan [spring-boot-starter-parent]'e dayanmaktadır. Üst projede zaten mevcut olan bağımlılıklar için sürüm belirtilmez. Üst projede tanımlanan sürüm kullanılacaktır. Diğer bağımlılıklar ise normal şekilde beyan edilir;
  • 14-17. satırlar: Spring Data için;
  • satır 18-22: JUnit testleri için;
  • satır 23-26: SGBD ve MySQL5 için JDBC sürücüsü;
  • satır 27-34: Commons DBCP bağlantı havuzu;
  • satır 35-38: JSON yönetim kütüphanesi;
  • satır 39-43: Google koleksiyon yönetimi kütüphanesi;

[spring-boot-starter-parent]'in 1.1.0.RC1 sürümü, kütüphanelerin aşağıdaki sürümlerini kullanır:

<activemq.version>5.9.1</activemq.version>
    <aspectj.version>1.8.0</aspectj.version>
    <codahale-metrics.version>3.0.2</codahale-metrics.version>
    <commons-beanutils.version>1.9.1</commons-beanutils.version>
    <commons-collections.version>3.2.1</commons-collections.version>
    <commons-dbcp.version>1.4</commons-dbcp.version>
    <commons-digester.version>2.1</commons-digester.version>
    <commons-pool.version>1.6</commons-pool.version>
    <commons-pool2.version>2.2</commons-pool2.version>
    <crashub.version>1.3.0-beta20</crashub.version>
    <flyway.version>3.0</flyway.version>
    <freemarker.version>2.3.20</freemarker.version>
    <gemfire.version>7.0.2</gemfire.version>
    <gradle.version>1.6</gradle.version>
    <groovy.version>2.3.2</groovy.version>
    <h2.version>1.3.175</h2.version>
    <hamcrest.version>1.3</hamcrest.version>
    <hibernate-entitymanager.version>4.3.1.Final</hibernate-entitymanager.version>
    <hibernate-jpa-api.version>1.0.1.Final</hibernate-jpa-api.version>
    <hibernate-validator.version>5.0.3.Final</hibernate-validator.version>
    <hibernate.version>4.3.1.Final</hibernate.version>
    <hikaricp.version>1.3.8</hikaricp.version>
    <hornetq.version>2.4.1.Final</hornetq.version>
    <hsqldb.version>2.3.2</hsqldb.version>
    <httpasyncclient.version>4.0.1</httpasyncclient.version>
    <httpclient.version>4.3.3</httpclient.version>
    <jackson.version>2.3.3</jackson.version>
    <java.version>1.6</java.version>
    <javassist.version>3.18.1-GA</javassist.version>
    <jedis.version>2.4.1</jedis.version>
    <jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
    <jetty.version>8.1.14.v20131031</jetty.version>
    <joda-time.version>2.3</joda-time.version>
    <jolokia.version>1.2.0</jolokia.version>
    <jstl.version>1.2</jstl.version>
    <junit.version>4.11</junit.version>
    <liquibase.version>3.0.8</liquibase.version>
    <log4j.version>1.2.17</log4j.version>
    <logback.version>1.1.2</logback.version>
    <mockito.version>1.9.5</mockito.version>
    <mongodb.version>2.12.1</mongodb.version>
    <mysql.version>5.1.30</mysql.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <reactor.version>1.1.1.RELEASE</reactor.version>
    <servlet-api.version>3.0.1</servlet-api.version>
    <slf4j.version>1.7.7</slf4j.version>
    <snakeyaml.version>1.13</snakeyaml.version>
    <solr.version>4.7.2</solr.version>
    <spock.version>0.7-groovy-2.0</spock.version>
    <spring-amqp.version>1.3.4.RELEASE</spring-amqp.version>
    <spring-batch.version>3.0.0.RELEASE</spring-batch.version>
    <spring-boot.version>1.1.0.RC1</spring-boot.version>
    <spring-data-releasetrain.version>Dijkstra-RELEASE</spring-data-releasetrain.version>
    <spring-hateoas.version>0.12.0.RELEASE</spring-hateoas.version>
    <spring-integration.version>4.0.2.RELEASE</spring-integration.version>
    <spring-loaded.version>1.2.0.RELEASE</spring-loaded.version>
    <spring-mobile.version>1.1.1.RELEASE</spring-mobile.version>
    <spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
    <spring-security.version>3.2.4.RELEASE</spring-security.version>
    <spring-social-facebook.version>1.1.1.RELEASE</spring-social-facebook.version>
    <spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version>
    <spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version>
    <spring-social.version>1.1.0.RELEASE</spring-social.version>
    <spring.version>4.0.5.RELEASE</spring.version>
    <thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extras-springsecurity3.version>
    <thymeleaf-layout-dialect.version>1.2.4</thymeleaf-layout-dialect.version>
    <thymeleaf.version>2.1.3.RELEASE</thymeleaf.version>
    <tomcat.version>7.0.54</tomcat.version>
    <velocity-tools.version>2.0</velocity-tools.version>
    <velocity.version>1.7</velocity.version>

2.5. JPA varlıkları

JPA nesneleri, veritabanı tablolarındaki satırları kapsülleyecek nesnelerdir.

  

[AbstractEntity] sınıfı, [Personne, Creneau, Rv] varlıklarının üst sınıfıdır. Tanımı şöyledir:


package rdvmedecins.entities;

import java.io.Serializable;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

@MappedSuperclass
public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Long id;
    @Version
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // başlatma
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // alıcı ve ayarlayıcılar
    ..
}
    1. satır: [@MappedSuperclass] açıklaması, açıklanan sınıfın JPA ve [@Entity] varlıklarının üst sınıfı olduğunu belirtir;
  • 15-17. satırlar: Her bir varlığın [id] birincil anahtarını tanımlar. [@Id] etiketlemesi, [id] alanını birincil anahtar haline getirir. [@GeneratedValue(strategy = GenerationType.AUTO)] açıklaması, bu birincil anahtarın değerinin SGBD tarafından üretildiğini ve herhangi bir üretim modunun zorunlu kılınmadığını belirtir;
  • 18-19. satırlar: Her bir varlığın sürümünü tanımlar. JPA uygulaması, varlık her değiştirildiğinde bu sürüm numarasını artıracaktır. Bu numara, iki farklı kullanıcı tarafından varlığın aynı anda güncellenmesini önlemek için kullanılır: U1 ve U2 adlı iki kullanıcı, sürüm numarası V1 olan E varlığını okur. U1, E'yi değiştirir ve bu değişikliği veritabanına kaydeder: sürüm numarası bu durumda V1+1'e geçer. U2 da E'yi değiştirir ve bu değişikliği veritabanına kaydeder: veritabanındaki sürümden (V1+1) farklı bir sürüme (V1) sahip olduğu için bir istisna alır;
  • 29-33. satırlar: [build] yöntemi, [AbstractEntity]'in iki alanını başlatmaya olanak tanır. Bu yöntem, [AbstractEntity] örneğinin referansını bu şekilde başlatır;
  • satır 36-44: Sınıfın [equals] yöntemi yeniden tanımlanmıştır: iki varlık, aynı sınıf adına ve aynı id tanımlayıcısına sahipse eşit kabul edilecektir;

[Personne] varlığı, [Medecin] ve [Client] varlıklarının üst sınıfıdır:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // kişi özellikleri
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;

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

    // parametreli oluşturucu
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // alıcı ve ayarlayıcılar
    ...
}
    1. satır: [@MappedSuperclass] açıklaması, açıklanan sınıfın JPA ve [@Entity] varlıklarının üst sınıfı olduğunu belirtir;
  • 10-15. satırlar: Bir kişinin unvanı (Melle), adı (Jacqueline) ve soyadı (Tatou) vardır. Tablonun sütunları hakkında herhangi bir bilgi verilmemiştir. Dolayısıyla, varsayılan olarak alanlarla aynı isimleri taşıyacaklardır;

[Medecin] varlığı şöyledir:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne {

    private static final long serialVersionUID = 1L;

    // varsayılan oluşturucu
    public Medecin() {
    }

    // parametreli oluşturucu
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}
    1. satır: Sınıf, JPA varlığıdır;
    1. satır: veritabanındaki [MEDECINS] tablosuyla ilişkilidir;
    1. satır: [Medecin] varlığı, [Personne] varlığından türetilmiştir;

Bir doktor şu şekilde başlatılabilir:

Medecin m=new Medecin("Mr","Paul","Tatou");

Ayrıca, ona bir kimlik ve sürüm atamak istersek şu şekilde yazabiliriz:

Medecin m=new Medecin("Mr","Paul","Tatou").build(10,1);

burada [build] yöntemi, [AbstractEntity]'te tanımlanan yöntemdir.

[Client] varlığı şu şekildedir:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "clients")
public class Client extends Personne {

    private static final long serialVersionUID = 1L;

    // varsayılan oluşturucu
    public Client() {
    }

    // parametreli oluşturucu
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    // kimlik
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }

}
    1. satır: sınıf, JPA varlığıdır;
    1. satır: veritabanındaki [CLIENTS] tablosuyla ilişkilidir;
    1. satır: [Client] varlığı, [Personne] varlığından türetilmiştir;

[Creneau] varlığı şöyledir:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // RV zaman aralığının özellikleri
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // bir randevu bir hekime bağlıdır
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // yabancı anahtar
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;

    // varsayılan oluşturucu
    public Creneau() {
    }

    // parametreli üretici
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }

    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }

    // yabancı anahtar
    public long getIdMedecin() {
        return idMedecin;
    }

    // setter'lar - getter'lar
    ...
}
    1. satır: sınıf, JPA varlığıdır;
    1. satır: veritabanındaki [CRENEAUX] tablosuyla ilişkilidir;
    1. satır: [Creneau] varlığı, [AbstractEntity] varlığından türetilmiştir ve bu nedenle [id] tanımlayıcısını ve [version] sürümünü miras almıştır;
    1. satır: zaman aralığının başlangıç saati (14);
    1. satır: zaman aralığının başlangıç dakikası (20);
    1. satır: zaman aralığının bitiş saati (14);
  • satır 19: zaman aralığının bitiş dakikası (40);
  • satır 22-24: randevu sahibinin doktoru. [CRENEAUX] tablosu, [MEDECINS] tablosuna bir yabancı anahtara sahiptir. Bu ilişki, satır 22-24 ile ifade edilir;
    1. satır: [@ManyToOne] notu, birden çok (zaman aralığı) ile bir (doktor) arasındaki ilişkiyi belirtir. [fetch=FetchType.LAZY] özniteliği, kalıcılık bağlamından bir [Creneau] varlığı istendiğinde ve bu varlığın veritabanında aranması gerektiğinde, [Medecin] varlığının onunla birlikte getirilmeyeceğini belirtir. Bu modun avantajı, [Medecin] varlığının yalnızca geliştirici talep ettiğinde aranmasıdır. Böylece bellek tasarrufu sağlanır ve performans artar;
    1. satır: [CRENEAUX] tablosundaki yabancı anahtar sütununun adını belirtir;
  • 27-28. satırlar: [MEDECINS] tablosundaki yabancı anahtar;
    1. satır: [ID_MEDECIN] sütunu 23. satırda zaten kullanılmıştır. Bu, sütunun iki farklı yolla değiştirilebileceği anlamına gelir; ancak JPA standardı bunu kabul etmez. Bu nedenle [insertable = false, updatable = false] öznitelikleri eklenir; böylece sütun yalnızca okunabilir hale gelir;

[Rv] varlığı şöyledir:


package rdvmedecins.entities;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // bir Rv'nin özellikleri
    @Temporal(TemporalType.DATE)
    private Date jour;

    // bir rv bir müşteriye bağlıdır
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // bir RV bir zaman dilimine bağlıdır
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // dış anahtarlar
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

    // varsayılan üretici
    public Rv() {
    }

    // parametrelerle
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }

    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }

    // yabancı anahtarlar
    public long getIdCreneau() {
        return idCreneau;
    }

    public long getIdClient() {
        return idClient;
    }

    // alıcı ve ayarlayıcılar
...
}
    1. satır: sınıf, JPA varlığıdır;
  • satır 15: veritabanındaki [RV] tablosuyla ilişkilidir;
    1. satır: [Rv] varlığı, [AbstractEntity] varlığından türetilmiştir ve bu nedenle [id] tanımlayıcısını ve [version] sürümünü miras almıştır;
    1. satır: randevu tarihi;
    1. satır: Java'daki [Date] türü hem tarih hem de saat içerir. Burada sadece tarihin kullanıldığı belirtilmektedir;
  • satır 24-26: Bu randevunun alındığı müşteri. [RV] tablosu, [CLIENTS] tablosuna bir yabancı anahtara sahiptir. Bu ilişki, satır 24-26 ile ifade edilmektedir;
  • satır 29-31: randevunun zaman aralığı. [RV] tablosu, [CRENEAUX] tablosuna yabancı anahtar içerir. Bu ilişki, satır 29-31 ile gösterilmektedir;
  • 34-35. satırlar: [idClient] yabancı anahtarı;
  • 36-37. satırlar: [idCreneau] yabancı anahtarı;

2.6. [DAO] katmanı

[DAO] katmanını Spring Data ile uygulayacağız:

  

[DAO] katmanı, dört Spring Data arayüzü ile uygulanır:

  • [ClientRepository]: JPA ve [Client] varlıklarına erişim sağlar;
  • [CreneauRepository]: JPA ve [Creneau] varlıklarına erişim sağlar;
  • [MedecinRepository]: JPA ve [Medecin] varlıklarına erişim sağlar;
  • [RvRepository]: JPA ve [Rv] varlıklarına erişim sağlar;

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


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
    1. satır: [MedecinRepository] arayüzü, [CrudRepository] arayüzünün yöntemlerini miras alır ve başka yöntem eklemez;

[ClientRepository] arayüzü şöyledir:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
    1. satır: [ClientRepository] arayüzü, [CrudRepository] arayüzünün yöntemlerini devralmakla yetinir ve başka yöntem eklemez;

[CreneauRepository] arayüzü şöyledir:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Creneau;

public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // bir doktorun randevu saatleri listesi
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
    1. satır: [CreneauRepository] arayüzü, [CrudRepository] arayüzünün yöntemlerini devralır;
  • 10-11. satırlar: [getAllCreneaux] yöntemi, bir doktorun randevu zaman aralıklarını almayı sağlar;
  • satır 11: parametre, doktorun kimlik numarasıdır. Sonuç, [Iterable<Creneau>] nesnesi biçiminde bir randevu aralığı listesidir;
  • satır 10: [@Query] açıklaması, yöntemi uygulayan JPQL (Java Persistence Query Language) sorgusunu belirtmeye olanak tanır. [?1] parametresi, yöntemin [idMedecin] parametresiyle değiştirilecektir;

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


package rdvmedecins.repositories;

import java.util.Date;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Rv;

public interface RvRepository extends CrudRepository<Rv, Long> {

    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
    1. satır: [RvRepository] arayüzü, [CrudRepository] arayüzünün yöntemlerini devralır;
  • 12-13. satırlar: [getRvMedecinJour] yöntemi, belirli bir gün için bir doktorun randevularını almayı sağlar;
    1. satır: Parametreler, doktorun kimliği ve gündür. Sonuç, bir [Iterable<Rv>] nesnesi biçiminde bir randevu listesidir;
    1. satır: [@Query] açıklaması, yöntemi uygulayan JPQL sorgusunu belirtmeye olanak tanır. [?1] parametresi, yöntemin [idMedecin] parametresiyle; [?2] parametresi ise yöntemin [jour] parametresiyle değiştirilecektir. Aşağıdaki JPQL sorgusu tek başına yeterli değildir:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

çünkü Rv sınıfındaki [Client] ve [Creneau] türündeki alanlar [FetchType.LAZY] modunda elde edilir; bu da, bu alanların elde edilebilmesi için açıkça talep edilmesi gerektiği anlamına gelir. Bu işlem, JPQL sorgusunda [left join fetch entité] sözdizimi kullanılarak gerçekleştirilir; bu sözdizimi, işaret edilen varlığı almak için yabancı anahtarın işaret ettiği tablo ile bir birleştirme yapılmasını talep eder;

2.7. [métier] katmanı

  
  • [IMetier], [métier] katmanının arayüzüdür ve [Metier] ise bunun uygulamasıdır;
  • [AgendaMedecinJour] ve [CreneauMedecinJour], iki iş varlığıdır;

2.7.1. Varlıklar

[CreneauMedecinJour] varlığı, bir zaman aralığını ve bu aralıkta alınmış olası randevuyu ilişkilendirir:


package rdvmedecins.domain;

import java.io.Serializable;

import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // alanlar
    private Creneau creneau;
    private Rv rv;

    // oluşturucular
    public CreneauMedecinJour() {

    }

    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
        this.rv=rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }

    // alıcı ve ayarlayıcılar
...
}
    1. satır: zaman aralığı;
    1. satır: olası randevu – aksi takdirde null;

[AgendaMedecinJour] varlığı, belirli bir gün için bir doktorun randevu takvimi, yani randevularının listesidir:


package rdvmedecins.domain;

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

import rdvmedecins.entities.Medecin;

public class AgendaMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // alanlar
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

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

    }

    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }

    // getter ve setter'lar
...
}
    1. satır: doktor;
    1. satır: ajandadaki gün;
    1. satır: randevulu veya randevusuz zaman aralıkları;

2.7.2. Hizmet

[métier] katmanının arayüzü şu şekildedir:


package rdvmedecins.metier;

import java.util.Date;
import java.util.List;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;

public interface IMetier {

    // müşteri listesi
    public List<Client> getAllClients();

    // doktor listesi
    public List<Medecin> getAllMedecins();

    // bir doktorun randevu aralıkları listesi
    public List<Creneau> getAllCreneaux(long idMedecin);

    // belirli bir günde bir doktorun randevu listesi
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // ID'siyle bir müşteriyi bul
    public Client getClientById(long id);

    // kimlik numarasıyla bir müşteriyi bulma
    public Medecin getMedecinById(long id);

    // ID'siyle tanımlanan bir randevuyu bulma
    public Rv getRvById(long id);

    // ID'siyle tanımlanan bir zaman aralığını bulma
    public Creneau getCreneauById(long id);

    // bir RV eklemek
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);

    // bir RV'i sil
    public void supprimerRv(Rv rv);

    // meslek
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

Yorumlar, her bir yöntemin işlevini açıklamaktadır.

[IMetier] arayüzünün uygulaması, aşağıdaki [Metier] sınıfıdır:


package rdvmedecins.metier;

import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;

import com.google.common.collect.Lists;

@Service("métier")
public class Metier implements IMetier {

    // depolar
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;

    // arayüz uygulaması
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }

    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }

    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }

    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }

    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }

}
    1. satır: [@Service] anotasyonu, anotasyon uygulanan sınıfı Spring tarafından yönetilen bir bileşen haline getiren bir Spring anotasyonudur. Bir bileşene isim verilebilir veya verilmeyebilir. Bu bileşene [métier] adı verilmiştir;
    1. satır: [Metier] sınıfı, [IMetier] arayüzünü uygular;
    1. satır: [@Autowired] anotasyonu bir Spring anotasyonudur. Bu şekilde anotasyonla işaretlenen alanın değeri, Spring tarafından belirtilen tür veya ada sahip bir Spring bileşeninin referansı ile başlatılacak (enjekte edilecektir). Burada [@Autowired] anotasyonu bir isim belirtmemektedir. Dolayısıyla tür bazlı enjeksiyon yapılacaktır;
    1. satır: [medecinRepository] alanı, [MedecinRepository] türündeki bir Spring bileşeninin referansı ile başlatılacaktır. Bu, daha önce sunduğumuz [MedecinRepository] arayüzünü uygulamak için Spring Data tarafından oluşturulan sınıfın referansı olacaktır;
  • 30-35. satırlar: Bu işlem, incelenen diğer üç arayüz için de tekrarlanır;
  • 39-41. satırlar: [getAllClients] yönteminin uygulanması;
    1. satır: [ClientRepository] arayüzünün [findAll] yöntemini kullanıyoruz. Bu yöntem, [Iterable<Client>] türünde bir değer döndürür; biz de bunu statik [Lists.newArrayList] yöntemi ile [List<Client>] türüne dönüştürürüz. [Lists] sınıfı, Google Guava kütüphanesinde tanımlanmıştır. [pom.xml]'te bu bağımlılık şu şekilde içe aktarılmıştır:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • 38-86. satırlar: [IMetier] arayüzünün yöntemleri, [DAO] katmanındaki sınıfların yardımıyla uygulanır;

Yalnızca 88. satırdaki yöntem, [métier] katmanına özgüdür. Bu yöntem, sadece verilere erişimden ibaret olmayan bir iş işlevi gerçekleştirdiği için buraya yerleştirilmiştir. Bu yöntem olmasaydı, [métier] katmanını oluşturmanın bir anlamı olmazdı. [getAgendaMedecinJour] yöntemi şu şekildedir:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // doktorun randevu saatleri listesi
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // aynı doktorun aynı gün için randevu listesi
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // Alınan randevulardan bir sözlük oluşturulur
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // istenen gün için ajanda oluşturulur
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // doktor
        agenda.setMedecin(getMedecinById(idMedecin));
        // gün
        agenda.setJour(jour);
        // randevu zaman aralıkları
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // randevu aralıklarının doldurulması
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // takvim satırı
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // zaman aralığı
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // zaman aralığı boş mu yoksa rezerve mi?
            if (hReservations.containsKey(idCreneau)) {
                // zaman aralığı dolu - rezervasyon kaydediliyor
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // sonuç verilir
        return agenda;
    }

Okuyucunun yorumları incelemesi önerilir. Algoritma şu şekildedir:

  • belirtilen doktorun tüm randevu zaman aralıkları alınır;
  • belirtilen gün için tüm randevuları alınır;
  • bu iki bilgiyle, bir zaman aralığının boş mu yoksa dolu mu olduğu belirlenir;

2.8. Projenin yapılandırması

  

[DomainAndPersitenceConfig] sınıfı, projenin tamamını yapılandırır:


package rdvmedecins.config;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {

    // veri kaynağı MySQL
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        return dataSource;
    }

    // sağlayıcı JPA - Spring Boot tarafından kullanılan varsayılan değerlerden memnunsanız gerekli değildir
    // burada, günlükleri etkinleştirmek/devre dışı bırakmak için tanımlanır SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory ve TransactionManager, Spring Boot tarafından varsayılan değerlerle tanımlanmıştır

}
    1. satır: [EntityManagerFactory] ve [TransactionManager] bean'lerini tanımlamayacağız. Bunun için Spring Boot'un [@EnableAutoConfiguration] anotasyonunu (17. satır) kullanacağız;
  • 24-32. satırlar: MySQL5 veri kaynağını tanımlar. Bu, Spring Boot tarafından genellikle tahmin edilemeyen bir bean'dir;
  • satır 36-43: Ayrıca, Hibernate’in [showSql] özniteliğini false olarak ayarlamak için JPA uygulamasını da yapılandırıyoruz (satır 39). Varsayılan olarak bu öznitelik true değerindedir;
  • şu an için Spring tarafından yönetilen tek bileşenler, 25. ve 37. satırlardaki bean'ler ile otomatik yapılandırma yoluyla eklenen [EntityManagerFactory] ve [TransactionManager] bean'leridir. [métier] ve [DAO] katmanlarındaki bean'leri eklememiz gerekiyor;
    1. satır, [CrudRepository] arayüzünden miras alan [rdvmdecins.repositories] paketindeki arayüzleri Spring bağlamına ekler;
    1. satır, Spring bağlamına [rdvmedecins] paketindeki ve Spring anotasyonu içeren tüm alt sınıfları ekler. [rdvmdecins.metier] paketinde, [@Service] anotasyonuna sahip [Metier] sınıfı bulunacak ve Spring bağlamına eklenecektir;
    1. satır: Spring Boot tarafından varsayılan olarak bir [entityManagerFactory] bean'i tanımlanacaktır. Bu bean'e, yönetmesi gereken JPA varlıklarının nerede olduğunu belirtmek gerekir. Bunu yapan 19. satırdır;
    1. satır: [CrudRepository] arayüzünden miras alan arayüzlerin yöntemlerinin bir işlem içinde yürütülmesi gerektiğini belirtir;

2.9. [métier] katmanının testleri

  

[rdvmedecins.tests.Metier] sınıfı, Spring / JUnit 4 test sınıfıdır:


package rdvmedecins.tests;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {

    @Autowired
    private IMetier métier;

    @Test
    public void test1(){
        // müşteri listesi
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // doktorların görüntülenmesi
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // bir doktorun randevu aralıklarının görüntülenmesi
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // belirli bir günde bir doktorun randevu listesi
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // RV ekle
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // doğrulama
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // aynı günün aynı randevu aralığına bir RV ekleme
        // bir istisna oluşturmalıdır
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // hata kaydedilir
            erreur = true;
        }
        // bir hata olup olmadığı kontrol ediliyor
        Assert.assertTrue(erreur);
        // RV listesi
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // takvim görüntüleme
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // bir RV'i sil
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // doğrulama
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // yardımcı yöntem - bir koleksiyondaki öğeleri görüntüler
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
    1. satır: [@SpringApplicationConfiguration] anotasyonu, daha önce incelenen [DomainAndPersistenceConfig] yapılandırma dosyasının kullanılmasını sağlar. Böylece test sınıfı, bu dosyada tanımlanan tüm bean'lerden yararlanabilir;
    1. satır: [@RunWith] anotasyonu, Spring'in JUnit ile entegrasyonunu sağlar: sınıf, bir JUnit testi olarak çalıştırılabilecektir. [@RunWith], bir JUnit anotasyonudur (9. satır), oysa [SpringJUnit4ClassRunner] sınıfı bir Spring sınıfıdır (12. satır);
  • 26-27. satırlar: [métier] katmanına ilişkin bir referansın test sınıfına enjeksiyonu;
  • birçok test sadece basit görsel testlerdir:
    • satır 32-33: müşteri listesi;
    • satır 35-36: doktor listesi;
    • 39-40. satırlar: bir doktorun randevu aralıkları listesi;
    • satır 43: bir doktorun randevu listesi;
  • satır 50: yeni bir randevunun eklenmesi. [ajouterRv] yöntemi, randevuyu ek bir bilgi, yani birincil anahtarı id ile döndürür;
  • satır 53: bu birincil anahtar kullanılarak veritabanında randevu aranır;
  • satır 54: aranan randevu ile bulunan randevunun aynı olup olmadığı kontrol edilir. [Rv] varlığının [equals] yönteminin yeniden tanımlandığını hatırlatırız: iki randevu, aynı id'ye sahipse eşittir. Burada bu durum, eklenen randevunun veritabanına başarıyla kaydedildiğini göstermektedir;
  • 61-73. satırlar: Aynı randevuyu ikinci kez eklemeye çalışıyoruz. Benzersizlik kısıtlaması olduğu için bu işlem SGBD tarafından reddedilmelidir:

CREATE TABLE IF NOT EXISTS `rv` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `JOUR` date NOT NULL,
  `ID_CLIENT` bigint(20) NOT NULL,
  `ID_CRENEAU` bigint(20) NOT NULL,
  `VERSION` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
  KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
  KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;

Yukarıdaki 8. satır, [JOUR, ID_CRENEAU] kombinasyonunun benzersiz olması gerektiğini belirtir; bu da aynı gün içinde aynı zaman aralığına iki randevu eklenmesini engeller.

    1. satır: Bir istisna oluştuğu doğrulanır;
    1. satır: az önce randevu eklenen doktorun takvimi istenir;
    1. satır: Eklenen randevunun doktorun takviminde yer aldığını kontrol edilir;
  • satır 82: eklenen randevu silinir;
  • satır 84: veritabanından silinen randevuyu aranır;
  • satır 85: null işaretçisinin alındığını kontrol ederiz; bu, aranan randevunun mevcut olmadığını gösterir;

Testin yürütülmesi başarılı:

 

2.10. Konsol programı

  

Konsol programı basittir. Yabancı bir anahtarı nasıl alabileceğinizi gösterir:


package rdvmedecins.boot;

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

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

public class Boot {
    // başlatma
    public static void main(String[] args) {
        // yapılandırma hazırlığı
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // çalıştırma
        ConfigurableApplicationContext context = app.run(args);
        // iş
        IMetier métier = context.getBean(IMetier.class);
        try {
            // bir RV ekleniyor
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // doğrulama
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // Spring bağlamını kapatma
        context.close();
    }

    // yardımcı yöntem - bir koleksiyondaki öğeleri görüntüler
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

Program bir randevu ekler ve ardından randevunun eklendiğini doğrular.

    1. satır: [SpringApplication] sınıfı, [DomainAndPersistenceConfig] yapılandırma sınıfını kullanacaktır;
    1. satır: Uygulamanın başlangıç günlükleri silinir;
    1. satır: [SpringApplication] sınıfı çalıştırılır. Bu sınıf, bir Spring bağlamı, yani kayıtlı bean'lerin listesini döndürür;
    1. satır: [IMetier] arayüzünü uygulayan bean'e bir referans alınır. Dolayısıyla bu, [métier] katmanına bir referanstır;
  • 27-31. satırlar: Bugün için, 1 numaralı müşteri adına 1 numaralı zaman diliminde yeni bir randevu eklenir. Müşteri ve zaman dilimi, yalnızca tanımlayıcıların kullanıldığını göstermek amacıyla tamamen uydurulmuştur. Burada sürüm değeri başlatılmıştır, ancak herhangi bir değer girebilirdik. Bu değer burada kullanılmamaktadır;
  • satır 34: 1 numaralı randevu aralığına sahip doktoru öğrenmek istiyoruz. Bunun için veritabanına gidip 1 numaralı randevu aralığını aramamız gerekiyor. [FetchType.LAZY] modunda olduğumuz için, doktor randevu aralığıyla birlikte getirilmiyor. Ancak, doktorun birincil anahtarını almak için [Creneau] varlığında bir [idMedecin] alanı öngörülmüştür;
    1. satır: doktorun birincil bilgisi alınır;
    1. satır: doktorun randevu listesi görüntülenir;

Konsol çıktısı şu şekildedir:

1
2
3
4
Ajout d'un Rv le [10/06/2014] dans le créneau 1 pour le client 1
Rv ajouté = Rv[113, Tue Jun 10 16:51:01 CEST 2014, 1, 1]
Liste des rendez-vous
Rv[113, 2014-06-10, 1, 1]

2.11. Spring'e Giriş MVC

Şimdi web katmanının oluşturulmasına geçiyoruz. Bu katman, esas olarak belirli URL'leri işleyen ve JSON (Javascript Object Notation) formatında bir metin satırı ile yanıt veren yöntemlerden oluşur. Bu web katmanı, bazen API web olarak da adlandırılan bir web arayüzüdür. Bu arayüzü, Spring ekosisteminin bir başka dalı olan Spring MVC ile uygulayacağız. Öncelikle, [http://spring.io]'te bulunan kılavuzlardan birini inceleyeceğiz.

2.11.1. Demo projesi

  • [1]'te, Spring kılavuzlarından birini içe aktarıyoruz;
  • dosyasını [2] dosyasına aktarıyoruz;
  • [3]'te, Maven projesini seçiyoruz;
  • [4]'te kılavuzun son sürümünü alıyoruz;
  • [5]'te onaylıyoruz;
  • [6]'te, içe aktarılan proje;

Standart URL aracılığıyla erişilebilen ve JSON metni sağlayan web hizmetleri genellikle REST (REpresentational Durum Aktarımı) hizmetleri olarak adlandırılır. Bu belgede, oluşturacağımız hizmeti kısaca bir web hizmeti / JSON olarak adlandıracağım. Bir hizmet, belirli kurallara uyuyorsa Restful olarak adlandırılır. Ben bu kurallara uymaya çalışmadım.

Şimdi içe aktarılan projeyi, öncelikle Maven yapılandırmasını inceleyelim.

2.11.2. Maven Yapılandırması

[pom.xml] dosyası şu şekildedir:


<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • 10-14. satırlar: [Spring Data] projesinde olduğu gibi, burada da [Spring Boot] ana projesi bulunur;
  • 17-20. satırlar: [spring-boot-starter-web] artefaktı, bir Spring projesi olan MVC için gerekli kütüphaneleri beraberinde getirir. Özellikle yerleşik bir Tomcat sunucusu içerir. Uygulama bu sunucu üzerinde çalıştırılacaktır;
  • satır 21-24: Jackson kütüphanesi, bir Java nesnesini JSON dizesine dönüştürmeyi ve bunun tersini gerçekleştirir;

Bu yapılandırma ile birlikte gelen kütüphanelerin sayısı oldukça fazladır:

Yukarıda Tomcat sunucusundaki üç arşiv görülmektedir.

2.11.3. Bir Spring hizmetinin mimarisi REST

Spring MVC, MVC olarak bilinen mimari modelini (Model – Görünüm – Denetleyici) şu şekilde uygular:

Bir istemciden gelen isteğin işlenmesi şu şekilde gerçekleşir:

  1. istek - istenen URL'ler http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... biçimindedir. [Dispatcher Servlet], gelen URL'leri işleyen Spring sınıfıdır. Bu sınıf, URL'i onu işlemesi gereken eyleme "yönlendirir". Bu eylemler, [Contrôleurs] olarak adlandırılan belirli sınıfların yöntemleridir. Burada MVC'in C kısmı, [Dispatcher Servlet, Contrôleur, Action] dizesidir. Gelen URL'i işlemek için herhangi bir eylem yapılandırılmamışsa, [Dispatcher Servlet] servleti, istenen URL'in bulunamadığını bildirir (404 hatası NOT FOUND);
  1. işleme
  • seçilen eylem, [Dispatcher Servlet] servletinin kendisine ilettiği parami parametrelerini kullanabilir. Bu parametreler çeşitli kaynaklardan gelebilir:
    • URL'in [/param1/param2/...] yolundan,
    • URL,'in [p1=v1&p2=v2] parametrelerinden
    • tarayıcı tarafından istekle birlikte gönderilen parametrelerden;
  • kullanıcının isteğinin işlenmesi sırasında, eylem [metier] ve [2b] katmanlarına ihtiyaç duyabilir. Müşterinin isteği işlendikten sonra, bu işlem çeşitli yanıtları tetikleyebilir. Klasik bir örnek şudur:
    • istek düzgün bir şekilde işlenemediğinde bir hata sayfası
    • aksi takdirde bir onay sayfası
  • eylem, belirli bir görünümün görüntülenmesini ister: [3]. Bu görünüm, görünüm modeli olarak adlandırılan verileri gösterecektir. Bu, MVC'teki M'dir. Eylem, bu M şablonunu [2c] oluşturacak ve bir V görünümünün görüntülenmesini isteyecektir [3];
  1. yanıt - seçilen V görünümü, eylemin oluşturduğu M şablonunu kullanarak, müşteriye göndermesi gereken HTML yanıtının dinamik kısımlarını başlatır ve ardından bu yanıtı gönderir.

Bir web hizmeti / JSON için, önceki mimari biraz değiştirilmiştir:

  • [4a]'te, bir Java sınıfı olan model, bir kütüphane tarafından JSON dizesine dönüştürülür;
  • [4b]'te, bu JSON dizesi tarayıcıya gönderilir;

2.11.4. C denetleyicisi

  

İçe aktarılan uygulama aşağıdaki denetleyiciye sahiptir:


package hello;

import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public @ResponseBody
    Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • 9. satır: [@Controller] adnotasyonu, [GreetingController] sınıfını bir Spring denetleyicisi haline getirir; yani, bu sınıfın yöntemleri URL'leri işlemek üzere kaydedilir;
  • 15. satır: [@RequestMapping] anotasyonu, yöntemin işlediği URL'i belirtir; burada bu, URL ve [/greeting]'tir. Daha sonra göreceğiz ki bu URL parametrelerle ayarlanabilir ve bu parametreleri geri almak mümkündür;
  • 16. satır: [@ResponseBody] açıklaması, yöntemin bir görünüm için şablon (JSP, JSF, Thymeleaf, ...) için bir şablon oluşturup bunu daha sonra istemci tarayıcısına göndermez; bunun yerine tarayıcıya gönderilecek yanıtı kendisi üretir. Burada, [Greeting] türünde bir nesne üretir (satır 18). Burada açıkça görülmese de, bu nesne tarayıcıya gönderilmeden önce önce JSON'e dönüştürülecektir. Projenin bağımlılıkları arasında JSON kütüphanesinin bulunması, Spring Boot'un otomatik yapılandırma yoluyla projeyi bu şekilde yapılandırmasına neden olur;
  • 17. satır: [greeting] yönteminin [String name] adlı bir parametresi vardır. [@RequestParam(value = "name", required = false, defaultValue = "World"] anotasyonu, bu parametrenin [name](@RequestParam(value = "name")) adlı bir parametre ile başlatılması gerektiğini belirtir. Bu parametre, bir GET veya bir POST'in parametresi olabilir. Bu parametre zorunlu değildir (required = false). Bu durumda, yöntemin [name] parametresi [World] (defaultValue = "World") değeriyle başlatılacaktır.

2.11.5. M şablonu

Önceki yöntemle oluşturulan M şablonu, aşağıdaki [Greeting] nesnesidir:

  

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

Bu nesnenin JSON dönüşümü, {"id":n,"content":"metin"} karakter dizisini oluşturacaktır. Sonuç olarak, denetleyici yöntemi ile üretilen JSON dizisi şu şekilde olacaktır:

{"id":2,"content":"Hello, World!"}

veya

{"id":2,"content":"Hello, John!"}

2.11.6. Proje yapılandırması

  

Proje, aşağıdaki [Application] sınıfı ile yapılandırılır:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 11. satır: İlginç bir şekilde, bu sınıf konsol uygulamalarına özgü bir [main] yöntemi ile çalıştırılabilir. Durum gerçekten de böyledir. 12. satırdaki [SpringApplication] sınıfı, bağımlılıklar arasında bulunan Tomcat sunucusunu başlatacak ve REST hizmetini bu sunucuya dağıtacaktır;
  • 4. satır: [SpringApplication] sınıfının [Spring Boot] projesine ait olduğu görülüyor;
  • 12. satır: ilk parametre projeyi yapılandıran sınıftır, ikincisi ise olası parametrelerdir;
  • 8. satır: [@EnableAutoConfiguration] anotasyonu, Spring Boot'tan projenin yapılandırılmasını ister;
  • 7. satır: [@ComponentScan] anotasyonu, [Application] sınıfını içeren klasörün Spring bileşenlerini aramak üzere taranmasını sağlar. Bir bileşen bulunacaktır: [GreetingController] sınıfı, [@Controller] anotasyonuna sahiptir ve bu anotasyon onu bir Spring bileşeni haline getirir;

2.11.7. Projenin çalıştırılması

Projeyi çalıştıralım:

 

Aşağıdaki konsol günlükleri elde edilir:

____ _ __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 14:31:36.435  INFO 11744 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 11744 (D:\Temp\wksSTS\gs-rest-service-complete\target\classes started by ST in D:\Temp\wksSTS\gs-rest-service-complete)
2014-06-11 14:31:36.473  INFO 11744 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7684af0b: startup date [Wed Jun 11 14:31:36 CEST 2014]; root of context hierarchy
2014-06-11 14:31:36.966  INFO 11744 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-06-11 14:31:37.760  INFO 11744 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-11 14:31:37.955  INFO 11744 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-11 14:31:37.956  INFO 11744 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-06-11 14:31:38.053  INFO 11744 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-11 14:31:38.054  INFO 11744 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1584 ms
2014-06-11 14:31:38.596  INFO 11744 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-11 14:31:38.598  INFO 11744 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-11 14:31:38.919  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.125  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-06-11 14:31:39.129  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-06-11 14:31:39.130  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] türündeki işleyiciye
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] QZXW2HTML türündeki işleyici BW2NsYXNzIG9yZy5zcHJpbmdmcmFtZXdvcmsud2ViLnNlcnZsZXQucmVzb3VyY2UuUmVzb3VyY2VIdHRwUmVxdWVzdEhhbmRsZXJdZQX
2014-06-11 14:31:39.448  INFO 11744 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-11 14:31:39.490  INFO 11744 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-11 14:31:39.492  INFO 11744 --- [           main] hello.Application                        : Started Application in 3.45 seconds (JVM running for 3.93)
  • 12. satır: Tomcat sunucusu 8080 numaralı bağlantı noktasında başlatılır (11. satır);
  • satır 16: [DispatcherServlet] servleti mevcuttur;
  • 19. satır: [GreetingController.greeting] yöntemi tespit edildi;

Web uygulamasını test etmek için URL ve [http://localhost:8080/greeting]'e erişilir:

 

Beklenen JSON dizesini başarıyla alıyoruz. Sunucu tarafından gönderilen HTTP başlıklarını incelemek ilginç olabilir. Bunun için, [Advanced Rest Client] adlı Chrome eklentisini kullanacağız (bkz. Ekler):

  • [1]'te, istenen URL;
  • [2]'te, GET yöntemi kullanılır;
  • [3]'te, yanıt JSON;
  • [4]'te, sunucu JSON biçiminde bir yanıt gönderdiğini belirtmiştir;
  • [5]'te, aynı URL isteniyor, ancak bu sefer POST ile;
  • [7]'te, bilgiler sunucuya [urlencoded] biçiminde gönderilir;
  • [6]'te, name parametresi ve değeri;
  • [8]'te, tarayıcı sunucuya [urlencoded] bilgilerini gönderdiğini bildirir;
  • [9]'te, sunucunun JSON yanıtı;

2.11.8. Çalıştırılabilir arşiv oluşturma

Eclipse dışında çalıştırılabilir bir arşiv oluşturmak mümkündür. Gerekli yapılandırma [pom.xml] dosyasında bulunur:


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>
  • 9-12. satırlar, çalıştırılabilir arşivi oluşturacak eklentiyi tanımlar;
  • 3. satır, projenin çalıştırılabilir sınıfını tanımlar;

İşlem şu şekilde gerçekleştirilir:

  • [1]'te: bir Maven hedefi çalıştırılır;
  • [2]'e: iki hedef (goal) vardır: [clean], Maven projesinden [target] klasörünü silmek için; [package] ise bu klasörü yeniden oluşturmak içindir;
  • [3]'te: Oluşturulan [target] klasörü bu klasörün içine yerleştirilecektir;
  • [4]'te: hedef oluşturulur;

Konsolda görüntülenen günlüklerde, [spring-boot-maven-plugin] eklentisinin göründüğünden emin olun. Yürütülebilir arşivi oluşturan bu eklentidir.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

Bir konsol kullanarak oluşturulan klasöre giriyoruz:

1
2
3
4
5
6
7
8
9
D:\Temp\wksSTS\gs-rest-service-complete\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • 5. satır: oluşturulan arşiv;

Bu arşiv şu şekilde çalıştırılır:

D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

Artık web uygulaması başlatıldığına göre, bir tarayıcıyla erişilebilir:

 

2.11.9. Uygulamayı bir Tomcat sunucusuna dağıtma

Spring Boot, geliştirme modunda çok kullanışlı olsa da, üretim ortamındaki bir uygulamanın gerçek bir Tomcat sunucusuna dağıtılması muhtemeldir. Bunun için şu adımları izleyin:

[pom.xml] dosyasını aşağıdaki şekilde değiştirin:


<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>
....
</project>

Değişiklikler iki yerde yapılmalıdır:

  • 9. satır: Bir war arşivi (Web ARchive) oluşturulacağını belirtmek gerekir;
  • 26-30. satırlar: [spring-boot-starter-tomcat] artefaktına bir bağımlılık eklenmelidir. Bu artefakt, Tomcat'in tüm sınıflarını projenin bağımlılıkları arasına ekler;
  • 29. satır: Bu artefakt [provided]'tir; yani ilgili arşivler oluşturulan WAR dosyasına yerleştirilmeyecektir. Aslında bu arşivler, uygulamanın çalışacağı Tomcat sunucusunda bulunacaktır;

Ayrıca web uygulamasını yapılandırmak gerekir. [web.xml] dosyası yoksa, bu işlem [SpringBootServletInitializer] sınıfından miras alan bir sınıfla yapılır:

  

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


package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ApplicationInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

}
  • 6. satır: [ApplicationInitializer] sınıfı, [SpringBootServletInitializer] sınıfını genişletir;
  • 9. satır: [configure] yöntemi yeniden tanımlanmıştır (8. satır);
  • 10. satır: Projeyi yapılandıran sınıf belirtilir;

Projeyi çalıştırmak için şu adımları uygulayabilirsiniz:

  • [1]'te, proje Eclipse'te kayıtlı sunuculardan birinde çalıştırılır;
  • [2]'te, varsayılan olarak mevcut olan [tc Server Developer] seçilir. Bu, Tomcat'in bir varyantıdır;

Bu işlem tamamlandıktan sonra, bir tarayıcıda URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] adresini girebiliriz:

 

Artık bir WAR arşivi oluşturmayı biliyoruz. Bundan sonra, Spring Boot ve çalıştırılabilir JAR arşivi ile çalışmaya devam edeceğiz.

2.11.10. Yeni bir web projesi oluşturma

Yeni bir web projesi oluşturmak için şu adımları izleyebiliriz:

  • [1]'te: Dosya / Yeni / Spring Starter Projesi
  • [2]'te: [Web]'i seçin. Bir web hizmetinde / JSON, görünümler bulunmadığından görünüm kütüphaneleri seçilmez;
  • oluşturulan proje bir Maven projesi olacaktır. [3]'te, oluşturulacak Maven artefaktının grubunu, [4]'te ise artefaktın adını girin;
  • [5]'te, Spring'in projenin yapılandırma sınıfını yerleştireceği paketin adı girilir;
  • [6]'e Eclipse projesine bir ad verilir – bu, [4]'ten farklı olabilir;
 

2.12. [web] katmanı

  

Web katmanını birkaç adımda oluşturacağız:

  • 1. adım: kimlik doğrulaması olmayan, çalışır durumda bir web katmanı;
  • 2. adım: Spring Security ile kimlik doğrulamanın kurulması;
  • 3. adım: CORS ve [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]'in kurulması. Web hizmetimizin istemcisi, web hizmetimizle aynı etki alanına ait olması gerekmeyen bir Angular web istemcisi olacaktır. Varsayılan olarak, web hizmeti izin vermedikçe bu istemci hizmetimize erişemez. Bunun nasıl yapılacağını göreceğiz;

2.12.1. Maven Yapılandırması

Projenin [pom.xml] dosyası şu şekildedir:


<modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.mvc</groupId>
    <artifactId>rdvmedecins-webapi-v1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-webapi-v1</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.spring4.rdvmedecins</groupId>
            <artifactId>rdvmedecins-metier-dao</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
  • 7-11. satırlar: üst Maven projesi;
  • 13-16. satırlar: MVC adlı Spring projesi için bağımlılıklar;
  • 17-21. satırlar: [métier, DAO, JPA] katman projesinin bağımlılıkları;

2.12.2. Web hizmeti arayüzü

  • [1]'te, yukarıda belirtildiği gibi, tarayıcı yalnızca belirli bir sözdizimine sahip sınırlı sayıda URL isteğinde bulunabilir;
  • [4]'te, bir JSON yanıtı alır;

Web hizmetimizin yanıtlarının tümü, aşağıdaki [Reponse] türündeki bir nesnenin JSON dönüşümüne karşılık gelen aynı biçime sahip olacaktır:


package rdvmedecins.web.models;

public class Reponse {

    // ----------------- özellikler
    // işlem durumu
    private int status;
    // yanıt JSON
    private Object data;

    // ---------------üreticiler
    public Reponse() {
    }

    public Reponse(int status, Object data) {
        this.status = status;
        this.data = data;
    }

    // yöntemler
    public void incrStatusBy(int increment) {
        status += increment;
    }

    // ----------------------alıcı ve ayarlayıcılar
...
}
  • 7. satır: yanıtın hata kodu 0: OK, diğer durumlar: KO;
  • 9. satır: yanıtın gövdesi;

Şimdi, web hizmetinin arayüzünü gösteren ekran görüntülerini sunuyoruz / JSON:

[/getAllClients] muayenehanesindeki tüm hastaların listesi

[/getAllMedecins] muayenehanesindeki tüm doktorların listesi

Bir doktorun randevu saatleri listesi [/getAllCreneaux/{idMedecin}]

Bir doktorun randevu listesi [/getRvMedecinJour/{idMedecin}/{yyyy-aa-gg}

Bir doktorun ajandası [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]

Randevu eklemek / silmek için Chrome eklentisini kullanıyoruz [Advanced Rest Client] çünkü bu işlemler POST ile yapılır.

Randevu ekleme [/ajouterRv]

  • [0]'te, web hizmetinin URL'i;
  • [1]'e, POST yöntemi kullanılır;
  • [2]'te, web hizmetine {gün, idClient, idCreneau} biçiminde iletilen bilgilerin metni;
  • [3]'te, istemci web hizmetine bilgileri JSON biçiminde gönderdiğini belirtir;

Buna karşılık gelen yanıt şöyledir:

  • [4]'te: istemci, gönderdiği verilerin JSON biçiminde olduğunu belirten başlığı gönderir;
  • [5]'e: web hizmeti, kendisinin de JSON formatında veri gönderdiğini bildirir;
  • [6]'te: web hizmetinin JSON yanıtı. [data] alanı, eklenen randevunun JSON biçimini içerir;

Yeni randevunun varlığı şu şekilde doğrulanabilir:

Bir randevuyu silme [/supprimerRv]

  • [1]'te, web hizmetinin URL'i;
  • [2]'te, POST yöntemi kullanılır;
  • [3]'te, {idRv} biçiminde web hizmetine iletilen bilgilerin metni JSON;
  • [4]'te, istemci web hizmetine JSON bilgilerini gönderdiğini belirtir;

Buna karşılık gelen yanıt şöyledir:

  • [5]'te: [status] alanı 0 değerindedir, bu da işlemin başarılı olduğunu gösterir;

Randevunun silindiği doğrulanabilir:

Yukarıda, [Mme GERMAN] numaralı hastanın randevusu artık mevcut değildir.

Web hizmeti ayrıca kimlik numaraları aracılığıyla kayıtları geri getirme imkanı da sunar:

Tüm bu URL öğeleri, şimdi tanıtacağımız [RdvMedecinsController] denetleyicisi tarafından işlenir.

2.12.3. [RdvMedecinsController] denetleyicisinin iskeleti

  

[RdvMedecinsController] denetleyicisi şu şekildedir:


package rdvmedecins.web.controllers;

import java.text.ParseException;
...

@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // uygulama hata mesajları
        messages = application.getMessages();
    }

    // doktor listesi
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
...
    }

    // müşteri listesi
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients() {
...
    }

    // bir doktorun randevu aralıkları listesi
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }

    // bir doktorun randevu listesi
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }

    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
    }

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
    }

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(
            @PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }
}
  • 6. satır: [@RestController] açıklaması, [RdvMedecinsController] sınıfını bir Spring denetleyicisi haline getirir. Ayrıca, bu not sayesinde URL'i işleyen yöntemler, otomatik olarak JSON'e dönüştürülecek bir yanıt üretecektir;
  • 9-10. satırlar: Buraya Spring tarafından [ApplicationModel] türünde bir nesne enjekte edilecektir;
  • 13. satır: [@PostConstruct] anotasyonu, sınıfın örneklendirilmesinden hemen sonra çalıştırılacak bir yöntemi işaretler. Bu yöntem çalıştırıldığında, Spring tarafından enjekte edilen nesneler kullanılabilir hale gelir;
  • tüm yöntemler aşağıdaki [Reponse] türünde bir nesne döndürür:

package rdvmedecins.web.models;

public class Reponse {

    // ----------------- özellikler
    // işlem durumu
    private int status;
    // yanıt
    private Object data;
...
}

Bu nesne, istemci tarayıcısına gönderilmeden önce JSON olarak serileştirilir;

  • 20. satır: [@RequestMapping] anotasyonu, yöntemin çağrılma koşullarını belirler. Burada yöntem, URL'ten gelen GET isteğini işler. Eğer bu URL, bir POST tarafından talep edilseydi, reddedilirdi ve Spring MVC, web istemcisine bir HTTP hata kodu gönderirdi;
  • satır 32: URL, {idMedecin} ile ayarlanır. Bu parametre, satır 33'teki [@PathVariable] açıklamasıyla alınır;
  • satır 33: Tek parametre olan [long idMedecin], değerini URL'teki {idMedecin} parametresinden alır. URL içindeki parametre ile yöntemin parametresi farklı isimlere sahip olabilir. Burada dikkat edilmesi gereken nokta, [@PathVariable("idMedecin")]'in String türünde olması (URL'in tamamı bir String'dir), oysa [long idMedecin] parametresinin [long] türünde olmasıdır. Tür dönüşümü otomatik olarak gerçekleştirilir. Bu tür dönüşümü başarısız olursa HTTP hata kodu döndürülür;
  • 65. satır: [@RequestBody] etiketinin anlamı, isteğin gövdesidir. Bir GET isteğinde neredeyse hiçbir zaman gövde bulunmaz (ancak eklenmesi mümkündür). Bir POST isteğinde ise çoğu zaman bulunur (ancak eklenmemesi de mümkündür). URL ve [ajouterRv] için, web istemcisi POST'inde aşağıdaki JSON dizesini gönderir:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

[@RequestBody PostAjouterRv post] sözdizimi (65. satır) ile birlikte, yöntemin 64. satırda JSON [consumes = "application/json; charset=UTF-8"] değerini beklemesi, web istemcisi tarafından gönderilen JSON dizesinin [PostAjouter] türünde bir nesneye deserialize edilmesine neden olacaktır. Bu nesne şöyledir:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // gönderi verileri
    private String jour;
    private long idClient;
    private long idCreneau;

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

Burada da gerekli tür değişiklikleri otomatik olarak gerçekleşecektir;

  • 69-70. satırlarda, URL ve [/supprimerRv] için benzer bir mekanizma bulunmaktadır. Gönderilen JSON dizesi şöyledir:
{"idRv":116}

ve [PostSupprimerRv] türü ise şöyledir:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // gönderim verileri
    private long idRv;

    // getter ve setter'lar
    ...
}

2.12.4. Web hizmetinin şablonları

  

[Reponse, PostAjouterRv, PostSupprimerRv] modellerini daha önce tanıtmıştık. [ApplicationModel] modeli ise şöyledir:


package rdvmedecins.web.models;

import java.util.Date;
...

@Component
public class ApplicationModel implements IMetier {

    // [métier] katmanı
    @Autowired
    private IMetier métier;

    // [métier] katmanından gelen veriler
    private List<Medecin> médecins;
    private List<Client> clients;
    // hata mesajları
   private List<String> messages;

    @PostConstruct
    public void init() {
        // doktorlar ve müşteriler alınır
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

    // getter
    public List<String> getMessages() {
        return messages;
    }

    // ------------------------- [métier] katmanı arayüzü
    @Override
    public List<Client> getAllClients() {
        return clients;
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }

    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }

    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }

    @Override
    public void supprimerRv(Rv rv) {
        métier.supprimerRv(rv);
    }

    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }

}
  • 6. satır: [@Component] anotasyonu, [ApplicationModel] sınıfını bir Spring bileşeni haline getirir. Şimdiye kadar gördüğümüz tüm Spring bileşenleri gibi (@Controller hariç), bu türden yalnızca tek bir nesne örneklenecektir (singleton);
  • 7. satır: [ApplicationModel] sınıfı, [IMetier] arayüzünü uygular;
  • 10-11. satırlar: [métier] katmanına ait bir referans Spring tarafından enjekte edilir;
  • 19. satır: [@PostConstruct] anotasyonu, [init] yönteminin [ApplicationModel] sınıfının örneklenmesinden hemen sonra çalıştırılmasını sağlar;
  • 23-24. satırlar: doktor ve müşteri listeleri [métier] katmanından alınır;
  • 26. satır: Bir istisna meydana gelirse, istisna yığınındaki mesajlar 17. satırdaki alana kaydedilir;

[ApplicationModel] sınıfı bize iki amaçla hizmet edecektir:

  • doktor ve hasta (müşteri) listelerini depolamak için önbellek olarak;
  • denetleyiciler için tek bir arayüz olarak;

Web katmanının mimarisi şu şekilde gelişmektedir:

  • [2b]'te, denetleyici(ler)in yöntemleri [ApplicationModel] tekil nesnesiyle iletişim kurar;

Bu strateji, önbellek yönetimi konusunda esneklik sağlar. Şu anda doktorların randevu saatleri önbelleğe alınmamaktadır. Bunları önbelleğe almak için [ApplicationModel] sınıfını değiştirmek yeterlidir. Bu, denetleyici üzerinde hiçbir etki yaratmaz; denetleyici, daha önce olduğu gibi [List<Creneau> getAllCreneaux(long idMedecin)] yöntemini kullanmaya devam edecektir. Değiştirilecek olan, [ApplicationModel] sınıfındaki bu yöntemin uygulamasıdır.

2.12.5. Statik Sınıf

[Static] sınıfı, "iş" veya "web" ile ilgili olmayan bir dizi statik yardımcı yöntemi içerir:

  

Kodu şu şekildedir:


package rdvmedecins.web.helpers;

import java.text.SimpleDateFormat;
...

public class Static {

    public Static() {
    }

    // bir istisnaya ait hata mesajları listesi
    public static List<String> getErreursForException(Exception exception) {
        // istisnanın hata mesajları listesi alınır
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }

    // Object --> Map eşleştiricileri
    // --------------------------------------------------------
....
}
  • 12. satır: [ApplicationModel] sınıfının [init] yönteminde (aşağıda 8. satırda) kullanılan [Static.getErreursForException] yöntemi:

    @PostConstruct
    public void init() {
        // doktorlar ve müşteriler alınır
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

Bu yöntem, bir [exception] istisnasının [exception.getMessage()] hata mesajlarını ve bunun içerdiği [exception.getCause()] hata mesajlarını kullanarak bir [List<String>] nesnesi oluşturur.

[Static] sınıfı, daha sonra karşılaştığımızda üzerinde duracağımız başka yardımcı yöntemler de içerir.

Şimdi web hizmetindeki URL işlemlerini ayrıntılı olarak inceleyeceğiz. Bu işlemde üç ana sınıf rol oynamaktadır:

  • [RdvMedecinsController] denetleyicisi;
  • [Static] yardımcı yöntem sınıfı;
  • [ApplicationModel] önbellek sınıfı;
  

2.12.6. Denetleyicinin [init] yöntemi

[RdvMedecinsController] denetleyicisi (bkz. 2.12.3. paragraf), örneklendirilmesinden hemen sonra çalıştırılan [init] yöntemine sahiptir:


    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // uygulamanın hata mesajları
        messages = application.getMessages();
}
  • 8. satır: [ApplicationModel] uygulama önbelleğinde depolanan hata mesajları, 3. satırdaki alanda yerel olarak saklanır. Bu, yöntemlerin uygulamanın doğru şekilde başlatılıp başlatılmadığını bilmesini sağlar.

2.12.7. URL [/getAllMedecins]

URL [/getAllMedecins], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi tarafından işlenir:


    // doktor listesi
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // doktor listesi
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • 5. satır: Uygulamanın doğru şekilde başlatılıp başlatılmadığına bakılır (messages==null). Aksi takdirde, status=-1 ve data=messages değerlerini içeren bir yanıt gönderilir;
  • 10. satır: Aksi takdirde, status değeri 0 olacak şekilde doktor listesi döndürülür. [application.getAllMedecins()] yöntemi, önbellekte bulunan bir listeyi döndürmekle yetindiği için istisna oluşturmaz. Bununla birlikte, doktorların artık önbellekte tutulmaması ihtimaline karşı bu istisna yönetimi korunacaktır;

Uygulamanın hatalı bir şekilde başlatılması durumunu henüz ele almadık. SGBD ve MySQL5'i durduralım, web hizmetini başlatıp ardından URL ve [/getAllMedecins]'i isteyelim:

Image

Gerçekten de bir hata alıyoruz. Normal bir durumda, şu görünümle karşılaşırız:

2.12.8. URL [/getAllClients]

URL [/getAllClients], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi tarafından işlenir:


    // hasta listesi
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // uygulama durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // müşteri listesi
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

Bu yöntem, daha önce incelenen [getAllMedecins] yöntemine benzerdir. Elde edilen sonuçlar şunlardır:

2.12.9. URL [/getAllCreneaux/{idMedecin}]

URL ve [/getAllCreneaux/{idMedecin}], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi ile işlenir:


// bir doktorun randevu aralıkları listesi
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // uygulama durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // doktor bilgisi alınır
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // doktorun randevu saatleri
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // yanıtın gönderilmesi
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • 9. satır: [id] parametresiyle tanımlanan doktor, yerel bir yönteme çağrılır:

    private Reponse getMedecin(long id) {
        // doktorun bilgilerinin alınması
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Bu doktor mevcut mu?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // tamam
        return new Reponse(0, médecin);
}

Bu yöntemden, [0,1,2] içinde bir status ile geri dönülür. [getAllCreneaux] yönteminin koduna geri dönelim:

  • 10-12. satırlar: status!=0 ise, yanıt hemen döndürülür;
  • 13. satır: doktorun bilgilerini alıyoruz;
  • 17. satır: bu doktorun randevu saatlerini alıyoruz;
  • 22. satır: Yanıt olarak [Static.getListMapForCreneaux(créneaux)] nesnesini göndeririz;

[Creneau] sınıfının tanımını hatırlayalım:


@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // RV randevu aralığının özellikleri
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // bir randevu bir doktorla ilişkilidir
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // yabancı anahtar
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • 13. satır: Doktor, [FetchType.LAZY] modunda aranır;

[DAO] katmanında [getAllCreneaux] yöntemini uygulayan JPQL isteğini hatırlayalım:


@Query("select c from Creneau c where c.medecin.id=?1")

[c.medecin.id] notasyonu, [CRENEAUX] ve [MEDECINS] tabloları arasında birleştirme yapılmasını zorunlu kılar. Bu nedenle sorgu, her birinde ilgili doktorun da bulunduğu tüm randevu aralıklarını döndürür. Bu randevu aralıklarını JSON'e serileştirdiğimizde, her birinde doktorun JSON dizesi görünür. Bu gereksizdir. Bu nedenle, bir [Creneau] nesnesini serileştirmek yerine, sadece istenen alanları içerecek bir [Map] nesnesini serileştireceğiz.

Başlangıçta incelediğimiz koda geri dönelim:


// yanıt verilir
return new Reponse(0, Static.getListMapForCreneaux(créneaux));

[Static.getListMapForCreneaux] yöntemi şu şekildedir:


    // List<Creneau> --> List<Map>
    public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
        // <String, Object> türündeki sözlükler listesi
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // listeyi döndürüyoruz
        return liste;
}

ve [Static.getMapForCreneau] yöntemi şöyledir:


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // Yapılması gereken bir şey var mı?
        if (créneau == null) {
            return null;
        }
        // sözlük <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", créneau.getId());
        hash.put("hDebut", créneau.getHdebut());
        hash.put("mDebut", créneau.getMdebut());
        hash.put("hFin", créneau.getHfin());
        hash.put("mFin", créneau.getMfin());
        // sözlüğü oluşturuyoruz
        return hash;
}
  • 8. satır: bir sözlük oluşturulur;
  • 9-13. satırlar: JSON dizesinde saklamak istediğimiz alanları buraya ekliyoruz. [medecin] alanı burada yer almıyor;
  • 15. satır: bu sözlüğü döndürüyoruz;

Elde edilen sonuçlar şunlardır:

ya da zaman aralığı mevcut değilse şunlar:

ya da veritabanına erişimde hata olması durumunda şunlar:

2.12.10. URL [/getRvMedecinJour/{idMedecin}/{jour}]

URL ve [/getRvMedecinJour/{idMedecin}/{jour}], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi ile işlenir:


// bir doktorun randevu listesi
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // tarihi kontrol ediyoruz
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, null);
        }
        // doktoru alıyoruz
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // randevularının listesi
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // yanıt veriliyor
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • 31. satır: List<Rv> nesnesi yerine List<Map<String,Object>> nesnesi döndürülür. [Rv] sınıfının tanımını hatırlayalım:

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // Randevunun özellikleri
    @Temporal(TemporalType.DATE)
    private Date jour;

    // bir randevu bir müşteriye bağlıdır
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // Bir randevu bir zaman aralığına bağlıdır
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // dış anahtarlar
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

...

}
  • 11. satır: müşteri, [FetchType.LAZY] modu ile aranıyor;
  • 18. satır: [FetchType.LAZY] modu ile randevu aralığı aranır;

Randevuları arayan JPQL sorgusunu hatırlayalım:


@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")

[client] ve [creneau] alanlarını geri getirmek için açıkça birleştirme işlemleri yapılır. Ayrıca, [cr.medecin.id=?1] birleştirme işlemi nedeniyle doktor bilgisi de elde edilecektir. Dolayısıyla doktor, her randevunun JSON dizesinde görünecektir. Oysa bu yinelenen bilgi gereksizdir. Yöntemin koduna geri dönelim:

  • 31. satır: JSON olarak serileştirilecek sözlüğü kendimiz oluşturuyoruz;

Bir randevu için oluşturulan sözlük şöyledir:


    // RV --> Harita
    public static Map<String, Object> getMapForRv(Rv rv) {
        // Yapılması gereken bir şey mi var?
        if (rv == null) {
            return null;
        }
        // <String,Object> sözlüğü
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("client", rv.getClient());
        hash.put("creneau", getMapForCreneau(rv.getCreneau()));
        // sözlüğü geri veriyoruz
        return hash;
}
  • 11. satır: Daha önce sunduğumuz [Creneau] nesnesinin sözlüğünü alıyoruz;

Elde edilen sonuçlar şunlardır:

ya da gün bilgisi yanlış olan şu sonuçlar:

ya da doktor bilgisi yanlış olanlar:

2.12.11. URL [/getAgendaMedecinJour/{idMedecin}/{jour}]

URL ve [/getAgendaMedecinJour/{idMedecin}/{jour}], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // tarihi kontrol ediyoruz
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
        }
        // doktor bilgilerini alıyoruz
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // takvimini alıyoruz
        AgendaMedecinJour agenda = null;
        try {
            agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // tamam
        return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
    }
}
  • 30. satırda, List<Map<String,Object>> türünde bir nesne döndürülür.

[Static.getMapForAgendaMedecinJour] yöntemi şu şekildedir:


    // AgendaMedecinJour --> Harita
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // yapılacak bir şey var mı?
        if (agenda == null) {
            return null;
        }
        // sözlük <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("medecin", agenda.getMedecin());
        hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
        List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
        for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
            créneaux.add(getMapForCreneauMedecinJour(créneau));
        }
        hash.put("creneauxMedecin", créneaux);
        // sözlüğü oluşturuyoruz
        return hash;
}

Oluşturulan sözlük üç alana sahiptir:

  • [medecin]: ajandanın sahibi olan doktor. Bu bilgiyi sakladık çünkü bu bilgi sadece bir kez yer alırken, önceki durumlarda her JSON dizesinde tekrarlanıyordu;
  • [jour]: ajandanın günü;
  • [creneauxMedecin]: doktorun randevu aralıkları listesi ve bu aralıkta olası bir randevu;
  1. satırda kullanılan [getMapForCreneauMedecinJour] yöntemi şöyledir:

    // CreneauMedecinJour --> harita
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // Yapılması gereken bir şey var mı?
        if (créneau == null) {
            return null;
        }
        // <String,Object> sözlüğü
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // sözlüğü geri veriyoruz
        return hash;
}
  • 9-10. satırlar: Daha önce incelenen sözlükler, [Creneau] ve [Rv] türleri için kullanılır; dolayısıyla bu türler [Medecin] nesnesini içermez;

Elde edilen sonuçlar şunlardır:

ya da gün bilgisi yanlışsa şunlar:

ya da doktor numarası geçersizse şunlar:

2.12.12. URL [/getMedecinById/{id}]

URL [/getMedecinById/{id}], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // doktoru alıyoruz
        return getMedecin(id);
}
  1. satırda, [getMedecin] yöntemi şu şekildedir:

    private Reponse getMedecin(long id) {
        // doktoru alıyoruz
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // doktor mevcut mu?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // tamam
        return new Reponse(0, médecin);
}

Elde edilen sonuçlar şunlardır:

ya da doktor numarası yanlışsa şunlar:

2.12.13. URL [/getClientById/{id}]

URL [/getClientById/{id}], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // uygulama durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // müşteriyi alıyoruz
        return getClient(id);
}
  1. satırda, [getClient] yöntemi şu şekildedir:

    private Reponse getClient(long id) {
        // müşteriyi alıyoruz
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // müşteri mevcut mu?
        if (client == null) {
            return new Reponse(2, null);
        }
        // tamam
        return new Reponse(0, client);
}

Elde edilen sonuçlar şunlardır:

veya müşteri numarası yanlışsa şu sonuçlar:

2.12.14. URL [/getCreneauById/{id}]

URL [/getCreneauById/{id}], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // uygulama durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // zaman aralığı alınıyor
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // sonuç
        return réponse;
}
  1. satırda, [getCreneau] yöntemi şu şekildedir:

    private Reponse getCreneau(long id) {
        // zaman aralığı alınıyor
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // zaman aralığı mevcut mu?
        if (créneau == null) {
            return new Reponse(2, null);
        }
        // tamam
        return new Reponse(0, créneau);
}

Elde edilen sonuçlar şunlardır:

veya zaman aralığı numarası yanlışsa şunlar:

2.12.15. URL [/getRvById/{id}]

URL [/getRvById/{id}], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // randevu alın
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // sonuç
        return réponse;
}
  1. satırda, [getRv] yöntemi şu şekildedir:

    private Reponse getRv(long id) {
        // Rv alınıyor
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Rv mevcut mu?
        if (rv == null) {
            return new Reponse(2, null);
        }
        // tamam
        return new Reponse(0, rv);
}
  1. satırda, [Static.getMapForRv2] yöntemi şu şekildedir:

// Rv --> Harita
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // Yapılması gereken bir şey var mı?
        if (rv == null) {
            return null;
        }
        // <String,Object> sözlüğü
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("idClient", rv.getIdClient());
        hash.put("idCreneau", rv.getIdCreneau());
        // sözlüğü geri veriyoruz
        return hash;
    }

Elde edilen sonuçlar şunlardır:

ya da randevu numarası yanlışsa şu sonuçlar:

2.12.16. URL [/ajouterRv]

URL [/ajouterRv], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // uygulamanın durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // gönderilen değerleri alıyoruz
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // tarihi kontrol ediyoruz
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(6, null);
        }
        // zaman aralığını alıyoruz
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // müşteriyi alıyoruz
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // Rv ekleniyor
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // yanıt verilir
        return new Reponse(0, Static.getMapForRv(rv));
    }

Burada daha önce görülmemiş hiçbir şey yoktur. 41. satırda, 36. satırda eklenen randevu iade edilir.

[Advanced Rest Client] istemcisiyle elde edilen sonuçlar şuna benzer:

ya da örneğin var olmayan bir zaman aralığı numarası girilirse şuna benzer:

2.12.17. URL [/supprimerRv]

URL [/supprimerRv], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi ile işlenir:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // uygulama durumu
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // gönderilen değerleri alıyoruz
        long idRv = post.getIdRv();
        // Rv'yi alıyoruz
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // Rv'yi siliyoruz
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // tamam
        return new Reponse(0, null);
    }

'ten elde edilen sonuçlar şunlardır:

ya da randevu numarası mevcut değilse şunlar:

Denetleyiciyle işimiz bitti. Şimdi projeyi nasıl yapılandıracağımıza bakalım.

2.12.18. Web hizmetinin yapılandırılması

  

[AppConfig] yapılandırma sınıfı şu şekildedir:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {

}
  • 9. satır: Spring Boot'un, projenin Classpath'inde bulacağı arşivlere göre projeyi yapılandırabilmesi için [AutoConfiguration] moduna geçiyoruz;
  • 10. satır: Spring bileşenlerinin [rdvmedecins.web] paketinde ve alt paketlerinde aranması istenir. Böylece, [rdvmedecins.web.controllers] paketindeki şu bileşenler bulunur:
    • [@RestController RdvMedecinsController], [rdvmedecins.web.controllers] paketinde;
    • [@Component ApplicationModel], [rdvmedecins.web.models] paketinde;
  • 11. satır: [rdvmedecins-metier-dao] projesindeki bean'lere erişebilmek için bu projeyi yapılandıran [DomainAndPersistenceConfig] sınıfı içe aktarılır;

2.12.19. Web hizmetinin çalıştırılabilir sınıfı

  

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


package rdvmedecins.web.boot;

import org.springframework.boot.SpringApplication;

import rdvmedecins.web.config.AppConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  1. satırda, statik [SpringApplication.run] yöntemi, ilk parametre olarak projenin yapılandırma sınıfı olan [AppConfig] ile çalıştırılır. Bu yöntem, projenin otomatik yapılandırmasını gerçekleştirecek, bağımlılıklarda bulunan yerleşik Tomcat sunucusunu başlatacak ve [RdvMedecinsController] denetleyicisini bu sunucuya dağıtacaktır.

Çalıştırma sırasında oluşturulan günlükler şunlardır:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.0.0.RELEASE)

2014-06-12 17:30:41.261  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Starting Boot on Gportpers3 with PID 9388 (D:\data\istia-1314\polys\istia\angularjs-spring4\dvp\rdvmedecins-webapi\target\classes started by ST)
2014-06-12 17:30:41.306  INFO 9388 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@a1e932e: startup date [Thu Jun 12 17:30:41 CEST 2014]; root of context hierarchy
2014-06-12 17:30:42.058  INFO 9388 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'org.springframework.boot.autoconfigure.AutoConfigurationPackages': replacing [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] with [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
2014-06-12 17:30:42.866  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$fd7a7b18] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.900  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.915  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.920  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:43.164  INFO 9388 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-12 17:30:43.403  INFO 9388 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-12 17:30:43.403  INFO 9388 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.52
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2279 ms
2014-06-12 17:30:44.117  INFO 9388 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-12 17:30:44.119  INFO 9388 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-12 17:30:44.662  INFO 9388 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-06-12 17:30:44.707  INFO 9388 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-06-12 17:30:44.839  INFO 9388 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-12 17:30:44.842  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-06-12 17:30:44.844  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-06-12 17:30:45.189  INFO 9388 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-12 17:30:45.616  INFO 9388 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2014-06-12 17:30:45.783  INFO 9388 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-06-12 17:30:46.729  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.825  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllMedecins],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins()
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getMedecinById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCreneauById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getClientById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllClients],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllClients()
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/ajouterRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv)
2014-06-12 17:30:46.828  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/supprimerRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv)
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] türündeki işleyiciye
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] QZXW2HTML türündeki işleyici BW2NsYXNzIG9yZy5zcHJpbmdmcmFtZXdvcmsud2ViLnNlcnZsZXQucmVzb3VyY2UuUmVzb3VyY2VIdHRwUmVxdWVzdEhhbmRsZXJdZQX
2014-06-12 17:30:47.131  INFO 9388 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-12 17:30:47.169  INFO 9388 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-12 17:30:47.170  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Started Boot in 6.302 seconds (JVM running for 6.906)
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-06-12 17:30:55.538  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
  • 17. satır: Tomcat sunucusu başlatılır;
  • 23-31. satırlar: [métier, DAO, JPA] katmanları başlatılır;
  • 34. satır: URL [/getRvMedecinJour/{idMedecin}/{jour}]'i işleyen yöntem keşfedildi. Denetleyicinin yöntemlerini keşfetme süreci 44. satıra kadar tekrarlanır;
  • satır 52: Spring servleti MVC [DispatcherServlet], web istemcilerinden gelen taleplere yanıt vermeye hazırdır;

Artık bir web istemcisiyle sorgulanabilen, çalışır durumda bir web hizmetimiz var. Şimdi bu hizmetin güvenliğini ele alacağız: sadece belirli kişilerin doktor randevularını yönetebilmesini istiyoruz. Bunun için Spring ekosisteminin bir parçası olan Spring Security çerçevesini kullanacağız.

2.13. Spring Security’ye Giriş

Aşağıdaki 1 ila 3. adımları izleyerek bir Spring kılavuzunu yeniden içe aktaracağız:

  

Proje aşağıdaki öğelerden oluşmaktadır:

  • [templates] klasöründe, projenin HTML sayfaları bulunur;
  • [Application]: projenin yürütülebilir sınıfıdır;
  • [MvcConfig]: Spring yapılandırma sınıfıdır; MVC;
  • [WebSecurityConfig]: Spring Security yapılandırma sınıfıdır;

2.13.1. Maven Yapılandırması

[3] projesi bir Maven projesidir. Bağımlılıklarını öğrenmek için [pom.xml] dosyasını inceleyelim:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>
  • 1-5. satırlar: proje bir Spring Boot projesidir;
  • 8-11. satırlar: Dinamik HTML sayfaları oluşturmaya olanak tanıyan [Thymeleaf] çerçevesine bağımlılık. Bu çerçeve, yakın zamana kadar varsayılan olarak kullanılan JSP (Java Server Pages) sayfalarının ve Spring MVC görünüm çerçevesinin yerini alabilir;
  • 12-15. satırlar: Spring Security çerçevesine bağımlılık;

2.13.2. Thymeleaf görünümleri

  

[home.html] görünümü şu şekildedir:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • [th:xx] öznitelikleri, Thymeleaf öznitelikleridir. Bu öznitelikler, HTML sayfası istemciye gönderilmeden önce Thymeleaf tarafından işlenir. İstemci bunları görmez;
  • 12. satır: [th:href="@{/hello}"] özniteliği, <a> etiketinin [href] özniteliğini oluşturacaktır. [@{/hello}] değeri, [<context>/hello] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;

Oluşturulan HTML kodu şöyledir:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p>
        Click <a href="/hello">here</a> to see a greeting.
    </p>
</body>
</html>
  • 10. satır: uygulama bağlamı / kök dizinidir;

[hello.html] görünümü şöyledir:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • 9. satır: [th:inline="text"] özniteliği, <h1> etiketinin metnini oluşturacaktır. Bu metin, değerlendirilmesi gereken bir $ ifadesi içerir. [[${#httpServletRequest.remoteUser}]] öğesi, geçerli HTTP isteğinin [RemoteUser] özniteliğinin değeridir. Bu, oturum açmış kullanıcının adıdır;
  • 10. satır: bir HTML formu. [th:action="@{/logout}"] özniteliği, [form] etiketinin [action] özniteliğini oluşturacaktır. [@{/logout}] değeri, [<context>/logout] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;

Oluşturulan HTML kodu şöyledir:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1>Hello user!</h1>
    <form method="post" action="/logout">
        <input type="submit" value="Sign Out" />
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>
  • 8. satır: Hello [[${#httpServletRequest.remoteUser}]]! ifadesinin çevirisi;
  • 9. satır: @{/logout} ifadesinin çevirisi;
  • 11. satır: (name özniteliği) _csrf adlı gizli bir alan;

[login.html]'in son görünümü şöyledir:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • 9. satır: [th:if="${param.error}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL'in [error] (http://context/login?error) parametresini içermesi durumunda oluşturulmasını sağlar;
  • 10. satır: [th:if="${param.logout}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL etiketinde [logout] parametresi (http://context/login?logout) varsa oluşturulmasını sağlar;
  • 11-23. satırlar: bir HTML formu;
  • 11. satır: Form, <context> web uygulamasının bağlamı olan URL [<context>/login] adresine gönderilecektir;
  • 13. satır: [username] adlı bir giriş alanı;
  • 17. satır: [password] adlı bir giriş alanı;

Oluşturulan HTML kodu şöyledir:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>

    <form method="post" action="/login">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>

21. satırda, Thymeleaf'in [_csrf] adlı gizli bir alan eklediği görülmektedir.

2.13.3. Spring MVC yapılandırması

  

[MvcConfig] sınıfı, Spring MVC çerçevesini yapılandırır:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • 7. satır: [@Configuration] anotasyonu, [MvcConfig] sınıfını bir yapılandırma sınıfı haline getirir;
  • 8. satır: [MvcConfig] sınıfı, [WebMvcConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
  • 10. satır: üst sınıfın bir yönteminin yeniden tanımlanması;
  • satır 11-16: [addViewControllers] yöntemi, URL'leri HTML görünümlerine ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
URL
görünüm
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

[html] uzantısı ve [templates] klasörü, Thymeleaf tarafından kullanılan varsayılan değerlerdir. Bunlar yapılandırma yoluyla değiştirilebilir. [templates] klasörü, projenin Classpath kök dizininde bulunmalıdır:

Yukarıdaki [1] örneğinde, [main] ve [resources] klasörlerinin her ikisi de kaynak klasörlerdir (source folders). Bu, içeriklerinin projenin Classpath kök dizininde yer alacağı anlamına gelir. Dolayısıyla, [2] içinde, [hello] ve [templates] klasörleri Classpath'in kök dizininde yer alacaktır.

2.13.4. Spring Security Yapılandırması

  

[WebSecurityConfig] sınıfı, Spring Security çerçevesini yapılandırır:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • 9. satır: [@Configuration] anotasyonu, [WebSecurityConfig] sınıfını bir yapılandırma sınıfı haline getirir;
  • 10. satır: [@EnableWebSecurity] anotasyonu, [WebSecurityConfig] sınıfını bir Spring Security yapılandırma sınıfı haline getirir;
  • 11. satır: [WebSecurity] sınıfı, [WebSecurityConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
  • 12. satır: üst sınıfın bir yöntemi yeniden tanımlanmıştır;
  • 13-16. satırlar: [configure(HttpSecurity http)] yöntemi, uygulamanın çeşitli URL'lerine erişim haklarını tanımlamak üzere yeniden tanımlanmıştır;
  • 14. satır: [http.authorizeRequests()] yöntemi, URL'leri erişim haklarıyla ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
URL
kural
kod
/, /home
kimlik doğrulaması yapılmadan erişim

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Yalnızca kimlik doğrulaması ile erişim
http.anyRequest().authenticated();
  • 15. satır: kimlik doğrulama yöntemini tanımlar. Kimlik doğrulama, herkesin erişebileceği bir URL [/login] formu aracılığıyla yapılır [http.formLogin().loginPage("/login").permitAll()]. Oturum kapatma (logout) işlemi de herkes tarafından gerçekleştirilebilir.
  • 19-21. satırlar: kullanıcıları yöneten [configure(AuthenticationManagerBuilder auth)] yöntemini yeniden tanımlar;
  • 20. satır: Kimlik doğrulama, "sabit" olarak tanımlanmış kullanıcılarla yapılır ([auth.inMemoryAuthentication()]). Burada bir kullanıcı, [user] kullanıcı adı, [password] şifre ve [USER] rolü ile tanımlanmıştır. Aynı role sahip kullanıcılara aynı haklar verilebilir;

2.13.5. Çalıştırılabilir sınıf

  

[Application] sınıfı aşağıdaki gibidir:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}
  • 8. satır: [@EnableAutoConfiguration] anotasyonu, Spring Boot'tan (3. satır) geliştiricinin açıkça yapmayacağı yapılandırmayı yapmasını ister;
  • 9. satır: [Application] sınıfını bir Spring yapılandırma sınıfı haline getirir;
  • 10. satır: Spring bileşenlerini aramak üzere [Application] sınıfının bulunduğu klasörün taranmasını ister. [MvcConfig] ve [WebSecurityConfig] sınıfları, [@Configuration] anotasyonuna sahip oldukları için bu şekilde tespit edilecektir;
  • 13. satır: yürütülebilir sınıfın [main] yöntemi;
  • 14. satır: [SpringApplication.run] statik yöntemi, [Application] yapılandırma sınıfını parametre olarak alarak yürütülür. Bu süreci daha önce de görmüştük ve projenin Maven bağımlılıklarında bulunan gömülü Tomcat sunucusunun başlatılacağını ve projenin bu sunucuya dağıtılacağını biliyoruz. Dört adet URL'in [/, /home, /login, /hello] tarafından yönetildiğini ve bazılarının erişim haklarıyla korunduğunu görmüştük.

2.13.6. Uygulama Testleri

Öncelikle, kabul edilen dört URL'ten biri olan URL'i [/]'ten talep ederek başlayalım. Bu, [/templates/home.html] görünümüne bağlıdır:

 

Talep edilen URL, [/] herkes tarafından erişilebilir. Bu yüzden onu elde ettik. [here] bağlantısı şöyledir:

Click <a href="/hello">here</a> to see a greeting.

Bağlantıya tıklandığında URL ve [/hello] dosyaları istenecektir. Bu dosya korumalıdır:

URL
kural
kod
/, /home
kimlik doğrulaması yapılmadan erişim

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
sadece kimlik doğrulaması ile erişim
http.anyRequest().authenticated();

Bunu elde etmek için kimlik doğrulaması yapılmalıdır. Spring Security, istemci tarayıcısını kimlik doğrulama sayfasına yönlendirecektir. Görülen yapılandırmaya göre, bu sayfa URL [/login]'dir. Bu sayfaya herkes erişebilir:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Böylece [1] adresini elde ediyoruz:

Elde edilen sayfanın kaynak kodu şöyledir:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • 7. satırda, orijinal [login.html] sayfasında bulunmayan gizli bir alan ortaya çıkıyor. Bunu Thymeleaf eklemiştir. CSRF (Cross Site Request Forgery) olarak adlandırılan bu kod, bir güvenlik açığını ortadan kaldırmayı amaçlamaktadır. Bu jetonun, kimlik doğrulamanın kabul edilmesi için Spring Security’ye geri gönderilmesi gerekir;

Hatırlayacağımız üzere, Spring Security tarafından yalnızca user/password kullanıcısı tanınır. [2]'e başka bir şey girersek, [3]'te bir hata mesajı içeren aynı sayfayı alırız. Spring Security, tarayıcıyı URL [http://localhost:8080/login?error] adresine yönlendirdi. [error] parametresinin varlığı, şu etiketin görüntülenmesini tetikledi:


<div th:if="${param.error}">Invalid username and password.</div>

Şimdi, beklenen değerleri girelim: kullanıcı adı/şifre [4]:

  • [4] olarak oturum açtığımızda;
  • [5] adresinde, Spring Security bizi URL ve [/hello] adreslerine yönlendirir, çünkü giriş sayfasına yönlendirildiğimizde talep ettiğimiz adres URL idi. Kullanıcının kimliği, [hello.html]'teki şu satırda görüntülenmiştir:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

[5] sayfası aşağıdaki formu görüntüler:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

[Sign Out] düğmesine tıklandığında, URL ve [/logout] sayfalarında bir POST işlemi gerçekleştirilecektir. Bu dosya, URL ve [/login] dosyaları gibi herkes tarafından erişilebilir:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

URL / views ilişkimizde, URL ve [/logout] için herhangi bir tanımlama yapmadık. Ne olacak? Deneyelim:

  • [6] sayfasında, [Sign Out] düğmesine tıklıyoruz;
  • [7]'e geldiğimizde, URL [http://localhost:8080/login?logout] sayfasına yönlendirildiğimizi görüyoruz. Bu yönlendirmeyi Spring Security talep etmiştir. URL içinde [logout] parametresinin bulunması, görünümde şu satırın görüntülenmesine neden oldu:

<div th:if="${param.logout}">You have been logged out.</div>

2.13.7. Sonuç

Önceki örnekte, önce web uygulamasını yazıp ardından güvenliğini sağlayabilirdik. Spring Security müdahaleci değildir. Zaten yazılmış bir web uygulamasının güvenliğini de sağlayabiliriz. Ayrıca, şu noktaları da keşfettik:

  • bir kimlik doğrulama sayfası tanımlamak mümkündür;
  • kimlik doğrulama işlemi, Spring Security tarafından verilen CSRF jetonuyla birlikte yapılmalıdır;
  • kimlik doğrulama başarısız olursa, URL içinde bir error parametresi de bulunacak şekilde kimlik doğrulama sayfasına yönlendiriliriz;
  • kimlik doğrulama başarılı olursa, kimlik doğrulama işlemi gerçekleştiğinde istenen sayfaya yönlendiriliriz. Ara sayfa üzerinden geçmeden doğrudan kimlik doğrulama sayfasına erişilmeye çalışılırsa, Spring Security bizi URL [/] sayfasına yönlendirir (bu durum ele alınmamıştır);
  • POST ile URL [/logout] sayfasını talep ederek oturumdan çıkılır. Spring Security, URL'te logout parametresiyle bizi kimlik doğrulama sayfasına yönlendirir;

Tüm bu sonuçlar, Spring Security'nin varsayılan davranışlarına dayanmaktadır. Bu davranışlar, [WebSecurityConfigurerAdapter] sınıfındaki belirli yöntemleri yeniden tanımlayarak yapılandırma yoluyla değiştirilebilir.

Önceki öğretici, bundan sonra pek işimize yaramayacak. Aslında şunları kullanacağız:

  • kullanıcıları, şifrelerini ve rollerini depolamak için bir veritabanı;
  • HTTP başlık tabanlı kimlik doğrulama;

Burada yapmak istediğimiz şey için oldukça az sayıda öğretici bulunmaktadır. Önerilecek çözüm, çeşitli kaynaklardan derlenen kodların bir birleşimidir.

2.14. Randevu web hizmetinde güvenlik ayarlarının yapılması

2.14.1. Veritabanı

[rdvmedecins] veritabanı, kullanıcıları, şifrelerini ve rollerini içerecek şekilde güncelleniyor. Üç yeni tablo ekleniyor:

Image

[USERS] tablosu: kullanıcılar

  • ID: birincil anahtar;
  • VERSION: satır sürüm bilgisi sütunu;
  • IDENTITY: kullanıcının açıklayıcı kimliği;
  • LOGIN: kullanıcının kullanıcı adı;
  • PASSWORD: kullanıcının şifresi;

USERS tablosunda şifreler açık metin olarak depolanmaz:

 

Şifreleri şifreleyen algoritma, BCRYPT algoritmasıdır.

[ROLES] tablosu: roller

  • ID: birincil anahtar;
  • VERSION: satırın sürüm bilgisi sütunu;
  • NAME: rol adı. Varsayılan olarak, Spring Security, ROLE_XX biçiminde adlar bekler; örneğin, ROLE_ADMIN veya ROLE_GUEST;
 

[USERS_ROLES] tablosu: USERS / ROLES birleştirme tablosu

Bir kullanıcı birden fazla role sahip olabilir, bir rol ise birden fazla kullanıcıyı bir araya getirebilir. [USERS_ROLES] tablosu ile somutlaştırılan çok-çok ilişkisi mevcuttur.

  • ID: birincil anahtar;
  • VERSION: satırın sürüm bilgisi sütunu;
  • USER_ID: bir kullanıcının kimliği;
  • ROLE_ID: bir rolün kimliği;
 

Veritabanında değişiklik yaptığımız için, [métier, DAO, JPA] projesinin tüm katmanlarının değiştirilmesi gerekir:

2.14.2. [métier, DAO, JPA] adlı yeni Eclipse projesi

İlk proje olan [rdvmedecins-metier-dao]'i [rdvmedecins-metier-dao-v2] olarak kopyalıyoruz:

  • [1]'e: yeni proje;
  • [2]'e: güvenlik önlemleri kapsamında yapılan değişiklikler tek bir paket olan [rdvmedecins.security]'te birleştirildi. Bu yeni öğeler [JPA] ve [DAO] katmanlarına aittir, ancak basitlik açısından bunları tek bir pakette bir araya getirdim.

2.14.3. Yeni varlıklar [JPA]

JPA katmanı üç yeni öğe tanımlar:

  

[User] sınıfı, [USERS] tablosunun görüntüsüdür:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // özellikler
    private String identity;
    private String login;
    private String password;

    // üretici
    public User() {
    }

    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }

    // kimlik
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }

    // alıcı ve ayarlayıcılar
....
}
  • 9. satır: Sınıf, diğer varlıklar için halihazırda kullanılan [AbstractEntity] sınıfını genişletir;
  • 13-15. satırlar: sütunlar, kendileriyle ilişkili alanlarla aynı adı taşıdıkları için sütun adları belirtilmemiştir;

[Role] sınıfı, [ROLES] tablosunun aynısıdır:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // özellikleri
    private String name;

    // üreticiler
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // kimlik
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }

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

[UserRole] sınıfı, [USERS_ROLES] tablosunun görüntüsüdür:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // bir UserRole, bir User'a başvurur
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // bir UserRole bir Role'a başvurur
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // alıcı ve ayarlayıcılar
...
}
  • 15-17. satırlar: [USERS_ROLES] tablosundan [USERS] tablosuna yabancı anahtarı oluşturur;
  • 19-21. satırlar: [USERS_ROLES] tablosundan [ROLES] tablosuna yabancı anahtarı tanımlar;

2.14.4. [DAO] katmanındaki değişiklikler

[DAO] katmanına üç yeni [Repository] eklenmiştir:

  

[UserRepository] arayüzü, [User] varlıklarına erişimi yönetir:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // kullanıcı kimliği ile tanımlanan bir kullanıcının rol listesi
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // kullanıcı adı ve şifresiyle tanımlanan bir kullanıcının rol listesi
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // kullanıcı adıyla kullanıcı arama
    User findUserByLogin(String login);
}
  • 9. satır: [UserRepository] arayüzü, Spring Data'nın [CrudRepository] arayüzünü genişletir (4. satır);
  • 12-13. satırlar: [getRoles(User user)] yöntemi, [id] ile tanımlanan bir kullanıcının tüm rollerini almayı sağlar
  • 16-17. satırlar: Aynı işlem, ancak kullanıcı adı ve şifre ile tanımlanan bir kullanıcı için geçerlidir;

[RoleRepository] arayüzü, [Role] varlıklarına erişimi yönetir:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // adına göre bir rolün aranması
    Role findRoleByName(String name);

}
  • 5. satır: [RoleRepository] arayüzü, [CrudRepository] arayüzünü genişletir;
  • 8. satır: Bir rol, adı üzerinden aranabilir;

[userRoleRepository] arayüzü, [UserRole] varlıklarına erişimi yönetir:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • 5. satır: [UserRoleRepository] arayüzü, [CrudRepository] arayüzünü yeni yöntemler eklemeden genişletmekle yetinir;

2.14.5. Kullanıcı ve rol yönetimi sınıfları

  

Spring Security, aşağıdaki [UsersDetail] arayüzünü uygulayan bir sınıfın oluşturulmasını zorunlu kılar:

 

Bu arayüz, burada [AppUserDetails] sınıfı tarafından uygulanmaktadır:


package rdvmedecins.security;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // özellikler
    private User user;
    private UserRepository userRepository;

    // oluşturucular
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------arayüz
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // alıcı ve ayarlayıcılar
    ...
}
  • 10. satır: [AppUserDetails] sınıfı, [UserDetails] arayüzünü uygular;
  • 15-16. satırlar: Sınıf, bir kullanıcıyı (15. satır) ve bu kullanıcının ayrıntılarını elde etmeyi sağlayan depoyu (16. satır) kapsüller;
  • satır 22-25: sınıfı bir kullanıcı ve onun deposu ile örneklendiren oluşturucu;
  • satır 28-35: [UserDetails] arayüzünün [getAuthorities] yönteminin uygulaması. Bu yöntem, [GrantedAuthority] türünde veya türevinde öğelerden oluşan bir koleksiyon oluşturmalıdır. Burada, 15. satırdaki kullanıcının rollerinden birinin adını kapsayan türetilmiş [SimpleGrantedAuthority] türünü (32. satır) kullanıyoruz;
  • 31-33. satırlar: 15. satırdaki kullanıcının rol listesi taranarak [SimpleGrantedAuthority] türünde bir öğe listesi oluşturulur;
  • satır 38-40: [UserDetails] arayüzünün [getPassword] yöntemini uygular. Satır 15'teki kullanıcının şifresi döndürülür;
  • satır 38-40: [UserDetails] arayüzünün [getUserName] yöntemini uygular. 15. satırdaki kullanıcının kullanıcı adı döndürülür;
  • 47-50. satırlar: kullanıcının hesabı hiçbir zaman süresi dolmaz;
  • 52-55. satırlar: Kullanıcının hesabı hiçbir zaman engellenmez;
  • 57-60. satırlar: Kullanıcının kimlik bilgileri hiçbir zaman geçerliliğini yitirmez;
  • 62-65. satırlar: kullanıcının hesabı her zaman aktiftir;

Spring Security ayrıca [AppUserDetailsService] arayüzünü uygulayan bir sınıfın varlığını zorunlu kılar:

 

Bu arayüz, aşağıdaki [AppUserDetails] sınıfı tarafından uygulanır:


package rdvmedecins.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // kullanıcı adıyla kullanıcı aranıyor
        User user = userRepository.findUserByLogin(login);
        // bulundu mu?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // kullanıcının ayrıntılarını döndürme
        return new AppUserDetails(user, userRepository);
    }

}
  • 9. satır: Sınıf bir Spring bileşeni olacak, dolayısıyla kendi bağlamında kullanılabilir olacaktır;
  • 12-13. satırlar: [UserRepository] bileşeni buraya enjekte edilecektir;
  • satır 16-25: [UserDetailsService] arayüzünün (satır 10) [loadUserByUsername] yönteminin uygulanması. Parametre, kullanıcının giriş adıdır;
  • satır 18: kullanıcı, kullanıcı adı aracılığıyla aranır;
  • 20-22. satırlar: kullanıcı bulunamazsa bir istisna atılır;
  • 24. satır: bir [AppUserDetails] nesnesi oluşturulur ve görüntülenir. Bu nesne, [UserDetails] türündedir (16. satır);

2.14.6. [DAO] katmanının testleri

  

Öncelikle, bir rol atamalı kullanıcı oluşturabilen [CreateUser] adlı bir yürütülebilir sınıf oluşturuyoruz:


package rdvmedecins.security;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // sözdizimi: kullanıcı adı şifre roleName

        // üç parametre gereklidir
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parametreleri alıyoruz
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // Spring bağlamı
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // rol zaten mevcut mu?
        Role role = roleRepository.findRoleByName(roleName);
        // mevcut değilse oluşturulur
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // Kullanıcı zaten mevcut mu?
        User user = userRepository.findUserByLogin(login);
        // yoksa oluşturulur
        if (user == null) {
            // şifreyi bcrypt ile hash'liyoruz
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // kullanıcı kaydedilir
            user = userRepository.save(new User(login, login, crypt));
            // rol ile ilişki oluşturulur
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // kullanıcı zaten mevcutsa, istenen role sahip mi?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // bulunmazsa, rol ile ilişki oluşturulur
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // Spring bağlamı kapatılıyor
        context.close();
    }

}
  • 17. satır: Sınıf, bir kullanıcıyı tanımlayan üç argüman bekler: kullanıcı adı, şifre ve rol;
  • 25-27. satırlar: Bu üç parametre alınır;
  • 29. satır: Spring bağlamı, [DomainAndPersistenceConfig] yapılandırma sınıfından oluşturulur. Bu sınıf, önceki projede zaten mevcuttu. Aşağıdaki şekilde değiştirilmelidir:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • 1. satır: Artık [rdvmedecins.security] paketinde [Repository] bileşenleri olduğunu belirtmek gerekir;
  • 4. satır: [rdvmedecins.security] paketinde artık JPA varlıklarının bulunduğunu belirtmek gerekir;

Kullanıcı oluşturma koduna geri dönelim:

  • 30-32. satırlar: kullanıcıyı oluştururken işimize yarayabilecek üç [Repository] referansını alıyoruz;
  • 34. satır: Rolün zaten mevcut olup olmadığına bakıyoruz;
  • 36-38. satırlar: Eğer yoksa, veritabanında oluşturulur. Adı [ROLE_XX] şeklinde olacaktır;
  • 40. satır: Kullanıcı adının zaten mevcut olup olmadığına bakılır;
  • 42-49. satırlar: Kullanıcı adı mevcut değilse, veritabanında oluşturulur;
  • 44. satır: Şifre şifrelenir. Burada Spring Security'nin [BCrypt] sınıfı kullanılır (4. satır). Dolayısıyla bu çerçevenin arşivlerine ihtiyacımız vardır. [pom.xml] dosyası yeni bir bağımlılık içerir:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 46. satır: Kullanıcı veritabanına kaydedilir;
  • 48. satır: kullanıcıyı rolüne bağlayan ilişki de veritabanına kaydedilir;
  • 51-57. satırlar: kullanıcı zaten mevcutsa – bu durumda, atamak istediğimiz rolün mevcut roller arasında olup olmadığına bakılır;
  • satır 59-61: aranan rol bulunamazsa, kullanıcıyı rolüyle ilişkilendirmek için [USERS_ROLES] tablosuna bir satır eklenir;
  • olası istisnalara karşı herhangi bir önlem alınmamıştır. Bu, bir kullanıcıyı bir rolle hızlı bir şekilde oluşturmak için kullanılan bir destek sınıfıdır.

Sınıf, [x x guest] argümanlarıyla çalıştırıldığında, veritabanında aşağıdaki sonuçlar elde edilir:

[USERS] tablosu

 

[ROLES] tablosu

 

[USERS_ROLES] tablosu

 

Şimdi, JUnit'in bir testi olan ikinci sınıf [UsersTest]'i ele alalım:

  

package rdvmedecins.security;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;

import com.google.common.collect.Lists;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // [admin] kullanıcısını alıyoruz
        User user = userRepository.findUserByLogin("admin");
        // şifresinin [admin] olup olmadığını kontrol eder
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // admin / admin rolünü kontrol ediyoruz
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // [admin] kullanıcısını alıyoruz
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // şifresinin [admin] olup olmadığını kontrol ediyoruz
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // admin / admin rolünü kontrol ediyoruz
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // yardımcı yöntem - bir koleksiyondaki öğeleri görüntüler
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • 27-34. satırlar: görsel test. Tüm kullanıcılar rolleriyle birlikte görüntülenir;
  • 36-46. satırlar: [UserRepository] deposunu kullanarak [admin] kullanıcısının [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğunu kontrol ediyoruz;
  • 41. satır: [admin], şifrelenmemiş şifredir. Veritabanında, BCrypt algoritmasına göre şifrelenmiştir. [ BCrypt.checkpw] yöntemi, şifrelenmiş açık metin şifresinin veritabanındaki şifreyle tam olarak eşleşip eşleşmediğini doğrulamaya yarar;
  • 48-59. satırlar: [admin] kullanıcısının, [appUserDetailsService] hizmetini kullanarak [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğu doğrulanır;

Testler aşağıdaki günlük kayıtlarıyla başarıyla tamamlanır:

User[guest,guest,$2a$10$Gzyp54mvkgMH0SPQkXo.Zeu.DvJ/Ql50PRXLf2FkolMTs7fr6A2J2]
Roles :
Role[ROLE_GUEST]
User[admin,admin,$2a$10$m79V6MKt9GPDdpjSulyqReqUioqYwXy8ollt/.ia15FhX2fym3AE6]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$ph5y/1H89YC11oGVLB49fON.dZwnu44bAOKMK1FFl//xjAvsr/Ese]
Roles :
Role[ROLE_USER]
User[x,x,$2a$10$dAKd2SuQplR1iFhoBUUFs.XiA0lYxNqOmrkv97Gbr5KBoHzEi/5HG]
Roles :
Role[ROLE_GUEST]

2.14.7. Ara sonuç

Spring Security için gerekli sınıfların eklenmesi, orijinal projede çok az değişiklikle gerçekleştirilebildi. Bunları hatırlayalım:

  • [pom.xml] dosyasına Spring Security bağımlılığının eklenmesi;
  • veritabanında üç ek tablonun oluşturulması;
  • JPA varlıklarının ve [rdvmedecins.security] paketindeki Spring bileşenlerinin oluşturulması;

Bu çok elverişli durum, veritabanına eklenen üç tablonun mevcut tablolardan bağımsız olmasından kaynaklanmaktadır. Hatta bunları ayrı bir veritabanına bile yerleştirebilirdik. Bu, bir kullanıcının doktorlardan ve müşterilerden bağımsız bir varlığa sahip olduğu kararlaştırıldığı için mümkün olmuştur. Eğer doktorlar ve müşteriler potansiyel kullanıcılar olsaydı, [USERS] tablosu ile [MEDECINS] ve [CLIENTS] tabloları arasında bağlantılar oluşturmak gerekirdi. Bu durum, mevcut proje üzerinde önemli bir etki yaratırdı.

2.14.8. [web] katmanının Eclipse projesi

Önceki [rdvmedecins-webapi] projesi, [rdvmedecins-webapi-v2] ve [1] projelerinde kopyalanmıştır:

Yapılması gereken tek değişiklik, Spring Security'nin yapılandırılması gereken [rdvmedecins.web.config] paketindedir. Daha önce bir Spring Security yapılandırma sınıfıyla karşılaşmıştık:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Aynı yaklaşımı izleyeceğiz:

  • 11. satır: [WebSecurityConfigurerAdapter] sınıfını genişleten bir sınıf tanımlayın;
  • 13. satır: Web hizmetinin çeşitli URL'lerine erişim haklarını tanımlayan bir [configure(HttpSecurity http)] yöntemi tanımlayın;
  • 19. satır: kullanıcıları ve rollerini tanımlayan [configure(AuthenticationManagerBuilder auth)] yöntemini tanımlayalım;

Spring Security yapılandırması, [SecurityConfig] sınıfı tarafından sağlanır:


package rdvmedecins.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import rdvmedecins.security.AppUserDetailsService;

@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // kimlik doğrulama, [appUserDetailsService] bean'i tarafından yapılır
        // şifre, Bcrypt karma algoritmasıyla şifrelenir
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // şifre, Authorization: Basic xxxx başlığıyla iletilir
        http.httpBasic();
        // Yalnızca ADMIN rolü uygulamayı kullanabilir
        http.authorizeRequests() //
                .antMatchers("/", "/**") // tüm URL'ler
                .hasRole("ADMIN");
    }
}
  • 14-15. satırlar: Örnekteki anotasyonlar buraya da aktarılmıştır;
  • 17-18. satırlar: Uygulamanın kullanıcılarına erişim sağlayan [AppUserDetails] sınıfı enjekte edilmiştir;
  • 20-21. satırlar: [configure(HttpSecurity http)] yöntemi, kullanıcıları ve rollerini tanımlar. Bu yöntem, parametre olarak [AuthenticationManagerBuilder] türünü alır. Bu parametre, iki bilgiyle zenginleştirilir:
    • 18. satırdaki, kayıtlı kullanıcılara erişim sağlayan [appUserDetailsService] hizmetine bir referans. Burada, kullanıcıların bir veritabanında kayıtlı olduğunun belirtilmediğine dikkat edilmelidir. Dolayısıyla bunlar bir önbellekte bulunabilir, bir web hizmeti tarafından sağlanabilir, ...
    • şifre için kullanılan şifreleme türü. Burada, BCrypt algoritmasını kullandığımızı hatırlatırız;
  • 27-40. satırlar: [configure(HttpSecurity http)] yöntemi, web hizmetinin URL'lerine erişim haklarını tanımlar;
  • 30. satır: Giriş projesinde, Spring Security'nin varsayılan olarak bir CSRF jetonu (Cross Site Request Forgery) yönettiğini ve kimlik doğrulaması yapmak isteyen kullanıcının bu jetonu sunucuya geri göndermesi gerektiğini görmüştük. Burada bu mekanizma devre dışı bırakılmıştır;
  • 32. satır: HTTP başlığıyla kimlik doğrulama modu etkinleştiriliyor. İstemci, aşağıdaki HTTP başlığını göndermelidir:
Authorization:Basic code

burada code, "login:password" dizesinin Base64 algoritmasıyla kodlanmış halidir. Örneğin, admin:admin dizesinin Base64 kodlaması YWRtaW46YWRtaW4= şeklindedir. Dolayısıyla, kullanıcı adı [admin] ve şifre [admin] olan kullanıcı, kimlik doğrulaması için aşağıdaki HTTP başlığını gönderecektir:

Authorization:Basic YWRtaW46YWRtaW4=
  • 34-36. satırlar: Web hizmetindeki tüm URL öğelerinin, [ROLE_ADMIN] rolüne sahip kullanıcılar tarafından erişilebilir olduğunu belirtir. Bu, bu role sahip olmayan bir kullanıcının web hizmetine erişemeyeceği anlamına gelir;

Uygulamanın tamamını yapılandıran [AppConfig] sınıfı şu şekilde değişir:

  

package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {

}
  • Değişiklik 11. satırda yapılmıştır: Artık kullanılacak iki yapılandırma dosyası olduğu belirtilmiştir: [DomainAndPersistenceConfig] ve [SecurityConfig].

2.14.9. Web hizmeti testleri

Web hizmetini Chrome istemcisi [Advanced Rest Client] ile test edeceğiz. Kimlik doğrulama başlığı HTTP'i belirtmemiz gerekecek:

Authorization:Basic code

burada [code], [login:password] dizesinin Base64 kodudur. Bu kodu oluşturmak için aşağıdaki programı kullanabilirsiniz:

  

package rdvmedecins.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // iki argüman bekleniyor: kullanıcı adı ve şifre
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // iki argüman alınır
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // dizgiyi kodlar
        byte[] data = Base64.encode(chaîne.getBytes());
        // Base64 kodlamasını görüntüler
        System.out.println(new String(data));
    }

}

Bu programı [admin admin] argümanlarıyla çalıştırırsak:

  

şu sonucu elde ederiz:

YWRtaW46YWRtaW4=

Artık HTTP kimlik doğrulama başlığını oluşturmayı öğrendiğimize göre, artık güvenli hale getirilmiş web hizmetini başlatıyoruz. Ardından Chrome istemcisi [Advanced Rest Client] ile tüm doktorların listesini talep ediyoruz:

  • [1] ile doktorların URL bilgilerini talep ediyoruz;
  • [2]'te, GET yöntemiyle;
  • [3]'te, kimlik doğrulama başlığı olarak HTTP'i veriyoruz. [YWRtaW46YWRtaW4=] kodu, [admin:admin] dizesinin Base64 kodlamasıdır;
  • [4]'te, HTTP komutunu gönderiyoruz;

Sunucunun yanıtı şöyledir:

  • [1]'te, HTTP kimlik doğrulama başlığı;
  • [2]'te, sunucu JSON yanıtını geri gönderir;
  • [3]'te, doktor listesi.

Şimdi, hatalı bir kimlik doğrulama başlığıyla HTTP isteği göndermeyi deneyelim. Bu durumda yanıt şu şekildedir:

  • [1] ve [3]: HTTP kimlik doğrulama başlığı;
  • [2]: web hizmetinin yanıtı;

Şimdi user / user kullanıcısını deneyelim. Bu kullanıcı mevcut ancak web hizmetine erişimi yok. Base64 kodlama programını [user user] argümanıyla çalıştırırsak:

  

şu sonucu elde ederiz:

dXNlcjp1c2Vy
  • [1] ve [3]: HTTP kimlik doğrulama başlığı;
  • [2]: web hizmetinin yanıtı. Bu, önceki [401 Unauthorized] yanıtından farklıdır. Bu sefer kullanıcı doğru bir şekilde kimlik doğrulaması yapmıştır ancak URL'e erişmek için yeterli haklara sahip değildir;

2.15. Conclusion

İstemci/sunucu uygulamamızın genel mimarisini hatırlayalım:

Güvenli bir web hizmeti artık çalışır durumda. Angular istemcisi JS'in oluşturulması sırasında ortaya çıkacak sorunlar nedeniyle bu hizmetin değiştirilmesi gerekeceğini göreceğiz. Ancak sorunu çözmek için sorunla karşılaşmayı bekleyeceğiz. Şimdi, doktorların randevularını yönetmek için bir web arayüzü sunacak olan Angular istemcisini oluşturacağız.