Skip to content

4. Eylemler: model

Bir Spring uygulamasının mimarisine geri dönelim MVC:

Önceki bölümde, [1] isteğini, onu işleyecek denetleyiciye ve [2a] eylemine yönlendiren süreci inceledik; bu mekanizmaya yönlendirme denir. Ayrıca, bir eylemin tarayıcıya verebileceği farklı yanıtları da ele aldık. Şu ana kadar, kendilerine sunulan isteği kullanmayan eylemleri ele aldık. Bir [1] isteği, Spring MVC tarafından eyleme bir model biçiminde sunulan çeşitli bilgileri beraberinde taşır. Bu terimi, eylem tarafından üretilen bir görünüm V [2c]’in M modeli ile karıştırmamak gerekir:

  • Müşterinin HTTP isteği, [1] olarak gelir;
  • [2] aşamasında, istekte yer alan bilgiler [3] eylem şablonuna dönüştürülür; bu, genellikle (ancak zorunlu olarak değil) bir sınıftır ve [4] eylemine girdi olarak kullanılır;
  • [4]'te, eylem bu şablondan yola çıkarak bir yanıt oluşturacaktır. Bu yanıtın iki bileşeni olacaktır: bir V görünümü ([6]) ve bu görünümün M şablonu ([5]);
  • V [6] görünümü, müşteriye yönelik HTTP yanıtını oluşturmak için kendi M modeli olan [5]'i kullanacaktır.

MVC şablonunda, [4] eylemi C'nin (denetleyici) bir parçasıdır; [5] görünüm şablonu M'dir ve [6] görünümü ise V'dir.

Bu bölümde, doğası gereği karakter dizileri olan istek tarafından taşınan bilgiler ile çeşitli türlerdeki özelliklere sahip bir sınıf olabilen eylem modeli arasındaki bağlantı mekanizmaları incelenmektedir.

Not: [Modèle d'action] terimi tanınan bir terim değildir.

Bu yeni eylemler için yeni bir denetleyici oluşturuyoruz:

  

[ActionModelController] denetleyicisi şimdilik şu şekilde olacaktır:


package istia.st.springmvc.controllers;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class ActionModelController {

}
  • 5. satır: [@RestController] notunun, müşteriye gönderilen yanıtın denetleyicinin eylemlerinin sonucunun bir karakter dizisi olarak serileştirilmesi olduğunu hatırlatırız;

4.1. [/m01]: GET'in parametreleri

Aşağıdaki [/m01] eylemini ekliyoruz:



    // ----------------------- GET ile parametrelerin alınması------------------------
    @RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m01(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • 4. satır: Eylem, [nom] ve [age] adlı iki parametre kabul eder. Bu parametreler, HTTP GET isteğinde aynı isimleri taşıyan parametrelerle başlatılacaktır;

Chrome'da [1-3] komutunun sonuçları şunlardır:

  • [1]'te, GET isteği, [nom] ve [age] parametreleriyle;
  • [3]'te, [/m01] eyleminin bu parametreleri başarıyla aldığını görüyoruz;

4.2. [/m02]: POST'in parametreleri

Aşağıdaki [/m02] eylemini ekliyoruz:



    // ----------------------- POST ile parametreleri alma------------------------
    @RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m02(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • 4. satır: Eylem, [nom] ve [age] adlı iki parametreyi kabul eder. Bu parametreler, HTTP POST isteğinde aynı isimleri taşıyan parametrelerle başlatılacaktır;

[Advanced rest Client] ile elde edilen sonuçlar şunlardır:

  • [1-3]'te, POST sorgusu, [nom] ve [age] parametreleriyle;
  • [4-5]'e, POST isteğinin başlığı HTTP [Content-Type] olarak belirlenir. Bunun [Content-Type: application/x-www-form-urlencoded] olması gerekir;
  • [6]'te, [Form Data], POST işleminin parametre listesini verir. Burada [nom] ve [age] parametreleri görülmektedir;
  • [7]'te, [/m02] eyleminin [nom] ve [age] parametrelerini başarıyla aldığını gösteren sunucu yanıtı; ;

4.3. [/m03]: aynı adlı parametreler

2.5.2.8 numaralı paragrafta, çoklu seçim listesinin sunucuya aynı isimde parametreler gönderebileceğini görmüştük. Bir eylemin bunları nasıl alabileceğini inceleyelim. Aşağıdaki [/m03] eylemini ekliyoruz:


    // ----------------------- aynı adlı parametreleri alma-----------------
    @RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m03(String nom[]) {
        return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
  • 2. satır: Eylem, [ad] adlı bir parametre kabul eder. Burada, istek türü belirtilmediğinden, bu parametre, ister GET ister POST olsun, bu adı taşıyan tüm parametrelerle başlatılacaktır;

Sonuçlar şöyledir:

  • POST ve [1] ile [2] parametreleri gönderilir;
  • ayrıca URL ve [3]'e de parametreler eklenir;
  • [4]'te, [nom] ile aynı adı taşıyan dört parametre bulunur: [Query String parameters], URL'in parametreleridir; [Form Data] ise gönderilen parametrelerdir;
  • [5]'te, [/m03] eyleminin [nom] adlı dört parametreyi aldığı görülmektedir;

4.4. [/m04]: eylemin parametrelerini bir Java nesnesine eşlemek

Şu yeni [/m04] eylemini ele alalım:


    // ------ parametreleri bir nesneye (Command Object) eşleme ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Personne m04(Personne personne) {
        return person;
}
  • 3. satır: eylemin parametresi, aşağıdaki türde bir kişidir:

public class Personne {

    // kimlik
    private Integer id;
    // ad
    private String nom;
    // yaş
    private int age;
....
    // alıcı ve ayarlayıcılar
...
}
  • [Personne personne] parametresini oluşturmak için Spring, MVC'i [new Personne()]'e dönüştürür;
  • ardından, oluşturulan nesnenin [id, nom, age] alan adlarını taşıyan parametreler varsa, bu parametreleri setter yöntemleri aracılığıyla alanlara atayarak nesneyi oluşturur;
  • 4. satır: Eylem, [Personne] türünde bir değer döndürür; bu değer, müşteriye gönderilmeden önce bir karakter dizisi olarak serileştirilir. Varsayılan olarak, gerçekleştirilen serileştirmenin jSON türü olduğunu görmüştük. Dolayısıyla istemci, bir kişiye ait jSON dizesini almalıdır;

İşte bir örnek:

  • [1]'te, [id, nom, age] parametreleri kullanılarak [Personne] nesnesi oluşturulur;
  • [2]'e, bu kişinin jSON dizesi;

Bir kişinin tüm alanları gönderilmezse ne olur? Deneyelim:

  • [2]'e dönüştürüldüğünde, yalnızca [id] parametresi başlatılmıştır;

4.5. [/m05]: URL'in öğelerini alalım

Şu yeni [/m05] eylemi:


    // ----------------------- URL nesnesinden öğeleri alma ------------------------
    @RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a", a);
        map.put("b", b);
        return map;
}
  • 2. satır: işlenen URL, [/m05/{a}/x/{b}] biçimindedir; burada {param}, URL'in bir parametre öğesidir;
  • 3. satır: URL'in parametre öğeleri, [@PathVariable] açıklamasıyla alınır;
  • 4-6. satırlar: Alınan [a] ve [b] öğeleri bir sözlüğe eklenir;
  • 7. satır: Yanıt, bu sözlükteki jSON dizesi olacaktır;

Sonuçlar şu şekildedir:

 

4.6. [/m06]: URL öğelerinden ve parametrelerden öğeleri al

Şu yeni [/m06] eylemi:


    // -------- URL'ten öğeleri ve parametreleri alma---------------
    @RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("a", a);
        map.put("b", b);
        map.put("c", c);
        return map;
}
  • 3. satır: hem URL ve [Integer a, Double b] öğeleri hem de bir parametre (GET veya POST) [Double c] alınır;
  • 4-7. satırlar: bu öğeler bir sözlüğe eklenir;
  • 8. satır: Bu, istemcinin yanıtını oluşturur; dolayısıyla istemci, bu sözlükten jSON dizesini alır;

İşte sonuçlar:

 

[http://localhost:8080/m06/100/x/200.43/] yolunun sonundaki / işaretine dikkat edin. Bu işaret olmadan, aşağıdaki gibi hatalı bir sonuç elde edilir:

 

4.7. [/m07]: isteğin tamamına erişme

Şu yeni eylem [/m07]'i ele alalım:


    // ------ HttpServletRequest sorgusuna erişim ------------------------
    @RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m07(HttpServletRequest request) {
        // HTTP başlıkları
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuffer buffer = new StringBuffer();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
        }
        return buffer.toString();
}
  • 3. satır: Spring'den MVC'e, istekle ilgili elde edilebilecek tüm bilgileri kapsayan [HttpServletRequest request] nesnesini enjekte etmesi istenir;
  • 5-10. satırlar: İsteğin tüm başlıklarını HTTP alır ve bunları bir karakter dizisi halinde birleştirip müşteriye göndeririz (11. satır);

Sonuçlar şu şekildedir:

  • [1]'te, isteğin HTTP başlıkları;
  • [2]'e, yanıt. Burada, isteğin tüm HTTP başlıklarını bulabiliriz.

4.8. [/m08]: [Writer] nesnesine erişim

Şu eylemi ele alalım:


    // ----------------------- writer enjeksiyonu ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Bonjour le monde !");
}
  • 3. satır: Spring MVC, müşteriye gönderilecek yanıt akışına yazım yapmayı sağlayan [Writer writer] nesnesini enjekte eder;
  • 3. satır: eylem, [void] türünde bir değer döndürür; bu, müşteriye verilecek yanıtı kendisinin oluşturması gerektiğini gösterir;
  • 4. satır: Müşteriye gönderilecek yanıt akışına bir metin eklenir;

Sonuçlar şöyledir:

  • [2]'te, HTTP [Content-Type] başlığının gönderilmediğini görüyoruz;
  • [3]'te yanıt;

4.9. [/m09]: HTTP başlığına erişim

Şu eylemi ele alalım:


    // ----------------------- RequestHeader enjeksiyonu ------------------------
    @RequestMapping(value = "/m09", method = RequestMethod.GET)
    public String m09(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
}
  • 3. satır: [@RequestHeader("User-Agent")] açıklaması, HTTP ve [User-Agent] başlıklarını almayı sağlar;
  • 4. satır: Bu başlığın metni görüntülenir;

Sonuçlar şöyledir:

  • [2]'te, HTTP ve [User-Agent] başlıkları;
  • [3]'e dönüştü, [/m08] eylemi bu başlığı doğru bir şekilde aldı;

4.10. [/m10, /m11]: bir çereze erişme

Çerez genellikle, sunucunun:

  • sunucu tarafından ilk kez istemciye gönderilen bir başlıktır;
  • istemci daha sonra sistematik olarak sunucuya geri gönderir;

Öncelikle çerezi oluşturan bir eylem oluşturalım:


    // ----------------------- Çerez oluşturma ------------------------
    @RequestMapping(value = "/m10", method = RequestMethod.GET)
    public void m10(HttpServletResponse response) {
        response.addCookie(new Cookie("cookie1", "remember me"));
}
  • 3. satır: yanıt üzerinde tam kontrol sahibi olmak için [HttpServletResponse response] nesnesini ekliyoruz;
  • 4. satır: [cookie1] anahtarı ve [remember me] değeri ile bir çerez oluşturuyoruz (Not: Çerez değerindeki aksanlı karakterler hatalara neden olur);
  • 3. satır: Eylem hiçbir sonuç vermez. Ayrıca, yanıt gövdesine hiçbir şey yazmaz. Dolayısıyla istemci boş bir belge alacaktır. Yanıt, yalnızca bir çerezin HTTP başlığını eklemek için kullanılır;

Sonuçlara bakalım:

  • [1]: istek;
  • [2]: yanıt boştur;
  • [3]: eylem tarafından oluşturulan çerez;

Şimdi, tarayıcının bundan sonra her istekte göndereceği bu çerezi almak için bir eylem oluşturalım:


    // ----------------------- Çerez enjeksiyonu ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • 3. satır: [@CookieValue("cookie1")] notu, [cookie1] anahtar çerezini almamızı sağlar;
  • 4. satır: Bu değer, istemciye gönderilecek yanıt olacaktır;

Sonuçlara bakalım:

  • [2]'te, tarayıcının çerezi geri gönderdiğini görüyoruz;
  • [3]'te, işlemin çerezi başarıyla geri aldığı görülüyor;

4.11. [/m12]: POST'in gövdesine erişim

Gönderilen parametreler genellikle HTTP [Content-Type: application/x-www-form-urlencoded] başlığıyla birlikte gelir. Gönderilen dizinin tamamına erişilebilir. Aşağıdaki eylemi oluşturuyoruz:


    // ----------- String türündeki bir POST'in gövdesini alma------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • 3. satır: [@RequestBody] açıklaması, POST'in gövdesini almamızı sağlar. Burada, bunun [String] türü olduğunu varsayıyoruz;
  • 4. satır: Bu gövde müşteriye geri gönderilir;

İşte ilk örnek:

  • [2]'te, gönderilen değerler;
  • [3]'te, isteğin HTTP [Content-Type] başlığı;
  • [4]'te, sunucunun yanıtı;

Gönderilen parametreler, şimdiye kadar sıklıkla kullandığımız basit [p1=v1&p2=v2] biçimine her zaman uymayabilir. Daha karmaşık bir örnek ele alalım:

  • [2-3]: gönderilen değerler [clé:value] biçiminde girilir;
  • [5]'e, gönderilen dize;

[Content-Type: application/x-www-form-urlencoded] türü kullanıldığında, gönderilen dize [p1=v1&p2=v2] biçiminde olmalıdır. Herhangi bir şey göndermek istersek, [Content-Type: text/plain] türünü kullanırız. İşte bir örnek:

  • [2-3]'te, HTTP [Content-Type] başlığı oluşturulur. Varsayılan olarak [5], [6]'te tanımlanan yerine bu kullanılacaktır. [charset=utf-8] özniteliği önemlidir. Bu olmadan, gönderilen dizgedeki aksanlı karakterler kaybolur;
  • [4]'te, gönderilen dize [7]'te doğru şekilde alınır;

4.12. [/m13, /m14]: jSON'te gönderilen değerleri geri almak

HTTP [Content-Type: application/json] başlığıyla parametreler gönderilebilir. Aşağıdaki eylemi oluşturuyoruz:


    // ----------------------- bir POST nesnesinden jSON gövdesini alma
    @RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
    public String m13(@RequestBody Personne personne) {
        return personne.toString();
}
  • 2. satır: [consumes = "application/json"], eylemin bir jSON gövdesini beklediğini belirtir;
  • 3. satır: [@RequestBody], bu gövdeyi temsil eder. Bu açıklama, [Personne] türünde bir nesneye eşleştirilmiştir. jSON gövdesi, bu nesneye otomatik olarak ters serileştirilecektir;
  • 4. satır: Gönderilen jSON dizesi dışında bir değer döndürmek için [Personne].toString() yöntemi kullanılır;

İşte bir örnek:

  • [2]'e, gönderilen jSON dizesi;
  • [3]'e, istekteki [Content-Type];
  • [4]'e, sunucunun yanıtı;

Aynı işlemi farklı bir şekilde de yapabiliriz:


    // ----------------------- bir POST 2'nin jSON gövdesini almak -------------------
    @RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
    public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
        Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
        return personne.toString();
}
  • 2. satır: Yöntemin [text/plain] türünde bir akış beklediği belirtilmiştir. Spring MVC, isteğin gövdesini [String] türü olarak işleyecektir (3. satır);
  • 4. satır: jSON dizesi, bir [Personne] nesnesine deserialize edilir (bkz. paragraf 9.7, sayfa 542);

Sonuçlar şöyledir:

  • [3] yerine, [text/plain] yazılmalıdır;

4.13. [/m15]: oturumu geri al

Bir eylemin yürütme mimarisine geri dönelim:

Denetleyici sınıfı, istemcinin isteğinin başında örneklenir ve isteğin sonunda yok edilir. Bu nedenle, tekrar tekrar çağrılsa bile iki istek arasında verileri saklamak için kullanılamaz. İki tür veriyi saklamak isteyebiliriz:

  • web uygulamasının tüm kullanıcıları tarafından paylaşılan veriler. Bunlar genellikle salt okunur verilerdir;
  • aynı müşterinin istekleri arasında paylaşılan veriler. Bu veriler, Oturum (Session) adı verilen bir nesnede saklanır. Bu durumda, müşterinin belleğini ifade etmek için müşteri oturumu terimi kullanılır. Bir müşterinin tüm istekleri bu oturuma erişebilir. Bu istekler, oturumda bilgi depolayabilir ve okuyabilir.

Yukarıda, bir eylemin erişebildiği bellek türlerini göstermekteyiz:

  • çoğu zaman salt okunur veriler içeren ve tüm kullanıcılar tarafından erişilebilen uygulama belleği;
  • okuma/yazma verileri içeren ve aynı kullanıcının ardışık istekleri tarafından erişilebilen belirli bir kullanıcının belleği veya oturumu;
  • Yukarıda gösterilmese de, bir istek belleği veya istek bağlamı da mevcuttur. Bir kullanıcının isteği, birbirini izleyen birden fazla eylem tarafından işlenebilir. İstek bağlamı, eylem 1'in eylem 2'ye bilgi aktarmasını sağlar.

Bu farklı bellekleri açıklayan ilk örneği inceleyelim:


    // ----------------------- oturumu geri almak ------------------------
    @RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m15(HttpSession session) {
        // oturumdan [compteur] anahtar nesnesini alıyoruz
        Object objCompteur = session.getAttribute("compteur");
        // artırmak için tamsayıya dönüştürülüyor
        int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
        iCompteur++;
        // oturuma geri yerleştiriliyor
        session.setAttribute("compteur", iCompteur);
        // eylemin sonucu olarak döndürülür
        return String.valueOf(iCompteur);
}

Spring MVC, kullanıcının oturumunu [HttpSession] türünde bir nesnede tutar.

  • 3. satır: Spring MVC'ten, [HttpSession] nesnesini eylemin parametrelerine enjekte etmesi istenir;
  • 5. satır: Bu nesneden [compteur] adlı bir öznitelik alınır. Bir oturum, bir sözlük gibi davranır; yani bir dizi [clé, valeur] çifti içerir. Oturumda [compteur] anahtarı yoksa, null işaretçisi alınır;
  • 7. satır: [compteur] anahtarına atanan değer, [Integer] türü olacaktır;
  • 8. satır: Sayaç artırılır;
  • 10. satır: oturumdaki sayaç güncellenir;
  • 12. satır: Sayaç değeri istemciye gönderilir;

[/m15] ilk kez çalıştırıldığında:

  • 12. satırda sayaç değeri 1 olacaktır;
  • ikinci kez çalıştırıldığında, 5. satırda bu 1 değeri alınarak 2'ye yükseltilecektir;
  • ...

İşte bir yürütme örneği:

  • [1]'te, sayacın ilk değeri doğru bir şekilde elde edilir;
  • [2]'te, sunucu bir oturum çerezi göndermiştir. Bu çerezin anahtarı [JSESSIONID]'tir ve değeri her kullanıcı için benzersiz bir karakter dizisidir. Tarayıcının aldığı çerezleri her zaman geri gönderdiğini hatırlayalım. Dolayısıyla, [/m15] eylemini ikinci kez talep ettiğimizde, istemci bu çerezi geri gönderecek ve bu da sunucunun onu tanıyıp oturumuyla ilişkilendirmesini sağlayacaktır. Kullanıcının oturumu bu şekilde korunur;

İkinci isteğe bakalım:

  • [3]'te, istemcinin oturum çerezini geri gönderdiğini görüyoruz. Sunucunun yanıtında bu oturum çerezinin artık bulunmadığını fark edebiliriz. Artık tanınmak için çerezi gönderen istemcidir;
  • [4]'te, sayaçın ikinci değeri. Sayaç gerçekten artırılmış;

4.14. [/m16]: [session] kapsamındaki bir nesneyi geri alma

Bir kullanıcının oturumundaki tüm verileri tek bir nesneye koymak ve oturuma yalnızca bu nesneyi eklemek isteyebiliriz. Biz de bu yolu izliyoruz. Sayacı aşağıdaki [SessionModel] nesnesine koyuyoruz:

  

package istia.st.sprinmvc.models;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {

    private int compteur;

    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }

}
  • 7. satır: [@Component] anotasyonu, [SessionModel] sınıfını yaşam döngüsü Spring tarafından yönetilen bir bileşen haline getiren bir Spring anotasyonudur (5. satır);
  • 8. satır: [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] anotasyonu da bir Spring anotasyonudur (3-4. satırlar). Spring, MVC anotasyonuyla karşılaştığında, ilgili sınıf oluşturulur ve kullanıcının oturumuna eklenir. [proxyMode = ScopedProxyMode.TARGET_CLASS] özniteliği önemlidir. Spring MVC, bu öznitelik sayesinde tüm kullanıcılar için tek bir örnek (singleton) değil, kullanıcı başına bir örnek oluşturur;
  • 11. satır: sayaç;

Bu yeni Spring bileşeninin tanınması için, [Application] sınıfındaki uygulama yapılandırmasının kontrol edilmesi gerekir:


package istia.st.springmvc.main;

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({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 9. satır: Spring bileşenleri [istia.st.springmvc.controllers] paketinde aranır. Bu artık yeterli değildir. Bu satırı şu şekilde değiştiriyoruz:

@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })

[SessionModel] sınıfının bulunduğu paketi ekledik.

Şimdi şu eylemi ekliyoruz:


    @Autowired
    private SessionModel session;
    
    // ------ oturum kapsamındaki bir nesneyi yönetme [Autowired] -----------
    @RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m16() {
        session.setCompteur(session.getCompteur() + 1);
        return String.valueOf(session.getCompteur());
}
  • 1-2. satırlar: Spring bileşeni [SessionModel], denetleyiciye [@Autowired] olarak enjekte edilir. Burada, bir Spring denetleyicisinin bir singleton olduğunu hatırlatmak gerekir. Dolayısıyla, daha dar kapsamlı bir bileşeni (burada [Session] kapsamında olan) denetleyiciye enjekte etmek çelişkili bir durumdur. İşte burada, [SessionModel] bileşeninin [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] anotasyonu devreye girer. Denetleyici kodu, 2. satırdaki [session] alanına her eriştiğinde, denetleyici tarafından o anda işlenmekte olan isteğin oturumunu sağlamak için bir proxy yöntemi yürütülür;
  • 6. satır: eylem parametrelerinde [HttpSession] nesnesine artık ihtiyaç duyulmaz;
  • 7. satır: Sayaç alınır/artırılır;
  • 8. satır: Değer geri döndürülür;

İşte bir yürütme örneği:

İlk sefer

İkinci sefer

Şimdi, ikinci bir kullanıcıyı temsil edecek başka bir tarayıcı ele alalım. Burada Opera tarayıcısını kullanacağız:

Yukarıda, [1]'te görüldüğü gibi, bu ikinci kullanıcı 1 değerinde bir sayaç değeri alıyor. Bu da, onun oturumunun ilk kullanıcının oturumundan farklı olduğunu gösteriyor. İstemci/sunucu iletişimine bakarsak (Opera için de Ctrl-Shift-I tuşlarına basın), [2] örneğinde bu ikinci kullanıcının, birinci kullanıcıdan farklı bir oturum çerezi olduğunu görürüz. Oturumların bağımsızlığını sağlayan da budur.

4.15. [/m17]: [application] kapsamındaki bir nesneyi almak

Bir eylemin yürütme mimarisine geri dönelim:

Kullanıcı oturumunu nasıl oluşturacağımızı biliyoruz. Şimdi, içeriği salt okunur olacak ve tüm kullanıcılar tarafından erişilebilir bir [application] kapsam nesnesi oluşturacağız. [application] kapsam nesnesinin nesnesi olacak [ApplicationModel] sınıfını tanıtıyoruz:

 

package istia.st.springmvc.models;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

@Component
public class ApplicationModel {

    // sayaç
    private AtomicLong compteur = new AtomicLong(0);

    // alıcı ve ayarlayıcılar
    public AtomicLong getCompteur() {
        return compteur;
    }

    public void setCompteur(AtomicLong compteur) {
        this.compteur = compteur;
    }

}
  • 5. satır: [@Component] anotasyonu, [ApplicationModel] sınıfının Spring tarafından yönetilen bir bileşen olmasını sağlar. Spring bileşenlerinin varsayılan niteliği [singleton] türüdür: bileşen, Spring konteyneri örneklendiğinde, yani genellikle uygulamanın başlatılması sırasında tek bir örnek olarak oluşturulur. Bu yaşam döngüsünü, tüm kullanıcıların erişebileceği yapılandırma bilgilerini singleton içinde depolamak için kullanabiliriz;
  • 11. satır: [AtomicLong] türünde bir sayaç. Bu tür, atomik olarak adlandırılan [incrementAndGet] yöntemine sahiptir. Bu, bu yöntemi çalıştıran bir iş parçacığının, ilk iş parçacığı tarafından sayacın değeri okunması (Get) ile artırılması (increment) arasında başka bir iş parçacığının sayacın değerini okumayacağından emin olduğu anlamına gelir; aksi takdirde iki iş parçacığı sayacın aynı değerini okuyacak ve sayacın değeri iki yerine bir artmış olacaktı;

Aşağıdaki yeni [/m17] eylemini oluşturuyoruz:


@Autowired
    private ApplicationModel application;

    // ----- uygulama kapsamındaki bir nesneyi yönetme [Autowired] ------------------------
    @RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m17() {
        return String.valueOf(application.getCompteur().incrementAndGet());
    }
  • 1-2. satırlar: [ApplicationModel] bileşenini denetleyiciye ekliyoruz. Bu bir singleton'dur. Dolayısıyla her kullanıcı aynı nesneye bir referansa sahip olacaktır;
  • 7. satır: [application] kapsam sayacını artırdıktan sonra geri döndürüyoruz;

İşte iki örnek, biri Chrome ile, diğeri Opera ile:

Yukarıda, her iki tarayıcının da aynı sayaçla çalıştığı görülüyor; oturumda ise durum böyle değildi. Bu iki tarayıcı, her ikisi de [application] kapsamındaki verilere erişimi olan iki farklı kullanıcıyı temsil ediyor. Genel olarak, yukarıda sayaçta yapıldığı gibi, [application] kapsamındaki nesnelere okuma/yazma bilgileri yerleştirilmemelidir. Zira, farklı kullanıcıların yürütme iş parçacıkları, [application] kapsamındaki verilere aynı anda erişmektedir. Yazma bilgisi varsa, yukarıda [AtomicLong] türüyle yapıldığı gibi yazma erişimlerini senkronize etmek gerekir. Eşzamanlı erişimler, programlama hatalarına yol açar. Bu nedenle, [application] kapsamındaki nesnelere yalnızca salt okunur bilgiler yerleştirilmesi tercih edilir.

4.16. [/m18]: [session] kapsamındaki bir nesneyi [@SessionAttributes] ile almak

[session] kapsamındaki bilgileri almak için başka bir yol daha vardır. Aşağıdaki nesneyi oturuma ekleyeceğiz:


package istia.st.springmvc.models;

public class Container {
    // sayaç
    public int compteur=10;

    // getter ve setter'lar
    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }
}

Bu nesneyi aşağıdaki iki eylemle kullanacağız:


    // [@SessionAttribute] kullanımı ----------------------
    @RequestMapping(value = "/m18", method = RequestMethod.GET)
    public void m18(HttpSession session) {
        // burada [container] anahtarını oturuma ekliyoruz
        session.setAttribute("container", new Container());
    }

    // [@ModelAttribute] kullanımı ----------------------
    // oturum anahtarı [container] buraya eklenecektir
    @RequestMapping(value = "/m19", method = RequestMethod.GET)
    public String m19(@ModelAttribute("container") Container container) {
        container.setCompteur(1 + container.getCompteur());
        return String.valueOf(container.getCompteur());
    }
  • 3-6. satırlar: [/m18] eylemi herhangi bir sonuç vermez. Bu eylem, yalnızca oturumda [container] anahtarıyla bir nesne oluşturmak için kullanılır;
  • 11. satır: [/m19] eyleminde, [@ModelAttribute] etiketini kullanıyoruz. Bu anotasyonun davranışı oldukça karmaşıktır. Bu anotasyonun [container] parametresi çeşitli şeyleri, özellikle de oturumdaki bir nesneyi işaret edebilir. Bunun için söz konusu nesnenin, sınıfın kendisi üzerinde [@SessionAttributes] anotasyonu ile tanımlanmış olması gerekir:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • Yukarıdaki 2. satır, [container] anahtarının oturum özniteliklerinin bir parçası olduğunu belirtir;

Özetleyelim:

  • [/m18]'te, [container] anahtarı oturuma eklenir;
  • [@SessionAttributes({"container"})] etiketlemesi, bu anahtarın [@ModelAttribute("container")] ile etiketlenmiş bir parametreye eklenebilmesini sağlar;
  • Aşağıdaki çalışma örneğinde görünmese de, [@ModelAttribute] ile etiketlenmiş bir bilgi, otomatik olarak V görünümüne aktarılan M modelinin bir parçası olur;

İşte bir yürütme örneği. Öncelikle, [container] anahtarını [/m18] ve [1] eylemleriyle oturuma ekliyoruz. Ardından, sayaçın arttığını görmek için [/m19] eylemini iki kez çağırıyoruz.

4.17. [/m20-/m23]: [@ModelAttribute] ile bilgi ekleme

Şu yeni eylemi ele alalım:


    // p özniteliği tüm [Model] görünüm şablonlarında yer alacaktır ----------------
    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
    }

    // ---------------@ModelAttribute'in örneklenmesi --------------------------
    // oturumda bulunuyorsa enjekte edilecektir
    // denetleyici bu öznitelik için bir yöntem tanımlamışsa eklenecektir
    // String → öznitelik türü dönüştürücüsü varsa, URL'in alanlarından gelebilir
    // aksi takdirde varsayılan oluşturucu ile oluşturulur
    // ardından modelin öznitelikleri, GET veya POST parametreleriyle başlatılır
    // nihai sonuç, eylem tarafından üretilen modelin bir parçası olacaktır
    
    // p özniteliği argümanlara eklenir------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Personne m20(@ModelAttribute("p") Personne personne) {
        return personne;
}
  • 2-5. satırlar: [p] adlı bir model özniteliği tanımlar. Bu, bir V görünümünün M modelidir; bu model, Spring'de [Model] türüyle temsil edilir. Bir model, [clé, valeur] çiftlerinden oluşan bir sözlük gibi davranır. Burada, [p] anahtarı, [getPersonne] yöntemi tarafından oluşturulan [Personne] nesnesiyle ilişkilendirilmiştir. Yöntemin adı herhangi bir şey olabilir;
  • 17. satır: [p] anahtar şablon özniteliği, eylemin parametrelerine enjekte edilir. Bu enjeksiyon, 8-12. satırlardaki kurallara göre gerçekleştirilir. Burada, 9. satırda tanımlanan durum söz konusudur. Dolayısıyla 17. satırda [Personne personne] parametresi, [Personne(7,'abcd',14)] nesnesi olacaktır;
  • 18. satır: Doğrulama amacıyla [personne] nesnesi döndürülür. Bu nesne, müşteriye gönderilmeden önce jSON olarak serileştirilecektir.

İşte bir örnek:

 

Şimdi şu eylemi inceleyelim:


    // --------- p özniteliği, V görünümünün M şablonuna otomatik olarak eklenir
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

Bir V görünümünü görüntülemek isteyen bir eylem, bu görünümün M modelini oluşturmalıdır. Spring MVC, bunu eylem parametrelerine enjekte edilebilen [Model] türüyle yönetir. Başlangıçta bu model boştur veya [@ModelAttribute] anotasyonu ile etiketlenmiş bilgileri içerir. Eylem, bu modeli bir görünüme aktarmadan önce zenginleştirir veya zenginleştirmez.

  • 3. satır: M şablonunun eklenmesi;
  • 4. satır: İçinde ne olduğunu görmek istiyoruz. Müşteriye göndermek için modeli bir karakter dizisi olarak serileştiriyoruz. Burada [Personne.toString] yöntemi kullanılacak. Dolayısıyla bu yöntemin mevcut olması gerekir;

İşte bir çalıştırma örneği:

 

Yukarıda şu komutları görüyoruz:


    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
}

şablonda [p, Personne(7,'abcd',14)] adlı bir giriş oluşturduğunu görüyoruz. Bu her zaman böyledir.

Şimdi şu durumu ele alalım:


    // aksi takdirde varsayılan oluşturucu ile oluşturulur
    // ardından şablon öznitelikleri, GET veya POST parametreleriyle başlatılır

ve şu eylemle:


    // --------- [param1] model özniteliği, modelin bir parçasıdır ancak başlatılmamıştır
    @RequestMapping(value = "/m22", method = RequestMethod.GET)
    public String m22(@ModelAttribute("param1") String p1, Model model) {
        return model.toString();
}
  • 3. satır: [param1] anahtar şablon özniteliği mevcut değil. Bu durumda, ilişkili türün bir varsayılan oluşturucusu olmalıdır. Burada [String] türü için durum böyledir, ancak [@ModelAttribute("param1") Integer p1] yazamayız çünkü [Integer] sınıfının varsayılan oluşturucusu yoktur;
  • 4. satır: şablonu döndürerek [param1] anahtar şablon özniteliğinin şablona dahil olup olmadığını kontrol ediyoruz;

İşte bir yürütme örneği:

 

[param1] şablon özniteliği şablonda mevcut olmakla birlikte, ilişkili değerin [toString] yöntemi bu değer hakkında herhangi bir bilgi vermez.

Şimdi, modele bir bilgiyi açıkça eklediğimiz aşağıdaki eylemi ele alalım:


    // --------- [param2] model özniteliği, modele açıkça eklenmiştir
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2",p2);
        return model.toString();
}
  • 4. satır: 3. satırda alınan [p2] değeri, [param2] anahtarıyla ilişkili olarak modele eklenir:

İşte bir yürütme örneği:

 

Eylem parametresi bir nesne ise kurallar değişir. İşte ilk örnek:


    // ------ [unePersonne] model özniteliği, modele otomatik olarak eklenmiştir
    @RequestMapping(value = "/m23b", method = RequestMethod.GET)
    public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
        return model.toString();
}

Eylem, kendisine verilen şablonu değiştirmez. Sonuç şöyledir:

[@ModelAttribute("unePersonne") Personne p1] etiketinin, [p1] kişisini [unePersonne] anahtarıyla ilişkilendirerek modele eklediği görülmektedir.

Şimdi şu eylemi ele alalım:


    // --------- p1 kişisi şablona otomatik olarak eklenmiştir
    // -------- anahtar olarak sınıfının adı, ilk harfi küçük harfle yazılmış olarak
    @RequestMapping(value = "/m23c", method = RequestMethod.GET)
    public String m23c(Personne p1, Model model) {
        return model.toString();
}
  • 4. satır: [@ModelAttribute] açıklaması eklenmemiştir;

Sonuç şöyledir:

Görüldüğü gibi, [Personne p1] parametresinin varlığı, [p1] kişisini modele eklemiştir; bu kişi, [personne] anahtarıyla ilişkilendirilmiştir; bu anahtar, ilk harfi küçük harf olan [Personne] sınıfının adıdır.

4.18. [/m24]: eylem modelinin doğrulanması

Aşağıdaki [ActionModel01] eylem modelini ele alalım:

 

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // veri
    @NotNull
    private Integer a;
    @NotNull
    private Double b;

    // getter ve setter'lar
...
    }
  • 8. ve 9. satırlar: [@NotNull] açıklaması, açıklanan verinin null değerini alamayacağını belirten bir doğrulama kısıtlamasıdır;

Şimdi şu eylemi inceleyelim:


    // ----------------------- bir şablonun doğrulanması ------------------------
    @RequestMapping(value = "/m24", method = RequestMethod.GET)
    public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // hatalar var mı?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            // hata listesini gözden geçirme
            for (FieldError error : result.getFieldErrors()) {
                buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
                        String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
            }
            map.put("errors", buffer.toString());
        } else {
            // hata yok
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
}
  • 3. satır: Bir [ActionModel01] nesnesi oluşturulacak ve [a, b] alanları aynı isimdeki parametrelerle başlatılacaktır. [@Valid] anotasyonu, geçerlilik kısıtlamalarının doğrulanması gerektiğini belirtir. Bu doğrulamanın sonuçları, [BindingResult] türündeki parametreye (ikinci parametre) yerleştirilecektir. Aşağıdaki doğrulamalar yapılacaktır:
    • [@NotNull] açıklamaları nedeniyle, [a] ve [b] parametrelerinin mevcut olması gerekir;
    • [Integer a] türü nedeniyle, doğası gereği [String] türünde olan [a] parametresinin [Integer] türüne dönüştürülebilir olması gerekir;
    • [Double b] türü nedeniyle, doğası gereği [String] türü olan [b] parametresi, [Double] türüne dönüştürülebilir olmalıdır;

[@Valid] ek açıklaması ile, doğrulama hataları [BindingResult result] parametresine aktarılacaktır. [@Valid] ek açıklaması olmadan, doğrulama hataları eylemin çökmesine neden olur ve sunucu, 500 (Dahili sunucu hatası) durum koduyla birlikte HTTP yanıtını istemciye gönderir.

  • 3. satır: İşlemin sonucu [Map] türündedir. Bu sonucun jSON dizesi müşteriye gönderilecektir. İki tür sözlük oluşturulur:
    • başarısızlık durumunda, ['errors', value] girdisine sahip bir sözlük; burada [value], tüm hataları açıklayan bir karakter dizisidir (13. satır);
    • başarılı olması durumunda, ['data',value] girdisine sahip bir sözlük oluşturulur; burada [value], kendisi de iki girdiye sahip bir sözlüktür: ['a', value], ['b', value] (satır 19);
  • 9-12. satırlar: Tespit edilen her [error] hatası için [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()] dizesi oluşturulur:
    • İlk öğe, hatalı alan olan [a] veya [b]’tir;
    • ikinci öğe reddedilen değerdir, örneğin [x],
    • üçüncü öğe ise hata kodlarının listesidir. Bunların işlevlerini birazdan inceleyeceğiz;
    • dördüncü öğe hata kodudur. Bu, önceki listenin bir parçasıdır;
    • son öğe ise varsayılan hata mesajıdır. Aslında birden fazla hata mesajı olabilir;

İşte birkaç yürütme örneği:

Yukarıda şunu görüyoruz:

  • 'x' değerinin [ActionModel01.a] alanına atanması başarısız olmuş ve hata mesajı bunun nedenini açıklıyor;
  • 'y' değerinin [ActionModel01.b] alanına atanması başarısız oldu ve hata mesajı bunun nedenini açıklıyor;

[a] alanındaki hata kodlarına dikkat edelim: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Hata mesajını özelleştirmemiz gerektiğinde bu hata kodlarına tekrar değineceğiz. Hata kodunun [typeMismatch] olduğu not edilsin.

Başka bir örnek:

Burada, [a] ve [b] parametreleri geçilmemiştir. Bu durumda, [ActionModel01] eylem modelindeki [@NotNull] doğrulayıcıları devreye girmiştir;

Sonunda doğru değerler elde edildi:

4.19. [m/24]: hata mesajlarının özelleştirilmesi

Önceki örneğin ekran görüntüsüne geri dönelim:

Yukarıda varsayılan hata mesajlarını görüyoruz. Bunları gerçek bir uygulamada kullanamayacağımız açıktır. Bu hata mesajlarını tanımlamak mümkündür. Bunun için hata kodlarından yararlanacağız. Yukarıda, [a] alanı için hatanın şu kodlara sahip olduğunu görüyoruz: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Bu hata kodları, en kesin olandan en az kesin olana doğru sıralanır:

  • [typeMismatch.actionModel01.a]: [a] alanında, [ActionModel01] türünde bir tür hatası;
  • [typeMismatch.a]: [a] adlı alanda tür hatası;
  • [typeMismatch.java.lang.Integer]: Integer türünde bir tür hatası;
  • [typeMismatch]: tür hatası;

Ayrıca, [error.getCode()] tarafından elde edilen [a] alanındaki hata kodunun [typeMismatch] olduğu da görülmektedir (yukarıdaki ekran görüntüsüne bakınız).

Hata mesajlarını bir özellik dosyasına yerleştireceğiz:

  

Yukarıdaki [messages.properties] dosyası şu şekilde olacaktır:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier

Her satır şu şekilde olacaktır:

    clé=message

Burada anahtar bir hata kodu, mesaj ise bu kodla ilişkili hata mesajı olacaktır.

İki alan için hata kodlarını hatırlayalım:

  • [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], [a] parametresi geçersiz olduğunda;
  • [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ], [b] parametresi geçersiz olduğunda;
  • [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull], [a] parametresi eksik olduğunda;
  • [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull], [b] parametresi eksik olduğunda;

[messages.properties] dosyası, olası tüm hata durumları için bir hata mesajı içermelidir. Şu durum için:

  • [a] ve [b] parametrelerinin eksik olması durumunda, [NotNull] kodu kullanılacaktır;
  • [a] parametresinin hatalı olması durumunda, iki [typeMismatch.actionModel01.a, typeMismatch] kodu için mesajlar ekledik. Hangisinin kullanılacağını göreceğiz;
  • [b] parametresinin hatalı olması durumunda, [typeMismatch] kodu kullanılacaktır;

[messages.properties] dosyasının kullanılması için Spring'i yapılandırmak gerekir:

  

[Application] sınıfındaki yapılandırma açıklamalarını kaldırıyoruz:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}
  • 8. satır: Spring Boot uygulaması başlatılır. [SpringApplication.run] statik yönteminin ilk parametresi, artık uygulamayı yapılandıran sınıftır;

[Config] sınıfı şöyledir:


package istia.st.springmvc.main;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}
  • 11-13. satırlar: Daha önce [Application] sınıfında bulunan yapılandırma anotasyonları burada yer almaktadır;
  • 14. satır: Bir Spring uygulaması olan MVC'i yapılandırmak için [WebMvcConfigurerAdapter] sınıfını genişletmek gerekir;
  • satır 15: [@Bean] anotasyonu, bir Spring bileşeni olan bir singleton ekler;
  • satır 16: [messageSource] (yöntemin adı) adlı bir bean tanımlanır. Bu bean, uygulamanın mesaj dosyalarını tanımlamak için kullanılır ve mutlaka bu ada sahip olmalıdır;
  • satır 17-19: Spring'e mesaj dosyasının:
    • projenin Classpath'indeki [i18n] klasöründe olduğunu (18. satır),
    • adının [messages.properties] olduğunu belirtir (satır 18). Aslında [messages] terimi, mesaj dosyalarının adının kendisi değil, kök adıdır. Uluslararasılaştırma kapsamında, yönetilen her kültür için birer tane olmak üzere birden fazla mesaj dosyası bulunabileceğini göreceğiz. Böylece Fransızca için [messages_fr.properties] ve İngilizce için [messages_en.properties] gibi dosyalar olabilir. [messages] köküne eklenen sonekler standartlaştırılmıştır. Herhangi bir şey yazamazsınız;

STS projesinde, [i18n] klasörünü kaynaklar klasörüne koymak gerekir, çünkü bu klasör projenin Classpath'ine eklenir:

  

Bu dosyayı kullanmak için aşağıdaki yeni eylemi oluşturuyoruz:


// bir şablonun doğrulanması, hata mesajlarının yönetimi ------------------------
    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
        // sonuç sözlüğü
        Map<String, Object> map = new HashMap<String, Object>();
        // Spring uygulama bağlamı
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // yerel ayar
        Locale locale = RequestContextUtils.getLocale(request);
        // hatalar mı var?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (FieldError error : result.getFieldErrors()) {
                // hata kodlarına göre hata mesajı arama
                // mesaj dosyalarında mesaj aranıyor
                // tablo biçiminde hata kodları
                String[] codes = error.getCodes();
                // dize biçiminde
                String listCodes = String.join(" - ", codes);
                // arama
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // bulundu mu?
                if (msg == null) {
                    throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
                }
                // bulundu - hata mesajı, hata mesajları dizisine eklenir
                buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
                        String.join(" - ", msg)));
            }
            map.put("errors", buffer.toString());
        } else {
            // tamam
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
    }

Bu kod, [/m24] eyleminin koduna benzerdir. Farklılıkları açıklayalım:

  • 3. satır: [HttpServletRequest request] isteğini eylemin parametrelerine ekliyoruz. Buna ihtiyacımız olacak;
  • 7-8. satırlar: Spring bağlamını alıyoruz. Bu bağlam, uygulamanın tüm Spring bean'lerini içerir. Ayrıca mesaj dosyalarına erişim sağlar;
  • 10. satır: Uygulamanın yerel ayarını alıyoruz. Bu terim biraz ileride açıklanacaktır;
  • satır 15-31: Her hata için, bu hata kodlarından birine karşılık gelen bir mesaj aranır. Mesajlar, [error.getCodes()]'te bulunan kodların sırasına göre aranır. Bir mesaj bulunur bulunmaz arama durdurulur;
  • 26. satır: [messages.properties]'te bir mesajı alma yöntemi:
    • ilk parametre, [messages.properties]'te aranan koddur,
    • ikincisi bir parametre dizisidir, çünkü bazen mesajlar parametrelerle ayarlanır. Burada durum böyle değildir,
    • üçüncüsü ise kullanılan yerel ayardır (10. satırdan alınır). Yerel ayar, kullanılan dili belirtir; [fr_FR] Fransa Fransızcası için, [en_US] ise USA'teki İngilizce içindir. Mesaj, messages_[locale].properties dosyasında aranır; örneğin [messages_fr_FR.properties]. Bu dosya yoksa, mesaj [messages_fr.properties] dosyasında aranır. Bu dosya da yoksa, mesaj [messages.properties] dosyasında aranır. Bizim için işe yarayacak olan son durum budur;
  • 25-29. satırlar: Biraz beklenmedik bir şekilde, bir mesaj dosyasında mevcut olmayan bir kodu aradığımızda, null işaretçisi yerine bir istisna ile karşılaşırız;
  • 33-35. satırlar: Hata mesajının bulunmaması durumu işlenir;
  • 37-38. satırlar: Hata dizesini oluşturuyoruz. Bu dizede, yerel ayarı ve bulunan hata mesajını dahil ediyoruz;

İşte bazı çalıştırma örnekleri:

 

Görüldüğü gibi:

  • uygulamanın yerel ayarı [fr_FR]'tir. Bunu başlatmak için herhangi bir işlem yapmadığımızdan bu bir varsayılan değerdir;
  • iki alan için de kullanılan mesaj şudur:

NotNull=Le champ ne peut être vide

Başka bir örnek:

 

Görüldüğü gibi:

  • [a] parametresi için kullanılan hata mesajı şöyledir:

typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
  • [b] parametresi için kullanılan hata mesajı şöyledir:

typeMismatch=Format invalide

Neden iki farklı hata mesajı var? [a] parametresi için iki olası hata mesajı vardı:


typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

Hata kodları, [error.getCodes()] tablosundaki sıraya göre incelenmiştir. Bu sıranın en spesifik koddan en genel koda doğru ilerlediği görülmüştür. Bu nedenle [typeMismatch.model01.a] kodu ilk olarak bulunmuştur.

4.20. [/m25]: Bir Spring uygulamasının uluslararasılaştırılması MVC

Artık hata mesajlarını Fransızca olarak özelleştirebildiğimize göre, bunları İngilizce olarak da görmek istiyoruz; bu da bizi bir Spring uygulamasının uluslararasılaştırılmasına götürüyor: MVC. Bunu yönetmek için, [Config] yapılandırma sınıfını genişleteceğiz ve şu hale getireceğiz:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}
  • 28-32. satırlar: Bir istek yakalayıcısı oluşturulur. Bir istek yakalayıcısı, [HandlerInterceptor] arayüzünü genişletir. Bu tür bir sınıf, gelen isteği bir eylem tarafından işlenmeden önce inceler. Burada [localeChangeInterceptor] yakalayıcısı, gelen istekte [lang] adlı bir parametreyi arayacak, GET veya POST adında bir parametre arar ve bu parametreye göre uygulamanın dil ayarını değiştirir. Dolayısıyla, parametre [lang=en_US] ise, uygulamanın dil ayarı USA'e göre İngilizce olacaktır;
  • 34-37. satırlar: önceki yakalayıcıyı eklemek için [WebMvcConfigurerAdapter.addInterceptors] yöntemini yeniden tanımlıyoruz;
  • satır 39-45: dil ayarının bir çerez içinde nasıl kapsülleneceğini belirlemek için kullanılır. Bir çerezin, istemci tarayıcısı tarafından sunucuya sistematik olarak geri gönderildiği için kullanıcı belleği görevi görebileceği bilinmektedir. Önceki [localeChangeInterceptor] yakalayıcısı, dil ayarını kapsülleyen bir çerez oluşturur. 42. satır, bu çereze [lang] adını verir. Çerez aynı zamanda yerel ayarı değiştirmek için de kullanılır;
  • 43. satır: [lang] çerezi yoksa, yerel ayarın [fr] olacağını belirtir;

Özetle, bir isteğin yerel ayarı iki şekilde belirlenebilir:

  • [lang] adlı bir parametre iletilerek;
  • [lang] adlı bir çerez gönderilerek. Bu çerez, önceki yöntemin uygulanmasının ardından otomatik olarak oluşturulur;

Bu yerel ayarı kullanmak için, [fr] ve [en] yerel ayarları için mesaj dosyaları oluşturacağız:

 

[messages_fr.properties] dosyası şu şekildedir:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

[messages_en.properties] dosyası şöyledir:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

[messages.properties] dosyası, [messages_en.properties] dosyasının bir kopyasıdır. Hatırlatmak gerekirse, [messages.properties] dosyası, isteğin yerel ayarına karşılık gelen hiçbir dosya bulunmadığında kullanılır. Bizim örneğimizde, kullanıcı [lang=en] parametresini gönderirse, [messages_en.properties] dosyası mevcut olmadığı için [messages.properties] dosyası kullanılacaktır. Dolayısıyla kullanıcıya mesajlar İngilizce olarak gösterilecektir.

Hadi deneyelim. Öncelikle, Chrome geliştirme ortamında (Ctrl-Shift-I) çerezlerinizi kontrol edin:

 

[lang] adlı bir çereziniz varsa, bunu silin. Ardından Chrome'da URL ve [http://localhost:8080/m25] dosyalarını isteyin:

 

Tarayıcı, aşağıdaki HTTP başlıklarını gönderdi:

GET /m25 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36
Referer: http://localhost:8080/m25
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4

Bu başlıklarda [lang] çerezi bulunmadığı görülüyor. Bu durumda kodumuz, [fr] yerel ayarını kullanıyor. Ekran görüntüsü de bunu gösteriyor. Başka bir durumu deneyelim:

  • [1]'te, yerel ayarı [en]'e geçirmek için [lang=en] parametresini kullandık;
  • [2]'te yeni yerel ayar görünüyor;
  • [3]'te mesaj İngilizceye çevrilmiştir;

Şimdi HTTP'teki yazışmalara bakalım:

 

Yukarıda, sunucunun bir [lang] çerezi gönderdiğini görüyoruz. Bunun önemli bir sonucu var: tarayıcı tarafından geri gönderilecek olan [lang] çerezi nedeniyle, bir sonraki isteğin yerel ayarı yeniden [en] olacaktır. Bu nedenle mesajları İngilizce tutmalıyız. Bunu kontrol edelim:

 

Yukarıda, yerel ayarın [en] olarak kaldığını görüyoruz. Tarayıcının sistematik olarak gönderdiği çerez nedeniyle, kullanıcı aşağıdaki gibi [lang] parametresini göndererek değiştirmediği sürece bu ayar değişmeyecektir:

 

4.21. [/m26]: eylem şablonuna yerel ayarın eklenmesi

Önceki örnekte, isteğin yerel ayarını almanın bir yolunu görmüştük:


    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
...
        // yerel
        Locale locale = RequestContextUtils.getLocale(request);
// hatalar mı var?

Yerel ayar, eylem parametrelerine doğrudan eklenebilir. İşte bir örnek:


    @RequestMapping(value = "/m26", method = RequestMethod.GET)
    public String m26(Locale locale) {
        return String.format("locale=%s", locale.toString());
}
 

Yukarıda görüldüğü gibi, istenen yerel ayarın geçerliliği kontrol edilmiyor. Ancak yine de, tarayıcının bir sonraki isteği, aldığı yerel ayar çerezi hatalı olduğu için sunucu tarafında bir istisna oluşturuyor.

4.22. [/m27]: Hibernate Validator ile bir modelin geçerliliğini kontrol etme

Şu yeni eylemi ele alalım:


    //Hibernate Validator ile bir modelin doğrulanması ------------------------
    @RequestMapping(value = "/m27", method = RequestMethod.POST)
    public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // hatalar mı var?
        if (result.hasErrors()) {
            // hata listesini gözden geçirme
            for (FieldError error : result.getFieldErrors()) {
                map.put(error.getField(),
                        String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
            }
        } else {
            // hata yok
            map.put("data", data);
        }
        return map;
}

Burada, daha önce birkaç kez gördüğümüz kod var:

  • 3. satır: [/m27] eylemi, bir POST aracılığıyla çağrılır;
  • 8-11. satırlar: Her hata, [champ, message] ile şu şekilde tanımlanacaktır:
    • alan: hatalı alan,
    • mesaj: ilgili hata mesajı ve hata kodları listesi;
  • 14. satır: hata yoksa, gönderilen değerlerin jSON dizisi döndürülür;
  1. satırda, aşağıdaki [ActionModel02] eylem şablonu kullanılır:
  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel02 {

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
    private Boolean assertFalse;
    
    @NotNull(message = "La donnée est obligatoire")
    @AssertTrue(message = "Seule la valeur [true] est acceptée")
    private Boolean assertTrue;
    
    @NotNull(message = "La donnée est obligatoire")
    @Future(message = "Il faut une date postérieure à aujourd'hui")
    private Date dateInFuture;
    
    @NotNull(message = "La donnée est obligatoire")
    @Past(message = "Il faut une date antérieure à aujourd'hui")
    private Date dateInPast;
    
    @NotNull(message = "La donnée est obligatoire")
    @Max(value = 100, message = "Maximum 100")
    private Integer intMax100;
    
    @NotNull(message = "La donnée est obligatoire")
    @Min(value = 10, message = "Minimum 10")
    private Integer intMin10;
    
    @NotNull(message = "La donnée est obligatoire")
    @NotBlank(message = "La chaîne doit être non blanche")
    private String strNotBlank;
    
    @NotNull(message = "La donnée est obligatoire")
    @Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
    private String strBetween4and6;
    
    @NotNull(message = "La donnée est obligatoire")
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
    private String hhmmss;
    
    @NotNull(message = "La donnée est obligatoire")
    @Email(message = "Adresse invalide")
    private String email;
    
    @NotNull(message = "La donnée est obligatoire")
    @Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
    private String str4;
    
    @Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
    @NotNull(message = "La donnée est obligatoire")
    private Integer int1014;
    
    @URL(message = "URL invalide")
    private String url;

    // getter ve setter'lar

...
}

Sınıf, iki paketten gelen doğrulama kısıtlamalarını kullanır:

  • 5-13. satırlarda [javax.validation.constraints];
  • [org.hibernate.validator.constraints], 15-19. satırlarda;

Bu iki paketin Maven bağımlılıkları projede mevcuttur:

  

Burada uluslararasılaştırılmış mesajlar kullanmayacağız, bunun yerine [message] özniteliği ile kısıtlamanın içinde tanımlanan mesajları kullanacağız. Bu eylemi test etmek için [Advanced Rest Client]'i kullanacağız:

  • [1-2]'te, POST sorgusu;
  • [3]'e dönüştürürken, kullanılacak başlık HTTP [Content-Type]'tir;
  • [4]'te, [Add new value] bağlantısı, [paramètre, value] çiftini eklemeye olanak tanır;
  • [5]'te, [ActionModel02] alanını ekleyin; burada [assertFalse] alanı:

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
  • [6]'e hatalı bir değer girerek hata mesajını görüntüleyin. Yukarıda, [@AssertFalse] kısıtlaması, [assertFalse] alanının [false] değerine sahip olmasını gerektirir;
  • [7]'te, sunucunun yanıtı: boş alanlara ilişkin [@NotNull] kısıtlaması tetiklendi ve ilgili hata mesajı görüntülendi;
  • [8]'te, [@AssertFalse] kısıtlamasının doğrulanmadığı [assertFalse] alanına ait mesaj ve bu hatanın kodları. Bu kodların uluslararasılaştırılmış mesajlarla ilişkilendirilebileceğini hatırlatırız;

İşte başka bir örnek:

 

Image

Okuyucunun, tüm verilerin geçerli olduğu POST'e kadar farklı hata durumlarını test etmesi önerilir:

Not: Tarih biçimi İngilizce biçimindedir: aa/gg/yyyy.

4.23. [/m28]: Hata mesajlarının dışa aktarılması

[ActionModel02] sınıfında mesajları "sabit" olarak yerleştirdik. Bunları mesaj dosyalarına aktarmak daha uygun olacaktır. [/m25] eyleminin örneğini takip ediyoruz. Aşağıdaki yeni [ActionModel03] eylem şablonunu oluşturuyoruz:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel03 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;
    
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    private Date dateInFuture;
    
    @NotNull
    @Past
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    private String url;

    // getter ve setter'lar
        ...
}

Hata mesajları, [messages.properties] dosyalarına aktarılır:

  

[messages_fr.properties] dosyası şöyledir:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide

Hata mesajları 4-16. satırlara eklenmiştir. Şu biçimdedir:

code=message

Kodlar rastgele seçilemez. Bunlar, önceki [/m27] eyleminde görüntülenen kodlardır. Örneğin:

Image

Mesaj dosyalarında, [int1014] alanı için yukarıdaki dört koddan biri kullanılmalıdır.

[messages_en.properties] dosyası şu şekildedir:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL

[ActionModel03] eylem şablonu, aşağıdaki eylem tarafından kullanılır:


// ----------------------- hata mesajlarının dışa aktarılması ------------------------
    @RequestMapping(value = "/m28", method = RequestMethod.POST)
    public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        // Spring uygulama bağlamı
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // yerel ayar
        Locale locale = RequestContextUtils.getLocale(request);
        // hatalar?
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                // hata kodlarına göre hata mesajı arama
                // mesaj dosyalarında mesaj aranıyor
                // tablo biçiminde hata kodları
                String[] codes = error.getCodes();
                // dize biçiminde
                String listCodes = String.join(" - ", codes);
                // arama
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // bulundu mu?
                if (msg == null) {
                    msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
                }
                // bulundu - hata sözlüğe ekleniyor
                map.put(error.getField(), msg);
            }
        } else {
            // hata yok
            map.put("data", data);
        }
        return map;
    }

Bu tür kodlar hakkında daha önce açıklamalarda bulunmuştuk. Asıl önemli olan tek şey 23. satırdır: Alınan hata mesajı, isteğin yerel ayarına bağlıdır.

İşte Fransızca bir örnek:

ve şimdi İngilizce bir örnek: