8. Vaka çalışması
8.1. Introduction
Bir muayenehane için randevu alma web uygulaması yazmayı planlıyoruz. Bu konu, 'AngularJS / Spring 4 Öğreticisi' belgesinde URL ve [http://tahe.developpez.com/angularjs-spring4/] bölümlerinde ele alınmıştır. Bu uygulamanın mimarisi şu şekildeydi:
![]() |
- [1]'te bir web sunucusu, tarayıcıya statik sayfalar sunar. Bu sayfalar, MVC modeline (Model – Görünüm – Denetleyici) dayalı bir AngularJS uygulamasını içerir. Buradaki model, hem görünümlerin hem de [Services] katmanı ile temsil edilen alanın modelidir;
- kullanıcı, tarayıcıda kendisine sunulan görünümlerle etkileşime girecektir. Kullanıcının eylemleri bazen Spring 4 [2] sunucusuna sorgu gönderilmesini gerektirecektir. Sunucu, isteği işleyecek ve bir yanıt (jSON [[${#httpServletRequest.remoteUser}]] [3]) döndürecektir. Bu yanıt, kullanıcıya sunulan görünümü güncellemek için kullanılacaktır.
Bu uygulamayı ele alıp Spring ile baştan sona MVC şeklinde uygulamayı öneriyoruz. Böylece mimari şu şekilde olur:
![]() |
Tarayıcı, Spring MVC ile uygulanan [Web 1] uygulamasına bağlanacak ve bu uygulama da yine Spring MVC ile uygulanan [Web 2] web hizmetinden verilerini alacaktır.
8.2. Uygulamanın Özellikleri
Okuyucunun, uygulamayı test ederek işlevlerini keşfetmesi önerilir. STS dosyasına, [etude-de-cas] klasöründeki Maven projelerini yükliyoruz:
![]() | ![]() |
Öncelikle, MySQL 5 [dbrdvmedecins] veritabanını [Wamp Server] aracıyla oluşturacağız (bkz. paragraf 9.5):
![]() |
- [1]'te, WampServer'ten [phpMyAdmin] aracını seçin;
- [2]'te, [Importer] seçeneği seçilir;
![]() |
- [3]'te, [database/dbrdvmedecins.sql] dosyasını seçin;
- [4]'te, dosyayı çalıştırın;
- [5]'te, veritabanı oluşturulur.
Ardından, veritabanına bağlı sunucuyu başlatmamız gerekiyor. Bu, [rdvmedecins-webjson-server] projesidir
![]() |
Sunucu, URL ve [http://localhost:8080] adreslerinde erişilebilir olacaktır. Bu ayar, projenin [application.properties] dosyasında değiştirilebilir:
![]() |
server.port=8080
Veritabanına erişim özellikleri, [rdvmedecins-metier-dao] projesindeki [DomainAndPersistenceConfig] sınıfında kaydedilmiştir:
![]() |
// 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;
}
SGBD ve MySQL'e başka kimlik bilgileriyle erişiyorsanız, işlem burada gerçekleşir.
Ardından, önceki sunucuyla aynı şekilde [rdvmedecins-springthymeleaf-server] sunucusunu başlatıyoruz:
![]() | ![]() |
Bu sunucu varsayılan olarak URL ve [http://localhost:8081] adreslerinde erişilebilir durumdadır. Yine, bu ayar projenin [application.properties] dosyasında yapılandırılabilir:
server.port=8081
Ayrıca, bu sunucu, veritabanına bağlı sunucunun URL değerini bilmelidir. Bu yapılandırma, yukarıdaki [AppConfig] sınıfında bulunur:
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// web hizmeti kökü / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// zaman aşımı (milisaniye cinsinden)
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
İlk sunucu 8080'den farklı bir bağlantı noktasında başlatılmışsa, 5. satırı değiştirmek gerekir.
Ardından bir tarayıcı kullanarak URL ve [http://localhost:8081/boot.html] adreslerine erişilir:
![]() |
- uygulamanın giriş sayfası olan [1] adresine;
- [2] ve [3], uygulamayı kullanmak isteyen kişinin kullanıcı adı ve şifresidir. İki kullanıcı vardır: admin/admin (kullanıcı adı/şifre) rolü (ADMIN) ve user/user rolü (USER). Yalnızca ADMIN rolüne sahip kullanıcılar uygulamayı kullanma hakkına sahiptir. USER rolü, bu kullanım durumunda sunucunun ne yanıt verdiğini göstermek amacıyla eklenmiştir;
- [4], sunucuya bağlanmayı sağlayan düğme;
- [5]'te, uygulamanın dili. İki seçenek vardır: varsayılan olarak Fransızca ve İngilizce;
- [6]'te, [rdvmedecins-springthymeleaf-server] sunucusunun URL'i;
![]() |
- [1]'te oturum açılır;
![]() |
- bağlandıktan sonra, randevu almak istediğiniz doktoru [2] ve randevu gününü [3] seçebilirsiniz. Doktor ve gün girildiğinde, randevu takvimi otomatik olarak görüntülenir:
![]() |
- doktorun takvimi görüntülendikten sonra, bir randevu aralığı rezerve edilebilir [5];
![]() |
- [6]'te randevu için hasta seçilir ve bu seçim [7]'te onaylanır;
![]() |
Randevu onaylandıktan sonra, otomatik olarak takvime geri dönülür ve yeni randevu artık takvime kaydedilmiştir. Bu randevu daha sonra [8] ile silinebilir.
Temel işlevler açıklanmıştır. Bunlar basittir. Son olarak dil yönetimine geçelim:
1

- [1]'te, Fransızca'dan İngilizce'ye geçilir;
![]() |
- [2]'te, takvim dahil olmak üzere görünüm İngilizceye geçti;
8.3. Veritabanı
![]() |
Bundan sonra [dbrdvmedecins] olarak adlandırılan 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 müşteriyi 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) atanır.
8.3.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)
8.3.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)
8.3.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 diliminin ait olduğu 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.
8.3.4. [RV] tablosu
Bu tablo, her doktor için ayrılan RV zaman dilimlerini listeler:
![]() |
- ID: RV'i benzersiz şekilde tanımlayan numara – birincil anahtar
- JOUR: RV'in günü
- ID_CRENEAU: RV'in zaman aralığı – [ID] tablosundaki [CRENEAUX] alanına ilişkin 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 tabloda, birleştirilen sütunların değerleri (JOUR, ID_CRENEAU) üzerinde tekillik kısıtlaması vardır:
[RV] tablosundaki bir satırın (JOUR1, ID_CRENEAU1) sütunları için (JOUR, ID_CRENEAU) için bu değer varsa, bu değer başka hiçbir yerde bulunamaz. Aksi takdirde, bu durum aynı hekim için aynı anda iki adet RV kaydedildiği 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.
8.3.5. Veritabanının Oluşturulması
[dbrdvmedecins] veritabanını oluşturmak için, bu [1-3] belgesindeki örneklerle birlikte [dbrdvmedecins.sql] adlı bir komut dosyası sağlanmaktadır:
![]() |
WampServer aracının [PhpMyAdmin] aracını kullanıyoruz:
![]() |
- [1]'te, WampServer'ten [phpMyAdmin] aracını seçiyoruz;
- [2]'te, [Importer] seçeneği seçilir;
![]() |
- [3]'te, [database/dbrdvmedecins.sql] dosyasını seçin;
- [4]'te, bunu çalıştırın;
- [5]'te, veritabanı oluşturulur.
8.4. Web hizmeti / jSON
![]() |
Yukarıdaki mimaride, şimdi Spring MVC çerçevesiyle oluşturulan / jSON web hizmetinin yapımına geçiyoruz. Bunu birkaç adımda 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 servisini yazacağız. Burada Spring MVC'i kullanacağız;
- ardından Spring Security ile kimlik doğrulama kısmını ekleyeceğiz.
Aşağıda, [http://tahe.developpez.com/angularjs-spring4/] belgesinin bir kopyası yer almaktadır; ancak bazı değişiklikler yapılmıştır.
8.4.1. 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, 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 son halini görebiliriz.
8.4.1.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.1.10.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 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 fazladır:
- 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.
8.4.1.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.
- 8. satır: JPA anotasyonu, [Customer] örneklerinin kalıcılığının (Oluşturma, Okuma, Güncelleme, Silme) 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 için başka bir açıklama bulunmamaktadır. 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ılmaz;
Kullanılan JPA uygulamasının hiçbir aşamada adının belirtilmediğine dikkat edilmelidir.
8.4.1.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 sınıf değil, bir arayüzdür (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ü olup burada [Customer] türüdür; ikincisi ise yönetilen öğelerin birincil anahtarının türü olup 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, CRUD işlemlerini (Oluştur – Oku – Güncelle – Sil) tanımlar; bu işlemler, JPA T türü üzerinde gerçekleştirilebilir:
- 8. 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, id birincil anahtarıyla tanımlanan bir T varlığının güncellenmesini de sağlar. Hangi işlemin gerçekleştirileceği, id birincil anahtarının 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;
- 10. satır: Aynı işlem, ancak bu kez bir varlık listesi için;
- 12. satır: findOne yöntemi, birincil anahtarı id ile tanımlanan bir T varlığını bulmaya olanak tanır;
- 22. 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 tanımlayıcı listesi verilen 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);
}
- 9. 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.
8.4.1.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();
}
}
- 10. 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.
- 11. 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ğundan, [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);
- 16. 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;
- 17. 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.
Aşağıdaki işlemler, yalnızca [CustomerRepository] arayüzünü uygulayan bean’in yöntemlerini kullanmaktadır. 50. satırda, bağlamın kapatıldığına dikkat edilmelidir. Konsol çıktıları şöyledir:
- 1-8. satırlar: Spring Boot projesinin logosu;
- 9. satır: [hello.Application] sınıfı çalıştırılır;
- 10. 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;
- 15. satır: [Hibernate] ortaya çıkıyor. Seçilen, JPA adlı bu uygulamadır;
- 19. 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österir;
- 21-22. satırlar: veritabanı oluşturulur. [CUSTOMER] tablosu oluşturulur. Bu, Hibernate'in JPA tanımlarından tabloları oluşturacak şekilde yapılandırıldığı anlamına gelir; burada [Customer] sınıfının JPA tanımı kullanılır;
- 27-31. satırlar: eklenen beş müşteri;
- 33635. satırlar: arayüzün [findOne] yönteminin sonucu;
- satır 37-40: [findByLastName] yönteminin sonuçları;
- 41. satır ve sonrası: Spring bağlamının kapatılmasına ait günlükler.
8.4.1.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.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring işlemleri -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.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>
...
</project>
- 2-18. satırlar: Spring'in temel kütüphaneleri;
- 19-29. satırlar: veritabanı işlemlerini yönetmek için Spring kütüphaneleri;
- 30-35. satırlar: ORM (Nesne-İlişkisel Eşleştirici) ile çalışmak için Spring kütüphanesi;
- satır 36-41: Veritabanına erişim için kullanılan Spring Data;
- satır 42-47: Uygulamayı başlatmak için Spring Boot;
- 54-59. satırlar: SGBD H2;
- satır 60-70: 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 çalıştırılabilir 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();
}
}
- 12. satır: [Main] sınıfında artık yapılandırma açıklamaları bulunmamaktadır;
- 16. 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;
}
}
- 22. satır: [@Configuration] anotasyonu, [Config] sınıfını bir Spring yapılandırma sınıfı haline getirir;
- 21. 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;
- 20. satır: [@EnableTransactionManagement] anotasyonu, [CrudRepository] arayüzlerinin yöntemlerinin bir işlem içinde gerçekleştirilmesi gerektiğini belirtir;
- 19. 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;
- 18. satır: [@ComponentScan] anotasyonu, Spring bileşenlerinin aranacağı klasörleri listelemeyi 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;
- 29. 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;
- 47. 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;
- 49. 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;
- 60. 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ı da aynı sonuçları vermektedir. Proje klasöründe, H2 adlı yeni bir veritabanı dosyası ortaya çıkmaktadır:
![]() |
Son olarak, Spring Boot'tan vazgeçebiliriz. İ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();
}
}
- 15. 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ı hiçbir bağımlılık kalmamıştır.
Çalıştırıldığında önceki ile aynı sonuçlar elde edilir.
8.4.1.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]: çalıştırılacak 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: JAR yürütülebilir bir arşiv biçiminde;
- [8]'te: oluşturulacak yürütülebilir dosyanın yolu ve adı belirtilir;
- [9]: [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:
8.4.1.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.2.0.RELEASE</version>
<relativePath/> <!-- depo'dan ü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;
- satır 13-17: 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() {
}
}
- 9. 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;
- 8. 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 tarafındaki kalıcılık katmanı projesini yazabiliriz.
8.4.2. 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ı;
8.4.3. Maven yapılandırması
![]() | ![]() | ![]() |
Projenin [pom.xml] dosyası şöyledir:
<?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.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring testi -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- sürücü JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- eşleştirici jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Googe Guava -->
<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>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<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;
- 15-18. satırlar: Spring Data için;
- 20-24. satırlar: JUnit testleri için;
- satır 26-29: [DAO] katmanının şifre şifreleme sınıflarından birini kullandığı Spring Security kütüphanesi için;
- satır 31-34: SGBD ve MySQL5 için JDBC sürücüsü;
- satır 36-39: JDBC Tomcat bağlantı havuzu. Bir bağlantı havuzu, bir veritabanına açık olan bağlantıları bir araya getirir. Kod bir bağlantı açmak istediğinde, bu bağlantı havuzdan talep edilir. Kod bağlantıyı kapattığında, bağlantı kapatılmaz, havuza iade edilir. Tüm bu işlemler kod düzeyinde şeffaf bir şekilde gerçekleşir. Bir bağlantının tekrar tekrar açılması ve kapatılması zaman açısından maliyetli olduğundan, bu sayede performans artışı sağlanır. Burada bağlantı havuzu, oluşturulur oluşturulmaz veritabanıyla belirli sayıda bağlantı kurar. Ardından, havuzda depolanan bağlantı sayısı yetersiz hale gelmedikçe, bağlantı açma veya kapatma işlemi gerçekleşmez. Bu durumda, havuz otomatik olarak yeni bağlantılar oluşturur;
- 41-44. satırlar: jSON yönetim kütüphanesi;
- satır 46-50: koleksiyon yönetimi için Google kütüphanesi;
8.4.4. JPA varlıkları
![]() |
JPA varlıkları, 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.IDENTITY)
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) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// alıcı ve ayarlayıcılar
..
}
- 11. 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] açıklaması, [id] alanını birincil anahtar haline getirir. [@GeneratedValue(strategy = GenerationType.IDENTITY)] açıklaması, bu birincil anahtarın değerinin SGBD tarafından oluşturulduğunu ve [IDENTITY] oluşturma modunun zorunlu olduğunu belirtir. SGBD ve MySQL için bu, birincil anahtarların SGBD tarafından [AUTO_INCREMENT] özniteliği kullanılarak oluşturulacağı anlamına gelir
- 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, varlığın iki farklı kullanıcı tarafından 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 edilir;
- satır 21-26: Bir sınıfın [equals] yöntemini yeniden tanımlarken, o sınıfın [hashCode] yöntemini de yeniden tanımlamak gerekir (satır 21-26). Kural şudur: [equals] yöntemi tarafından eşit kabul edilen iki varlık, aynı [hashCode] değerine sahip olmalıdır. Burada, bir varlığın [hashCode] değeri, birincil anahtarı olan [id] ile eşittir. Bir sınıfın [hashCode] değeri, özellikle değerleri o sınıfın örnekleri olan sözlüklerin yönetiminde kullanılır;
[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
...
}
- 6. 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 yapıcı
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- 6. satır: sınıf, JPA varlığıdır;
- 7. satır: veritabanındaki [MEDECINS] tablosuyla ilişkilidir;
- 8. 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());
}
}
- 6. satır: sınıf, JPA varlığıdır;
- 7. satır: veritabanındaki [CLIENTS] tablosuyla ilişkilidir;
- 8. 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
...
}
- 10. satır: sınıf, JPA varlığıdır;
- 11. satır: veritabanındaki [CRENEAUX] tablosuyla ilişkilidir;
- 12. 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;
- 16. satır: zaman aralığının başlangıç saati (14);
- 17. satır: zaman aralığının başlangıç dakikası (20);
- 18. 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;
- 22. 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ştiricinin talebi üzerine aranmasıdır. Böylelikle bellek tasarrufu sağlanır ve performans artışı elde edilir;
- 23. satır: [CRENEAUX] tablosundaki yabancı anahtar sütununun adını belirtir;
- 27-28. satırlar: [MEDECINS] tablosundaki yabancı anahtar;
- 27. 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
...
}
- 14. satır: sınıf, JPA varlığıdır;
- 15. satır: veritabanındaki [RV] tablosuyla ilişkilidir;
- 16. 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;
- 21. satır: randevu tarihi;
- 20. 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ı anahtara sahiptir. Bu ilişki, satır 29-31 ile gösterilmektedir;
- 34-35. satırlar: [idClient] yabancı anahtarı;
- 36-37. satırlar: [idCreneau] yabancı anahtarı;
8.4.5. [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> {
}
- 7. 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> {
}
- 7. 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);
}
- 8. 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;
- 11. satır: 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 yarar. [?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);
}
- 10. 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;
- 13. satır: Parametreler, doktorun kimliği ve gündür. Sonuç, bir [Iterable<Rv>] nesnesi biçiminde bir randevu listesidir;
- 12. 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;
8.4.6. [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;
8.4.6.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
...
}
- 12. satır: zaman aralığı;
- 13. 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
...
}
- 13. satır: doktor;
- 14. satır: ajandadaki gün;
- 15 numaralı hat: randevulu veya randevusuz hizmet saatleri;
8.4.6.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 ekle
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) {
...
}
}
- 24. 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;
- 25. satır: [Metier] sınıfı, [IMetier] arayüzünü uygular;
- 28. 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 herhangi bir ad belirtmemektedir. Dolayısıyla, türüne göre enjeksiyon yapılacaktır;
- 29. 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ı;
- 40. 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 uygulanmaktadır;
Yalnızca 88. satırdaki yöntem, [métier] katmanına özgüdür. Bu yöntem, basit bir veri erişiminden öte bir iş mantığı işlemi 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 takvim 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);
// rezervasyon 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 doktorun tüm randevuları alınır;
- bu iki bilgiyle, bir zaman aralığının boş mu yoksa dolu mu olduğunu belirleyebiliriz;
8.4.7. Spring projesinin yapılandırması
![]() |
[DomainAndPersistenceConfig] sınıfı, projenin tamamını yapılandırır:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
// varlık paketleri JPA
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// veri kaynağı MySQL
@Bean
public DataSource dataSource() {
// veri kaynağı TomcatJdbc
DataSource dataSource = new DataSource();
// JDBC yapılandırması
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// başlangıçta açık olan bağlantılar
dataSource.setInitialSize(5);
// sonuç
return dataSource;
}
// JPA sağlayıcısı Hibernate'dir
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
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;
}
}
- 17. satır: Bu sınıf, bir Spring yapılandırma sınıfıdır;
- 18. satır: Spring Data'nın [CrudRepository] arayüzlerinin bulunduğu paketler. Bunlar Spring bağlamına eklenecektir;
- 19. satır: [rdvmedecins] paketindeki ve Spring anotasyonu içeren tüm alt sınıfları Spring bağlamına ekler. [rdvmdecins.metier] paketinde, [Metier] sınıfı, [@Service] anotasyonuyla birlikte bulunacak ve Spring bağlamına eklenecektir;
- 26-39. satırlar: JDBC Tomcat bağlantı havuzunu yapılandırır (5. satır);
- 36. satır: Bağlantı havuzunda varsayılan olarak 5 açık bağlantı olacaktır. Bu satır örnek amaçlı gösterilmiştir. Bizim durumumuzda 1 bağlantı yeterli olacaktır. [DAO] katmanının birden fazla iş parçacığı tarafından kullanılması durumunda bu satır gerekli olacaktır. Bu durum, ileride [DAO] katmanının, doğası gereği aynı anda hizmet verilen birden fazla kullanıcıyı destekleyen bir web uygulamasına destek sağladığında geçerli olacaktır;
- 42-49. satırlar: Kullanılan JPA uygulaması bir Hibernate uygulamasıdır;
- satır 45: SQL için günlük kaydı yok;
- satır 46: Tabloların yeniden oluşturulmaması;
- satır 47: Kullanılan SGBD, MySQL'tir;
- satır 53-61: JPA katmanının EntityManagerFactory'sini tanımlar. Bu nesneden, JPA işlemlerini gerçekleştirmeye olanak tanıyan [EntityManager] nesnesi elde edilir;
- 57. satır: JPA varlıklarının bulunduğu paket veya paketler belirtilir;
- satır 58: JPA katmanına bağlanacak veri kaynağı belirtilir;
- satır 64-69: önceki EntityManagerFactory ile ilişkili işlem yöneticisi. Varsayılan olarak, Spring Data'nın [CrudRepository] arayüzlerinin yöntemleri bir işlem içinde gerçekleştirilir. İşlem, yönteme girilmeden önce başlatılır ve yöntemden çıkıldıktan sonra (commit veya rollback ile) sonlandırılır;
8.4.8. [métier] katmanındaki testler
[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 ekle
// 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);
}
}
}
- 22. 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;
- 23. 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;
- 50. satır: yeni bir randevunun eklenmesi. [ajouterRv] yöntemi, randevuyu ek bir bilgi, yani birincil anahtarı olan id ile döndürür;
- 53. satır: Veritabanında randevuyu aramak için bu birincil anahtar kullanılı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ı kimliğe 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.
- 73. satır: Bir istisna oluştuğu doğrulanır;
- 77. satır: az önce randevu eklenen doktorun takvimi istenir;
- 79. 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ı:
![]() |
8.4.9. Konsol programı
![]() |
Konsol programı basittir. Yabancı bir anahtarın nasıl alınacağını 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.
- 19. satır: [SpringApplication] sınıfı, [DomainAndPersistenceConfig] yapılandırma sınıfını kullanacaktır;
- 20. satır: Uygulamanın başlangıç günlükleri silinir;
- 22. 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;
- 24. 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ı zaman dilimine sahip doktoru öğrenmek istiyoruz. Bunun için veritabanına gidip 1 numaralı zaman dilimini aramamız gerekiyor. [FetchType.LAZY] modunda olduğumuz için, doktor zaman dilimiyle birlikte getirilmiyor. Ancak, doktorun birincil anahtarını almak için [Creneau] varlığında bir [idMedecin] alanı öngörülmüştür;
- 35. satır: doktorun birincil anahtarını alıyoruz;
- 36. satır: doktorun randevu listesi görüntülenir;
Konsol çıktıları şu şekildedir:
8.4.10. Günlük Yönetimi
Konsol günlükleri, [application.properties] ve [logback.xml] [1] adlı iki dosya ile yapılandırılır:
![]() |
[application.properties] dosyası, Spring Boot çerçeve yapısı tarafından kullanılır. Bu dosyada, Spring Boot tarafından varsayılan olarak alınan değerleri değiştirmek için çok sayıda parametre tanımlanabilir (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Dosyanın içeriği şu şekildedir:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- 1. satır: Hibernate günlük düzeyini kontrol eder – burada günlük yok
- 2. satır: Spring Boot başlığının görüntülenmesini kontrol eder - burada başlık yoktur
[logback.xml] dosyası, [logback] ve [2] günlük çerçevelerinin yapılandırma dosyasıdır:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodlayıcılara varsayılan olarak ch.qos.logback.classic.encoder.PatternLayoutEncoder türü atanır -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- günlük düzeyini kontrol eder -->
<root level="info"> <!-- kapalı, bilgi, hata ayıklama, uyarı -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Genel günlük seviyesi 9. satırdan kontrol edilir - burada [info] seviyesinde günlükler;
Bu, şu sonucu verir:
Hibernate'in günlük düzeyini [info] olarak ayarlarsak (diğer ayarları değiştirmeden):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
şu sonuç ortaya çıkar:
Günlük düzeyini (diğer ayarları değiştirmeden) [debug] olarak ayarlarsak:
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
şu sonuç ortaya çıkar:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. [web / jSON] katmanı
![]() |
![]() |
[web / jSON] 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;
8.4.11.1. 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" 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>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring MVC web katmanı -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test katmanı -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- DAO katmanı -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- 12-15. satırlar: üst Maven projesi;
- 19-22. satırlar: MVC adlı Spring projesi için bağımlılıklar;
- 24-28. satırlar: JUnit / Spring testleri için bağımlılıklar;
- 30-34. satırlar: [métier, DAO, JPA] katmanlarının projesine olan bağımlılıklar;
8.4.11.2. Web hizmeti arayüzü
![]() |
- yukarıdaki [1]'te, 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 [Response] türündeki bir nesnenin jSON dönüşümüne karşılık gelen aynı biçime sahip olacaktır:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- özellikler
// işlem durumu
private int status;
// olası hata mesajları
private List<String> messages;
// yanıt gövdesi
private T body;
// yapıcılar
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter ve setter'lar
...
}
- 7. satır: yanıtın hata kodu 0: OK, diğer durumlar: KO;
- 11. satır: hata varsa, hata mesajlarının listesi;
- 13. satır: yanıtın gövdesi;
Şimdi, web hizmeti / jSON arayüzünü gösteren ekran görüntülerini sunuyoruz:
[/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 eklentisi [Advanced Rest Client]'i kullanıyoruz, çünkü bu işlemler POST ile gerçekleştiriliyor.
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ı. [body] alanı, eklenen randevunun jSON biçimini içerir;
Yeni randevunun varlığı şu şekilde doğrulanabilir:
![]() |
Randevunun [50] kimliğini not edelim. Bu randevuyu sileceğiz.
[/supprimerRv] randevusunu silme
![]() |
- [1]'e, web hizmetinin URL'i;
- [2]'e, POST yöntemi kullanılır;
- [3]'te, web hizmetine {idRv} biçiminde 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 GERMAIN] 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, yakında tanıtacağımız [RdvMedecinsController] denetleyicisi tarafından işlenir.
8.4.11.3. Web hizmetinin yapılandırılması
![]() |
[AppConfig] yapılandırma sınıfı şu şekildedir:
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- 12. satır: [AppConfig] sınıfı, uygulamanın tamamını yapılandırır;
- 9. satır: [AppConfig] sınıfı bir Spring yapılandırma sınıfıdır;
- 10. satır: Spring bileşenlerinin [rdvmedecins.web] paketinde ve alt paketlerinde aranması istenir. Böylece şu bileşenler bulunacaktır:
- [@RestController RdvMedecinsController], [rdvmedecins.web.controllers] paketinde;
- [@Component ApplicationModel], [rdvmedecins.web.models] paketinde;
- 11. satır: [rdvmedecins-metier-dao] projesinin bean'lerine erişebilmek için bu projeyi yapılandıran [DomainAndPersistenceConfig] sınıfı içe aktarılır;
- 11. satır: [SecurityConfig] sınıfı, web uygulamasının güvenliğini yapılandırır. Şimdilik bunu göz ardı edeceğiz;
- 11. satır: [WebConfig] sınıfı, [web / jSON] katmanını yapılandırır;
[WebConfig] sınıfı şu şekildedir:
package rdvmedecins.web.config;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
// başlıklar için dispatcherservlet yapılandırması CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// eşleştiriciler jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- 20-25. satırlar: [dispatcherServlet] bean’ini tanımlar. [DispatcherServlet] sınıfı, Spring MVC çerçevesinin servlet’idir. Bu sınıf, [FrontController] rolünü üstlenir: Spring MVC sitesine yöneltilen istekleri yakalar ve sitenin denetleyicilerinden (Controller) biri tarafından işlenmesini sağlar;
- 22. satır: sınıfın örneklenmesi;
- 23. satır: Bu satır şimdilik göz ardı edilebilir;
- 27-30. satırlar: [dispatcherServlet] servleti, tüm URL isteklerini işler;
- 27-30. satırlar: projenin bağımlılıklarında bulunan yerleşik Tomcat sunucusunu etkinleştirir. Sunucu 8080 numaralı bağlantı noktasında çalışacaktır;
- satır 38-67: farklı jSON filtreleriyle yapılandırılmış dört adet jSON eşleştirici;
- satır 38-41: filtre içermeyen bir jSON eşleştirici;
- satır 43-49: jSON [jsonMapperShortCreneau] eşleştiricisi, [Creneau.medecin] alanını göz ardı ederek bir [Creneau] nesnesini serileştirir/deserileştirir;
- satır 51-59: jSON [jsonMapperLongRv] eşleştiricisi, [Rv] nesnesini [Rv.creneau.medecin] alanını göz ardı ederek serileştirir/deserileştirir;
- satır 61-67: jSON ve [jsonMapperShortRv] eşleştiricileri, [Rv] nesnesini, [Rv.creneau.medecin] alanını göz ardı ederek serileştirir / [Rv] nesnesini, [Rv.creneau] ve [Rv.client] alanlarını göz ardı ederek serileştirir/deserileştirir;
8.4.11.4. [ApplicationModel] sınıfı
![]() |
[ApplicationModel] sınıfı bize iki konuda yardımcı olacaktır:
- doktor ve hasta (müşteri) listelerini depolamak için önbellek olarak;
- denetleyiciler için tek bir arayüz olarak;
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@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;
private List<String> messages;
// yapılandırma verileri
private boolean CORSneeded = false;
private boolean secured = false;
@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(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// alıcı ve ayarlayıcılar
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- 19. 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 tek bir nesne örneklenecektir (singleton);
- 20. satır: [ApplicationModel] sınıfı, [IMetier] arayüzünü uygular;
- 23-24. satırlar: [métier] katmanına ait bir referans Spring tarafından enjekte edilir;
- 34. satır: [@PostConstruct] anotasyonu, [ApplicationModel] sınıfının örneklenmesinden hemen sonra [init] yönteminin çalıştırılmasını sağlar;
- 38-39. satırlar: [métier] katmanından doktor ve müşteri listeleri alınır;
- 41. satır: Bir istisna meydana gelirse, istisna yığınındaki mesajlar 17. satırdaki alana kaydedilir;
Web katmanının mimarisi şu şekilde gelişmektedir:
![]() |
- [2b]'te, kontrolörlerin 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.
8.4.11.5. Static 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.util.ArrayList;
import java.util.List;
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;
}
}
- 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üşterileri 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.
8.4.11.6. [RdvMedecinsController] denetleyicisinin iskeleti
![]() |
Şimdi web hizmetinin 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ı;
![]() |
[RdvMedecinsController] denetleyicisi şu şekildedir:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// mesaj listesi
private List<String> messages;
// eşleştiriciler jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// uygulama hata mesajları
messages = application.getMessages();
}
// doktor listesi
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// müşteri listesi
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// bir doktorun randevu listesi
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// bir doktorun randevu listesi
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- 35. satır: [@Controller] anotasyonu, [RdvMedecinsController] sınıfını bir Spring denetleyicisi haline getirir; bu, MVC'in C'sidir;
- 38-39. satırlar: Buraya Spring tarafından [ApplicationModel] türünde bir nesne enjekte edilecektir. Bunu daha önce tanıtmıştık;
- 41-42. satırlar: Buraya Spring tarafından [RdvMedecinsCorsController] türünde bir nesne enjekte edilecektir. Bu nesneyi ancak daha sonra tanıtacağız;
- satır 48-58: [WebConfig] yapılandırma sınıfında tanımlanan jSON eşleştiricileri;
- satır 60: [@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;
- satır 63: [ApplicationModel] nesnesinden olası hata mesajları alınır. Bu nesne, uygulamanın başlatılması sırasında oluşturulmuş ve doktorları ve müşterileri önbelleğe almaya çalışmıştır. Eğer bu işlem başarısız olursa, [messages!=null] nesnesi elde edilir. Bu, denetleyicinin yöntemlerinin uygulamanın doğru şekilde başlatılıp başlatılmadığını bilmesini sağlar;
- 67-118. satırlar: [web / jSON] hizmeti tarafından sunulan URL. Tüm yöntemler, aşağıdaki [Response<T>] türündeki bir nesnenin jSON dizesini döndürür:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- özellikler
// işlemin durumu
private int status;
// olası hata mesajları
private List<String> messages;
// yanıt gövdesi
private T body;
// yapıcılar
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// alıcı ve ayarlayıcılar
...
}
- 9. satır: bir hata kodu; 0, hata olmadığı anlamına gelir;
- 11. satır: [status!=0] ise, [messages] bir hata mesajları listesidir;
- 13. satır: Yanıtta kapsüllenmiş bir T nesnesi. Hata durumunda T’nin değeri null olur;
Bu nesne, istemci tarayıcısına gönderilmeden önce jSON olarak serileştirilir;
- 67. satır: Açıklanan URL, [/getAllMedecins]'tir. İstemci, isteğini yapmak için [GET] yöntemini kullanmalıdır (method = RequestMethod.GET). Eğer bu URL, bir POST tarafından talep edilseydi, reddedilirdi ve Spring MVC, web istemcisine bir HTTP hata kodu gönderirdi. Yöntem, yanıtı müşteriye kendisi geri gönderir (68. satır). Bu, bir karakter dizisi olacaktır (67. satır). HTTP [Content-type : application/json; charset=UTF-8] başlığı, müşteriye bir jSON dizisi alacağını belirtmek için gönderilecektir (satır 67);
- 77. satır: URL, {idMedecin} ile ayarlanır. Bu parametre, 79. satırdaki [@PathVariable] açıklamasıyla alınır;
- satır 79: [long idMedecin] parametresi, değerini URL ve [@PathVariable("idMedecin")]'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;
- 105. satır: [@RequestBody] açıklaması, isteğin gövdesini belirtir. Bir GET isteğinde neredeyse hiçbir zaman gövde bulunmaz (ancak bir gövde eklenebilir). Bir POST isteğinde ise çoğu zaman gövde bulunur (ancak gövde eklenmeyebilir). URL ve [ajouterRv] için, web istemcisi POST'inde aşağıdaki jSON dizesini gönderir:
[@RequestBody PostAjouterRv post] sözdizimi (105. satır) ile birlikte, yöntemin 103. satırda jSON [consumes = "application/json; charset=UTF-8"] değerini beklemesi, web istemcisi tarafından gönderilen jSON dizesinin [PostAjouterRv] türünde bir nesneye deserialize edilmesine neden olacaktır. Bu nesne şöyledir:
package rdvmedecins.web.models;
public class PostAjouterRv {
// gönderim verileri
private String jour;
private long idClient;
private long idCreneau;
// getter ve setter'lar
...
}
Burada da gerekli tür değişiklikleri otomatik olarak gerçekleşecektir;
- 107-109. 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
...
}
8.4.11.7. URL [/getAllMedecins]
URL ve [/getAllMedecins], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi ile işlenir:
// doktor listesi
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// yanıt
Response<List<Medecin>> response;
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// doktor listesi
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// yanıt
return jsonMapper.writeValueAsString(response);
}
- 9-10. satırlar: Uygulamanın doğru şekilde başlatılıp başlatılmadığı kontrol edilir (messages==null). Başlatılmamışsa, status=-1 ve body=messages ile bir yanıt gönderilir;
- 13. satır: aksi takdirde, [ApplicationModel] sınıfından doktor listesi istenir;
- 19. satır: jSON dizesini, jSON ve [jsonMapper] eşleştiricileriyle yanıt olarak gönderilir, çünkü [Medecin] sınıfındajSON filtresine sahip değildir. Yanıt hatasız (14. satır) veya hatalı (16. satır) olabilir. [application.getAllMedecins()] yöntemi, yalnızca önbellekte bulunan bir listeyi döndürdüğü için istisna oluşturmaz. Yine de, doktorların artık önbellekte tutulmaması durumuna karşı bu istisna yönetimini koruyacağız;
Uygulamanın hatalı bir şekilde başlatıldığı durumu henüz örneklemedik. 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:
![]() |
8.4.11.8. URL [/getAllClients]
URL [/getAllClients], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi tarafından işlenir:
// müşteri listesi
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// yanıt
Response<List<Client>> response;
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// müşteri listesi
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// yanıt
return jsonMapper.writeValueAsString(response);
}
Bu yöntem, daha önce incelenen [getAllMedecins] yöntemine benzerdir. Elde edilen sonuçlar şunlardır:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// yanıt
Response<List<Creneau>> response;
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// doktor bilgisi alınır
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// doktorun randevu saatleri
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// yanıt
return jsonMapperShortCreneau.writeValueAsString(response);
}
- 12. satır: [id] parametresiyle tanımlanan doktor, yerel bir yönteme çağrılır:
private Response<Medecin> getMedecin(long id) {
// doktor bilgisi alınıyor
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// doktor mevcut mu?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// tamam
return new Response<Medecin>(0, null, médecin);
}
Bu yöntemden, status değeriyle [0,1,2]'e geri dönülür. [getAllCreneaux] yönteminin koduna geri dönelim:
- 13-14. satırlar: status!=0 ise, hata içeren bir yanıt oluşturulur;
- 16. satır: doktor bilgisi alınır;
- 19. satır: bu doktorun randevu aralıkları alınır;
- 25. satır: yanıt olarak bir [List<Creneau>] nesnesi gönderilir. [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 zaman aralığının özellikleri
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// bir randevu bir doktora 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;
...
}
- 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 işlemini zorunlu kılar. Bu nedenle sorgu, doktorun tüm randevu aralıklarını ve her birinde doktorun adını içerecek şekilde sonuç verir. Bu randevu zaman aralıklarını jSON'e serileştirdiğimizde, her birinde doktorun jSON dizesi görünür. Bu gereksizdir. Serileştirmeyi kontrol etmek için iki şeye ihtiyacımız var:
- serileştirilecek nesneye erişim;
- serileştirilecek nesneyi yapılandırmak;
- madde, denetleyicide nesneye uygun jSON dönüştürücüsünün eklenmesiyle sağlanır:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
- madde, [rdvmedecins-metier-dao] projesinde tanımlanan [Creneau] sınıfına bir açıklama eklenerek sağlanır:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- 3. satır: jSON Jackson kütüphanesinden bir açıklama. Bu, [creneauFilter] adlı bir filtre oluşturur. Bu filtre yardımıyla, serileştirilmesi gereken ve serileştirilmemesi gereken alanları programlama yoluyla tanımlayabileceğiz;
[Creneau] nesnesinin serileştirilmesi, [getAllCreneaux] yönteminin aşağıdaki satırında gerçekleştirilir:
// yanıt
return jsonMapperShortCreneau.writeValueAsString(response);
jSON [jsonMapperShortCreneau] eşleştiricisi, [WebConfig] sınıfında şu şekilde tanımlanmıştır:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- 5. satır: [creneauFilter] adlı filtre, 4. satırdaki [creneauFilter] filtresiyle ilişkilendirilmiştir. Bu filtre, [Creneau] nesnesini [medecin] alanı hariç tutularak serileştirir;
[getAllCreneaux] yöntemi tarafından döndürülen sonuç, [Response<List<Creneau>] türünde olan jSON dizesidir.
Elde edilen sonuçlar şunlardır:
![]() |
ya da zaman aralığı mevcut değilse şunlar:
![]() |
Bu örnekten şu kuralı çıkarabiliriz:
- web sunucusunun / jSON yöntemleri, [Response<T>] türünde bir nesne döndürür ve bu nesne jSON olarak serileştirilir;
- T türünün bir veya daha fazla jSON filtresi varsa, onu serileştirmek için aynı filtrelere sahip bir eşleştirici kullanılır;
8.4.11.10. URL [/getRvMedecinJour/{idMedecin}/{jour}]
URL [/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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// yanıt
Response<List<Rv>> response=null;
boolean erreur = false;
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// tarih kontrol ediliyor
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// doktor bilgisi alınır
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// randevu listesi
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// yanıt
return jsonMapperLongRv.writeValueAsString(response);
}
- jSON dizesini [Response<List<Rv>>] türüne dönüştürmek gerekir. [Rv] sınıfında bir [Rv.creneau] alanı bulunur. Bu alan serileştirilirse, jSON [creneauFilter] filtresiyle karşılaşılır;
- 47. satır: 7. satırdaki [Response<List<Rv>>] türündeki nesne, jSON olarak serileştirilir;
- satırda randevu listesinin alındığı durumu inceleyelim. [rdvmedecins-metier-dao] projesindeki [Rv] sınıfı şu şekilde tanımlanmıştır:
@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ır;
- 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. Ancak bu yinelenen bilgi gereksizdir. [Creneau] nesnesi üzerinde jSON filtresini kullanarak bu sorunu nasıl çözebileceğimizi gördük. [Rv] sınıfındaki [client] ve [creneau] alanlarının [FetchType.LAZY] modları nedeniyle, yakında [rdvmedecins-metier-dao] projesinin [RV] sınıfına bir jSON filtresi uygulamamız gerekeceğini göreceğiz:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
[Rv] nesnesinin serileştirilmesini [rvFilter] filtresi ile kontrol edeceğiz. Görünüşe göre burada filtrelemeye gerek yok, çünkü [Rv] türündeki nesnenin tüm alanlarına ihtiyacımız var. Bununla birlikte, sınıfın jSON filtresine sahip olduğunu belirttiğimiz için, [Rv] türündeki bir nesnenin her serileştirilmesinde bu filtreyi tanımlamamız gerekir; aksi takdirde bir istisna ile karşılaşırız. Bunun için, [rdvMedecinsController] sınıfında tanımlanan aşağıdaki jSON eşleştiricisini kullanıyoruz:
@Autowired
private ObjectMapper jsonMapperLongRv;
Bu eşleştirici, [WebConfig] yapılandırma sınıfında şu şekilde tanımlanmıştır:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
- 4. satır: [Rv] nesnesinin tüm alanlarının serileştirilmesi gerektiğini belirtiriz;
- 5. satır: [Creneau] nesnesinde, [medecin] alanının serileştirilmemesi gerektiğini belirtiriz;
- 6. satır: [jsonMapperLongRv] nesnesindeki jSON filtrelerine [rvFilter] ve [creneauFilter] filtrelerini ekliyoruz;
Elde edilen sonuçlar şunlardır:
![]() |
ya da randevusuz bir gün için şu sonuçlar:
![]() |
ya da gün bilgisi yanlış olanlar:
![]() |
ya da yanlış doktor bilgisi içeren şu örnekler:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// yanıt
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// tarih kontrol ediliyor
Date jourAgenda = null;
if (!erreur) {
// tarih kontrol ediliyor
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// doktor bilgisi alınıyor
Medecin médecin = null;
if (!erreur) {
// doktor bilgisi alınıyor
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// takvimi alınıyor
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// yanıt
return jsonMapperLongRv.writeValueAsString(response);
}
- 6. ve 49. satırlar: jSON dizesi, bir [AgendaMedecinJour] türüne dönüştürülür ve bir [Response] nesnesine kapsüllenir;
[AgendaMedecinJour] türü şu şekildedir:
public class AgendaMedecinJour implements Serializable {
// alanlar
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
[CreneauMedecinJour] türü şöyledir:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// alanlar
private Creneau creneau;
private Rv rv;
[creneau] ve [rv] alanlarının, yapılandırılması gereken jSON filtreleri vardır. Daha önce karşılaştığımız jSON ve [jsonMapperLongRv] eşleştiricilerini kullanan [getAgendaMedecinJour] yönteminin 49. satırı bunu yapmaktadır:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
Elde edilen sonuçlar şunlardır:
![]() |
Yukarıda görüldüğü gibi, 28/01/2015 tarihinde Dr. PELISSIER'in saat 08:20'de Bayan Brigitte BISTROU ile randevusu vardır;
ya da tarih yanlışsa şunlar:
![]() |
ya da doktor numarası geçersizse şunlar:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// yanıt
Response<Medecin> response;
// uygulamanın durumu
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// yanıt
return jsonMapper.writeValueAsString(response);
}
- 5. ve 13. satırlar: Yöntem, jSON dizesini [Medecin] türüne dönüştürür. Bu türde jSON filtre açıklaması yoktur. Ayrıca, 14. satırda, jSON eşleştiricisi filtreler olmadan kullanılmaktadır;
- satırda, [getMedecin] yöntemi şu şekildedir:
private Response<Medecin> getMedecin(long id) {
// doktor aranıyor
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// doktor mevcut mu?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// tamam
return new Response<Medecin>(0, null, médecin);
}
Elde edilen sonuçlar şunlardır:
![]() |
veya doktor numarası yanlışsa şu sonuçlar:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// yanıt
Response<Client> response;
// uygulamanın durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// yanıt
return jsonMapper.writeValueAsString(response);
}
- 5. ve 13. satırlar: Yöntem, jSON dizesini [Client] türüne dönüştürür. Bu türde jSON filtreleri için herhangi bir açıklama yoktur. Ayrıca, 13. satırda, jSON eşleştiricisi filtreler olmadan kullanılmaktadır;
- satırda, [getClient] yöntemi şu şekildedir:
private Response<Client> getClient(long id) {
// müşteri bilgisi alınıyor
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// müşteri mevcut mu?
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// tamam
return new Response<Client>(0, null, client);
}
Elde edilen sonuçlar şunlardır:
![]() |
veya müşteri numarası yanlışsa şu sonuçlar:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// yanıt
Response<Creneau> response;
// uygulamanın durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// zaman aralığı iade ediliyor
response = getCreneau(id);
}
// yanıt
return jsonMapperShortCreneau.writeValueAsString(response);
}
- 5. ve 14. satırlar: Yöntem, jSON dizesini [Response<Creneau>] türünde döndürür;
- satırda, [getCreneau] yöntemi şu şekildedir:
private Response<Creneau> getCreneau(long id) {
// zaman aralığını geri alıyoruz
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// zaman aralığı mevcut mu?
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// tamam
return new Response<Creneau>(0, null, créneau);
}
[Creneau] varlığının kodunu hatırlayalım:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
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 doktora bağlı
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// dış anahtar
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- 14-16. satırlar: [medecin] alanı [fetch = FetchType.LAZY] modunda olduğu için, [id] aracılığıyla bir zaman aralığı arandığında bu alan geri getirilmez. Bu nedenle, onu serileştirme işleminden hariç tutmak gerekir. Bu hariç tutma yapılmazsa bir istisna oluşur. Bu istisna, [mapper] serileştirme nesnesinin, [medecin] alanını elde etmek için [getMedecin] yöntemini çağırmasından kaynaklanmaktadır. Ancak, JPA / Hibernate uygulamasında, [medecin] alanının [fetch = FetchType.LAZY] modu, [Creneau] nesnesini döndürdü; bu nesnenin [getMedecin] yöntemi, JPA bağlamında doktoru bulmak üzere programlanmıştır. Buna bir [proxy] nesnesi denir. Şimdi web uygulamasının mimarisini hatırlayalım:
![]() |
Denetleyici, [Contrôleurs / Actions] bloğunda bulunur. Bu blokta bulunduğumuzda, JPA bağlamı kavramı artık geçerli değildir. Bu bağlam, [DAO] katmanındaki işlemler sırasında oluşturulur. Bu işlemler bittiğinde ortadan kalkar. Dolayısıyla denetleyici, JPA bağlamına erişmeye çalıştığında, bağlamın kapalı olduğunu belirten bir istisna oluşur. Bu istisnayı önlemek için, [Rv] sınıfındaki [medecin] alanının serileştirilmesini engellemek gerekir. jSON - [jsonMapperShortCreneau] eşleştiricisi bunu yapar:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
Elde edilen sonuçlar şunlardır:
![]() |
veya zaman aralığı numarası yanlışsa şunlar:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// yanıt
Response<Rv> response;
// uygulamanın durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// randevu alınır
response = getRv(id);
}
// yanıt
return jsonMapperShortRv.writeValueAsString(response);
}
- 5. ve 14. satırlar: Yöntem, [Response<Rv>] türünde jSON dizesini döndürür;
- satırda, [getRv] yöntemi şu şekildedir:
private Response<Rv> getRv(long id) {
// Rv alınıyor
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Rv mevcut mu?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// tamam
return new Response<Rv>(0, null, rv);
}
[Rv] sınıfında, [fetch = FetchType.LAZY] etiketine sahip iki alan vardır: [creneau] ve [client] alanları. Dolayısıyla, bir [Rv]'i birincil anahtarı üzerinden aradığımızda bu alanlar getirilmez. Bu nedenle, daha önce belirtilen nedenlerle, bu alanları serileştirme işleminden hariç tutmak gerekir. [WebConfig] sınıfında tanımlanan aşağıdaki [jsonMapperShortRv] eşleştiricisi bunu gerçekleştirir:
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
Elde edilen sonuçlar şunlardır:
![]() |
ya da randevu numarası yanlışsa şunlar:
![]() |
8.4.11.16. URL [/ajouterRv]
URL [/ajouterRv], [RdvMedecinsController] denetleyicisi tarafından aşağıdaki yöntemle işlenir:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
// yanıt
Response<Rv> response = null;
boolean erreur = false;
// uygulamanın durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// gönderilen değerler alınır
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// gönderilen değerler alınıyor
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// tarih kontrol ediliyor
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// zaman aralığı alınır
Response<Creneau> responseCréneau = null;
if (!erreur) {
// zaman aralığı alınır
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// müşteri bilgisi alınır
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// müşteriyi alıyoruz
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// randevu ekleniyor
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// yanıt
return jsonMapperLongRv.writeValueAsString(response);
}
- 5. ve 67. satırlar: Yöntem, jSON dizesini [Response<Rv>] türünde döndürmelidir;
- 3. satır: [@RequestBody PostAjouterRv post] açıklaması, POST'in gövdesini alır ve bunu [PostAjouterRv post] parametresine yerleştirir. Bu gövde, jSON [consumes = "application/json; charset=UTF-8"]'e aittir ve aşağıdaki [PostAjouterRv] türüne otomatik olarak deserialize edilecektir:
public class PostAjouterRv {
// gönderinin verileri
private String jour;
private long idClient;
private long idCreneau;
...
- ardından, bir şekilde daha önce karşılaşılmış olan kodlar vardır;
- 67. satır: jSON, [creneauFilter] ve [rvFilter] filtrelerinin kurulması. Yöntem, jSON dizesini, 61. satırda elde edilen Rv değerine sahip [Response<Rv>] türüne dönüştürür. [Rv] nesnesi, bir [Creneau] nesnesini ve bir [Client] nesnesini kapsular. [Creneau] nesnesi, [Medecin] nesnesine [FetchType.LAZY] bağımlılığına sahiptir ve 36-44. satırlarda elde edilmiştir. JPA bağlamında birincil anahtarı aracılığıyla aranmış ve [FetchType.LAZY] bağımlılığı olmadan elde edilmiştir. Sonuç olarak,
- [Rv] nesnesinin tüm bağımlılıkları mevcuttur. Bunlar serileştirilebilir;
- [Creneau] nesnesi, [medecin] bağımlılığına sahip değildir. Dolayısıyla bu bağımlılığın serileştirilmemesi gerekir;
[WebConfig] sınıfında tanımlanan jSON [jsonMapperLongRv] eşleştiricisi bu kısıtlamaları karşılar:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
[Advanced Rest Client] istemcisiyle elde edilen sonuçlar şuna benzer:
![]() |
- [1]'te, POST'in URL'i;
- [2]'e, POST;
- [3]'e, gönderilen değer;
- [4a]'te, bu gönderilen değer jSON'ten alınmıştır;
![]() |
- [4b]'te, istemci jSON gönderdiğini belirtir;
- [5]'te, sunucu jSON'i geri gönderdiğini belirtir;
![]() |
- [6]'te, sunucunun eklediği randevuyu temsil eden jSON yanıtı yer almaktadır. Burada, eklenen randevunun [id] kimliği görülmektedir;
Var olmayan bir zaman aralığı numarası kullanıldığında şu sonuç elde edilir:
![]() |
8.4.11.17. URL [/supprimerRv]
URL [/supprimerRv], [RdvMedecinsController] denetleyicisinin aşağıdaki yöntemi ile işlenir:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// yanıt
Response<Void> response = null;
boolean erreur = false;
// başlıklar CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// uygulama durumu
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// gönderilen değerler alınır
long idRv = post.getIdRv();
// rv'yi alıyoruz
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// rv'nin silinmesi
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// yanıt
return jsonMapper.writeValueAsString(response);
}
- 5. satır: [Void] türü, [void] temel türüne karşılık gelen sınıftır;
- 5. ve 34. satırlar: Yöntem, jSON dizesini, jSON filtreleri içermeyen [Response<Void>] türünde döndürür. Bu nedenle, 34. satırda filtre içermeyen jSON eşleştiricisi kullanılır;
- 3. satır: Yöntemin parametresi, POST gövdesi, yani gönderilen değerdir. Bu değer, jSON [consumes = "application/json; charset=UTF-8"] biçiminde alınır ve otomatik olarak aşağıdaki [PostSupprimerRv] türüne deserialize edilir:
public class PostSupprimerRv {
// gönderilen veriler
private long idRv;
- 28. satır: Silme işlemi başarılı olduğunda, [status=0] ile bir yanıt gönderilir;
Elde edilen sonuçlar şunlardır:
![]() |
![]() |
- [5]'te, [status=0] alanı silme işleminin başarılı olduğunu gösterir;
Var olmayan bir randevu numarası kullanıldığında şu sonuç elde edilir:
![]() |
Denetleyiciyle işimiz bitti. Şimdi projeyi nasıl çalıştıracağımıza bakalım.
8.4.11.18. Web hizmetinin yürütülebilir sınıfı
![]() |
[Boot] [1] 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 gömülü Tomcat sunucusunu başlatacak ve [RdvMedecinsController] denetleyicisini bu sunucuya dağıtacaktır.
Günlükler, aşağıdaki [2] dosyaları tarafından kontrol edilir:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodlayıcılara varsayılan olarak ch.qos.logback.classic.encoder.PatternLayoutEncoder türü atanır -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- günlük seviyesi kontrolü -->
<root level="info"> <!-- kapalı, bilgi, hata ayıklama, uyarı -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- 9. satır: genel günlük seviyesi [info] olarak ayarlanır;
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
1-2. satırlar, uygulamanın belirli öğeleri için özel bir günlük düzeyine izin verir:
- satır 1: [web] katmanının günlüklerini istiyoruz;
- 2. satır: [JPA] katmanının günlüklerini istemiyoruz;
- 3. satır: Spring Boot başlığı gösterilmesin;
Çalıştırma sırasında elde edilen günlükler şunlardır:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- 18. satır: Tomcat sunucusu aktif;
- 21. satır: Spring ortamı başlatılıyor;
- 27-38. satırlar: Web hizmeti tarafından sunulan URL öğeleri tespit edildi;
- 44. satır: Tomcat sunucusu hazır ve 8080 numaralı bağlantı noktasında istekleri bekliyor;
[application.properties] dosyasını şu şekilde değiştirirsek:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
aşağıdaki günlük kayıtları elde edilir:
Ayrıca, [logback.xml] dosyasını şu şekilde değiştirirsek:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodlayıcılara varsayılan olarak ch.qos.logback.classic.encoder.PatternLayoutEncoder türü atanır -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- günlük seviyesi kontrolü -->
<root level="off"> <!-- kapalı, bilgi, hata ayıklama, uyarı -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
aşağıdaki günlükler elde edilir:
Görüldüğü gibi, konsolda görüntülenen günlükler üzerinde belirli bir kontrolümüz var. [info] seviyesi genellikle uygun günlük seviyesidir.
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.
8.4.12. 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;
8.4.12.1. Maven Yapılandırması
[3] projesi bir Maven projesidir. Bağımlılıklarını öğrenmek için [pom.xml] dosyasını inceleyelim:
<?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-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- etiket::güvenlik[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 10-14. satırlar: proje bir Spring Boot projesidir;
- 17-20. satırlar: [Thymeleaf] çerçevesine bağımlılık;
- 22-25. satırlar: Spring Security çerçevesine bağımlılık;
8.4.12.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>
- 12. satır: [th:href="@{/hello}"] özniteliği, <a> etiketinin [href] özniteliğini oluşturacaktır. [@{/hello}] değeri, [<context>/hello] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
[hello.html] görünümü şöyledir:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- 9. satır: [th:inline="text"] özniteliği, <h1> etiketinin metnini oluşturacaktır. Bu metin, değerlendirilmesi gereken bir $ ifadesi içerir. [[${#httpServletRequest.remoteUser}]] öğesi, geçerli HTTP isteğinin [RemoteUser] özniteliğinin değeridir. Bu, oturum açmış kullanıcının adıdır;
- 10. satır: bir HTML formu. [th:action="@{/logout}"] özniteliği, [form] etiketinin [action] özniteliğini oluşturacaktır. [@{/logout}] değeri, [<context>/logout] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- 8. satır: Hello [document.forms[0]]! ifadesinin çevirisi;
- 9. satır: @{/logout}'in çevirisi;
- 11. satır: (name özniteliği) adlı gizli bir alan _csrf;
Son görünüm [login.html] ş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>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- 9. satır: [th:if="${param.error}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL'in [error] (http://context/login?error) parametresini içermesi durumunda oluşturulmasını sağlar;
- 10. satır: [th:if="${param.logout}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL etiketinde [logout] parametresi (http://context/login?logout) varsa oluşturulmasını sağlar;
- 11-23. satırlar: bir HTML formu;
- 11. satır: Form, <context> web uygulamasının bağlamı olan URL [<context>/login] adresine gönderilecektir;
- 13. satır: [username] adlı bir giriş alanı;
- 17. satır: [password] adlı bir giriş alanı;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<div>
<label>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
28. satırda, Thymeleaf'in [_csrf] adlı gizli bir alan eklediği görülmektedir.
8.4.12.3. Spring MVC Yapılandırması
![]() |
[MvcConfig] sınıfı, Spring MVC çerçevesini yapılandırır:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- 7. satır: [@Configuration] anotasyonu, [MvcConfig] sınıfını bir yapılandırma sınıfı haline getirir;
- 8. satır: [MvcConfig] sınıfı, [WebMvcConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
- 10. satır: üst sınıfın bir yönteminin yeniden tanımlanması;
- satır 11-16: [addViewControllers] yöntemi, URL'leri HTML görünümlerine ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
URL | görünüm |
/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, [java] 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.
8.4.12.4. Spring Security Yapılandırması
![]() |
[WebSecurityConfig] sınıfı, Spring Security çerçevesini yapılandırır:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- 9. satır: [@Configuration] anotasyonu, [WebSecurityConfig] sınıfını bir yapılandırma sınıfı haline getirir;
- 10. satır: [@EnableWebSecurity] anotasyonu, [WebSecurityConfig] sınıfını bir Spring Security yapılandırma sınıfı haline getirir;
- 11. satır: [WebSecurity] sınıfı, [WebSecurityConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
- 12. satır: üst sınıfın bir yöntemi yeniden tanımlanmıştır;
- 13-16. satırlar: [configure(HttpSecurity http)] yöntemi, uygulamanın çeşitli URL'lerine erişim haklarını tanımlamak üzere yeniden tanımlanmıştır;
- 14. satır: [http.authorizeRequests()] yöntemi, URL'leri erişim haklarıyla ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
URL | kural | kod |
kimlik doğrulaması yapılmadan erişim | | |
Yalnızca kimlik doğrulaması yapılmış 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 kullanılabilir;
- 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;
8.4.12.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.
8.4.12.6. Uygulama Testleri
Öncelikle, kabul edilen dört URL dosyasından biri olan URL [/] dosyasını talep edelim. Bu dosya, [/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 [/hello] dosyası istenecektir. Bu dosya korumalıdır:
URL | kural | kod |
kimlik doğrulaması yapılmadan erişim | | |
sadece kimliği doğrulanmış kullanıcılar için 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]'tir. 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ığı, etiketinin görüntülenmesini tetikledi:
<div th:if="${param.error}">Invalid username and password.</div>
Şimdi, beklenen user/password değerlerini [4] olarak girelim:
![]() |
- [4]'te kimlik doğrulaması yapıyoruz;
- [5] adresinde, Spring Security bizi URL [/hello] adresine 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 derlememizde, 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'te [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>
8.4.12.7. Conclusion
Ö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 ile birlikte 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.
8.4.13. Randevu web hizmetinde güvenlik ayarlarının yapılması
8.4.13.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 birden fazla kullanıcıyı bir araya getirebilir. [USERS_ROLES] tablosu ile somutlaştırılan çok-çok ilişkisi vardır.
- 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:
![]() |
8.4.13.2. [métier, DAO, JPA] projesinden yeni STS projesi
[rdvmedecins-metier-dao] projesi şu şekilde gelişiyor:
![]() |
- [1]'e: yeni proje;
- [2]'e: güvenlik önlemlerinin dikkate alınmasıyla yapılan değişiklikler tek bir [rdvmedecins.security] paketinde birleştirilmiştir. Bu yeni öğeler [JPA] ve [DAO] katmanlarına aittir, ancak basitlik amacıyla tek bir pakette birleştirilmiştir.
8.4.13.3. Yeni varlıklar [JPA]
![]() |
JPA katmanı üç yeni öğeyi 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, kendilerine atanan alanlarla aynı adı taşıdıkları için sütun adı 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;
// özellikler
private String name;
// yapıcılar
public Role() {
}
public Role(String name) {
this.name = name;
}
// kimlik
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getter ve setter'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;
8.4.13.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;
- 20. satır: bir kullanıcıyı kullanıcı adı ile bulmak için;
[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);
}
- satır 5: [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;
8.4.13.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 [SimpleGrantedAuthority] türetilmiş 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 [AppUserDetailsService] 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;
- 18. satır: Kullanıcı, kullanıcı adı ile 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, 16. satırda belirtildiği gibi [UserDetails] türündedir;
8.4.13.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, ilk 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ı zaten mevcut mu diye kontrol edilir;
- 42-49. satırlar: kullanıcı adı yoksa, 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 ihtiyaç 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;
- satır 51-57: 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: [admin] kullanıcısının, [UserRepository] deposunu kullanarak [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğu kontrol ediliyor;
- 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, [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğu, [appUserDetailsService] hizmeti kullanılarak doğrulanır;
Testler aşağıdaki günlük kayıtlarıyla başarıyla tamamlanır:
8.4.13.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 paketinde JPA varlıklarının ve 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ı.
8.4.13.8. [web] katmanındaki STS projesi
![]() |
[rdvmedecins-webjson] projesi, [1]'ten şu şekilde gelişmektedir:
![]() |
En önemli değişiklikler, Spring Security'nin yapılandırılması gereken [rdvmedecins.web.config] paketinde yapılmalıdır. [AppConfig] ve [ApplicationModel] sınıflarında da bazı küçük değişiklikler vardır. 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ı adımları 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 [configure(HttpSecurity http)] yöntemini 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.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// kimlik doğrulama, [appUserDetailsService] bean'i tarafından yapılır
// şifre, BCrypt hash algoritmasıyla şifrelenir
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// güvenli uygulama mı?
if (application.isSecured()) {
// şifre, Authorization: Basic xxxx başlığıyla iletilir
http.httpBasic();
// HTTP OPTIONS yöntemi herkes için yetkilendirilmelidir
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// uygulamayı yalnızca ADMIN rolü kullanabilir
http.authorizeRequests() //
.antMatchers("/", "/**") // tüm URL
.hasRole("ADMIN");
// oturum yok
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- 15. satır: [SecurityConfig] sınıfı, bir Spring yapılandırma sınıfıdır;
- 16. satır: projenin güvenliğini sağlamak için;
- 19-20. satırlar: Uygulamanın kullanıcılarına erişim sağlayan [AppUserDetails] sınıfı eklenir;
- 21-22. satırlar: Web uygulamasının önbelleği olarak işlev gören [ApplicationModel] sınıfı eklenir. Burada, web uygulamasını tek bir yerden yapılandırmak için bu sınıfı da kullanmaya karar veriyoruz. 36. satırdaki [isSecured] boole değerini tanımlayan da bu sınıftır. Bu boole değeri, web uygulamasını güvenli hale getirir (true) veya getirmez (false);
- 25-29. 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 (28. satır):
- 20. 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ı oldukları bilgisinin görünmediğ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;
- 38-47. satırlar: [configure(HttpSecurity http)] yöntemi, web hizmetinin URL'lerine erişim haklarını tanımlar;
- 34. 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. Bu ayar, boole değeri (isSecured=false) ile birleştiğinde, web uygulamasının güvenlik önlemi olmadan kullanılmasına olanak tanır;
- 38. satır: HTTP başlığıyla kimlik doğrulama modu etkinleştirilir. İ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:
- 40-42. 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;
- 47. satır: Kullanıcının şifresi bir oturumda kaydedilebilir veya kaydedilmeyebilir. Kaydedilirse, kullanıcı yalnızca ilk seferde kimlik doğrulaması yapması gerekir. Sonraki seferlerde kimlik bilgileri istenmez. Burada oturumsuz bir mod seçilmiştir. Her istek, güvenlik kimlik bilgileriyle birlikte gönderilmelidir;
Uygulamanın tamamını yapılandıran [AppConfig] sınıfı şu şekilde değişir:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Değişiklik 11. satırda yapılmıştır: [SecurityConfig] yapılandırma sınıfı eklenmiştir;
Son olarak, [ApplicationModel] sınıfına bir boole değeri eklenir:
@Component
public class ApplicationModel implements IMetier {
...
// yapılandırma verileri
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- 6. satır: Güvenliği etkinleştirmek isteyip istemediğinize bağlı olarak [secured] boole değerini [true / false] olarak ayarlayın.
8.4.13.9. Web hizmeti testleri
Web hizmetini [Advanced Rest Client] Chrome istemcisiyle 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ı, şifre
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// iki argüman alınıyor
String chaîne = String.format("%s:%s", args[0], args[1]);
// dizesi şu şekilde kodlanır
byte[] data = Base64.encode(chaîne.getBytes());
// Base64 kodlamasını görüntülenir
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:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Ardından Chrome istemcisi [Advanced Rest Client] ile tüm doktorların listesini talep ediyoruz:
![]() |
- [1] adresinde, 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, web uygulamasının güvenliğiyle ilgili HTTP başlık listesi;
Doktorların listesi başarıyla alınır:
![]() |
Şimdi, hatalı bir kimlik doğrulama başlığıyla HTTP isteği göndermeyi deneyelim. Yanıt şu şekilde olur:
![]() |
- [1] ve [3]'te: 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;
Güvenli bir web hizmeti artık çalışır durumda. Etki alanları arası istekleri izin verecek şekilde bunu tamamlayacağız. Bu ihtiyaç [Tutoriel AngularJS / Spring 4] belgesinde ortaya çıkmıştır ve burada böyle bir ihtiyaç olmasa da yine de buna cevap vereceğiz.
8.4.14. Etki alanları arası isteklerin kurulması
Etki alanları arası istekler sorununu inceleyelim. [Tutoriel AngularJS / Spring 4] belgesinde, istemcinin AngularJS uygulaması olduğu bir istemci/sunucu uygulaması geliştirilmektedir:
![]() |
- Angular uygulamasının HTML / CSS / JS sayfaları, [1] sunucusundan gelmektedir;
- [2]'te, [dao] hizmeti başka bir sunucuya, yani [2] sunucusuna bir istek gönderir. Ancak bu, Angular uygulamasını çalıştıran tarayıcı tarafından yasaklanmıştır çünkü bu bir güvenlik açığıdır. Uygulama yalnızca geldiği sunucuya, yani [1] sunucusuna sorgu gönderebilir;
Aslında, tarayıcının Angular uygulamasının [2] sunucusuna sorgu göndermesini yasakladığını söylemek doğru değildir. Uygulama aslında bu sunucuya, kendisinden gelmeyen bir istemcinin kendisine sorgu göndermesine izin verip vermediğini sormaktadır. Bu paylaşım tekniğine CORS (Cross-Origin Resource Sharing) denir. [2] sunucusu, belirli HTTP başlıklarını göndererek onay verir.
Karşılaşılabilecek sorunları göstermek için, aşağıdaki özelliklere sahip bir istemci/sunucu uygulaması oluşturacağız:
- sunucu, bizim web sunucumuz / jSON olacaktır;
- istemci, / jSON web sunucusuna istekler gönderecek bir JavaScript koduyla donatılmış basit bir HTML sayfası olacaktır;
8.4.14.1. İstemci projesi
![]() |
Proje, aşağıdaki [pom.xml] dosyasını içeren bir Maven projesidir:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- depo'dan üst öğeyi arar -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- 14-19. satırlar: Bu bir Spring Boot projesidir;
- 29-32. satırlar: [spring-boot-starter-web] bağımlılığı kullanılıyor; bu bağımlılık, bir Tomcat sunucusu ve Spring MVC'i beraberinde getiriyor;
HTML sayfası şöyledir:
![]() |
Bu sayfa aşağıdaki kodla oluşturulur:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- yöntem HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- gönderilen değer -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- onay düğmesi -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- 6. satır: jQuery kütüphanesini içe aktarıyoruz;
- 7. satır: yazacağımız bir kod içe aktarılır;
[client.js] kodu şöyledir:
// genel veriler
var url;
var posted;
var response;
var method;
function requestServer() {
// formdaki bilgiler alınır
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// manuel olarak Ajax çağrısı yapılıyor
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// manuel olarak Ajax çağrısı yapılıyor
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// metin sonucu
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// sistem hatası
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// Ajax çağrısı manuel olarak yapılıyor
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// metin sonucu
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// sistem hatası
response.text(jqXHR.responseText);
}
})
}
// belge yüklenirken
$(document).ready(function() {
// sayfanın bileşenlerinin referansları alınırken
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Bu kodun anlamını okuyucunun kendisi çözmesine bırakıyoruz. Her şey daha önce bir şekilde karşımıza çıkmıştır. Ancak bazı satırlar açıklama gerektiriyor:
- 11. satır:
- [document], tarayıcı tarafından yüklenen belgeyi ifade eder; buna DOM (Belge Nesne Modeli) denir,
- [document.forms[0].elements['method']] ise belgedeki ilk formu belirtir; bir belgede birden fazla form bulunabilir. Burada ise sadece bir tane vardır,
- [document.forms[0].elements['method']] ise [name='method'] özniteliğine sahip form öğesini ifade eder. Bunlardan iki tane vardır:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- 11. satır:
- [document.forms[0].elements['method'].value], [name='method'] özniteliğine sahip bileşen için gönderilecek değerdir. Gönderilen değerin, işaretli radyo düğmesinin [value] özniteliğinin değeri olduğu bilinmektedir. Dolayısıyla burada, ['get', 'post'] dizelerinden biri olacaktır;
- 23-25. satırlar: HTTP [Authorization: Basic code] başlığını gerektiren bir sunucuya bağlanıyoruz. Sunucuyu sorgulayabilecek tek kullanıcı olan [admin / admin] için bu başlığı oluşturuyoruz;
- 26. satır: Kullanıcı, [/getAllMedecins, /supprimerRv, ...] türünde URL değerlerini girecektir. Dolayısıyla bu URL değerlerini tamamlamak gerekir;
- 28. satır: Sunucu, bir metin biçimi olan jSON değerini geri gönderir. Alınan haliyle görüntülenebilmesi için sonuç türü olarak [text/plain] türü belirtilir;
- 33. satır: sunucudan gelen metin yanıtının görüntülenmesi;
- satır 39: olası hata mesajının metin biçiminde görüntülenmesi;
- satır 52: İstemcinin jSON gönderdiğini belirtmek için;
Oluşturulan istemci/sunucu uygulamasında:
- istemci, URL [http://localhost:8081] adresinde erişilebilen bir web uygulamasıdır. Bu, şu anda geliştirmekte olduğumuz uygulamadır;
- sunucu, URL ve [http://localhost:8080] adreslerinde erişilebilen bir web uygulamasıdır. Bu, bizim web sunucumuzdur / jSON;
İstemci, sunucuyla aynı bağlantı noktasından erişilmediğinden, etki alanları arası istek sorunu ortaya çıkıyor. [http://localhost:8080] ve [http://localhost:8081], iki farklı etki alanıdır.
Spring Boot uygulaması, aşağıdaki [Client] yürütülebilir sınıfı tarafından başlatılan bir konsol uygulamasıdır:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
// statik sayfalar
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// dispatcherServlet yapılandırması
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// yerleşik Tomcat sunucusu
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- 14. satır: [Client] sınıfı bir Spring yapılandırma sınıfıdır;
- 15. satır: MVC adlı bir Spring uygulaması yapılandırılır. Bu anotasyon, bir dizi otomatik yapılandırmayı beraberinde getirir;
- 16. satır: Spring çerçevesinin (MVC) bazı varsayılan değerlerini yeniden tanımlamak için [WebMvcConfigurerAdapter] sınıfını genişletmek gerekir;
- 23-26. satırlar: [addResourceHandlers] yöntemi, uygulamanın statik kaynaklarının (html, css, js, ...) bulunduğu klasörleri belirtmeye olanak tanır. Burada, projenin Classpath'inde bulunan [static] klasörü belirtilir:
![]() |
- 29-37. satırlar: Spring servleti MVC'i tanımlayan [dispatcherServlet] bean'inin yapılandırması;
- 40-43. satırlar: Yerleşik Tomcat sunucusu 8081 numaralı bağlantı noktasında çalışacaktır;
8.4.14.2. URL [/getAllMedecins]
Şunları başlatıyoruz:
- web sunucusunu / json'u 8080 numaralı bağlantı noktasında;
- bu sunucunun istemcisini 8081 numaralı bağlantı noktasında;
ardından URL [http://localhost:8081/client.html] [1]'i çağırıyoruz:
![]() |
- [2]'te, URL ve [http://localhost:8080/getAllMedecins] üzerinde bir GET işlemi gerçekleştiriyoruz;
Sunucudan yanıt alamıyoruz. Geliştirici konsoluna (Ctrl-Shift-I) baktığımızda bir hata görüyoruz:
![]() |
- [1]'teyken, [Network] sekmesindeyiz;
- [2] örneğinde, yapılan HTTP isteğinin [GET] değil, [OPTIONS] olduğu görülmektedir. Etki alanları arası bir istek söz konusu olduğunda, tarayıcı sunucuya HTTP [OPTIONS] isteğini göndererek belirli koşulların karşılanıp karşılanmadığını kontrol eder. Bu durumda, istekler [5-6] işaretleriyle belirtilenlerdir;
- [5] isteğinde ise tarayıcı, URL hedefine bir GET ile ulaşılamasını sorar. [Access-Control-Request-Method] isteğinin başlığı, istenen yöntemin kabul edildiğini belirten HTTP [Access-Control-Allow-Methods] başlığıyla bir yanıt talep eder;
- [5]'te tarayıcı, HTTP [Origin: http://localhost:8081] başlığını gönderir. Bu başlık, belirtilen kaynağın kabul edildiğini belirten bir HTTP [Access-Control-Allow-Origin] başlığıyla yanıt verilmesini talep eder;
- [6]'te tarayıcı, HTTP, [accept] ve [authorization] başlıklarının kabul edilip edilmediğini sorar. [Access-Control-Request-Headers] istek başlığı, istenen başlıkların kabul edildiğini belirten HTTP ve [Access-Control-Allow-Headers] başlıklarını içeren bir yanıt bekler;
- [3]'te bir hata oluşur. Simgeye tıklandığında [4] hatası görüntülenir;
- [4]'te, sunucunun isteğin kaynağının kabul edilip edilmediğini belirten HTTP [Access-Control-Allow-Origin] başlığını göndermediğini belirten bir mesaj görüntülenir;
- [7]'te, sunucunun bu başlığı gerçekten göndermediği görülebilir. Sonuç olarak tarayıcı, başlangıçta istenen HTTP GET isteğini reddetmiştir;
Web sunucusunu / jSON değiştirmemiz gerekiyor. Web hizmetinin yapılandırma öğelerinden biri olan [ApplicationModel]'te ilk değişikliği yapıyoruz:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// yapılandırma verileri
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- 6. satır: Sunucu etki alanı dışındaki istemcileri kabul edip etmediğimizi belirten bir boole değeri oluşturuyoruz;
- 10-12. satırlar: bu bilgiye erişim yöntemi;
Ardından yeni bir Spring denetleyicisi MVC oluşturuyoruz:
![]() |
[RdvMedecinsCorsController] sınıfı şu şekildedir:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// seçeneklerin istemciye gönderilmesi
public void sendOptions(String origin, HttpServletResponse response) {
// CORS izinli mi?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// CORS başlığı ayarlanıyor
response.addHeader("Access-Control-Allow-Origin", origin);
// belirli başlıklara izin veriliyor
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// GET'e izin veriliyor
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// doktor listesi
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- 12-13. satırlar: [RdvMedecinsCorsController] sınıfı bir Spring denetleyicisidir;
- 33-36. satırlar: URL ve [/getAllMedecins] komutları, HTTP ve [OPTIONS] komutlarıyla çağrıldığında bunları işleyen bir eylemi tanımlar;
- 34. satır: [getAllMedecins] yöntemi şu parametreleri kabul eder:
- [@RequestHeader(value = "Origin", required = false)] nesnesi, isteğin HTTP [Origin] başlığını alacaktır. Bu başlık, isteğin göndereni tarafından gönderilmiştir:
HTTP ve [Origin] başlıklarının isteğe bağlı olduğu belirtilir ([required = false]). Bu durumda, başlık yoksa [String origin] parametresinin değeri null olacaktır. Varsayılan değer olan [required = true] ile başlık yoksa bir istisna atılır. Bu durumu önlemek istedik;
- 34. satır:
- isteği yapan müşteriye gönderilecek olan [HttpServletResponse response] nesnesi;
Bu iki parametre Spring tarafından eklenir;
- 35. satır: isteğin işlenmesi, 19-30. satırlardaki yönteme devredilir;
- 15-16. satırlar: [ApplicationModel] nesnesi enjekte edilir;
- 21-23. satırlar: Uygulama, etki alanları arası istekleri kabul edecek şekilde yapılandırılmışsa ve gönderen HTTP [Origin] başlığını göndermişse ve bu kaynak [http://localhost] ile başlıyorsa, o zaman etki alanları arası istek kabul edilir, aksi takdirde reddedilir;
- 25. satır: İstemci [http://localhost:port] etki alanında ise, HTTP başlığı gönderilir:
bu, sunucunun müşterinin kaynağını kabul ettiği anlamına gelir;
- 25. satır: HTTP [OPTIONS] isteğinde iki özel HTTP başlığını belirttik:
HTTP ve [Access-Control-Request-X] başlıklarına karşılık olarak sunucu, neyin izin verildiğini belirten HTTP ve [Access-Control-Allow-X] başlıklarıyla yanıt verir. 23-26. satırlar, isteğin kabul edildiğini belirtmek için yalnızca istemcinin isteğini tekrarlar;
Artık yeni testlere hazırız. Web hizmetinin yeni sürümünü başlatıyoruz ve sorunun hâlâ devam ettiğini görüyoruz. Hiçbir şey değişmemiştir. Yukarıdaki 35. satıra bir konsol çıktısı eklesek bile, bu çıktı hiçbir zaman görüntülenmez; bu da 34. satırdaki [getAllMedecins] yönteminin hiçbir zaman çağrılmadığını gösterir.
Biraz araştırma yaptıktan sonra, Spring MVC'in HTTP ve [OPTIONS] komutlarını varsayılan bir işleme tabi tuttuğunu keşfediyoruz. Dolayısıyla yanıtı her zaman Spring veriyor ve 34. satırdaki [getAllMedecins] yöntemi hiçbir zaman çağrılmıyor. Spring MVC’in bu varsayılan davranışı değiştirilebilir. Mevcut [WebConfig] sınıfını şu şekilde değiştiriyoruz:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
// başlıklar için dispatcherservlet yapılandırması CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// eşleme jSON
...
- 10-11. satırlar: [dispatcherServlet] bean'i, istemcilerin isteklerini yöneten servleti tanımlamak için kullanılır. Burada bu, Spring çerçevesinin servleti olan [DispatcherServlet] türündedir;
- 12. satır: [DispatcherServlet] türünde bir örnek oluşturulur;
- 13. satır: servlet'ten, HTTP ve [OPTIONS] komutlarını uygulamaya iletmesi istenir;
- 14. satır: servlet bu şekilde yapılandırılır;
Bu yeni yapılandırma ile testleri tekrar yapıyoruz. Aşağıdaki sonucu elde ediyoruz:
![]() |
- [1]'te, HTTP'e yönelik iki istek olduğunu görüyoruz: URL ve [http://localhost:8080/getAllMedecins];
- [2]'te, [OPTIONS] isteği;
- [3]'te ise, az önce sunucu yanıtında yapılandırdığımız üç HTTP başlığı;
Şimdi ikinci isteği inceleyelim:
![]() |
- [1]'e, incelenen istek;
- [2]'e, bu da GET isteğidir. İlk istek olan [OPTIONS] sayesinde tarayıcı, istediği bilgileri aldı. Şimdi başlangıçta istenen [GET] isteğini gerçekleştiriyor;
- [3]'te sunucunun yanıtı;
- [4]'te, sunucu jSON'i gönderir;
- [5]'te bir hata meydana geldi;
- [6]'te, hata mesajı;
Burada ne olduğunu açıklamak daha zor. Sunucudan gelen [3] yanıtı normaldir: [HTTP/1.1 200 OK]. Dolayısıyla istenen belge elimizde olmalı. Sunucu belgeyi gerçekten göndermiş olabilir, ancak tarayıcı, GET isteği için de yanıtın HTTP [Access-Control-Allow-Origin:http://localhost:8081] başlığını içermesini istediği için belgenin kullanılmasını engelliyor olabilir.
[RdvMedecinsController] denetleyicisini şu şekilde değiştiriyoruz:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// doktor listesi
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// yanıt
Response<List<Medecin>> response;
// başlıklar CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// uygulama durumu
...
- 1-2. satırlar: [RdvMedecinsCorsController] denetleyicisi eklenir;
- 7-8. satırlar: [getAllMedecins] yönteminin parametrelerine, müşteriye verilecek yanıtı kapsayan HttpServletResponse nesnesi ve HTTP [Origin] başlığı enjekte edilir;
- 12. satır: [RdvMedecinsCorsController] denetleyicisinin [sendOptions] yöntemine çağrı yapılır; bu yöntem, HTTP [OPTIONS] isteğini işlemek için çağrılan yöntemle aynıdır. Dolayısıyla, bu istek için kullanılanlarla aynı HTTP başlıklarını gönderecektir;
Bu değişiklikten sonra sonuçlar şu şekildedir:
![]() |
Doktor listesini başarıyla aldık.
8.4.14.3. Diğer URL ve [GET]
Şimdi, bir GET aracılığıyla sorgulanan diğer URL'leri gösteriyoruz. Denetleyicilerde, bunları işleyen eylemlerin kodu, daha önce URL ve [/getAllMedecins]'i işleyen eylemlerin modelini takip eder. Okuyucu, bu belgeyle birlikte verilen örneklerdeki kodu inceleyebilir. İşte bir örnek:
[RdvMedecinsCorsController] içinde
// bir doktorun randevu listesi
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
[RdvMedecinsController] içinde
// bir doktorun randevu listesi
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// yanıt
Response<List<Rv>> response = null;
boolean erreur = false;
// başlıklar CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// uygulama durumu
...
İşte çalıştırma ekran görüntüleri:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. URL [POST]
Şu örneği inceleyelim:
![]() |
- POST [1]'ten URL [2]'e bir işlem yapıyoruz;
- [3]'te, gönderilen değer. Bu, jSON dizisidir;
- toplamda, [id] 100 değerine sahip randevuyu silmeye çalışıyoruz;
Şu an için herhangi bir kod değişikliği yapmıyoruz. Elde edilen sonuç şöyledir:
![]() |
- [1]'te, [GET] isteklerinde olduğu gibi, tarayıcı tarafından bir [OPTIONS] isteği yapılır;
- [2] durumunda, [POST] isteği için erişim izni talep eder. Daha önce bu [GET] idi;
- [3]'te, HTTP ve [accept, authorization, content-type] başlıklarını göndermek için izin istenir. Daha önce sadece ilk iki başlık vardı;
[RdvMedecinsCorsController.sendOptions] yöntemini şu şekilde değiştiriyoruz:
public void sendOptions(String origin, HttpServletResponse response) {
// Cors izinli mi?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// başlık ayarlanıyor CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// belirli başlıklara izin veriliyor
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// GET'e izin veriliyor
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- 9. satır: HTTP ve [Content-Type] başlıklarını ekledik (büyük/küçük harf fark etmez);
- 11. satır: HTTP [POST] yöntemi eklendi;
Böylece [POST] yöntemleri, [GET] istekleriyle aynı şekilde işlenir. İşte URL ve [/supprimerRv] örnekleri:
[RdvMedecinsController]'te
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// yanıt
Response<Void> response = null;
boolean erreur = false;
// CORS başlıkları
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// uygulama durumu
if (messages != null) {
...
[RdvMedecinsCorsController]'te
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
Elde edilen sonuç şöyledir:
![]() |
URL ve [/ajouterRv] için şu sonuç elde edilir:
![]() |
8.4.14.5. Conclusion
Uygulamamız artık etki alanları arası istekleri desteklemektedir. Bu istekler, [ApplicationModel] sınıfındaki yapılandırma ile izin verilebilir veya reddedilebilir:
// yapılandırma verileri
private boolean corsAllowed = false;
8.5. Web hizmetinin programlanmış istemcisi / jSON
Yazmak istediğimiz uygulamanın genel mimarisine geri dönelim:
![]() |
Şemanın üst kısmı yazıldı. Bu, web sunucusudur / jSON. Şimdi alt kısma ve öncelikle [DAO] katmanına geçiyoruz. Bunu yazıp ardından bir konsol istemcisiyle test edeceğiz. Test mimarisi şu şekilde olacaktır:
![]() |
8.5.1. Konsol istemcisi projesi
Konsol istemcisinin STS projesi şu şekilde olacaktır:
![]() |
8.5.2. Maven Yapılandırması
Konsol istemcisinin [pom.xml] dosyası şu şekildedir:
<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>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- depo'dan üst öğeyi arama -->
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- Spring tarafından kullanılan jSON kütüphanesi -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Spring tarafından kullanılan bileşen RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>
- 15-20. satırlar: üst Spring Boot projesi;
- 24-27. satırlar: web sunucusunun konsol istemcisi / jSON, [spring-web] bağımlılığı tarafından sağlanan [RestTemplate] adlı bir bileşene dayanmaktadır;
- 29-36. satırlar: jSON nesnelerinin serileştirilmesi/deserileştirilmesi için jSON kütüphanesi gereklidir. Spring Web tarafından kullanılan Jackson kütüphanesinin bir varyantını kullanıyoruz;
- 38-41. satırlar: En alt düzeyde, [RestTemplate] bileşeni, TCP/IP soketleri aracılığıyla sunucuyla iletişim kurar. Bunlardan [timeout] değerini, yani sunucudan yanıt için maksimum bekleme süresini ayarlamak istiyoruz. [RestTemplate] bileşeni bu ayarı yapmamıza izin vermiyor. Bunu yapmak için, [RestTemplate] oluşturucusuna, [org.apache.httpcomponents.httpclient] bağımlılığı tarafından sağlanan bir alt düzey bileşen aktaracağız. İletişimin [timeout] değerini ayarlamamızı sağlayacak olan bu bağımlılıktır;
8.5.3. [rdvmedecins.client.entities] paketi
![]() |
[rdvmedecins.client.entities] paketi, /jSON web hizmetinin çeşitli URL'leri aracılığıyla gönderdiği tüm varlıklarını bir araya getirir. Bunları tekrar ayrıntılı olarak ele almayacağız. Sadece JPA ve [Client, Creneau, Medecin, Rv, Personne] varlıklarının tüm JPA ve jSON açıklamalarından arındırıldığını belirtmekle yetineceğiz. Örneğin, [Rv] sınıfı şöyledir:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// Rv günü
private Date jour;
// Bir randevu bir müşteriye bağlı
private Client client;
// bir randevu bir zaman aralığına bağlıdır
private Creneau creneau;
// dış anahtarlar
private long idClient;
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);
}
// alıcı ve ayarlayıcılar
...
}
8.5.4. [rdvmedecins.client.requests] paketi
![]() |
[rdvmedecins.client.requests] paketi, jSON değerinin URL, [/ajouterRv] ve [supprimerRv]'e gönderildiği iki sınıfı bir araya getirir. Bunlar, sunucu tarafındakilerle aynıdır.
8.5.5. [rdvmedecins.client.responses] paketi
![]() |
[Response], / jSON web hizmetinin tüm yanıtlarının türüdür. Bu, genel bir türdür:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- özellikler
// işlem durumu
private int status;
// olası hata mesajları
private List<String> messages;
// yanıt gövdesi
private T body;
// oluşturucular
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// alıcı ve ayarlayıcılar
...
}
- 5. satır: [T] türü, /jSON web hizmetinin URL değerine göre değişir;
8.5.6. [rdvmedecins.client.dao] paketi
![]() |
- [IDao], [DAO] katmanının arayüzüdür ve [Dao] ise bunun uygulamasıdır. Bu uygulamaya daha sonra tekrar döneceğiz;
8.5.7. [rdvmedecins.client.config] paketi
![]() |
[DaoConfig] sınıfı uygulamayı yapılandırır. Kod şöyledir:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// RestTemplate bileşeninin oluşturulması
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// sonuç
return restTemplate;
}
// eşleştiriciler jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- 13. satır: [DaoConfig] sınıfı bir Spring yapılandırma sınıfıdır;
- 14. satır: [rdvmedecins.client.dao] paketi, Spring bileşenlerini aramak üzere taranacaktır. Burada [Dao] bileşeni bulunur;
- satır 17-24: [restTemplate] adlı bir Spring singleton'ı tanımlar (yöntemin adı). Bu yöntem, Spring'in bir web hizmeti / jSON ile iletişim kurmak için sağladığı temel araç olan [RestTemplate] örneğini döndürür;
- 21. satır: [RestTemplate restTemplate = new RestTemplate() ;] yazılabilir. Çoğu durumda bu yeterlidir. Ancak burada, istemcinin [timeout] değerlerini sabitlemek istiyoruz. Bunun için, [RestTemplate] bileşenine, bu [timeout] değerlerini ayarlamamızı sağlayacak [HttpComponentsClientHttpRequestFactory] türünde bir alt düzey bileşen enjekte ediyoruz (20. satır). Gerekli Maven bağımlılığı sunulmuştur;
- 28-57. satırlar: jSON eşleştiricilerini tanımlar. Bunlar, [Response<T>] yanıtının T türünü serileştirmek için sunucu tarafında (bkz. paragraf 8.4.11.3) kullanılan jSON eşleştiricileridir. Aynı dönüştürücüler şimdi T türünü deserileştirmek için istemci tarafında kullanılacaktır;
8.5.8. [IDao] arayüzü
Uygulamanın mimarisine geri dönelim:
![]() |
[DAO] katmanı, [console] katmanı ile /jSON web hizmeti tarafından sunulan URL katmanları arasında bir adaptördür. [IDao] arayüzü şu şekilde olacaktır:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// web hizmetinin URL'si
public void setUrlServiceWebJson(String url);
// zaman aşımı
public void setTimeout(int timeout);
// kimlik doğrulama
public void authenticate(User user);
// müşteri listesi
public List<Client> getAllClients(User user);
// Doktor listesi
public List<Medecin> getAllMedecins(User user);
// bir doktorun randevu saatleri listesi
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// ID'siyle bir müşteri bul
public Client getClientById(User user, long id);
// ID'siyle bir müşteri bulma
public Medecin getMedecinById(User user, long id);
// ID'siyle bir randevuyu bulma
public Rv getRvById(User user, long id);
// ID'siyle tanımlanan bir randevu aralığını bulma
public Creneau getCreneauById(User user, long id);
// bir RV ekle
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// bir RV'i sil
public void supprimerRv(User user, long idRv);
// belirli bir günde bir doktorun randevu listesi
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// takvim
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- 14. satır: / jSON web hizmetinin kök dizinini (örneğin [http://localhost:8080]) belirlemeye yarayan yöntem;
- 17. satır: [timeout]'i istemci tarafında ayarlamaya yarayan yöntem. Bu parametreyi kontrol etmek istiyoruz çünkü bazı HTTP istemcileri, gelmeyecek bir yanıtı beklerken bazen çok uzun süre bekleyebiliyor;
- satır 20: Bir [login, passwd] kullanıcısını tanımlamaya yarayan yöntem. Kullanıcı tanınmazsa bir istisna oluşturur;
- satır 22-53: Web hizmeti tarafından sunulan her URL / jSON'e, sunulan URL'i işleyen sunucu tarafındaki yöntemin imzasından türetilen bir arayüz yöntemi eşleştirilir. Örnek olarak, aşağıdaki URL sunucusunu ele alalım:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
- 1. satır: [idMedecin] ve [jour]'in URL'in parametreleri olduğu görülmektedir. Bunlar, bu URL ile ilişkili istemci tarafındaki yöntemin giriş parametreleri olacaktır;
- 2. satır: Sunucu yönteminin [Response<String>] türünde bir değer döndürdüğü görülmektedir. Bu [String] türü, [AgendaMedecinJour] türündeki jSON değerinin türüdür. İstemci tarafında bu URL ile ilişkili yöntemin sonucunun türü [AgendaMedecinJour] olacaktır;
İstemci tarafında şu yöntem tanımlanır:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Bu imza, sunucu [status==0] ile birlikte [int status, List<String> messages, String body] yanıtı gönderdiğinde uygundur. Bu durumda [messages==null && body!=null] elde ederiz. [status!=0] olduğunda ise uygun değildir. Bu durumda [messages!=null && body==null] elde ederiz. Bir şekilde bir hata oluştuğunu belirtmemiz gerekir. Bunun için aşağıdaki gibi [RdvMedecinsException] türünde bir istisna oluşturacağız:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// hata kodu
private int status;
// hata mesajları listesi
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// alıcı ve ayarlayıcılar
...
}
- 9. ve 11. satırlar: istisna, sunucu tarafından gönderilen [Response<T>] nesnesinin [status, messages] alanlarının değerlerini devralacaktır;
- 5. satır: [RdvMedecinsException] sınıfı, [RuntimeException] sınıfını genişletir. Dolayısıyla bu, kontrol edilmeyen bir istisnadır; yani, bunu bir try/catch ile yönetmek ve arayüzün yöntem imzalarında beyan etmek zorunlu değildir;
Ayrıca, /jSON web hizmetini sorgulayan [IDao] arayüzünün tüm yöntemleri, aşağıdaki [User] türünü parametre olarak alır:
package rdvmedecins.client.entities;
public class User {
// veri
private String login;
private String passwd;
// oluşturucular
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// alıcı ve ayarlayıcılar
...
}
Nitekim, /jSON web hizmeti ile yapılan her veri alışverişinde bir HTTP kimlik doğrulama başlığı bulunmalıdır.
8.5.9. [rdvmedecins.clients.console] paketi
Artık [DAO] katmanının arayüzünü bildiğimize göre, konsol uygulamasını tanıtabiliriz.
![]() |
[Main] sınıfı şu şekildedir:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serileştirici jSON
static private ObjectMapper mapper = new ObjectMapper();
// bağlantı zaman aşımı (milisaniye cinsinden)
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// [DAO] katmanından bir referans alınır
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// web hizmeti / json'un URL'i ayarlanır
dao.setUrlServiceWebJson("http://localhost:8080");
// zaman aşımları milisaniye cinsinden ayarlanıyor
dao.setTimeout(TIMEOUT);
// Kimlik Doğrulama
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// müşteri listesi
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doktor listesi
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// 2 numaralı doktorun randevu aralıkları listesi
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// 1 numaralı müşteri
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// 2 numaralı doktor
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// 3 numaralı randevu
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// 4 numaralı randevu
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// randevu eklenmesi
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doktor 1'in 2015-01-08 tarihli randevu listesi
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Doktor 1'in 8 Ocak 2015 tarihli randevu takvimi
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// eklenen randevunun silinmesi
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Doktor 1'in 8 Ocak 2015 tarihli randevu listesi
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// bağlamın kapatılması
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- 19. satır: Sunucunun yanıtını görüntülememizi sağlayacak jSON serileştiricisi, 184. satır;
- 25. satır: [AnnotationConfigApplicationContext] bileşeni, bir Spring uygulamasının yapılandırma anotasyonlarını kullanabilen bir Spring bileşenidir. Uygulamayı yapılandıran [AppConfig] sınıfını bu bileşenin oluşturucusuna aktarıyoruz;
- 26. satır: [DAO] katmanına bir referans alıyoruz;
- 27-30. satırlar: bu katmanı yapılandırıyoruz;
- 32-169. satırlar: [IDao] arayüzündeki tüm yöntemleri test ediyoruz;
Elde edilen sonuçlar şunlardır:
09:20:56.935 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
Sonuçları kodla ilişkilendirme işini okuyucuya bırakıyoruz. Bu kod, [DAO] katmanındaki her yöntemin nasıl çağrılacağını göstermektedir. Sadece birkaç noktaya dikkat çekelim:
- 2-14. satırlar: Bir kimlik doğrulama hatası durumunda, sunucunun duruma göre HTTP, [403 Forbidden] veya [401 Unauthorized] durum kodunu döndürdüğünü gösterir;
- 30-31. satırlar: 1 numaralı doktora bir randevu (Rv) eklenir;
- satır 32-33: Bu randevu görülüyor. Gün içindeki tek randevu bu;
- satır 34-35: Bu randevu, doktorun ajandasında da görülüyor;
- satır 36-37: Randevu kaybolmuştur. Kod bu arada randevuyu silmiştir;
Konsol günlükleri aşağıdaki dosyalar tarafından kontrol edilir:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodlayıcılara varsayılan olarak ch.qos.logback.classic.encoder.PatternLayoutEncoder türü atanır -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- günlük seviyesi kontrolü -->
<root level="info"> <!-- kapalı, bilgi, hata ayıklama, uyarı -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. [DAO] katmanının uygulaması
Şimdi sıra, [DAO] katmanının kalbini, yani [IDao] arayüzünün uygulamasını tanıtmaya geldi. Bunu adım adım yapacağız.
![]() |
[IDao] arayüzü, [AbstractDao] soyut sınıfı ve bunun alt sınıfı olan [Dao] tarafından uygulanmaktadır.
Üst sınıf [AbstractDao] şu şekildedir:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// veri
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL web hizmeti / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// web istemcisinin istekleri için zaman aşımı süresi belirlenir
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// kullanıcı adı ve şifreyi base64 ile kodluyoruz - gereklilik
// Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// genel istek
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- 20. satır: Sınıf soyuttur, bu da onu bir Spring bileşeni olarak belirlememizi engeller. Bunun yerine, alt sınıfı bir Spring bileşeni olarak belirlenecektir;
- 23-24. satırlar: [AppConfig] yapılandırma sınıfında tanımladığımız [restTemplate] bean'ini enjekte ediyoruz;
- 25. satır: /jSON web hizmetinin kök URL'i;
- 32-38. satırlar: istemcinin sunucudan yanıt beklerkenki zaman aşımını belirler;
- satır 34: [restTemplate] bean'i oluşturulurken bu bean'e enjekte ettiğimiz [HttpComponentsClientHttpRequestFactory] bileşenini geri alıyoruz (bkz. [AppConfig]);
- satır 36: istemcinin sunucuyla bağlantı kurarken bekleyebileceği maksimum süreyi belirliyoruz;
- 37. satır: İstemcinin isteklerinden birine yanıt beklerken geçireceği maksimum bekleme süresini belirliyoruz;
Sunucuyla iletişim yöntemlerinin uygulaması, aşağıdaki genel yöntem içinde faktörlere ayrılacaktır:
// genel istek
protected String getResponse(User user, String url, String jsonPost) {
...
}
- 2. satır: [getResponse] yönteminin parametreleri şunlardır:
- [User user]: Bağlantıyı kuran kullanıcı;
- [String url]: sorgulanacak URL. Bu, URL'in son kısmıdır; ilk kısmı ise sınıfın [urlServiceWebJson] alanı tarafından sağlanır,
- [String jsonPost]: gönderilecek jSON dizesi. Bu değer mevcutsa, URL, POST ile istenecek, aksi takdirde GET ile istenecektir;
Devam edelim:
// genel istek
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL ile iletişime geçin
// jsonPost: jSON değeri gönderilmeli
try {
// isteğin yürütülmesi
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// istek yürütülüyor
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- 23-24. satırlar: sunucuya istekte bulunan ve yanıtını alan komut. [RestTemplate] bileşeni, sunucuyla iletişim kurmak için çok sayıda yöntem sunar. [exchange] dışında başka bir yöntem de seçilebilirdi. Çağrının ikinci parametresi, beklenen yanıtın türünü belirler; burada bu, bir dize olan jSON'tir. İlk parametre ise [RequestEntity] türündeki istektir (7. satır). [exchange] yönteminin sonucu, [ResponseEntity<String>] türündedir. [ResponseEntity] türü, sunucunun gönderdiği başlıklar (HTTP) ve belgeyi içeren tam yanıtı kapsar. Benzer şekilde, [RequestEntity] türü, HTTP başlıklarını ve varsa gönderilen değeri de içeren istemcinin tüm isteğini kapsar;
- 23. satır: Çağıran yönteme döndürülen, [ResponseEntity<String>] nesnesinin gövdesi, yani sunucu tarafından gönderilen jSON dizesidir;
- 9-21. satırlar: [RequestEntity] türündeki isteği oluşturmamız gerekiyor. Bu istek, isteği oluşturmak için GET mi yoksa POST mi kullanıldığına göre farklılık gösterir;
- 9. satır: GET için istek. [RequestEntity] sınıfı, GET, POST, HEAD... sorgularını oluşturmak için statik yöntemler sunar. [RequestEntity.get] yöntemi, bu sorguyu oluşturan çeşitli yöntemleri zincirleyerek bir GET sorgusu oluşturulmasına olanak tanır:
- [RequestEntity.get] yöntemi, hedef URL'i bir URI örneği biçiminde parametre olarak kabul eder,
- [accept] yöntemi, HTTP başlığının [Accept] öğelerini tanımlamaya olanak tanır. Burada, sunucunun göndereceği [application/json] türünü kabul ettiğimizi belirtiriz;
- bu yöntem zincirinin sonucu bir [HeadersBuilder] türüdür;
- 10-12. satırlar: [User user] parametresi null değilse, isteğe HTTP ve [Authorization] başlıklarını ekliyoruz;
- 13. satır: [HeadersBuilder.build] yöntemi, bu farklı bilgileri kullanarak isteğin [RequestEntity] türünü oluşturur;
- 15. satır: POST için sorgu. [RequestEntity.post] yöntemi, bunu oluşturan çeşitli yöntemleri zincirleyerek bir POST sorgusu oluşturulmasına olanak tanır:
- [RequestEntity.post] yöntemi, hedef URL'i bir URI örneği biçiminde parametre olarak kabul eder,
- [header] yöntemi, kullanılmak istenen HTTP başlıklarını tanımlamaya olanak tanır; burada yetki başlığıdır,
- ardından gelen [header] yöntemi, isteğe [Content-Type: application/json] başlığını ekleyerek, gönderilen değerin jSON dizesi biçiminde geleceğini belirtir;
- [accept] yöntemi, sunucunun göndereceği [application/json] türünü kabul ettiğimizi belirtmemizi sağlar;
- 17-19. satırlar: [User user] parametresi null değilse, isteğe HTTP [Authorization] başlığını ekleriz;
- 20. satır: [BodyBuilder.body] yöntemi, kaydedilen değeri belirler. Bu değer, genel [getResponse] yönteminin (2. satır) 2. parametresidir;
- satır 25-28: Herhangi bir hata meydana gelirse, [RdvMedecinsException] türünde bir istisna atılır;
26. ve 28. satırlardaki [getMessagesForException] yöntemi şöyledir:
// bir istisnaya ait hata mesajları listesi
protected static List<String> getMessagesForException(Exception exception) {
// istisnaya ait hata mesajları listesi alınır
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// mesaj, yalnızca !=null ve boş değilse alınır
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// sonraki neden
cause = cause.getCause();
}
return erreurs;
}
Özel [getBase64] yöntemi, HTTP kimlik doğrulama başlığı için 'login:passwd' dizesinin Base64 kodunu sağlar:
private String getBase64(User user) {
// kullanıcı adını ve şifresini base64 ile kodlar - Java 8 gerektirir
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
[Dao] sınıfı, [AbstractDao] sınıfını şu şekilde genişletir:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// jSON eşleştiricileri
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- 22. satır: [Dao] sınıfı bir Spring bileşenidir. Burada [@Service] anotasyonu kullanılmıştır. Şimdiye kadar kullanılan [@Component] anotasyonunu kullanmaya devam edilebilirdi;
- 26-36. satırlar: [DaoConfig] yapılandırma sınıfında tanımlanan dört jSON eşleştiricisinin enjeksiyonu;
[Dao] sınıfındaki yöntemlerin tümü aynı şemayı izler. Bir GET işlemi ile bir POST işlemini ayrıntılı olarak inceleyeceğiz.
Öncelikle bir [GET] sorgusu:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// yanıt
Response<AgendaMedecinJour> response;
// takvim
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// takvim AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// cevabın analizi
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- 5. satır: [getResponse] genel yöntemi çağrılır. Kullanılan etkin parametreler şunlardır:
- 1: kullanıcı;
- 2: hedef URL;
- 3: gönderilecek değer. Burada böyle bir değer yoktur;
- 5. satır: Çağrı, try / catch bloğu içine alınmamıştır. [getResponse] yöntemi, [RdvMedecinsException] türünde bir istisna tetikleyebilir. Bu istisna tetiklendiğinde, yukarıdaki [getAgendaMedecinJour] yöntemini çağıran yönteme iletilir;
- 8. satır:URL [/getAgendaMedecinJour], sunucu tarafında jSON jSON eşleştiricisi tarafından [Response<AgendaMedecinJour>] türüne serileştirilmiş bir jSON gönderirW2HTMLP005748ZQX eşleştiricisi tarafından sunucu tarafında jSON türüne serileştirilmiştir. Alınan jSON dizesini deserileştirmek için de aynı eşleştirici kullanılır;
- 10-13. satırlar: 9. satırda bir hata oluşursa, [RdvMedecinsException] türünde bir istisna atılır;
- satır 16-21: sunucu tarafından gönderilen yanıt analiz edilir;
- satır 17-18: sunucu bir hata bildirmişse, sunucu tarafından iletilen bilgilerle bir istisna atılır;
- satır 19-21: aksi takdirde doktorun randevu takvimi döndürülür;
İncelenecek POST isteği şu şekilde olacaktır:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// cevap
Response<Rv> response;
try {
// Rv
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// Rv Rv
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// yanıt analizi
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- 6. satır: [getResponse] yöntemi aşağıdaki parametrelerle çağrılır:
- 1: kullanıcı;
- 2: hedef URL,
- 3: gönderilen değer: Yöntemin parametre olarak aldığı bilgilerle oluşturulan [PostAjouter] türündeki jSON değeri aktarılır. Filtre içermeyen bir jSON eşleştiricisi kullanılır;
- 9. satır: Sunucu tarafında, sunucunun yanıtını serileştiren jSON [jsonMapperLongRv] eşleştiricisidir. İstemci tarafında ise bu aynı eşleştirici, yanıtı deserileştirmek için kullanılır;
- 6. satır: URL ve [/ajouterRv], jSON değerini [Response<Rv>] türüne dönüştürür;
- 4-11. satırlar: Burada, [getResponse] yöntemi bir try/catch bloğu içine alınmıştır, çünkü gönderilen değerin serileştirilmesi bir istisna oluşturabilir. [getResponse] yöntemi, [RdvMedecinsException] istisnasını tetikleyebilir. Bu durumda, yöntem yeniden çağrılır (satır 11-12);
Aşağıdaki kod (13-24. satırlar), az önce incelenen kodla benzerdir. Dolayısıyla, bir GET işlemiyle arasındaki tek fark, [getResponse] yönteminin ikinci parametresidir; bu parametre, gönderilecek değerin jSON değerini almalıdır.
Diğer yöntemler de aynı model üzerine kurulmuştur.
8.5.11. Anormallik
Çeşitli testler yapılırken, aşağıdaki [Anomalie] sınıfında özetlenen bir anomaliyle karşılaşılmıştır:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serileştirici jSON
static private ObjectMapper mapper = new ObjectMapper();
// bağlantı zaman aşımı (milisaniye cinsinden)
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// katmandan bir referans alınır [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// web hizmeti / json'un URL'i ayarlanır
dao.setUrlServiceWebJson("http://localhost:8080");
// zaman aşımları milisaniye cinsinden ayarlanıyor
dao.setTimeout(TIMEOUT);
// Kimlik doğrulama
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Kimlik Doğrulama
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Kimlik doğrulama
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// bağlamın kapatılması
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- 31-38. satırlar: [admin, admin] kullanıcısı doğrulanır;
- 40-47. satırlar: [admin, x] kullanıcısı doğrulanır; bu kullanıcının şifresi yanlıştır;
- 49-56. satırlar: mevcut ancak yetkisiz bir kullanıcı olan [user, user] kullanıcısının kimlik doğrulaması yapılır;
Sonuçlar şöyledir:
- 2. satır: Beklenmedik bir şekilde, [admin, x] kullanıcısı kabul edildi;
Kodun 33-38. satırlarını yorum satırına alırsak, şu sonucu elde ederiz:
ki bu beklenen sonuçtur. Sanki [admin, admin] kullanıcısı ilk kez başarılı bir şekilde oturum açtığında, sonraki oturumlar için şifresine artık gerek kalmamış gibi görünüyor. Durum tam da böyledir. Spring Security, varsayılan olarak bir oturum kullanır; bu sayede bir kullanıcı kimlik doğrulamasını yaptıktan sonra, sonraki isteklerde bunu tekrarlamasına gerek kalmaz. Web sunucusundaki / jSON yapılandırmasını değiştirerek bu durumun ortadan kalkmasını sağlayabiliriz:
![]() |
[SecurityConfig] dosyası şu şekilde değiştirilmelidir:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// oturum yok
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- 5. satırda güvenlik oturumu olmaması istenmektedir;
Bu, hatayı çözdü.
8.6. Spring / Thymeleaf sunucu kodu
8.6.1. Giriş
Oluşturulacak istemci/sunucu uygulama mimarisine geri dönelim:
![]() |
- [Web2] web sunucusu / jSON oluşturuldu;
- [Web1] istemcisinin [DAO] katmanı oluşturulmuştur;
[Web1] sunucusu ile istemci tarayıcıları arasındaki ilişki, sunucunun bir web sunucusu / jSON olduğu bir istemci / sunucu ilişkisidir. Aslında, [Web1], bir jSON dizesi içinde kapsüllenmiş HTML akışlarını iletecektir. İstemci/sunucu mimarisi şu şekildedir:
![]() |
- [2] istemci / [1] sunucu mimarisine sahibiz; burada istemci ve sunucu, jSON dilinde iletişim kuruyor;
- [1]'te, Spring MVC / Thymeleaf web katmanı, jSON formatında görünümler, görünüm parçaları ve veriler sunar. Dolayısıyla sunucu, [Web1] sunucusu gibi bir web sunucusu / jSON'tir. O da durum bilgisi içermez;
- [2]'te: uygulamanın başlatılması sırasında yüklenen görünüme gömülü JavaScript kodu katmanlar halinde yapılandırılmıştır:
- [présentation] katmanı kullanıcı etkileşimlerinden sorumludur,
- [DAO] katmanı, [Web2] sunucusu aracılığıyla verilere erişimden sorumludur;
- [2] istemcisi, sunucunun yükünü azaltmak amacıyla belirli görünümleri önbelleğe alacaktır;
Spring MVC / Thymeleaf ile uygulanan web sunucusu / jSON [Web1]'i birkaç adımda oluşturacağız:
- CSS Bootstrap çerçevesini inceleme;
- görünümlerin yazılması;
- denetleyicinin yazılması;
Ardından, ayrı bir aşamada, [Web1] sunucusunun JS istemcisini oluşturacağız. Bu istemcinin [Web1] sunucusuna karşı belirli bir bağımsızlığa sahip olduğunu açıkça göstermek için, onu STS yerine [Webstorm] aracıyla oluşturacağız.
Bundan sonra, kodun düzenlenmesi gibi önemli noktaları gözden kaçırmamıza neden olabilecek bazı ayrıntılar göz ardı edilecektir. Konuyla ilgilenen okuyucular, bu belgenin web sitesinde kodun tamamını bulabilirler.
8.6.2. STS projesi
![]() |
- [1]'te Java kodları;
- [2] dosyasında görünümler;
[pom.xml]'teki Maven yapılandırması ş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>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- 16-19. satırlar: proje bir Thymeleaf projesidir;
- 20-24. satırlar: az önce oluşturduğumuz [DAO] katmanına dayanmaktadır;
Java yapılandırması iki dosya ile sağlanır:
![]() |
[web] katmanı, aşağıdaki [WebConfig] dosyası ile yapılandırılır:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- QZXW2HTML katmanı yapılandırması BW3dlYl0ZQX
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// başlıklar için dispatcherservlet yapılandırması CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
Bu yapılandırmanın tüm unsurlarıyla bir şekilde karşılaşmışızdır. Şunu hatırlatmak yeterlidir: 42-47. satırlar, sunucuya etki alanları arası isteklerle (CORS) sorgulama yapmak istediğimizde gereklidir. Burada da durum böyle olacaktır.
[AppConfig] sınıfı, uygulamanın tamamını yapılandırır:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// web hizmeti kökü / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// zaman aşımı (milisaniye cinsinden)
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- 11. satır: [AppConfig], [DAO] katmanının ve [web] katmanının yapılandırmasını içe aktarır;
- 15-16. satırlar: Uygulamanın, doktorları ve müşterileri önbelleğe almak amacıyla uygulamanın önyükleme aşamasına erişmesini sağlayacak kimlik bilgileri;
- 18. satır: / jSON [Web1] web hizmetinin URL'i;
- 20. satır: Uygulamanın HTTP çağrıları için timeout;
- 22. satır: etki alanları arası çağrılara izin verip vermemeyi belirleyen bir boole değeri;
Son olarak, [application.properties]'te Tomcat sunucusu 8081 numaralı bağlantı noktasında çalışacak şekilde yapılandırılmıştır:
![]() |
server.port=8081
8.6.3. Uygulamanın işlevleri
Bu özellikler 8.2. paragrafında açıklanmıştır. Şimdi bunları tekrar özetleyelim. Bir tarayıcı kullanarak URL [http://localhost:8081/boot.html] adresine erişilir:
![]() |
- [1] adresinden uygulamanın giriş sayfasına;
- [2] ve [3], uygulamayı kullanmak isteyen kişinin kullanıcı adı ve şifresidir. İki kullanıcı vardır: bir rolü (ADMIN) olan admin/admin (kullanıcı adı/şifre) ve (USER). Yalnızca ADMIN rolüne sahip kullanıcılar uygulamayı kullanma hakkına sahiptir. USER rolü, bu kullanım durumunda sunucunun ne yanıt verdiğini göstermek amacıyla eklenmiştir;
- [4], sunucuya bağlanmayı sağlayan düğme;
- [5]'te, uygulamanın dili. İki seçenek vardır: varsayılan olarak Fransızca ve İngilizce;
- [6]'te, [rdvmedecins-springthymeleaf-server] sunucusunun URL'i;
![]() |
- [1]'te oturum açılır;
![]() |
- bağlandıktan sonra, randevu almak istediğiniz doktoru [2] ve randevu gününü [3] seçebilirsiniz. Doktor ve gün girildiğinde, randevu takvimi otomatik olarak görüntülenir:
![]() |
- doktorun randevu takvimini gördükten sonra, bir randevu aralığı rezerve edebilirsiniz [5];
![]() |
- [6]'te randevu için hasta seçilir ve bu seçim [7]'te onaylanır;
![]() |
Randevu onaylandıktan sonra, otomatik olarak takvime geri dönülür ve yeni randevu artık takvime kaydedilmiştir. Bu randevu daha sonra [8] ile silinebilir.
Temel işlevler açıklanmıştır. Bunlar basittir. Son olarak dil yönetimine geçelim:
![]() |
- [1]'te Fransızca'dan İngilizce'ye geçilir;
![]() |
- [2]'te, takvim de dahil olmak üzere görünüm İngilizceye geçiyor;
8.6.4. Adım 1: CSS Bootstrap çerçevesine giriş
![]() |
Yukarıdaki web istemcisinde, sayfalar HTML şimdi tanıtacağımız CSS Bootstrap [http://getbootstrap.com/] çerçevesini kullanacaktır.
8.6.4.1. Örnek proje
Örneklerin yapısı şu şekilde olacaktır:
![]() |
- [1]: projenin tamamı;
- [2]: Java kodları;
- [3]: JavaScript betikleri;
![]() |
- [4] dosyasında: JavaScript kütüphaneleri;
- [5]'te: Thymeleaf görünümleri;
- [6]'te: stil sayfaları;
8.6.4.1.1. Maven Yapılandırması
[pom.xml] dosyası, bir Thymeleaf Maven projesine aittir:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-bootstrap</name>
<description>Démos Bootstrap</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath /> <!-- depo'dan üst öğeyi ara -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. Java Yapılandırması
![]() |
[BootstrapDemo] sınıfı, Spring / Thymeleaf uygulamasını yapılandırır:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(BootstrapDemo.class, args);
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}
Bu tür bir kodla daha önce karşılaşmıştık.
8.6.4.1.3. Spring denetleyicisi
![]() |
[BootstrapController] denetleyicisi şu şekildedir:
package istia.st.rdvmedecins;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BootstrapController {
@RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bso1() {
return "bs-01";
}
@RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs02() {
return "bs-02";
}
@RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs03() {
return "bs-03";
}
@RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs04() {
return "bs-04";
}
@RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs05() {
return "bs-05";
}
@RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs06() {
return "bs-06";
}
@RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs07() {
return "bs-07";
}
@RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs08() {
return "bs-08";
}
}
Eylemler, yalnızca Thymeleaf tarafından işlenen görünümleri görüntülemek için vardır.
8.6.4.1.4. [application.properties] dosyası
[application.properties] dosyası, yerleşik Tomcat sunucusunu yapılandırır:
server.port=8082
8.6.4.2. Örnek 1: Jumbotron
[/bs-01] eylemi, aşağıdaki [bs-01.xml] görünümünü görüntüler:
![]() |
[bs-01.xml] görünümü şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- hata -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- 7. satır: Bootstrap çerçevesine ait CSS dosyası;
- 8. satır: yerel bir CSS dosyası;
- 13. satır: [1] dosyasını görüntüler;
- 19-21. satırlar: [2] dosyasını görüntüler;
- 11. satır: CSS [container] sınıfı, tarayıcının içinde bir görüntüleme alanı tanımlar;
- 19. satır: CSS [alert] sınıfı renkli bir alan görüntüler. [alert-danger] sınıfı önceden tanımlanmış bir renk kullanır. Bunların birkaç tanesi vardır: [alert-info, alert-warning,...];
[1] jumbotronu, aşağıdaki [jumbotron.xml] görünümü tarafından oluşturulur:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- 4. satır: alanın sınıfı CSS [jumbotron]'tir;
- 5. satır: [row] sınıfı, 12 sütunlu bir satır tanımlar;
- 6. satır: [col-md-2] sınıfı, satırda iki sütunlu bir alan tanımlar;
- 7. satır: Bu iki sütuna bir resim yerleştirilir;
- 9-15. satırlar: Diğer 10 sütuna metin yerleştirilir;
8.6.4.3. Örnek 2: Gezinme çubuğu
[/bs-02] eylemi, aşağıdaki [bs-02.xml] görünümünü görüntüler:
![]() |
Yeni özellik, giriş formu ve düğmeleriyle birlikte [1] gezinme çubuğudur:
[bs-02.xml] görünümü şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- komut dosyaları JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar1"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- bilgi -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 10. satır: jQuery dosyası içe aktarılır;
- 11. satır: yerel bir JS komut dosyası;
- 16. satır: gezinme çubuğu;
Gezinme çubuğu, aşağıdaki [navbar1.xml] görünümü tarafından oluşturulur:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- kimlik formu -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- 3. satır: [navbar] sınıfı, gezinme çubuğunun stilini belirleyecektir. [navbar-inverse] sınıfı, çubuğa siyah arka plan rengi verir. [navbar-fixed-top] sınıfı, tarayıcıda görüntülenen sayfayı 'kaydırdığımızda' gezinme çubuğunun ekranın üst kısmında kalmasını sağlar;
- 5-13. satırlar: [1] alanını tanımlar. Bu, genellikle benim anlamadığım bir dizi sınıftır. Bileşeni olduğu gibi kullanıyorum;
- 14-26. satırlar: komut çubuğunun "duyarlı" bir alanını tanımlar. Akıllı telefonlarda bu alan bir menü alanının içinde kaybolur;
- 15. satır: Şu anda gizlenmiş bir resim;
- 17-25. satırlar: [navbar-form] sınıfı, komut çubuğundaki bir formu biçimlendirir. [navbar-right] sınıfı ise bu formu komut çubuğunun sağına yerleştirir;
- satır 21-23: 17. satırdaki [2] formundaki iki giriş alanı. Bu alanlar, bir formun öğelerini biçimlendiren [form-group] sınıfının içindedir ve her biri [form-control] sınıfına sahiptir;
- 24. satır: Bir düğmeyi tanımlayan [btn] sınıfı, ona yeşil rengini veren [btn-success] sınıfıyla zenginleştirilmiştir;
- 24. satır: [Connexion] düğmesine tıklandığında, aşağıdaki JS işlevi yürütülür:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
İşte bir örnek:

8.6.4.4. Örnek 3: Liste düğmesi
[/bs-03] eylemi, aşağıdaki [bs-03.xml] görünümünü görüntüler:
![]() |
- Yenilik, “dropdown” olarak da adlandırılan [1] numaralı açılır liste öğesidir;
[bs-03.xml] görünümünün kodu şu şekildedir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar2"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- bilgi -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 11. satır: Liste düğmesi, Bootstrap'ın JS dosyasını gerektirir;
- 18. satır: yeni gezinme çubuğu;
[navbar2.xml] görünümü şöyledir:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- kimlik doğrulama formu -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- diller -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- başlangıç sayfası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initNavBar2();
/*]]>*/
</script>
</section>
- 25-40. satırlar: liste düğmesini tanımlar;
- 27. satır: [btn-danger] sınıfı, düğmeye kırmızı rengini verir;
- 32-39. satırlar: liste öğeleri. Bunlar, her biri bir JS işlevine bağlı bağlantılardır;
- 46-51. satırlar: belgenin yüklenmesinden sonra çalıştırılan bir JS komut dosyası;
JS [bs-03.js] komut dosyası şöyledir:
function initNavBar2() {
// dil açılır menüsü
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- satır 1-4: [dropdown]'i başlatan işlev. [$('.dropdown-toggle')], [dropdown-toggle] sınıfına sahip öğeyi bulur. Bu, liste düğmesidir (görünümün 28. satırı). Bu düğmeye, JS [bootstrap.js] dosyasında tanımlanan JS [dropdown()] işlevi uygulanır. Ancak bu işlemden sonra düğme, liste düğmesi gibi davranır;
- 10-21. satırlar: bir dil seçildiğinde yürütülen işlev;
İşte bir örnek:

8.6.4.5. Örnek 4: bir menü
[/bs-04] eylemi, aşağıdaki [bs-04.xml] görünümünü görüntüler:
![]() |
[1] adlı bir menü eklenmiştir.
[bs-04.xml] görünümü şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- bilgi -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 18. satır: yeni bir gezinme çubuğu eklenmiştir;
[navbar3.xml] görünümü şu şekildedir:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- sağdaki düğmeler -->
<div class="navbar-form navbar-right" role="form">
<!-- çıkış -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- diller -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- başlangıç sayfası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initNavBar3();
/*]]>*/
</script>
</section>
- 16-29. satırlar: her biri JS komut dosyasına bağlı dört seçenek içeren menü oluşturulur;
- 55-60. satırlar: sayfa yüklendiğinde çalıştırılan bir komut dosyası;
JS [bs-04.js] komut dosyası şöyledir:
...
function initNavBar3() {
// dil açılır menüsü
$('.dropdown-toggle').dropdown();
// hareketli resim
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// menü bağlantıları
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// bunlar bir sözlüğe ekleniyor
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// tüm bağlantılar gizleniyor
for ( var key in options) {
options[key].hide();
}
// istenenler görüntülenir
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- 2-18. satırlar: sayfanın başlatma işlevi;
- 4. satır: dil listesi düğmesini görüntülemek için;
- 6-7. satırlar: Animasyonlu resim gizlenir;
- 26-48. satırlar: hangi seçeneklerin görünür olması gerektiğini belirlemeye yarayan bir [setMenu] işlevi;
Geliştirici konsoluna (Ctrl-Shift-I) gidelim ve şu kodu girelim: [1]:
![]() |
Ardından tarayıcıya geri dönelim. Menü değişmiştir [2]:
8.6.4.6. Örnek 5: Bir açılır liste
[/bs-05] eylemi, aşağıdaki [bs-05.xml] görünümünü görüntüler:
![]() |
Yenilik [1]'te yer alıyor. Burada Bootstrap dışında sağlanan bir bileşen kullanıyoruz: [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
[bs-05.xml] görünümünün kodu şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content" th:include="choixmedecin">
</div>
<!-- bilgi -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 8. satır: açılır liste için gerekli olan CSS;
- 13. satır: açılır liste için gerekli olan JS dosyası;
- 24. satır: açılır liste;
[choixmedecin.xml] görünümü şu şekildedir:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- yerel komut dosyası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initChoixMedecin();
/*]]>*/
</script>
</section>
- satır 7-12: burada, özel bir [combobox] sınıfına sahip klasik bir [select] etiketi bulunmaktadır. [data-style="btn-primary"] özniteliği, bileşene mavi rengini verir;
- 16-21. satırlar: sayfa yüklendiğinde çalıştırılan bir komut dosyası;
JS [bs-05.js] dosyası şu şekildedir:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// doktor seçim listesi
$('#idMedecin').selectpicker();
// menü
setMenu([ "lnkAfficherAgenda" ]);
}
- 7-12. satırlar: sayfa yüklendiğinde çalıştırılan işlev;
- 9. satır: Sayfadaki [select] öğesini Bootstrap açılır listesine dönüştüren komut. [$('#idMedecin')], [select] öğesine ([choixmedecin] görünümünün 7. satırı) ve JS [selectpicker] işlevi, JS [bootstrap-select.js] dosyasından gelir;
- 11. satır: menü seçeneklerinden sadece biri görüntülenir;
- 2-5. satırlar: [Agenda] menü seçeneğine tıklandığında JS işlevi çalıştırılır;
- 3. satır: açılır listeden seçilen seçeneğin değeri alınır: [$('#idMedecin option:selected')] önce [id=idMedecin] bileşenini bulur, ardından bu bileşende seçilen seçeneği bulur. [..].val() işlemi daha sonra bulunan öğenin değerini, yani seçilen seçeneğin [value] özniteliğini alır;
İşte bir doktor seçimi örneği:
![]() |
8.6.4.7. Örnek 6: Takvim
[/bs-06] eylemi, aşağıdaki [bs-06.xml] görünümünü görüntüler:

Bir doktor veya tarih seçildiğinde, seçilen doktoru ve tarihi görüntüleyen JS işlevi tetiklenir. İşte bir örnek:
![]() |
"Dil listesi" düğmesi sayesinde takvimi (ve sadece takvimi) İngilizceye geçirebilirsiniz:

Bu, serideki en karmaşık örnektir. Takvim, [bootstrap-datepicker] ve [http://eternicode.github.io/bootstrap-datepicker] bileşenlerinden oluşur.
[bs-06.xml] görünümü şu şekildedir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- içerik -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- bilgi -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 8. satır: [bootstrap-datepicker] bileşenine ait CSS dosyası;
- 16. satır: [bootstrap-datepicker] bileşenine ait JS dosyası;
- 17. satır: Fransız takvimini yönetmek için JS dosyası. Varsayılan olarak İngilizce'dir;
- satır 15: JS dosyası, [moment] adlı kütüphaneden gelir ve çok sayıda zaman hesaplama işlevine erişim sağlar;
- 28. satır: takvim görünümü;
[choixmedecinjour.xml] görünümü şöyledir:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- yerel komut dosyası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- 17-23. satırlar: takvim;
- 18. satır: [btn-primary] sınıfı, takvime mavi rengini verir;
- 18. satır: [disabled="true"] özniteliği, tarihin elle girilmesini engeller. Mutlaka takvimden girilmesi gerekir;
- satır 16: takvim, bir [id="calendar_container"] bölümüne yerleştirilmiştir. Takvimin dilini değiştirmek için, takvimi silip yeniden oluşturmak gerekir. Bu nedenle, [id="calendar_container"] bileşeninin içeriğini sileceğiz ve yerine yeni dilde yeni takvimi ekleyeceğiz;
- 28-33. satırlar: sayfanın başlatma kodu;
JS [bs-06.js] dosyası şu şekildedir:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// takvim
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// takvim oluşturma
updateCalendar();
// doktor seçimi
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// menü
setMenu([]);
}
- 2. satır: Takvim, JS adlı çeşitli işlevler tarafından yönetilir. [calendar_infos] değişkeni, takvimle ilgili bilgileri toplayacaktır. Bu değişken, farklı işlevler tarafından görülebilmesi için küresel bir değişkendir;
- 6. satır: Takvimin konteyneri belirlenir;
- satır 7-12: takvim için kaydedilen bilgiler;
- 8. satır: takvimin konteynerine ilişkin bir referans,
- 9. satır: takvimin HTML kodu. Bu iki bilgi sayesinde takvimi silebilir ve yeniden oluşturabiliriz,
- 10. satır: bugünün tarihi [aaaa-mm-jj] biçiminde,
- satır 11: takvimin dili;
- satır 14: takvimin oluşturulması;
- satır 16: doktorlar açılır menüsü;
- satır 17-19: Bu açılır menüden seçilen değer her değiştiğinde, [afficherAgenda] yöntemi çalıştırılacaktır;
- 21. satır: gezinme çubuğunda menü yok;
[updateCalendar] işlevi şöyledir:
function updateCalendar(renew) {
if (renew) {
// mevcut takvimin yeniden oluşturulması
calendar_infos.container.html(calendar_infos.html);
}
// takvimin başlatılması
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// güncel tarihin seçilmesi
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// olaylar
calendar.datepicker().on('hide', function(e) {
// seçilen günün görüntülenmesi
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// yeni tarih not edilir
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// takvim bilgilerinin görüntülenmesi
afficherAgenda();
// seçilen günün görüntülenmesi
displayJour();
});
// seçilen günün görüntülenmesi
displayJour();
}
- satır 1: [updateCalendar] işlevi, mevcut olabilen veya olmayabilen bir parametre kabul eder. Eğer mevcutsa, takvim [calendar_infos]'te bulunan bilgilerden yeniden oluşturulur (satır 4);
- 7. satır: takvime referans verilir;
- satır 8-12: takvimin başlatma parametreleri;
- 9. satır: [aaaa-mm-jj] tarafından yönetilen tarih formatı,
- 10. satır: takvimde seçilebilecek ilk tarih. Burada bugünün tarihi. Bundan önceki tarihler seçilemez,
- 11. satır: takvimin dili. ['en'] ve ['fr'] olmak üzere iki dil olacaktır;
- satır 13: takvim yapılandırılmıştır;
- satır 15-17: [calendar_infos] tarihine başlandıysa, bu tarih takvimin güncel tarihi olarak belirlenir;
- 19-22. satırlar: Takvim her kapandığında, seçilen tarih görüntülenecektir;
- 23-30. satırlar: takvimde her tarih değişikliği olduğunda:
- 25. satır: Seçilen tarih [calendar_infos]'e kaydedilir,
- 27. satır: ajanda bilgileri görüntülenir,
- satır 29: seçilen gün görüntülenir;
- satır 32: varsa, seçilen gün görüntülenir;
Seçilen günü görüntüleyen [displayJour] yöntemi şöyledir:
// seçilen günü gösterir
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- 3. satır: Daha önce bir tarih seçilmişse (takvimin başlangıcında seçili bir tarih yoktur);
- satır 4: tarihin yazılacağı bileşeni belirler;
- 5. satır: Bu tarih İngilizce veya Fransızca olarak yazılabilir. [moment] kütüphanesinin dilini belirleriz;
- 6. satır: Seçilen tarih, seçilen dilde ve uzun formatta görüntülenir;
- 7. satır: Bu tarih görüntülenir;
İşte iki örnek:
![]() | ![]() |
Doktor veya tarih değişikliği yapıldığında, [afficherAgenda] yöntemi yürütülür:
function afficherAgenda() {
// doktor ve tarihi gösterir
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Örnek 7: 'responsive' bir HTML tablosu
Not: "responsive", bir bileşenin görüntülendiği ekranın boyutuna uyum sağlayabildiğini belirten İngilizce bir terimdir. Bunun bir örneğini göstereceğiz.
[/bs-07] eylemi, aşağıdaki [bs-07.xml] görünümünü (tam ekran) görüntüler:
![]() |
Yeni eklenen unsur, HTML [1] tablosudur. Bu tablo, JS [footable] kütüphanesi tarafından yönetilmektedir: [https://github.com/fooplugins/FooTable].
Tarayıcı penceresinin boyutunu küçültürsek, şu sonuç ortaya çıkar:
![]() |
- HTML tablosu ekran boyutuna uyum sağlamıştır;
- [1]'te, [Réserver] bağlantısını görmek için [+] işaretine tıklamak gerekir;
- [2]'te, [+] işaretine tıklandığında görülen şey;
[bs-07.xml] görünümü şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- içerik -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- bilgi -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 10. satır: [footable] kütüphanesindeki CSS;
- 19. satır: [footable] kütüphanesindeki JS;
- satır 31: bir ajandanın HTML tablosu;
[agenda.xml] görünümü şu şekildedir:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- başlangıç sayfası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initAgenda();
/*]]>*/
</script>
</body>
</html>
- 4. satır: tabloyu bir satıra ([row]) ve renkli bir çerçeveye ([alert alert-danger]) yerleştirir;
- 5. satır: tablo 6 sütun kaplayacaktır [col-md-6];
- 6. satır: HTML tablosu, Bootstrap [class='table'] ile biçimlendirilir;
- 9. satır: [data-toggle] özniteliği, satırı açıp kapatan [+/-] simgesini barındıran sütunu belirtir;
- 15. satır: [data-hide='phone'] özniteliği, ekran boyutu bir telefon ekranı boyutunda ise sütunun gizlenmesi gerektiğini belirtir. 'tablet' değeri de kullanılabilir;
- satır 31: JS işlevi, [Réserver] bağlantısına atanır;
- 46. satır: [Supprimer] bağlantısına JS işlevi atanır;
- 56-61. satırlar: sayfanın başlatılması;
Yukarıda kullanılan bazı CSS sınıfları, CSS ve [bootstrapDemo.css] dosyalarından gelmektedir:
@CHARSET "UTF-8";
#th sütunları {
text-align: center;
}
#td aralıkları {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
[status-*] stilleri, kütüphane sitesinde bulunan [footable] tablosunun kullanım örneğinden alınmıştır.
JS [bs-07.js] dosyasında, sayfa şu şekilde başlatılır:
function initAgenda() {
// zaman aralıkları tablosu
$("#creneaux").footable();
}
Hepsi bu kadar. [$("#creneaux")], "duyarlı" hale getirmek istediğimiz HTML tablosuna başvurur. Ayrıca, [Réserver] ve [Supprimer] bağlantıları ile ilişkili JS işlevleri de bulunmaktadır:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Örnek 8: Bir modal pencere
[/bs-08] eylemi, aşağıdaki [bs-08.xml] görünümünü görüntüler:

Daha önce [Réserver] bağlantısına tıklandığında bilgi kutusunda bir bilgi görüntülenirken, burada RV için bir müşteri seçmek üzere bir modal pencere görüntülenecektir:

Kullanılan bileşen, [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/] bileşenidir.
[bs-08.xml] görünümü şu şekildedir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- yerel komut dosyası -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- gezinme çubuğu -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- içerik -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- bilgi -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- 19. satır: modal pencereler için gerekli olan JS dosyası;
- 32. satır: [agenda-modal] görünümü, tek bir ayrıntı dışında [agenda] görünümüyle aynıdır: [Réserver] bağlantısını yöneten JS işlevi:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
[showDialogResa] işlevi, müşteri seçim modali penceresini görüntülemekle görevlidir;
- 33. satır: [resa.xml] görünümü, müşteri seçim modali kutusudur:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal başlık</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- başlangıç sayfası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initResa();
/*]]>*/
</script>
</section>
- 3-37. satırlar: modal pencere;
- 13-30. satırlar: bu pencerenin içeriği (ekranında görüntülenecekler);
- satır 31-34: iletişim kutusundaki düğmeler;
- 32. satır: JS ve [cancelDialogResa] işlevleri tarafından yönetilen bir [Annuler] düğmesi;
- satır 33: JS ve [validateResa] işlevleri tarafından yönetilen bir [Valider] düğmesi;
- satır 39-44: modal pencerenin başlatma komut dosyası;
Bu, aşağıdaki görünümü verir:
![]() |
Modal kutunun varsayılan olarak görüntülenmediğine dikkat edin. Bu nedenle, HTML kodu belgede mevcut olmasına rağmen, uygulama başlatıldığında bu kutu görünmez.
JS [bs-08.js] dosyası şu şekildedir:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// zaman aralığının kimliği kaydediliyor
this.idCreneau = idCreneau;
// rezervasyon diyalog penceresi görüntüleniyor
var resa = $("#resa");
resa.modal('show');
// günlük
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// diyalog penceresi gizleniyor
resa.modal('hide');
}
// rezervasyon onaylanır
function validateResa() {
// bilgileri alıyoruz
var idClient = $('#idClient option:selected').val();
// iletişim kutusu gizleniyor
resa.modal('hide');
// bilgiler
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// müşteri seçim listesi
$('#idClient').selectpicker();
// modal pencere
resa = $("#resa");
resa.modal({});
}
- 30-36. satırlar: modal pencerenin başlatma işlevi;
- 32. satır: modal pencere, başlatılması gereken bir açılır liste içerir;
- 34-35. satırlar: modal kutunun kendisinin başlatılması;
- 5-13. satırlar: JS bağlantısına eklenmiş [Réserver] işlevi;
- 7. satır: işlevin parametresi, 1. satırdaki küresel değişkene kaydedilir;
- 9-10. satırlar: modal pencere görünür hale getirilir;
- 12. satır: bilgi kutusuna bir bilgi kaydedilir;
- satır 15-18: [Annuler] düğmesinin yönetimi. Modal pencereyi gizlemekle yetiniyoruz (satır 17);
- 21-31. satırlar: [Valider] düğmesine bağlı JS işlevi;
- 23. satır: Seçilen müşterinin [value] özniteliği alınır;
- 25. satır: İletişim kutusu gizlenir;
- 27. satır: iki bilgiyi günlüğe kaydedilir: rezerve edilen zaman aralığının numarası ve hangi müşteri için olduğu;
8.6.5. Adım 2: Görünümlerin yazılması
Şimdi, [Web1] sunucusu tarafından sağlanan görünümleri ve bunların şablonlarını açıklayacağız.
![]() |
8.6.5.1. [navbar-start] görünümü
Bu görünüm, başlangıç sayfasının gezinme çubuğunu görüntüler:

[navbar-start.xml]'in kodu şöyledir:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- kimlik doğrulama formu -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- diller -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- sayfayı başlat -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initNavBarStart();
/*]]>*/
</script>
</section>
Bu görünümün şablonu yoktur. Aşağıdaki olay işleyicilere sahiptir:
évt | işleyici |
bağlantı düğmesine tıklama | |
[Français] bağlantısına tıklama | |
[English] bağlantısına tıklayın |
8.6.5.2. [jumbotron] görünümü
Bu, açılış sayfasında [navbar-start] gezinme çubuğunun altında görüntülenen görünümdür:

[jumbotron.xml] kodunun içeriği şöyledir:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
[jumbotron] görünümünün ne şablonu ne de olayları vardır.
8.6.5.3. [login] görünümü
Bu, açılış sayfasında jumbotronun altında gösterilen görünümdür:

[login.xml] kodunun içeriği şöyledir:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
Görünümün ne şablonu ne de olayları vardır.
8.6.5.4. [navbar-run] görünümü
Bu, oturum açma işlemi başarılı olduğunda görüntülenen gezinme çubuğudur:

[navbar-run.xml] kodunun içeriği şöyledir:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- sağdaki düğmeler -->
<form class="navbar-form navbar-right" role="form">
<!-- çıkış -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- diller -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- sayfayı başlat -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initNavBarRun();
/*]]>*/
</script>
</section>
Bu görünümün şablonu yoktur. Aşağıdaki olay işleyicilere sahiptir:
évt | işleyici |
çıkış düğmesine tıklama | |
[Français] bağlantısına tıklama | |
[English] bağlantısına tıklayın |
8.6.5.5. [accueil] görünümü
Bu, [navbar-run] gezinme çubuğunun hemen altında görüntülenen görünümdür:

[accueil.html] kodunun içeriği şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- takvim -->
<div id="agenda"></div>
<!-- yerel komut dosyası -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Şablonu şöyledir:
- [rdvmedecins.medecinItems] (8. satır): doktor listesi;
Mevcut haliyle, görünümde bir olay işleyicisi bulunmuyor gibi görünüyor. Aslında bunlar [initChoixMedecinJour] işlevinde tanımlanmıştır. Bu işlev, 466. sayfadaki 8.6.4.7 numaralı paragrafta ve özellikle 469. sayfada ele alınmıştır. Burada aşağıdaki olay işleyicileri bulunur:
evt | yönetici |
doktor seçimi | |
tarih seçimi |
8.6.5.6. [agenda] görünümü
[agenda] görünümü, bir doktorun ajandasındaki bir günü gösterir:

[agenda.xml] koduna ait görünüm şöyledir:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- rezervasyon -->
<section th:include="resa" />
</th:block>
<!-- sayfa başlatma -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initAgenda();
/*]]>*/
</script>
</body>
</html>
Bu görünümün şablonunda tek bir öğe bulunmaktadır:
- [agenda] (4. satır): ajandayı görüntülemek için özel olarak oluşturulmuş, biraz karmaşık bir şablon;
Aşağıdaki olay yöneticilerine sahiptir:
evt | yönetici |
[Supprimer] düğmesine tıklama | |
[Réserver] bağlantısına tıklayın |
47. satırdaki [resa] görünümü, kullanıcı [Réserver] bağlantısına tıkladığında görüntülenen görünümdür:

[resa.xml] kodunun içeriği şöyledir:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal başlık</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- sayfayı başlat -->
<script th:inline="javascript">
/*<![CDATA[*/
// sayfa başlatılıyor
initResa();
/*]]>*/
</script>
</body>
</html>
Bu şablonda tek bir öğe bulunmaktadır:
- [clientItems] (satır 24): müşteri listesi;
Aşağıdaki olay işleyicileri vardır:
evt | yönetici |
[Annuler] düğmesine tıklama | |
[Valider] düğmesine tıklayın |
8.6.5.7. [erreurs] görünümü
Kullanıcı tarafından istenen eylem gerçekleştirilemediğinde görüntülenen ekran şudur:

[erreurs.xml] kodu şöyledir:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-danger">
<h4>
<span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
</h4>
<ul>
<li th:each="message : ${erreurs}" th:text="${message}" />
</ul>
</div>
</section>
Şablonunda tek bir öğe bulunmaktadır:
- [erreurs] (8. satır): görüntülenecek hata listesi;
Görünümün olay işleyicisi yoktur.
8.6.5.8. Résumé
Aşağıdaki tablo, görünümleri ve şablonlarını göstermektedir:
görünüm | şablon | olay işleyicileri |
navbar-start | ||
jumbotron | ||
giriş | ||
navbar-run | ||
ana sayfa | ||
takvim | ||
Rezervasyon | ||
hatalar |
8.6.6. 3. Adım: İşlemlerin yazılması
[Web1] web hizmetinin mimarisine geri dönelim:
![]() |
Şimdi, [Web1] tarafından sunulan URL'lerin neler olduğunu ve bunların nasıl uygulandığını inceleyeceğiz:
8.6.6.1. [Web1] hizmeti tarafından sunulan URL'ler
Bunlar şunlardır:
- önceki görünümlerin her biri veya bunların bir bileşimi için birer URL;
- bir RV eklemek için bir URL;
- bir RV'i silmek için bir URL;
Hepsi aşağıdaki gibi bir [Reponse] türü yanıt verir:
public class Reponse {
// ----------------- özellikler
// işlem durumu
private int status;
// gezinme çubuğu
private String navbar;
// jumbotron
private String jumbotron;
// sayfanın ana içeriği
private String content;
// takvim
private String agenda;
...
}
- 5. satır: yanıt durumu: 1 (OK), 2 (hata);
- 7. satır: duruma göre [navbar-start] veya [navbar-run] görünümlerinin HTML akışı;
- 9. satır: [jumbotron] görünümüne ait HTML akışı;
- 13. satır: [agenda] görünümüne ait HTML akışı;
- 9. satır: duruma göre [accueil], [erreurs], [login] görünümlerinden gelen HTML akışı;
Gösterilen URL'ler şunlardır
[navbar-start] görünümünü [Reponse.navbar] içine yerleştirir | |
[navbar-run] görünümünü [Reponse.navbar] içine yerleştirir | |
[accueil] görünümünü [Reponse.content] içine yerleştir | |
[jumbotron] görünümünü [Reponse.jumbotron] içine yerleştirir | |
[agenda] görünümünü [Reponse.agenda] içine yerleştirir | |
[login] görünümünü [Reponse.content] içine yerleştirir | |
| |
[navbar-run] görünümünü [Reponse.navbar]'e, [jumbotron] görünümünü [Reponse.jumbotron]'e, [accueil] görünümünü [Reponse.content]'e, [agenda] görünümünü [Reponse.agenda]'e | |
seçilen randevuyu ekler ve yeni ajandayı [Reponse.agenda] dosyasına kaydeder | |
seçilen randevuyu siler ve yeni takvimi [Reponse.agenda] dosyasına kaydeder |
8.6.6.2. [ApplicationModel] tekil nesnesi
![]() |
[ApplicationModel] sınıfı tek bir örnek olarak oluşturulur ve uygulama denetleyicisine enjekte edilir. Kod şöyledir:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- 6. satır: [ApplicationModel] bir Spring bileşenidir;
- 7. satır: [DAO] katmanının arayüzünü uygulayan bir bileşendir. Bunu, eylemlerin [DAO] katmanını tanımak zorunda kalmaması, yalnızca [ApplicationModel] singleton'ını tanıması için yapıyoruz. Böylece [Web1]'in mimarisi şu şekilde olur:
![]() |
[ApplicationModel] sınıfının koduna geri dönelim:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// katman [DAO]
@Autowired
private IDao dao;
// yapılandırma
@Autowired
private AppConfig appConfig;
// katmandan gelen veriler [DAO]
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// yapılandırma verileri
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// istisna
private RdvMedecinsException rdvMedecinsException;
// üretici
public ApplicationModel() {
}
@PostConstruct
public void init() {
// yapılandırma
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// doktor ve müşteri açılır listeleri önbelleğe alınır
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// açılır listelerin öğeleri oluşturulur
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// alıcı ve ayarlayıcılar
...
// [IDao] arayüzünün uygulanması
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- 11. satır: [DAO] katmanının uygulamasının referansının eklenmesi. Daha sonra [IDao] arayüzünü uygulamak için bu referans kullanılır (64-80. satırlar);
- 14. satır: Uygulama yapılandırmasının eklenmesi;
- satır 33-37: Bu yapılandırma, uygulama mimarisinin çeşitli öğelerini yapılandırmak için kullanılır;
- satır 38-46: doktorlar ve müşteriler için açılır listeleri besleyecek bilgiler önbelleğe alınır. Dolayısıyla, bir doktor veya müşteri değiştiğinde uygulamanın yeniden başlatılması gerektiği varsayımıyla hareket ediyoruz. Buradaki amaç, bir Spring singleton'ının web uygulaması için önbellek görevi görebileceğini göstermektir;
[MedecinItem] ve [ClientItem] sınıflarının her ikisi de aşağıdaki [PersonneItem] sınıfından türetilmiştir:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// liste öğesi
private Long id;
private String texte;
// oluşturucu
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// getter ve setter'lar
...
}
- 8. satır: [id] alanı, açılır listedeki bir seçeneğin [value] özniteliğinin değeri olacaktır;
- 9. satır: [texte] alanı, açılır listedeki bir seçeneğin görüntülediği metin olacaktır;
8.6.6.3. [BaseController] sınıfı
![]() |
[BaseController] sınıfı, [RdvMedecinsController] ve [RdvMedecinsCorsController] denetleyicilerinin üst sınıfıdır. Bu üst sınıfın oluşturulması zorunlu değildi. Bu sınıfa, [RdvMedecinsController] sınıfındaki, biri hariç temel olmayan yardımcı yöntemler toplanmıştır. Bunlar üç gruba ayrılabilir:
- yardımcı yöntemler;
- görünümleri modelleriyle birleştiren yöntemler;
- bir eylemi başlatma yöntemi
| hata mesajlarının bir listesini sağlayan iki yardımcı yöntem. Bunlarla daha önce karşılaşmış ve kullanmıştık; |
| [accueil] görünümünü şablonsuz hale getirir |
| [agenda] görünümünü ve şablonunu oluşturur |
| [login] görünümünü şablonsuz olarak oluşturur |
| istenen eylem bir hatayla sonuçlandığında müşteriye yanıt verir |
| [RdvMedecinsController] denetleyicisinin tüm eylemlerini başlatma yöntemi |
Bu yöntemlerden ikisini inceleyelim.
[getPartialViewAgenda] yöntemi, oluşturulması en karmaşık olan görünüm olan ajanda görünümünü oluşturur. Kod şu şekildedir:
// akış [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// bağlamlar
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// sayfa şablonu oluşturulur [agenda]
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// şablonuyla birlikte ajanda
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- 9-10. satırlar: ajanda şablonunun iki öğesi:
- 9. satır: görüntülenen ajanda.
- 10. satır: kullanıcı randevu aldığında görüntülenen müşteri listesi;
7. satırdaki [setModelforAgenda] yöntemi şöyledir:
// [Agenda] sayfasının şablonu
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// sayfanın başlığı
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// randevu zaman aralıkları
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// doktor randevu aralığı
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// kimlik
modelCréneau.setId(créneau.getId());
// zaman aralığı
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// müşteri ve sipariş
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// sonraki zaman aralığı
i++;
}
// takvim şablonu veriliyor
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- 6. satır: Ajandanın bir başlığı vardır:

veya:

Tarih formatının dile bağlı olduğu görülüyor. Bu formatı mesaj dosyalarından alacağız (4. satır).
- 11-40. satırlar: Her zaman aralığı için şu görünümü görüntülemeliyiz:
![]()
ya da şu görünümü:
![]()
- 19-20. satırlar: zaman aralığını gösterir;
- 25-28. satırlar: zaman aralığı boşsa. Bu durumda [Réserver] düğmesi görüntülenmelidir;
- 31-36. satırlar: zaman aralığının dolu olduğu durum. Bu durumda hem müşteri bilgisi hem de [Supprimer] düğmesi görüntülenmelidir;
Daha ayrıntılı olarak açıklayacağımız diğer yöntem ise [getActionContext] yöntemidir. Bu yöntem, [RdvMedecinsController]'in her bir eyleminin başında çağrılır. İmzası şöyledir:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Bu yöntem, aşağıdaki [ActionContext] türünü döndürür:
public class ActionContext {
// tarih
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- 4. satır: eylemin Thymeleaf bağlamı;
- 5. satır: eylemin Spring bağlamı;
- 6. satır: eylemin yerel ayarı;
- 7. satır: olası hata mesajları listesi;
Parametreleri şunlardır:
- [lang]: 'en' veya 'fr' eylemi için istenen dil;
- [origin]: Etki alanları arası çağrı durumunda HTTP [origin] başlığı;
- [request]: işleme alınmakta olan HTTP isteği; buna bir süredir "eylem" denmektedir;
- [response]: bu isteğe verilecek yanıt;
- [result]: [RdvMedecinsController]'teki her eylem, geçerliliği test edilen bir POST değeri alır. [result], bu testin sonucudur;
- [rdvMedecinsController]: eylemlerin konteyner denetleyicisi;
[getActionContext] yöntemi şu şekilde uygulanır:
// eylemin bağlamı
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// dil?
if (lang == null) {
lang = "fr";
}
// yerel
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// Fransızca
locale = new Locale("fr", "FR");
} else {
// geri kalan her şey İngilizce
locale = new Locale("en", "US");
}
// başlıklar CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// başlatma hataları
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// POST hataları mı?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// hata yok
return actionContext;
}
- 3-15. satırlar: [lang] parametresinden yola çıkarak eylemin yerel ayarları belirlenir;
- 17. satır: Etki alanları arası istekler için gerekli olan HTTP başlıkları gönderilir. Ayrıntılara girmeyeceğiz. Kullanılan teknik, 8.4.14 numaralı paragrafta açıklanan tekniğidir;
- satır 19: hatasız bir [ActionContext] nesnesi oluşturulur;
- 21. satır: 8.6.6.2. paragrafında, [ApplicationModel] singletonunun hem hastaları hem de doktorları almak için veritabanına eriştiğini görmüştük. Bu erişim başarısız olabilir. Bu durumda ortaya çıkan istisnayı kaydediyoruz. 21. satırda bu istisnayı alıyoruz;
- 22-25. satırlar: Uygulamanın başlatılması sırasında bir istisna meydana gelirse, herhangi bir işlem yapılamaz. Bu durumda, tüm işlemler için istisnanın hata mesajlarını içeren bir [ActionContext] nesnesi döndürülür;
- 27-20. satırlar: [result] parametresini analiz ederek gönderilen değerin geçerli olup olmadığını belirleriz. Geçersizse, uygun hata mesajlarını içeren bir [ActionContext] nesnesi döndürürüz;
- satır 32: hata olmayan durum;
Şimdi [RdvMedecinsController] denetleyicisinin eylemlerini inceleyeceğiz
8.6.6.4. [/getNavBarStart] eylemi
[/getNavBarStart] eylemi, [navbar-start] görünümünü döndürür. İmzası şöyledir:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
Aşağıdaki [Reponse] türünü döndürür:
public class Reponse {
// ----------------- özellikler
// işlem durumu
private int status;
// gezinme çubuğu
private String navbar;
// jumbotron
private String jumbotron;
// sayfa gövdesi
private String content;
// takvim
private String agenda;
...
}
ve aşağıdaki parametrelere sahiptir:
- [PostLang postlang]: aşağıdaki gönderilen değer:
public class PostLang {
// veriler
@NotNull
private String lang;
...
}
[PostLang] sınıfı, gönderilen tüm değerlerin üst sınıfıdır. Nitekim, müşteri her zaman eylemin hangi dilde gerçekleştirileceğini belirtmelidir.
[getNavbarStart] yöntemi şu şekilde uygulanır:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// eylem bağlamları
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// görünüm geri gönderiliyor [navbar-start]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- 7. satır: eylemin başlatılması;
- 10-13. satırlar: eylemin başlatma yöntemi hata bildirmişse, bu hatalar durum kodu 2 ile birlikte müşteriye gönderilir (12. satır):
- 15-18. satırlar: [navbar-start] görünümü, durum kodu 1 ile gönderilir:
Bundan sonra, yalnızca yenilikleri detaylı olarak ele alacağız.
8.6.6.5. [/getNavbarRun] eylemi
[/getNavBarRun] eylemi, [navbar-run] görünümünü oluşturur:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// eylem bağlamları
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// [navbar-run] görünümü geri gönderiliyor
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
Bu eylem iki tür yanıt verebilir:
- hata içeren yanıt (satır 10-13):
- [navbar-run] görünümünü içeren yanıt:
8.6.6.6. [/getJumbotron] eylemi
[/getJumbotron] eylemi, [jumbotron] görünümünü döndürür:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// eylem bağlamları
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// [jumbotron] görünümü geri gönderilir
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
Eylem iki tür yanıt verebilir:
- hata içeren yanıt (satır 10-13):
- [jumbotron] görünümünü içeren yanıt:
8.6.6.7. [/getLogin] eylemi
[/getLogin] eylemi, [login] görünümünü döndürür:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// eylemin bağlamları
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// [login] görünümü geri gönderiliyor
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
Eylem iki tür yanıt verebilir:
- hata içeren yanıt (9-11. satırlar):
- [login] görünümünü içeren yanıt:
8.6.6.8. [/getAccueil] eylemi
[/getAccueil] eylemi, [accueil] görünümünü döndürür. İmzası şöyledir:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- 3. satırda, gönderilen değer aşağıdaki gibi [PostUser] türündedir:
public class PostUser extends PostLang {
// veriler
@NotNull
private User user;
...
}
- 1. satır: [PostUser] sınıfı, [PostLang] sınıfını genişletir ve dolayısıyla bir dil içerir;
- 4. satır: Görünümü almak isteyen kullanıcı;
Uygulama kodu şöyledir:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// eylem bağlamları
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// [accueil] görünümü korumalıdır
try{
// kullanıcı
User user = postUser.getUser();
// kimlik bilgileri kontrol ediliyor [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// bir hata döndürülüyor
return getViewErreurs(thymeleafContext, e.getMessages());
}
// [accueil] görünümü döndürülüyor
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- 15-22. satırlar: [accueil] sayfasının korumalı olduğu ve bu nedenle kullanıcının kimlik doğrulaması yapması gerektiği unutulmamalıdır;
Eylem iki tür yanıt verebilir:
- hata içeren yanıt (11. ve 21. satırlar):
- [accueil] görünümünü içeren yanıt (satır 24-27):
8.6.6.9. [/getNavbarRunJumbotronAccueil] eylemi
[/getNavbarRunJumbotronAccueil] eylemi, [navbar-run, jumbotron, accueil] görünümlerini oluşturur. İmzası şöyledir:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- 3. satır: gönderilen değer [PostUser] türündedir;
Eylemin uygulaması şöyledir:
// navigasyon çubuğu + jumbotron + ana sayfa
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// eylem bağlamları
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// [accueil] görünümü korumalıdır
try {
// kullanıcı
User user = postUser.getUser();
// kimlik bilgileri kontrol ediliyor [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// hata mesajı gönderiliyor
return getViewErreurs(thymeleafContext, e.getMessages());
}
// yanıt gönderiliyor
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
Eylem iki tür yanıt verebilir:
- hata içeren yanıt (satır 13, 23):
- [navbar-run, jumbotron, accueil] görünümlerini içeren yanıt (satır 26-31):
8.6.6.10. [/getAgenda] eylemi
[/getAgenda] eylemi, [agenda] görünümünü döndürür. İmzası şöyledir:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- 3. satır: gönderilen değer, aşağıdaki gibi [PostGetAgenda] türündedir:
public class PostGetAgenda extends PostUser {
// veriler
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- 1. satır: [PostGetAgenda] sınıfı, [PostUser] sınıfını genişletir ve dolayısıyla bir dil ve bir kullanıcı içerir;
- 5. satır: Takvimini almak istediğiniz doktorun numarası;
- 8. satır: İstenen takvim günü;
Uygulama şu şekildedir:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// eylemin bağlamları
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// gönderimin geçerliliği kontrol ediliyor
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// görünüm döndürülüyor [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- 14. satıra kadar, artık klasik hale gelmiş bir kod vardır;
- 16-21. satırlar: gönderilen değer üzerinde ek bir doğrulama yapılır. Tarih, bugünkü tarihten sonra veya bugünkü tarihe eşit olmalıdır. Bunu doğrulamak için bir doğrulayıcı kullanılır:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// randevu için seçilen gün
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// tarihler yyyy-MM-dd biçimine dönüştürülüyor
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// seçilen gün bugünün tarihinden önce olmamalıdır
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- 19. satır: Doğrulayıcı, [PostGetAgenda] ve [PostValiderRv] olmak üzere iki sınıf için çalışır;
[/getAgenda] eyleminin koduna geri dönelim:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// eylem
try {
// doktorun randevu takvimi
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// yanıt
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// [erreurs] görünümü döndürülür
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// [erreurs] görünümüne geri dönülüyor
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- 9-10. satırlar: gönderilen parametrelerle doktorun randevu takvimi isteniyor;
- 12-13. satırlar: randevu takvimi döndürülür:
- 17. ve 21. satırlar: hatalı bir yanıt döndürülür:
8.6.6.11. [/getNavbarRunJumbotronAccueilAgenda] eylemi
[/getNavbarRunJumbotronAccueilAgenda] eylemi, [navbar-run, jumbotron, accueil, agenda] görünümlerini döndürür. Uygulaması şu şekildedir:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// eylemin bağlamları
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// takvim
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
// yanıt gönderiliyor
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- 15-18. satırlar: [/getAgenda] eyleminin varlığından yararlanarak bu eylemi çağırırız. Ardından yanıtta yer alan status'i inceleriz (16. satır). Bir hata tespit edilirse, daha ileri gitmeyiz ve yanıtı geri göndeririz;
- 20. satır: İstenen görünümler gönderilir:
8.6.6.12. [/supprimerRv] eylemi
[/supprimerRv] eylemi, bir randevuyu silmeye yarar. İmzası şöyledir:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- 3. satır: gönderilen değer, aşağıdaki gibi [PostSupprimerRv] türündedir:
public class PostSupprimerRv extends PostUser {
// veriler
@NotNull
private Long idRv;
..
}
- 1. satır: [PostSupprimerRv] sınıfı, [PostUser] sınıfını genişletir ve dolayısıyla bir dil ve bir kullanıcı içerir;
- satır 5: silinecek randevunun numarası;
Eylemin uygulaması şu şekildedir:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// eylemin bağlamları
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// gönderilen değerler
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// Randevu siliniyor
AgendaMedecinJour agenda = null;
try {
// randevu geri alınır
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// ilgili randevu siliniyor
application.supprimerRv(user, idRv);
// doktorun randevu takvimi yeniden oluşturuluyor
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// yeni takvim döndürülür
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// [erreurs] görünümünü döndürür
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// [erreurs] görünümüne geri dönülür
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- 22. satır: Silinmesi gereken randevu alınır. Eğer yoksa, bir istisna oluşur;
- 23-25. satırlar: Bu randevudan yola çıkarak ilgili doktor ve gün belirlenir. Bu bilgiler, doktorun randevu takvimini yeniden oluşturmak için gereklidir;
- 27. satır: Randevu silinir;
- 29. satır: doktorun yeni randevu takvimi istenir. Bu önemlidir. Az önce boşalan zaman diliminin yanı sıra, uygulamanın diğer kullanıcıları da takvimde değişiklikler yapmış olabilir. Kullanıcıya takvimin en güncel sürümünü geri göndermek önemlidir;
- 31-34. satırlar: takvim geri gönderilir:
8.6.6.13. [/validerRv] eylemi
[/validerRv] eylemi, bir doktorun takvimine randevu ekler. İmzası şöyledir:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- 3. satır: gönderilen değer, aşağıdaki gibi [PostValiderRv] türündedir:
public class PostValiderRv extends PostUser {
// tarih
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- 1. satır: [PostValiderRv] sınıfı, [PostUser] sınıfını genişletir ve dolayısıyla bir dil ve bir kullanıcı içerir;
- satır 5: zaman aralığı numarası;
- 7. satır: Rezervasyonun yapıldığı müşterinin numarası;
- 10. satır: randevu günü;
Eylemin uygulaması şu şekildedir:
// randevunun onaylanması
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// eylemin bağlamları
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// hatalar mı?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// randevu tarihinin geçerliliği kontrol ediliyor
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// [erreurs] görünümü döndürülüyor
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// gönderilen değerler
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// eylem
try {
// zaman aralığıyla ilgili bilgiler alınır
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// Rv ekleniyor
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// takvimi yeniden oluşturuyoruz
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// yeni takvimi döndürüyoruz
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// görünümü geri döndürüyoruz [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// [erreurs] görünümünü geri döndür
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
Kod, [/supprimerRv] eyleminin koduna benzerdir.
8.6.7. Adım 4: Spring/Thymeleaf sunucusu testleri
Şimdi, Chrome eklentisi [Advanced Rest Client] (bkz. paragraf 9.6) ile yukarıdaki çeşitli eylemleri test edeceğiz.
8.6.7.1. Testlerin yapılandırılması
Tüm eylemler, gönderilen bir değer bekler. Aşağıdaki jSON dizesinin varyantlarını göndereceğiz:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Bu gönderilen değer, çoğu eylem için gereksiz bilgiler içerir. Ancak bu bilgiler, bunları alan eylemler tarafından göz ardı edilir ve hata oluşturmaz. Bu gönderilen değerin avantajı, gönderilmesi gereken farklı değerleri tek bir değerde toplamasıdır.
8.6.7.2. [/getNavbarStart] eylemi
![]() |
- [1]'e, test edilen eylem;
- [2]'e, gönderilen değere;
- [3]'e, gönderilen değer jSON dizesidir;
- [4]'te, [navbar-start] görünümü İngilizce olarak talep edilir;
Elde edilen sonuç şöyledir:
![]() |
[navbar-start] görünümünü İngilizce olarak aldık (vurgulanan alanlar).
Şimdi bir hata yapalım. Gönderilen değerin [lang] özniteliğini null olarak değiştiriyoruz. Aşağıdaki sonucu alıyoruz:
![]() |
[lang] alanının zorunlu olduğunu belirten bir hata yanıtı (durum 2) aldık.
8.6.7.3. [/getNavbarRun] eylemi
Aşağıdaki gönderilen değerle [getNavbarRun] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
8.6.7.4. [/getJumbotron] eylemi
Aşağıdaki değerle [getJumbotron] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
8.6.7.5. [/getLogin] eylemi
Aşağıdaki değerle [getLogin] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
8.6.7.6. [/getAccueil] eylemi
Aşağıdaki değerle [getAccueil] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Bilinmeyen bir kullanıcıyla yeniden deniyoruz:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Uygulamayı kullanma yetkisi olmayan mevcut bir kullanıcıyla yeniden deniyoruz:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
8.6.7.7. [/getAgenda] eylemi
Aşağıdaki gönderilen değerle [getAgenda] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Bugünden önceki bir tarihle yeniden deniyoruz:
![]() |
Var olmayan bir doktorla yeniden deniyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
8.6.7.8. [/getNavbarRunJumbotronAccueil] eylemi
Aşağıdaki gönderilen değerle [getNavbarRunJumbotronAccueil] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Bilinmeyen bir kullanıcıyla da aynı sonuç elde edilir:
![]() |
8.6.7.9. [/getNavbarRunJumbotronAccueilAgenda] eylemi
Aşağıdaki gönderilen değerle [getNavbarRunJumbotronAccueilAgenda] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Var olmayan bir doktor giriyoruz:
![]() |
8.6.7.10. [/supprimerRv] eylemi
Aşağıdaki gönderilen değerle [supprimerRv] eylemini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
93 numaralı Rv mevcut değil. Elde edilen sonuç şöyledir:
![]() |
Mevcut bir randevu ile:
![]() |
Veritabanında randevunun gerçekten silindiği doğrulanabilir. Yeni takvim geri gönderilir.
8.6.7.11. [/validerRv] eylemi
Aşağıdaki değerle [validerRv] işleminin gerçekleştirilmesini talep ediyoruz:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Elde edilen sonuç şöyledir:
![]() |
Veritabanında randevunun başarıyla oluşturulduğunu doğrulayabiliriz. Yeni ajanda geri gönderildi.
Aynı işlemi, mevcut olmayan bir zaman aralığı numarasıyla da yapıyoruz:
![]() |
Aynı işlemi, mevcut olmayan bir müşteri numarasıyla da yapıyoruz:
![]() |
8.6.8. 5. adım: JavaScript istemci kodu
[Web1] sunucu mimarisine geri dönelim:
![]() |
[Web1] sunucusunun [2] istemcisi, APU türünde bir JavaScript istemcisidir (Tek Sayfalı Uygulama):
- istemci, bir web sunucusundan (illa ki [Web1] olması gerekmez) önyükleme sayfasını ister;
- Ajax çağrıları yoluyla [Web1] sunucusundan sonraki sayfaları ister;
Bu istemciyi oluşturmak için [Webstorm] aracını kullanacağız (bkz. paragraf 9.8). Bu araç bana STS'ten daha kullanışlı geldi. En büyük avantajı, kod yazarken otomatik tamamlama özelliği sunması ve refactoring'in bazı seçeneklerini de içermesidir. Bu, birçok hatayı önler.
8.6.8.1. JS projesi
JS projesinin dizin yapısı şu şekildedir:
![]() |
- [1] içinde, JS istemcisi bir bütün olarak yer alır. [boot.html] başlangıç sayfasıdır. Tarayıcı tarafından yüklenen tek sayfa budur;
- [2]'te, Bootstrap bileşenlerinin stil sayfaları;
- [3] dosyasında, uygulama tarafından kullanılan birkaç resim bulunur;
![]() |
- [4] dosyasında, JS komut dosyaları bulunur. İşte bizim çalışmamız bu dosyada yer almaktadır;
- [5] dosyasında, kullanılan kütüphaneler: JS (özellikle jQuery) ve Bootstrap bileşenlerinin kütüphaneleri;
8.6.8.2. Kod mimarisi
Kod üç katmana ayrılmıştır:
![]() |
- [présentation] katmanı, [boot.xml] sayfasının başlatma işlevlerini ve çeşitli Bootstrap bileşenlerinin başlatma işlevlerini bir araya getirir. Bu katman, [ui.js] dosyası ile uygulanır;
- [événements] katmanı, [présentation] katmanındaki tüm olay işleyicilerini bir araya getirir. Bu katman, [evts.js] dosyası tarafından uygulanır;
- [DAO] katmanı, HTTP isteklerini [Web1] sunucusuna gönderir. Bu katman, [dao.js] dosyası tarafından uygulanır;
8.6.8.3. [présentation] katmanı
![]() |
[présentation] katmanı, aşağıdaki [ui.js] dosyası ile uygulanmaktadır:
//katmanı [présentation]
var ui = {
// küresel değişkenler;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// görünüm bileşenleri
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// olay katmanı
var evts = {};
// [dao] katmanı
var dao = {};
// ------------ belge hazır
$(document).ready(function () {
// belge başlatma
console.log("document.ready");
// sayfa bileşenleri
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
// giriş sayfasını geri getirebilmek için sayfayı belleğe alıyoruz
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL hizmeti
$("#urlService").val(ui.urlService);
});
// ------------------------ Bootstrap bileşenlerinin başlatma işlevleri
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// seçilen günü görüntüler
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- Katmanları birbirinden ayırmak için, bunların üç nesneye yerleştirilmesine karar verilmiştir:
- [ui], [présentation] katmanı için (satır 2-27),
- [evts], olay yönetimi katmanı için (satır 29),
- [dao], [DAO] katmanı için (satır 31);
Katmanların bu üç nesneye ayrılması, değişken ve işlev adlarında bir dizi çakışmayı önler. Her katman, katmanı kapsayan nesnenin önekini taşıyan değişkenleri ve işlevleri kullanır.
- satır 38-44: Görüntülenen görünümlerden bağımsız olarak her zaman mevcut olacak alanlar saklanır. Bu, tekrarlanan ve gereksiz jQuery aramalarını önler;
- 46-49. satırlar: Kullanıcı oturumu kapattığında ve dilini değiştirmediyse sayfayı yeniden görüntüleyebilmek için başlangıç sayfası yerel olarak saklanır;
- satır 54-83: Bootstrap bileşenlerinin başlatma işlevleri. Bunların tümü, 8.6.4 numaralı paragrafta yapılan incelemede ele alınmıştır;
8.6.8.4. [événements] katmanının yardımcı işlevleri
![]() |
Olay işleyicileri, [evts.js] dosyasına yerleştirilmiştir. Olay işleyicileri tarafından düzenli olarak kullanılan birkaç işlev vardır. Bunları şimdi tanıtacağız:
// bekleme süreci başlıyor
evts.beginWaiting = function () {
// bekleme başlangıcı
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// bekleme sonu
evts.stopWaiting = function () {
// bekleme sonu
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// sonuç görüntüleniyor
evts.showResult = function (result) {
// alınan veriler görüntüleniyor
var data = result.data;
// durum analiz ediliyor
switch (result.status) {
case 1:
// hata mı?
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// hatanın görüntülenmesi
evts.showException(data);
break;
}
};
// ------------ çeşitli işlevler
evts.showException = function (data) {
// hata ekranı
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- 2. satır: Herhangi bir asenkron [DAO] eylemi gerçekleştirilmeden önce [evts.beginwaiting] işlevi çağrılır;
- 4-5. satırlar: Bekleme animasyonu görüntülenir;
- 6-7. satırlar: hataların ve istisnaların (bunlar aynı şey değildir) görüntülenme alanı gizlenir;
- 8. satır: Asenkron bir işlemin devam ettiği belirtilir;
- 12. satır: Asenkron [DAO] eylemi sonucunu verdikten sonra [evts.stopwaiting] işlevi çağrılır;
- 14. satır: Asenkron işlemin tamamlandığı görülür;
- 15. satır: Bekleme animasyonu gizlenir;
- 20. satır: [evts.showResult] işlevi, [DAO] adlı asenkron işlemin [result] sonucunu görüntüler. Sonuç, şu biçimdeki bir JS nesnesidir: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- 47-50. satırlar: [result.status==2] durumunda kullanılır. Bu durum, [Web1] sunucusunun bir hata başlığı (örneğin 403 forbidden) içeren bir yanıt gönderdiğinde ortaya çıkar. Bu durumda [data], sunucu tarafından hatayı bildirmek için gönderilen jSON dizesidir;
- 25. satır: [Web1] sunucusundan geçerli bir yanıt alındığı durum. Bu durumda [data] alanı, sunucudan gelen yanıtı içerir: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- 27. satır: [Web1] sunucusunun bir hata yanıtı gönderdiği durum {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':hatalar};
- 28-29. satırlar: [erreurs] görünümü görüntülenir;
- satır 31-33: navigasyon çubuğunun görüntülenmesi (varsa);
- satır 34-36: jumbotronun görüntülenmesi (varsa);
- satır 37-39: [data.content] alanının görüntülenmesi (varsa). Duruma göre [accueil, agenda] görünümlerinden birini temsil eder;
- satır 40-43: takvim yeniden oluşturulduysa, ihtiyaç duyulduğunda her seferinde aramak zorunda kalmamak için bileşenlerine ait bazı referanslar alınır;
- satır 54: [evts.showException] işlevi, [data] parametresinde bulunan istisna metnini görüntülemekle görevlidir;
- 57-58. satırlar: istisna metni görüntülenir;
- 58. satır: istisnanın başlığı, o anda seçili olan dile göre değişir;
[evts.js] dosyası 300'den fazla kod satırı içeriyor; bunların hepsini tek tek yorumlamayacağım. Bu katmanın işleyişini göstermek için sadece birkaç örnek vereceğim.
8.6.8.5. Bir kullanıcının oturum açması

Bir kullanıcının oturum açması aşağıdaki işlevle sağlanır:
// ------------------------ bağlantı
evts.connecter = function () {
// gönderilecek değerler alınır
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// sunucunun URL değeri belirleniyor
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// istek parametreleri
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// istek gönderiliyor
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- 4-5. satırlar: kullanıcının kullanıcı adı ve şifresi alınır;
- 7-8. satırlar: URL hizmeti, [Web1] hizmetinden alınır. Bu değer, hem [ui] katmanında hem de [dao] katmanında saklanır;
- satır 10-16: gönderilecek değer: o anki dil ve oturum açmaya çalışan kullanıcı;
- satır 17-23: [sendMeBack] nesnesi, çağrılacak olan [DAO] işlevine aktarılan ve bu işlevin 22. satırdaki işlevine geri döndürmesi gereken bir nesnedir. Burada [sendMeBack] nesnesi, oturum açmaya çalışan kullanıcıyı kapsar;
- 25-29. satırlar: [evts.execute] işlevi, bir dizi asenkron eylemi yürütebilir. Burada, tek bir eylemden oluşan bir liste aktarılır. Bu eylemin alanları şunlardır:
- [name]: Yürütülecek asenkron eylemin adı,
- [post]: [Web1] sunucusuna gönderilecek değer,
- [sendMeBack]: asenkron eylemin sonucu ile birlikte geri döndürmesi gereken değer;
[evts.execute] işlevini ayrıntılı olarak incelemeden önce, 22. satırdaki [evts.connecterDone] işlevine bir göz atalım. Bu, çağrılan [DAO] asenkron işlevinin sonucunu döndürmesi gereken işlevdir:
evts.connecterDone = function (result) {
// sonuç görüntüleniyor
evts.showResult(result);
// bağlantı başarılı mı?
if (result.status == 1 && result.data.status == 1) {
// sayfa
ui.page = "accueil-sans-agenda";
// kullanıcı kaydediliyor
ui.user = result.sendMeBack.user;
}
};
- 3. satır: [Web1] sunucusu tarafından döndürülen sonuç görüntülenir;
- 5. satır: Bu sonuçta hata yoksa, yeni sayfanın türü (7. satır) ve kimliği doğrulanmış kullanıcı (9. satır) kaydedilir;
[evts.execute] işlevi bir dizi asenkron eylem gerçekleştirir:
// bir dizi eylemin yürütülmesi
evts.execute = function (actions) {
// işlem devam ediyor mu?
if (evts.travailEnCours) {
// hiçbir işlem yapılmıyor
return;
}
// bekleme
evts.beginWaiting();
// eylemlerin yürütülmesi
dao.doActions(actions, evts.stopWaiting);
};
- 2. satır: [actions] parametresi, yürütülecek asenkron eylemlerin listesidir;
- satır 4-7: Yürütme, halihazırda başka bir işlem devam etmiyorsa kabul edilir;
- 9. satır: bekleme başlatılır;
- 11. satır: [DAO] katmanından eylem dizisini yürütmesi istenir. İkinci parametre, dizideki tüm eylemler sonuçlarını verdiğinde yürütülecek işlevin adıdır;
Şu anda [dao.doActions] işlevini ayrıntılı olarak ele almayacağız. Başka bir olayı inceleyeceğiz.
8.6.8.6. Dil değişikliği

Dil değişikliği aşağıdaki işlev tarafından gerçekleştirilir:
// ------------------------ dil değişikliği
evts.setLang = function (lang) {
// dil değişikliği mi?
if (lang == ui.langue) {
// hiçbir şey yapılmıyor
return;
}
// yeni dil
ui.langue = lang;
// hangi sayfa çevrilmeli?
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- 2. satır: [lang] parametresi yeni dildir: 'fr' veya 'en';
- 4-7. satırlar: Yeni dil mevcut dil ile aynıysa, hiçbir işlem yapılmaz;
- 9. satır: yeni dil kaydedilir;
- 12-20. satırlar: Dil değişikliği durumunda, tarayıcıda o anda görüntülenen sayfanın yeniden oluşturulması gerekir. Üç olası sayfa vardır:
- [login] adlı sayfa, burada görüntülenen sayfa kimlik doğrulama sayfasıdır,
- [accueil-sans-agenda] adlı sayfa; bu, başarılı bir kimlik doğrulamasının hemen ardından görüntülenen sayfadır,
- [accueil-avec-agenda] adlı sayfa; bu, ilk takvim görüntülendiğinde açılan sayfadır. Ardından, kullanıcı oturumu kapatana kadar bu sayfa kalıcı olarak görüntülenir;
Şimdi [accueil-avec-agenda] sayfasını ele alacağız. Bu işlevin üç versiyonu vardır:
![]() |
- [ getAccueilAvecAgenda-one] sürümü tek bir asenkron eylemi yürütür;
- [ getAccueilAvecAgenda-parallel] sürümü, dört asenkron eylemi paralel olarak yürütür;
- [ getAccueilAvecAgenda-sequence] sürümü, dört asenkron eylemi arka arkaya yürütür;
8.6.8.7. [ getAccueilAvecAgenda-one] işlevi
Bu işlev şöyledir:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// istek parametreleri
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// istek
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- 4-9. satırlar: Gönderilecek değer, oturum açmış kullanıcıyı, istenen dili, randevu takvimini görmek istediğimiz doktorun numarasını ve istenen takvim gününü içerir;
- 10-12. satırlar: [sendMeBack] nesnesi, 11. satırdaki işlevine geri gönderilecek nesnedir. Burada herhangi bir bilgi içermez;
- satır 14-18: [accueil-avec-agenda] (satır 15) adlı bir asenkron eylemin yürütülmesi;
- 11. satır: [accueil-avec-agenda] adlı asenkron eylem sonucunu verdiğinde yürütülecek işlev;
11. satırdaki [evts.getAccueilAvecAgendaDone] işlevi, [accueil-avec-agenda] adlı asenkron işlevin sonucunu görüntüler:
evts.getAccueilAvecAgendaDone = function (result) {
// sonuç görüntüleme
evts.showResult(result);
// yeni sayfa?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- 1. satır: [result], [accueil-avec-agenda] adlı asenkron işlevin sonucudur;
- 3. satır: Bu sonuç görüntülenir;
- 5. satır: Hatasız bir sonuçsa, yeni sayfayı not ederiz (6. satır);
8.6.8.8. [ getAccueilAvecAgenda-parallel] işlevi
Bu işlev şöyledir:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// eylemler [navbar-run, jumbotron, accueil, agenda] içinde //
// navbar-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// jumbotron
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// ana sayfa
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// takvim
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'gün': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// eylemlerin yürütülmesi //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- 51. satır: Bu sefer dört asenkron eylem yürütülür. Bunlar paralel olarak yürütülecektir;
- satır 5-13: [navbar-run] gezinme çubuğunu alan [navbarRun] eyleminin tanımı;
- 12. satır: Asenkron eylem [navbarRun] sonucunu verdiğinde yürütülecek işlev;
- satır 15-23: [jumbotron] görünümünü alan [jumbotron] eyleminin tanımı;
- 22. satır: [jumbotron] asenkron eylemi sonucunu verdiğinde çalıştırılacak işlev;
- satır 25-34: [accueil] görünümünü alan [accueil] eyleminin tanımı;
- satır 33: asenkron eylem [accueil] sonuç verdiğinde çalıştırılacak işlev;
- satır 36-49: [agenda] eyleminin tanımı; bu eylem, [jumbotron] görünümünü alır;
- satır 48: asenkron eylem [agenda] sonuç verdiğinde çalıştırılacak işlev;
8.6.8.9. [ getAccueilAvecAgenda-sequence] işlevi
Bu işlev şöyledir:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// eylemler [navbar-run, jumbotron, accueil, agenda] sırayla
// takvim
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin' : ui.idMedecin,
'gün' : ui.jourAgenda,
"caller" : evts.getAgendaDone
};
// ana sayfa
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// jumbotron
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// navbar-run
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// eylemlerin sırayla yürütülmesi
evts.execute([ navbarRun ])
};
- 54. satır: [navbarRun] eylemi yürütülür. Bu işlem tamamlandığında, bir sonrakine geçilir: [jumbotron], satır 51. Bu eylem de sırayla yürütülür. Bu işlem tamamlandığında, bir sonrakine geçilir: [accueil], satır 40. Bu da sırayla yürütülür. Bu işlem tamamlandığında, bir sonrakine geçilir: [agenda], satır 29. Bu da sırayla yürütülür. Bu işlem tamamlandığında, [agenda] eyleminin bir sonraki eylemi olmadığı için işlem durdurulur.
8.6.8.10. [DAO] katmanı
![]() |
[dao.js] dosyası, [DAO] katmanındaki tüm işlevleri bir araya getirir. Bunları sırayla ele alacağız:
// URL sunucu tarafından sunulan
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- arayüz
// sunucu URL'si
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- 16-18. satırlar: URL hizmetinin [Web1] değerini belirlemeye yarayan işlev;
- 2-13. satırlar: Asenkron bir eylemin adını, sorgulanacak [Web1] sunucusunun URL'sine bağlayan sözlük;
// ------------------ eylemlerin genel yönetimi
// bir dizi asenkron eylemin yürütülmesi
dao.doActions = function (actions, done) {
// eylemlerin işlenmesi
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// asenkron DAO isteği
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- 3. satır: [dao.doActions] işlevi, bir dizi asenkron eylem olan [actions]'i yürütür. [done] parametresi, tüm eylemler sonuçlarını verdiğinde yürütülecek işlevdir;
- 7-12. satırlar: Asenkron eylemler paralel olarak yürütülür. Ancak, bunlardan birinin bir sonraki eylemi varsa, bu eylem kendisinden önceki eylemin sonunda yürütülür;
- 9. satır: [pending] durumunda bir [Deferred] nesnesi;
- satır 10: Bu nesne [resolved] durumuna geçtiğinde, [dao.actionDone] işlevi yürütülecektir;
- satır 11: listedeki i numaralı eylem asenkron olarak yürütülür. 3. satırdaki [done] parametresi, bu eyleme parametre olarak aktarılır;
Her asenkron eylemin sonunda yürütülen [dao.actionDone] işlevi şöyledir:
// bir sonuç alındı
dao.actionDone = function (result) {
// arayan?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// sonraki?
if (sendMeBack && sendMeBack.next) {
// asenkron DAO isteği
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// bitti mi?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// tamamlandı mı?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- 2. satır: [dao.actionDone] işlevi, yürütülecek eylemler listesindeki eşzamansız eylemlerden birinin sonucu olan [result] değerini alır;
- 4-7. satırlar: Tamamlanan asenkron eylem, sonucun geri gönderileceği bir işlev belirtmişse, bu işlev çağrılır;
- 9-14. satırlar: Tamamlanan asenkron eylemin bir sonraki eylemi varsa, bu eylem de sırayla yürütülür;
- 16. satır: Bir eylem tamamlanır. Tamamlanan eylem sayacı artırılır. Belirsiz sayıda sonraki eylemi olan bir eylem, bir eylem olarak sayılır;
- satır 19-21: Başlangıçta, dizideki tüm eylemler sonuçlarını verdiğinde yürütülmek üzere [done] işlevi belirtilmişse, bu işlev şimdi yürütülür;
[dao.doAction] yöntemi asenkron bir eylem yürütür:
// eylem yürütülüyor
dao.doAction = function (deferred, action, done) {
// eyleme eklenecek done işlevi
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// eylemin yürütülmesi
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- 4-10. satırlar: az önce gördüğümüz gibi, yürütülecek asenkron eylemin sonucunu işleyecek fonksiyonun [done] fonksiyonuna erişimi olmalıdır. Bunun için, bu fonksiyonu [sendMeBack] nesnesine yerleştiriyoruz; bu nesne, asenkron işlemin sonucunun bir parçası olacaktır;
- 12. satır: [Web1] sunucusuna HTTP çağrısı yapan [dao.executePost] işlevini çalıştırıyoruz. Hedef URL, yürütülecek eylemin adıyla ilişkili olan URL'tir;
[dao.executePost] işlevi, HTTP çağrısını gerçekleştirir:
// HTTP isteği
dao.executePost = function (deferred, sendMeBack, url, post) {
// Ajax çağrısı manuel olarak yapılıyor
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// sonuç döndürülüyor
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// hatayı döndürüyoruz
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
Bu fonksiyonu daha önce ele almış ve yorumlamıştık. Sadece 9. satırda, URL işlevinin, [Web1] sunucusundaki URL işleviyle, eylem adıyla ilişkili URL işlevinin birleştirilmesini hedeflediğine dikkat edelim.
8.6.8.11. Önyükleme sayfası
![]() |

[boot.html] önyükleme sayfası yukarıdaki görünümü gösterir. Bu, tarayıcı tarafından doğrudan yüklenen tek sayfadır. Diğer sayfalar Ajax çağrılarıyla elde edilir. Kod şöyledir:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Bootstrap çekirdeği CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- Bu şablon için özel stiller -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!-- Bootstrap çekirdeği JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- kullanıcı komut dosyaları -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- giriş formu -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- diller -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Bootstrap Jumbotron -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- hata panelleri -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!-- içerik -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- sayfa başlatma -->
<script>
// sayfa başlatılıyor
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- Bu tür bir sayfayla Bootstrap ile ilgili bölümde (8.6.4. paragraf) daha önce karşılaşmıştık;
- 99-105. satırlar: [présentation] katmanındaki bazı öğelerin başlatılması;
- 27. satırda, [getAccueilAvecAgenda-sequence.js] komut dosyası kullanılır. Bu satırdaki komut dosyasını değiştirerek, [accueil-avec-agenda] sayfasını elde etmek için üç farklı davranış elde edilir:
- [getAccueilAvecAgenda-one.js], tek bir HTTP çağrısıyla sayfayı alır,
- [getAccueilAvecAgenda-parallel.js], dört adet eşzamanlı HTTP çağrısıyla sayfayı alır;
- [getAccueilAvecAgenda-sequence.js], dört ardışık HTTP çağrısıyla sayfayı alır;
8.6.8.12. Tests
Testleri yapmanın çeşitli yolları vardır. Burada [Webstorm] aracını kullanacağız:
![]() |
- [1]'te bir proje açıyoruz. Test edilecek sitenin statik dizin yapısını (HTML, CSS, JS) içeren [2] klasörünü seçiyoruz;
![]() |
- [3] dosyasına, statik site;
- [4-5]'e, [boot.html] sayfası yüklenir;
![]() |
- [5]'te, [Webstorm] tarafından barındırılan bir sunucunun, [63342] bağlantı noktasından [boot.html] sayfasını sunduğu görülür. Bu, anlaşılması gereken önemli bir noktadır; çünkü bu, [boot.html] sayfasındaki komut dosyalarının, [localhost:8081] üzerinde çalışan [Web1] sunucusuna etki alanı ötesi çağrılar yapacağı anlamına gelir. [boot.html] sayfasını yükleyen tarayıcı, bu sayfayı [localhost:63342] adresinden yüklediğini bilir. Dolayısıyla, bu sayfanın [localhost:8081] sitesine çağrı yapmasına izin vermeyecektir, çünkü bağlantı noktası aynı değildir. Bu nedenle, 8.4.14. paragrafında açıklanan etki alanı arası çağrıları uygulayacaktır. Bu nedenle, [Web1] uygulamasının bu etki alanı arası çağrıları kabul edecek şekilde yapılandırılması gerekir. Bu ayar, Spring / Thymeleaf sunucusundaki [AppConfig] dosyasında yapılır:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// yönetici / yönetici
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// web hizmeti kökü / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// zaman aşımı (milisaniye cinsinden)
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
JS istemcisinin testlerini okuyucuya bırakıyoruz. Bu istemci, 8.6.3. paragrafında açıklanan işlevleri yeniden üretebilmelidir.
JS istemcisi doğru olarak onaylandıktan sonra, etki alanları arası istekleri yetkilendirmek zorunda kalmamak için onu [Web1] sunucu klasörüne dağıtabiliriz:
![]() |
Yukarıda, test edilen siteyi [src / main / resources / static] klasörüne kopyaladık. Ardından URL ve [http://localhost:8081/boot.html]'i talep edebiliriz:

Artık etki alanları arası istekler gerekmiyor ve [Web1] sunucusunun [AppConfig] yapılandırma dosyasına şunları yazabiliriz:
// CORS
private final boolean CORS_ALLOWED=false;
Yukarıdaki uygulama çalışmaya devam edecektir. [Webstorm] uygulamasına geri dönersek, artık çalışmayacaktır:


Geliştirici konsoluna (Ctrl-Shift-I) bakıldığında hatanın nedeni görülür:

Bu, yetkisiz etki alanları arası istek hatasıdır.
8.6.8.13. Conclusion
Aşağıdaki JS mimarisini oluşturduk:
![]() |
- katmanlar oldukça net bir şekilde ayrılmıştır;
- APU türünde bir uygulamamız var (Tek Sayfalı Uygulama). Artık bu özellik sayesinde çeşitli mobil cihazlar (Android, IoS, Windows Phone) için yerel bir uygulama oluşturabileceğiz;
- asenkron eylemleri paralel, sıralı veya her ikisinin bir karışımı olarak yürütebilen bir model oluşturduk;
8.6.9. 6. adım: Android için yerel uygulama oluşturma
[Phonegap] [http://phonegap.com/] aracı, bir IoS / JS / HTML uygulamasından mobil cihazlar (Android, [Phonegap], Windows 8, ...) için bir yürütülebilir dosya oluşturur. Bu amaca ulaşmanın çeşitli yolları vardır. En basit olanını kullanıyoruz: Phonegap sitesinde bulunan çevrimiçi bir araç olan [http://build.phonegap.com/apps]. Bu araç, dönüştürülecek statik sitenin zip dosyasını yükleyecektir. Başlangıç sayfasının adı [index.html] olmalıdır. Bu nedenle, [boot.html] adlı sayfayı [index.html] olarak yeniden adlandırıyoruz:
![]() |
ardından klasörü zip'liyoruz, burada [rdvmedecins-client-js-03]. Daha sonra Phonegap sitesine gidiyoruz: [http://build.phonegap.com/apps]:
![]() |
- [1] adresine gitmeden önce, bir hesap oluşturmanız gerekebilir;
- [1] adresinde işlemi başlatıyoruz;
- [2]'te, yalnızca bir Phonegap uygulamasına izin veren ücretsiz bir plan seçiyoruz;
![]() |
- [3] adımında, [4] adlı sıkıştırılmış uygulamayı indirin;
![]() |
- [5]'te uygulamaya bir isim verin;
- [6]'te, uygulamayı derleyin. Bu işlem 1 dakika sürebilir. Farklı mobil platformların simgeleri derleme işleminin tamamlandığını gösterene kadar bekleyin;
![]() |
- yalnızca Android [7] ve Windows [8] ikili dosyaları oluşturulmuştur;
- Android ikili dosyasını indirmek için [7]'e tıklayın;
![]() |
- [9] dosyasına indirilen [apk] ikili dosyasını ekleyin;
Bir Android tablet için [GenyMotion] emülatörünü başlatın (bkz. paragraf 9.9):
![]() |
Yukarıda, Android 19 sürümündeki API ile bir tablet emülatörü başlatılıyor. Emülatör başlatıldıktan sonra,
- yan taraftaki kilidi (varsa) sürükleyip bırakarak kilidi açın;
- fare ile indirdiğiniz [PGBuildApp-debug.apk] dosyasını sürükleyip emülatörün üzerine bırakın. Dosya bu şekilde kurulacak ve çalıştırılacaktır;
![]() |
URL değerini [1] olarak değiştirmeniz gerekir. Bunun için bir komut penceresinde [ipconfig] komutunu (aşağıdaki 1. satır) yazın; bu komut, bilgisayarınızdaki çeşitli IP adreslerini gösterecektir:
C:\Users\Serge Tahé>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 15 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet Connexion au réseau local :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
...
Ya IP Wi-Fi adresini (6-9. satırlar) ya da IP yerel ağ adresini (11-17. satırlar) not edin. Ardından, web sunucusundaki URL bölümünde bu IP adresini kullanın:
![]() |
Bunu yaptıktan sonra, web hizmetine bağlanın:
![]() |
Uygulamayı emülatörde test edin. Çalışması gerekir. Sunucu tarafında, CORS başlıklarının [ApplicationModel] sınıfında izin verilip verilmeyeceği belirlenebilir:
// CORS
private final boolean CORS_ALLOWED=false;
Bu, Android uygulaması için önemli değildir. Uygulama bir tarayıcıda çalışmaz. Oysa CORS başlıklarının gerekliliği tarayıcıdan kaynaklanır, sunucudan değil.
8.6.10. Vaka çalışmasının sonucu
Aşağıdaki mimariyi geliştirdik:
![]() |
Bu, karmaşık bir 3 katmanlı mimaridir. Amacı, [Web2] katmanını yeniden kullanmaktı; bu katman, [AngularJS-Spring MVC] uygulamasının sunucu katmanıydı ve [Tutoriel AngularJS / Spring 4] belgesindenURL ve [http://tahe.developpez.com/angularjs-spring4/] uygulamalarında yeniden kullanmak amacıyla tasarlanmıştı. 3 katmanlı mimarinin var olmasının tek nedeni budur. Oysa [AngularJS-Spring MVC] uygulamasında, [Web2]'in istemcisi bir [AngularJS] istemcisiyken, burada ise [Web2]'in istemcisi, [jQuery] / [Spring MVC / Thymeleaf]'ten oluşan 2 katmanlı bir mimaridir. Katman sayısını artırdığımız için performans kaybı yaşayacağız.
Burada incelenen uygulama, zaman içinde üç farklı belgede geliştirilmiştir:
- [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile]'ten URL'e ve [http://tahe.developpez.com/java/primefaces/]'e. Vaka çalışması o dönemde JSF2 / Primefaces çerçeveleriyle geliştirilmişti. Primefaces, JavaScript yazma ihtiyacını ortadan kaldıran AJAX tabanlı bileşenlerden oluşan bir kütüphanedir. O dönemde geliştirilen uygulama, burada incelenen uygulamadan daha az karmaşıktı. Bilgisayarlar için klasik bir web sürümü ve telefonlar için bir mobil sürümü vardı;
- [Tutoriel AngularJS / Spring 4]'ten URL'e ve [http://tahe.developpez.com/angularjs-spring4/]'e. O dönemde geliştirilen uygulama, bu belgede incelenen uygulama ile aynı özelliklere sahipti. Uygulama ayrıca Android platformuna da uyarlanmıştı;
- bu belge;
Bu çalışmadan benim için şu noktalar öne çıkıyor:
- [Primefaces] uygulaması yazılması en kolay olanıydı ve mobil web sürümü de yüksek performans gösterdi. Bu uygulama için JavaScript bilgisi gerekmiyor. Uygulamayı farklı mobil cihazların OS platformlarına yerel olarak uyarlamak mümkün değildir, ancak bu gerekli mi? Uygulamanın stilini değiştirmek zor görünüyor. Nitekim Primefaces stil sayfalarıyla çalışıyoruz. Bu bir dezavantaj olabilir;
- [AngularJS-Spring MVC] uygulamasını yazmak karmaşıktı. [AngularJS] çerçevesini tam olarak kavramak istediğimizde, bana oldukça zor geldi. [client Angular] / [service web / jSON implémenté par Spring MVC] mimarisi özellikle temiz ve yüksek performanslıdır. Bu mimari, tüm web uygulamaları için tekrarlanabilir. Bana en umut verici gelen mimari budur, çünkü hem istemci tarafında hem de sunucu tarafında farklı beceriler gerektirir (istemci tarafında JS+HTML+CSS, sunucu tarafında Java veya başka bir şey), bu da istemci ve sunucuyu paralel olarak geliştirmeye olanak tanır;
- bu belgede 3 katmanlı bir mimariyle geliştirilen uygulama için [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC] mimarisiyle geliştirilen uygulama için, bazıları [jQuery+Spring MVC+Thymelaf] teknolojisini [AngularJS] teknolojisine göre daha kolay kavrayabilir. Yazdığımız JavaScript istemcisinin [DAO] katmanı, diğer uygulamalarda da yeniden kullanılabilir;

























































































































































































































































































