Problem: üç endpoint, üç xəta formatı
Bir endpoint yanlış girişdə düz mətn qaytarır, ikincisi message sahəsi olan map, üçüncüsü isə log-da stack trace olan default Spring xəta səhifəsini. Mobil komanda üç fərqli parser yazır. Sahə validasiyadan keçməyəndə istifadəçi "email düzgün deyil" əvəzinə "Bad Request" görür. Daha pisi, gözlənilməz exception response body-də class adını və ya SQL parçasını sızdırır.
Ardıcıl xəta idarəsinin iki yarısı var: yanlış girişi aydın qaydalarla erkən rədd etmək və hər uğursuzluğu bir proqnozlaşdırıla bilən cavab formasına çevirmək.
DTO üzərində Bean Validation
Bean Validation (Spring Boot 3-də Jakarta Validation) request tipində constraint-ləri elan etməyə imkan verir. spring-boot-starter-validation dependency-sini əlavə edin, sonra DTO-nu annotasiya edin:
public record RegisterRequest( @NotBlank @Email @Size(max = 254) String email, @NotBlank @Size(min = 12, max = 72) String password, @NotNull @Past LocalDate birthDate, @Valid @NotNull AddressRequest address) {}Validasiyanı controller parametrində @Valid ilə aktivləşdirin:
@PostMapping("/users")ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request) { userService.register(request); return ResponseEntity.status(HttpStatus.CREATED).build();}Constraint pozulduqda Spring metodunuzun gövdəsi işləməzdən əvvəl MethodArgumentNotValidException atır. İç-içə address sahəsindəki @Valid-ə diqqət edin: istəməsəniz, validasiya iç-içə obyektlərə keçmir.
Path variable və request parametrləri üçün controller class-ını @Validated ilə işarələyib constraint-ləri birbaşa parametrlərə qoyursunuz. Spring 6.1 və sonrakı versiyalarda orada uğursuzluq HandlerMethodValidationException kimi bildirilir və onu da idarə etməlisiniz.
Bean Validation formanı yoxlayır: məcburilik, uzunluq, format, diapazon. Verilənlər bazasına sorğu göndərməməlidir. "Email artıq qeydiyyatdadır" biznes qaydasıdır və service-ə aiddir.
Custom constraint
Eyni qayda bir neçə DTO-da təkrarlananda onu constraint edin. Nümunə: müəyyən formatda Azərbaycan mobil nömrəsi.
@Documented@Constraint(validatedBy = PhoneValidator.class)@Target({FIELD, PARAMETER, RECORD_COMPONENT})@Retention(RUNTIME)public @interface ValidPhone { String message() default "invalid phone number"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}public class PhoneValidator implements ConstraintValidator<ValidPhone, String> { private static final Pattern PHONE = Pattern.compile("^\\+994\\d{9}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value == null || PHONE.matcher(value).matches(); // null @NotNull-ın işidir }}null üçün true qaytarmaq qaydadır: dəyər məcburidirsə, @NotNull-u öz constraint-inizlə birləşdirin ki, hər annotasiyanın bir işi olsun.
Xətalar üçün vahid yer: @RestControllerAdvice
Controller advice bütün controller-lərdən gələn exception-ları tutur. Spring 6 RFC 9457 "Problem Details for HTTP APIs" formatını modelləşdirən ProblemDetail class-ını təqdim edir (o, RFC 7807-ni əvəz edir). Media type application/problem+json-dur.
@RestControllerAdviceclass ApiExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ProblemDetail problem = ProblemDetail.forStatusAndDetail( HttpStatus.BAD_REQUEST, "Bir və ya bir neçə sahə yanlışdır."); problem.setTitle("Validasiya uğursuz oldu"); problem.setProperty("code", "VALIDATION_FAILED"); problem.setProperty("errors", ex.getBindingResult().getFieldErrors().stream() .map(e -> Map.of("field", e.getField(), "message", String.valueOf(e.getDefaultMessage()))) .toList()); return ResponseEntity.badRequest().body(problem); } @ExceptionHandler(EmailAlreadyUsedException.class) ProblemDetail handleConflict(EmailAlreadyUsedException ex) { ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); problem.setProperty("code", "EMAIL_ALREADY_USED"); return problem; } @ExceptionHandler(Exception.class) ProblemDetail handleUnexpected(Exception ex) { log.error("Unhandled exception", ex); ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); problem.setProperty("code", "INTERNAL_ERROR"); return problem; // exception mesajı yoxdur, stack trace yoxdur }}ResponseEntityExceptionHandler standart Spring MVC exception-larını artıq problem details-ə çevirir; çıxışı zənginləşdirmək istədiyiniz metodları override edirsiniz. Daxili davranışı spring.mvc.problemdetails.enabled=true ilə də aktivləşdirə bilərsiniz, bu zaman default handler həmin exception-lar üçün application/problem+json istehsal edir.
Client-in aldığı cavab belə görünür:
{ "type": "about:blank", "title": "Validasiya uğursuz oldu", "status": 400, "detail": "Bir və ya bir neçə sahə yanlışdır.", "instance": "/users", "code": "VALIDATION_FAILED", "errors": [{ "field": "email", "message": "must be a well-formed email address" }]}Client-lər üçün sabit xəta kodları
İnsan üçün nəzərdə tutulmuş detail mətnin formasına və ya dilinə görə dəyişə bilər. Client-lər onu parse etməməlidir. Maşın tərəfindən oxunan code müqavilədir: EMAIL_ALREADY_USED, VALIDATION_FAILED, ORDER_NOT_FOUND. Kodları bir enum-da və ya sabitlər class-ında saxlayın, sənədləşdirin və heç vaxt fərqli məna üçün təkrar istifadə etməyin. Problem kateqoriyası üçün sənədləşmə dərc edirsinizsə, type üzvü onu müəyyən edən URI ola bilər; yoxdursa about:blank məqbuldur.
Status kodlarını şüurlu seçin: səhv və ya yanlış giriş üçün 400, olmayan resurs üçün 404, cari vəziyyətlə ziddiyyət üçün 409, komandanız üstün tutursa semantik validasiya səhvləri üçün 422 (ardıcıl olun), autentifikasiya və avtorizasiya üçün 401 və 403, 500 isə yalnız həqiqi server xətaları üçün.
Tez-tez edilən səhvlər
- Gözlənilməz xətalarda exception mesajını qaytarmaq. Mesajda cədvəl adları, class adları və ya fayl yolları ola bilər.
- Controller-də əl ilə
ifilə yoxlamaq: hər endpoint bunu bir az fərqli edir. - İç-içə obyektlərdə və kolleksiya elementlərində
@Valid-i unutmaq (List<@Valid LineRequest>). - Validator içində bazaya sorğu göndərmək. Əlavə sorğu heç kimin gözləmədiyi yerdə gizlənir, yoxlama isə yenə race condition-a açıqdır; unikallığı baza constraint-i ilə təmin edin və pozuntunu tərcümə edin.
- İstifadəçi girişini log injection və ya client-də markup riskini düşünmədən xəta mesajına əks etdirmək.
- Təhlükəsizlik xətaları (401/403) üçün tətbiq xətalarından fərqli forma. Security entry point-ləri eyni formatı yazacaq şəkildə konfiqurasiya edin.
- Tərcümə olunmuş mətni müqavilə kimi istifadə etmək. Kodun əsasında client-də tərcümə edin və ya
Accept-Language-i yalnızdetailüçün işlədin.
Trade-off-lar
Problem details alətlərlə dəstəklənən standartdır, amma genişlənmə üzvlərinə yer buraxır, buna görə komanda code və errors üzərində hələ də razılaşmalıdır. Çox ətraflı validasiya mesajı istifadəçiyə kömək edir, amma hücumçuya da kömək edə bilər (məsələn, login zamanı email-in, yoxsa parolun yanlış olduğunu göstərmək). Qeydiyyat formalarında səxavətli, autentifikasiya axınlarında isə qeyri-müəyyən olun.
Validasiya qrupları və yeniləmə ssenariləri
Eyni DTO bəzən həm yaratma, həm yeniləmə üçün istifadə olunur, amma tələblər fərqlənir: yaratmada email məcburidir, qismən yeniləmədə isə yox. Bean Validation qrupları bunu həll edə bilər, amma qrupları çoxaldıqca DTO oxunmaz olur. Daha sadə yol ayrı record-lar yazmaqdır: CreateUserRequest və UpdateUserRequest. Kiçik təkrar sonradan çaşdırıcı şərtlərdən yaxşıdır. Qismən yeniləmə (PATCH) üçün isə hansı sahənin göndərilmədiyini, hansının açıq şəkildə null göndərildiyini ayırmaq lazımdırsa, bunu ayrıca dizayn qərarı kimi qəbul edin.
Xəta müqaviləsinin testi
Xəta cavabları API-nizin hissəsidir, ona görə testə layiqdir. Baza tələb etmədiyi üçün web slice test kifayətdir (@MockitoBean Spring Framework 6.2 / Spring Boot 3.4 və daha yeni versiyaları tələb edir; daha köhnə Boot 3.x versiyalarında @MockBean istifadə edin):
@WebMvcTest(UserController.class)class UserControllerValidationTest { @Autowired MockMvc mvc; @MockitoBean UserService userService; @Test void invalidEmailReturnsProblemDetail() throws Exception { mvc.perform(post("/users").contentType(APPLICATION_JSON) .content("{\"email\":\"nope\",\"password\":\"long-enough-pass\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("VALIDATION_FAILED")) .andExpect(jsonPath("$.errors[0].field").value("email")); }}code və field üzərində yoxlama aparın, mesajın dəqiq mətni üzərində heç vaxt. Fallback handler üçün də bir test əlavə edin ki, refaktorinq exception mesajlarının yenidən səssizcə sızmasına səbəb olmasın.
Bu testlər sənəd rolunu da oynayır: yeni komanda üzvü xətanın client-ə necə göründüyünü testdən oxuya bilər. Həmçinin bu müqavilə dəyişəndə, məsələn sahə adı dəyişəndə və ya yeni kod əlavə olunanda, mobil və front end komandaları ilə razılaşdırılmış dəyişiklik kimi davranın, çünki onların kodu bu cavabı parse edir.
Mesajlar və dillər
Constraint mesajlarını ValidationMessages.properties fayluna çıxarmaq və request locale-ına görə həll etmək olar. Bu, server tərəfində render olunan formalar üçün faydalıdır; JSON API üçün isə bir çox komanda yalnız sahə adını və EMAIL_INVALID kimi kodu göndərməyi, mesajı isə hər client-in öz dilində göstərməsini üstün tutur. Hansı yolu seçsəniz də, bunu bir dəfə qərarlaşdırın və hər yerdə tətbiq edin. Azərbaycan və ingilis dillərini eyni cavabda qarışdırmaqdan çəkinin.
Yoxlama siyahısı
- Hər request body annotasiya olunub və controller parametrində
@Validvar. - İç-içə obyektlər və kolleksiya elementləri yoxlanılır.
- Tək
@RestControllerAdviceapplication/problem+jsonistehsal edir. - Hər xətanın sabit
code-u var; sahə xətaları sahəni və mesajı göstərir. - Gözlənilməz exception-lar ümumi body qaytarır və server tərəfdə stack trace ilə log-lanır.
- Testlər ən azı bir validasiya xətasını, bir biznes xətasını və fallback handler-i əhatə edir.
Sonra nə oxumalı
- Layer-lərin birlikdə necə işlədiyi: Spring Boot tətbiqində layering.
- Handler-i qidalandıran exception-ları dizayn edin: Java-da exception dizaynı.
- Giriş validasiyası həm də təhlükəsizlik nəzarətidir: OWASP siyahısı ilə API təhlükəsizliyi.
- Spring bacarıqlarınızı bacarıq qiymətləndirməsi ilə yoxlayın.