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:
[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.
-
- 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:
-
- 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;
-
- 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;
-
- 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);
}
-
- 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;
- 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:
Bu nedenle, T türünün [something] adlı bir alanı olması gerekir. Böylece
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();
}
}
-
- 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:
- kullanılacak JPA uygulamasını tanımlayan bir [EntityManagerFactory],
- kullanılacak veri kaynağını tanımlayan bir [DataSource],
- kullanılacak işlem yöneticisini tanımlayan bir [TransactionManager];
Burada bu bean'lerin hiçbiri tanımlanmamıştır.
-
- 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);
-
- 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;
-
- 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:
- 1-8. satırlar: Spring Boot projesinin logosu;
-
- satır: [hello.Application] sınıfı çalıştırılır;
-
- 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;
-
- satır: [hibernate] ortaya çıkıyor. Seçilen, JPA adlı bu uygulamadır;
-
- 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ı;
-
- 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();
}
}
-
- satır: [Main] sınıfında artık yapılandırma ek açıklamaları bulunmamaktadır;
-
- 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;
}
}
-
- satır: [@Configuration] anotasyonu, [Config] sınıfını bir Spring yapılandırma sınıfı haline getirir;
-
- 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;
-
- satır: [@EnableTransactionManagement] anotasyonu, [CrudRepository] arayüzlerinin yöntemlerinin bir işlem içinde gerçekleştirilmesi gerektiğini belirtir;
-
- 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;
-
- 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;
-
- 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;
-
- 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;
-
- 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;
-
- 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();
}
}
-
- 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:
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:
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() {
}
}
-
- 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;
-
- 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:
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
..
}
-
- 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
...
}
-
- 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());
}
}
-
- satır: Sınıf, JPA varlığıdır;
-
- satır: veritabanındaki [MEDECINS] tablosuyla ilişkilidir;
-
- satır: [Medecin] varlığı, [Personne] varlığından türetilmiştir;
Bir doktor şu şekilde başlatılabilir:
Ayrıca, ona bir kimlik ve sürüm atamak istersek şu şekilde yazabiliriz:
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());
}
}
-
- satır: sınıf, JPA varlığıdır;
-
- satır: veritabanındaki [CLIENTS] tablosuyla ilişkilidir;
-
- 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
...
}
-
- satır: sınıf, JPA varlığıdır;
-
- satır: veritabanındaki [CRENEAUX] tablosuyla ilişkilidir;
-
- 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;
-
- satır: zaman aralığının başlangıç saati (14);
-
- satır: zaman aralığının başlangıç dakikası (20);
-
- 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;
-
- 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;
-
- satır: [CRENEAUX] tablosundaki yabancı anahtar sütununun adını belirtir;
- 27-28. satırlar: [MEDECINS] tablosundaki yabancı anahtar;
-
- 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
...
}
-
- satır: sınıf, JPA varlığıdır;
- satır 15: veritabanındaki [RV] tablosuyla ilişkilidir;
-
- 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;
-
- satır: randevu tarihi;
-
- 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> {
}
-
- 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> {
}
-
- 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);
}
-
- 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);
}
-
- 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;
-
- satır: Parametreler, doktorun kimliği ve gündür. Sonuç, bir [Iterable<Rv>] nesnesi biçiminde bir randevu listesidir;
-
- 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:
çü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
...
}
-
- satır: zaman aralığı;
-
- 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
...
}
-
- satır: doktor;
-
- satır: ajandadaki gün;
-
- 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) {
...
}
}
-
- 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;
-
- satır: [Metier] sınıfı, [IMetier] arayüzünü uygular;
-
- 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;
-
- 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ı;
-
- 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
}
-
- 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;
-
- satır, [CrudRepository] arayüzünden miras alan [rdvmdecins.repositories] paketindeki arayüzleri Spring bağlamına ekler;
-
- 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;
-
- 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;
-
- 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);
}
}
}
-
- 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;
-
- 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.
-
- satır: Bir istisna oluştuğu doğrulanır;
-
- satır: az önce randevu eklenen doktorun takvimi istenir;
-
- 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.
-
- satır: [SpringApplication] sınıfı, [DomainAndPersistenceConfig] yapılandırma sınıfını kullanacaktır;
-
- satır: Uygulamanın başlangıç günlükleri silinir;
-
- 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;
-
- 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;
-
- satır: doktorun birincil bilgisi alınır;
-
- satır: doktorun randevu listesi görüntülenir;
Konsol çıktısı şu şekildedir:
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:
- 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);
- 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];
- 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:
veya
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:
____ _ __ _ _
- 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.
Bir konsol kullanarak oluşturulan klasöre giriyoruz:
- 5. satır: oluşturulan arşiv;
Bu arşiv şu şekilde çalıştırılır:
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:
[@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:
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:

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;
- 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);
}
- 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);
}
- 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;
}
- 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;
}
- 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);
}
- 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);
}
}
- 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:
- 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:
- 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:
- 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:
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:
görünüm | |
/templates/home.html | |
/templates/hello.html | |
/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:
kural | kod | |
kimlik doğrulaması yapılmadan erişim | | |
Yalnızca kimlik doğrulaması ile erişim |
- 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:
Bağlantıya tıklandığında URL ve [/hello] dosyaları istenecektir. Bu dosya korumalıdır:
kural | kod | |
kimlik doğrulaması yapılmadan erişim | | |
sadece kimlik doğrulaması ile erişim |
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:
- 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:
[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:

[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:
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:
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:
- 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:
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:
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:
![]() |
- [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.

















































































































































