Eğitim Portalı/Spring/Doğrulama (Validation)
Spring02-spring/08-validation

Doğrulama (Validation)

Bir uygulamanın en kırılgan sınırı, dış dünyadan veri aldığı yerdir: kullanıcı formları, API istekleri, dosya içerikleri. "Yaş negatif olmasın", "e-posta geçerli olsun", "ad boş olmasın" gibi kuralları her yerde elle…

Doğrulama (Validation)

Bir uygulamanın en kırılgan sınırı, dış dünyadan veri aldığı yerdir: kullanıcı formları, API istekleri, dosya içerikleri. "Yaş negatif olmasın", "e-posta geçerli olsun", "ad boş olmasın" gibi kuralları her yerde elle if/else ile kontrol etmek hem yorucu hem de hataya açıktır. Bean Validation (JSR-380), bu kuralları anotasyonlarla bildirimsel olarak tanımlamanı ve tek bir mekanizmayla uygulamanı sağlar. Spring bunu derinlemesine destekler.

Bildirimsel kurallar

Kuralları, doğrulanacak alanların üstüne anotasyon olarak koyarsın:

class KullaniciKaydi {
    @NotBlank(message = "ad boş olamaz")   String ad;
    @Email                                  String eposta;
    @Min(18) @Max(120)                      int yas;
    @Size(min = 8)                          String parola;
}

En sık kullanılan kısıtlar: @NotNull, @NotBlank, @NotEmpty, @Size, @Min/@Max, @Email, @Pattern, @Positive/@Negative, @Past/@Future. Doğrulamayı bir Validator çalıştırır ve tüm ihlalleri tek seferde toplar (ilk hatada durmaz):

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<KullaniciKaydi>> ihlaller = validator.validate(kayit);

Örnek 1 (./Ornek1.java) geçerli ve hatalı bir kayıt formunu doğrular ve hatalı olanın tüm ihlallerini (alan + mesaj) listeler.

Bean Validation bir standarttır (API); en yaygın uygulaması Hibernate Validator'dır. Spring Boot, spring-boot-starter-validation ile bunu hazır getirir.

Özel (custom) kurallar

Standart kısıtlar yetmediğinde — "güçlü parola", "TC kimlik no", "IBAN", "ürün kodu formatı" gibi domain kuralları — kendi anotasyonunu yazabilirsin. İki parça gerekir: @Constraint ile işaretli bir anotasyon ve mantığı içeren bir ConstraintValidator:

@Constraint(validatedBy = GucluParolaValidator.class)
@Target(FIELD) @Retention(RUNTIME)
@interface GucluParola { String message() default "..."; /* groups, payload */ }

class GucluParolaValidator implements ConstraintValidator<GucluParola, String> {
    public boolean isValid(String v, ConstraintValidatorContext c) { /* kural */ }
}

Artık @GucluParola tek anotasyonla her yerde kullanılır (DRY) ve standart @Email ile aynı mekanizmaya dahildir. Örnek 2 (./Ornek2.java) bir "güçlü parola" kuralını uçtan uca yazar.

Spring entegrasyonu: @Validated ve @Valid

Spring, doğrulamayı manuel Validator çağırmadan otomatikleştirir:

  • Metot doğrulaması: Bir bean'i @Validated ile işaretler, metot parametrelerine kısıt koyarsan, Spring her çağrıda parametreleri otomatik doğrular ve ihlalde ConstraintViolationException fırlatır. Örnek 3 (./Ornek3.java) bunu gösterir (MethodValidationPostProcessor + @Validated).
  • Web/controller doğrulaması: Spring MVC'de bir @RequestBody DTO'yu @Valid ile işaretlersin; Spring gelen JSON'u doğrular, hata varsa MethodArgumentNotValidException ile isteği 400 Bad Request'e çevirir. Bunu Spring Boot bölümünde, gerçek bir REST endpoint'inde göreceğiz.
@PostMapping("/kullanici")
public ... kayit(@Valid @RequestBody KullaniciKaydi k) { ... } // otomatik doğrulama

@Validated ve metot doğrulaması proxy tabanlıdır (AOP); bu yüzden self-invocation tuzağı burada da geçerlidir. @Valid ise iç içe (nested) nesneleri de doğrulamak için kullanılır.

Özet

Bean Validation ile kuralları anotasyonla bildirmeyi ve Validator ile tüm ihlalleri toplamayı (Örnek 1), @Constraint + ConstraintValidator ile özel kurallar yazmayı (Örnek 2) ve @Validated ile Spring'in metot parametrelerini otomatik doğrulamasını (Örnek 3) öğrendik. Bu, uygulamanın "giriş kapısını" temiz ve güvenli tutmanın standart yoludur.

Bununla Spring Framework çekirdeği ve temel uygulama katmanlarını (veri erişimi, transaction, validation) tamamlamış olduk. Güvenlik (Spring Security) bir web bağlamında çok daha anlamlı olduğundan, onu Spring Boot bölümünde gerçek bir uygulama üzerinde ele alacağız. Sırada: Spring Boot.

Kod Örnekleri(3)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: Bean Validation — kuralları anotasyonla bildir, Validator ile topluca kontrol et.
2// Gerçek senaryo: bir kayıt formundaki tüm hataları tek seferde toplamak.
3// Çalıştırma: portal Spring + hibernate-validator classpath'iyle çalıştırır.
4import jakarta.validation.Validation;
5import jakarta.validation.Validator;
6import jakarta.validation.constraints.Email;
7import jakarta.validation.constraints.Max;
8import jakarta.validation.constraints.Min;
9import jakarta.validation.constraints.NotBlank;
10import jakarta.validation.constraints.Size;
11
12public class Ornek1 {
13
14    // Kurallar, anotasyonlarla doğrudan alanların üstünde TANIMLANIR (bildirimsel).
15    static class KullaniciKaydi {
16        @NotBlank(message = "ad boş olamaz")
17        String ad;
18
19        @Email(message = "geçerli bir e-posta girin")
20        String eposta;
21
22        @Min(value = 18, message = "yaş en az 18 olmalı")
23        @Max(value = 120, message = "yaş en fazla 120 olabilir")
24        int yas;
25
26        @Size(min = 8, message = "parola en az 8 karakter olmalı")
27        String parola;
28
29        KullaniciKaydi(String ad, String eposta, int yas, String parola) {
30            this.ad = ad; this.eposta = eposta; this.yas = yas; this.parola = parola;
31        }
32    }
33
34    public static void main(String[] args) {
35        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
36
37        var gecerli = new KullaniciKaydi("Ada", "ada@site.com", 30, "guclu-parola");
38        var hatali = new KullaniciKaydi("", "gecersiz-eposta", 15, "123");
39
40        System.out.println("Geçerli kayıt -> ihlal sayısı: " + validator.validate(gecerli).size());
41
42        System.out.println("\nHatalı kayıt -> ihlaller:");
43        // validate() TÜM ihlalleri tek seferde döndürür (ilk hatada durmaz).
44        validator.validate(hatali).forEach(v ->
45                System.out.println("  - " + v.getPropertyPath() + ": " + v.getMessage()));
46
47        System.out.println("""
48
49                --- Bean Validation (JSR-380) ---
50                Kuralları if/else ile elle yazmak yerine anotasyonla bildirirsin: @NotBlank, @Email,
51                @Min/@Max, @Size, @NotNull, @Pattern, @Positive...
52                Validator tüm nesneyi gezip ihlalleri toplar. Spring MVC'de bu işi @Valid otomatik yapar
53                ve hataları 400 Bad Request'e çevirir (Spring Boot bölümünde göreceğiz).""");
54    }
55}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: Özel (custom) doğrulama kuralı — kendi @Constraint anotasyonun ve ConstraintValidator'ın.
2// Gerçek senaryo: "güçlü parola" kuralını yeniden kullanılabilir bir anotasyona dönüştürmek.
3// Çalıştırma: portal bu dosyayı derleyip Spring + hibernate-validator classpath'iyle çalıştırır.
4package com.egitim.spring.validation;
5
6import jakarta.validation.Constraint;
7import jakarta.validation.ConstraintValidator;
8import jakarta.validation.ConstraintValidatorContext;
9import jakarta.validation.Payload;
10import jakarta.validation.Validation;
11import jakarta.validation.Validator;
12
13import java.lang.annotation.ElementType;
14import java.lang.annotation.Retention;
15import java.lang.annotation.RetentionPolicy;
16import java.lang.annotation.Target;
17
18public class Ornek2 {
19
20    // 1) Özel anotasyon: hangi validator'ın kontrol edeceğini @Constraint ile belirtiriz.
21    @Constraint(validatedBy = GucluParolaValidator.class)
22    @Target(ElementType.FIELD)
23    @Retention(RetentionPolicy.RUNTIME)
24    @interface GucluParola {
25        String message() default "parola en az 8 karakter, bir büyük harf ve bir rakam içermeli";
26        Class<?>[] groups() default {};
27        Class<? extends Payload>[] payload() default {};
28    }
29
30    // 2) Kuralın asıl mantığı: ConstraintValidator. (public: doğrulayıcı reflection ile örneklenir.)
31    public static class GucluParolaValidator implements ConstraintValidator<GucluParola, String> {
32        @Override
33        public boolean isValid(String deger, ConstraintValidatorContext ctx) {
34            if (deger == null) return false;
35            boolean uzun = deger.length() >= 8;
36            boolean buyukVar = deger.chars().anyMatch(Character::isUpperCase);
37            boolean rakamVar = deger.chars().anyMatch(Character::isDigit);
38            return uzun && buyukVar && rakamVar;
39        }
40    }
41
42    // 3) Kullanım: artık tek anotasyonla uygulanır.
43    static class Hesap {
44        @GucluParola
45        String parola;
46        Hesap(String parola) { this.parola = parola; }
47    }
48
49    public static void main(String[] args) {
50        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
51
52        String[] denemeler = {"abc", "uzunamarakamyok", "Guclu123", "ZAYIF123"};
53        for (String p : denemeler) {
54            var ihlaller = validator.validate(new Hesap(p));
55            String sonuc = ihlaller.isEmpty() ? "GEÇERLİ"
56                    : "GEÇERSİZ (" + ihlaller.iterator().next().getMessage() + ")";
57            System.out.printf("  %-18s -> %s%n", "'" + p + "'", sonuc);
58        }
59
60        System.out.println("""
61
62                --- Özel kural ne kazandırır? ---
63                'Güçlü parola' kuralını bir kez yazıp her yerde @GucluParola ile uygularsın (DRY).
64                @Constraint + ConstraintValidator, standart anotasyonların (@Email vb.) aynı mekanizmasıdır;
65                yani kendi domain kurallarını (TC kimlik, IBAN, ürün kodu...) standart sisteme eklersin.""");
66    }
67}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: Spring entegrasyonu — @Validated ile metot parametrelerinin OTOMATİK doğrulanması.
2// @Validated proxy tabanlıdır; bu yüzden portal bu dosyayı derleyip çalıştırır.
3package com.egitim.spring.validation;
4
5import jakarta.validation.ConstraintViolationException;
6import jakarta.validation.constraints.Min;
7import jakarta.validation.constraints.NotBlank;
8import org.springframework.context.annotation.AnnotationConfigApplicationContext;
9import org.springframework.context.annotation.Bean;
10import org.springframework.context.annotation.Configuration;
11import org.springframework.validation.annotation.Validated;
12import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
13
14public class Ornek3 {
15
16    public static void main(String[] args) {
17        var ctx = new AnnotationConfigApplicationContext(Config.class);
18        SiparisServisi servis = ctx.getBean(SiparisServisi.class);
19
20        // Geçerli çağrı: sorunsuz.
21        servis.siparisVer("Klavye", 2);
22
23        // Geçersiz çağrılar: Spring, metoda GİRMEDEN parametreleri doğrular ve hata fırlatır.
24        denemeHatali(() -> servis.siparisVer("", 2), "boş ürün adı");
25        denemeHatali(() -> servis.siparisVer("Mouse", 0), "adet < 1");
26
27        ctx.close();
28        System.out.println("""
29
30                --- @Validated metot doğrulaması ---
31                @Validated + MethodValidationPostProcessor ile metot parametrelerindeki kısıtlar
32                (@NotBlank, @Min...) çağrı anında OTOMATİK kontrol edilir; ihlalde ConstraintViolationException.
33                Spring MVC'de @Valid + @RequestBody ile gelen DTO'lar da aynı şekilde doğrulanır
34                ve hata 400 Bad Request'e çevrilir (Spring Boot bölümünde).""");
35    }
36
37    static void denemeHatali(Runnable r, String aciklama) {
38        try {
39            r.run();
40            System.out.println("  (beklenmedik: hata fırlamadı) " + aciklama);
41        } catch (ConstraintViolationException e) {
42            System.out.println("  Reddedildi (" + aciklama + "): " + e.getConstraintViolations().iterator().next().getMessage());
43        }
44    }
45
46    @Configuration
47    public static class Config {
48        // Bu post-processor, @Validated bean'lerin metotlarını doğrulayan proxy'yi kurar.
49        @Bean static MethodValidationPostProcessor mvpp() { return new MethodValidationPostProcessor(); }
50        @Bean SiparisServisi siparisServisi() { return new SiparisServisi(); }
51    }
52
53    // @Validated: bu bean'in metot parametreleri doğrulamaya tabi olsun.
54    @Validated
55    public static class SiparisServisi {
56        public void siparisVer(@NotBlank(message = "ürün adı zorunlu") String urun,
57                               @Min(value = 1, message = "adet en az 1 olmalı") int adet) {
58            System.out.println("Sipariş alındı: " + adet + " adet " + urun);
59        }
60    }
61}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.