Spring MVC uygulamalarında RFC 9457 tabanlı Problem Details ile tutarlı spring rest api hata sözleşmesi kurun; doğrulama, gözlemlenebilirlik, geriye uyumluluk ve test ayrıntılarını uygulayın.
Spring MVC'de Problem Details ile Hata Sözleşmesini Yönetmek
Spring MVC ve spring rest api için hata sözleşmesini tasarlamak
Bir spring rest api için istemcinin güvenebileceği şey HTTP durum kodundan fazlasıdır: hata gövdesindeki alanların adı, anlamı ve değişim politikası da sözleşmedir. Spring Framework'ün ProblemDetail tipi, RFC 9457'nin type, title, status, detail ve instance alanlarını üretir. type alanına rastgele bir string yerine dokümante edilebilir, sürümlenebilir bir URI koyun; örneğin https://api.acme.dev/problems/validation-failed. Bu URI'nin HTTP ile erişilebilir olması zorunlu değildir, ancak ekiplerin hata kataloğunu aynı yerde tutmasını kolaylaştırır.
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(CustomerNotFoundException.class)
ResponseEntity<ProblemDetail> customerNotFound(
CustomerNotFoundException ex,
HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"Requested customer does not exist");
problem.setType(URI.create("https://api.acme.dev/problems/customer-not-found"));
problem.setTitle("Customer not found");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "CUSTOMER_NOT_FOUND");
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}
}errorCode, istemci davranışını bağlamak için title veya insan okunur detail'dan daha güvenli bir uzantıdır. Örneğin mobil uygulama yalnızca CUSTOMER_NOT_FOUND üzerinden yönlendirme yapabilir; çeviri metni olan detail değiştiğinde akış kırılmaz. Buna karşılık SQL hata metni, Java sınıf adı, upstream URL'si veya kullanıcı e-postası gibi iç ayrıntıları uzantı alanlarına koymayın: bu alanlar loglardan farklı olarak API tüketicisine kalıcı biçimde açılır. İyi bir spring framework eğitimi müfredatında bu ayrım, exception sınıfı tasarımından önce API sözleşmesi olarak ele alınmalıdır.
spring boot eğitimi kapsamında doğrulama hatalarını alan bazında üretmek
Bean Validation hatalarını tek bir 400 metnine indirmek, istemcinin hangi alanı düzelteceğini tahmin etmesine neden olur. MethodArgumentNotValidException, genellikle @RequestBody @Valid kullanımından; HandlerMethodValidationException ise parametre/metot doğrulamasından gelir. Uygulamanızın kullandığı Spring sürümüne göre ikincisi oluşmayabilir; bu yüzden desteklediğiniz giriş noktalarını entegrasyon testiyle doğrulayın. Aşağıdaki örnek yalnızca izin verilen alanları döndürür ve rejectedValue'yu bilerek dışarıda bırakır.
record FieldViolation(String field, String code, String message) {}
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ProblemDetail> invalidBody(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<FieldViolation> violations = ex.getBindingResult().getFieldErrors().stream()
.map(error -> new FieldViolation(
error.getField(),
error.getCode(),
error.getDefaultMessage()))
.toList();
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Request validation failed");
problem.setType(URI.create("https://api.acme.dev/problems/validation-failed"));
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "VALIDATION_FAILED");
problem.setProperty("violations", violations);
return ResponseEntity.badRequest()
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}Önemli edge case: FieldError#getRejectedValue() parolayı, erişim belirtecini veya büyük bir JSON nesnesini içerebilir. Bu değeri Problem Details içine koymak hem veri sızıntısı hem de gereksiz yanıt boyutu üretir. Ayrıca Bean Validation mesajını doğrudan döndürmek yerine error.getCode() değerini sözleşme kodu olarak koruyup, kullanıcı arayüzü çevirisini istemciye bırakmak çoğu çoklu dil senaryosunda daha sürdürülebilirdir. Bir spring boot kursu örneğinde, bu davranışı Postman koleksiyonuyla değil, otomatik MockMvc testiyle sabitlemek gerekir.
spring security hatalarında kimlik doğrulama ayrımını korumak
spring security katmanındaki AuthenticationEntryPoint ve AccessDeniedHandler, denetleyiciye ulaşmadan yanıt ürettiği için @RestControllerAdvice tarafından yakalanmaz. Bu nedenle 401 ve 403 gövdelerinin aynı Problem Details biçiminde olması isteniyorsa bu iki bileşeni ayrıca yapılandırın. 401, istemcinin geçerli kimlik bilgisi sunmadığını; 403 ise kimliği doğrulanmış kullanıcının yetkisi olmadığını belirtmelidir. Kaynak varlığının gerçekten var olup olmadığını 403 gövdesinde açıklamak, yetkisiz kullanıcıya envanter bilgisi sızdırabilir.
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
AuthenticationEntryPoint unauthorized = (request, response, ex) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
response.getWriter().write("""
{"type":"https://api.acme.dev/problems/unauthorized",
"title":"Authentication required",
"status":401,
"errorCode":"AUTHENTICATION_REQUIRED"}
""");
};
return http
.exceptionHandling(errors -> errors.authenticationEntryPoint(unauthorized))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.build();
}Üretim kodunda yukarıdaki string yazımı yerine paylaşılan bir ObjectMapper ile ProblemDetail serileştirin; böylece özel Jackson modülleri ve tarih biçimleri iki farklı yerde ayrışmaz. Ancak response yazıldıktan sonra filtre zincirinin devam etmediğinden emin olun. Bir başka sık hata, JWT doğrulama hatasında ham JwtException mesajını döndürmektir; token imza algoritması, issuer veya süre bilgisi saldırgana tanı sinyali verebilir. Dış yanıtı sabit tutun, ayrıntıyı yalnızca yapılandırılmış sunucu loguna yazın.
microservices mimarisi ve spring cloud ile hata bağlamını taşımak
microservices mimarisi içinde bir servis diğerine HTTP ile çağrı yaptığında, downstream servisinin Problem Details gövdesini körlemesine yeniden yayınlamak doğru değildir. instance alanı downstream URI'sini, detail ise iç iş kuralını açığa çıkarabilir. Gateway veya orkestratör servis, dış sözleşmeye ait bir problem üretmeli; buna karşılık kök neden için trace kimliğini korumalıdır. Spring Cloud Gateway kullanılıyorsa istek sınırında X-Request-Id kabul/üretme politikası belirleyin ve bunu Problem Details'e yalnızca güvenli bir korelasyon anahtarı olarak ekleyin.
@Component
class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String requestId = Optional.ofNullable(request.getHeader("X-Request-Id"))
.filter(id -> id.matches("[a-f0-9-]{16,64}"))
.orElseGet(() -> UUID.randomUUID().toString());
MDC.put("requestId", requestId);
response.setHeader("X-Request-Id", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("requestId");
}
}
}Bu filtrede istemciden gelen değeri sınırsız kabul etmemek önemlidir: log forging için satır sonu, aşırı uzun değer veya kontrol karakteri taşınabilir. Micrometer Tracing kullanıyorsanız bağımsız bir X-Request-Id yerine trace/span kimliklerini log korelasyonu için kullanmayı değerlendirin; fakat trace ID biçimini herkese açık API sözleşmesi yapmadan önce gözlemlenebilirlik sağlayıcısı değişimlerini hesaba katın. spring cloud bileşenleri arasında hata eşlemesi yapılırken, dışarıya dönen errorCode ile iç servis hata kodu arasında açık bir mapping tablosu tutmak, servis adlarını istemci sözleşmesinden ayırır.
Hata yanıtı maliyetini JFR ile ölçmek ve sözleşmeyi test etmek
Hata yolu düşük trafikte görünmez olsa da hatalı istemci, bot veya toplu veri aktarımı sırasında CPU ve allocation kaynağı olabilir. Ölçmeden fillInStackTrace() kapatmak ya da global exception ayarlarını değiştirmek doğru değildir; hata ayıklama verisini kaybedebilirsiniz. Önce hata üreten bir endpoint'i sabit yükle çağırın, Java Flight Recorder ile jdk.JavaExceptionThrow, allocation ve gecikme dağılımını kaydedin. Ardından yalnızca dış yanıt için stack trace serileştirmeyi kapatıp aynı yük ve JVM parametreleriyle ikinci kaydı alın.
# 120 saniyelik hata-yolu kaydı
jcmd <pid> JFR.start name=problem-errors settings=profile duration=120s filename=/tmp/problem-errors.jfr
# Aynı istek gövdesiyle iki çalıştırmada p95 karşılaştırması
wrk -t4 -c64 -d120s -s invalid-request.lua http://localhost:8080/customers
# JMC'de: Java Application > Events > Java Exception Throw
# ve Memory > Allocation in new TLAB görünümlerini karşılaştırın.Karşılaştırmada yalnızca ortalama süreye bakmayın: aynı hata oranında p95/p99 gecikmesi, saniyedeki allocation ve atılan exception sayısı birlikte değerlendirilmelidir. Uygulanabilir değişiklik, hata gövdesinde stack trace döndürmeyi kapatmaktır; örneğin varsayılan hata işleme yolunu kullanan uygulamalarda server.error.include-stacktrace=never yapılandırması kullanılabilir. Kendi ProblemDetail handler'ınız varsa stack trace alanını zaten eklemeyin. Sözleşmeyi de MockMvc ile kilitleyin; aksi halde bir refactor sonrası application/json dönmeye başlayabilir.
@Test
void invalidRequestReturnsProblemJson() throws Exception {
mockMvc.perform(post("/customers")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"not-an-email\"}"))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith(
MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.type").value(
"https://api.acme.dev/problems/validation-failed"))
.andExpect(jsonPath("$.errorCode").value("VALIDATION_FAILED"))
.andExpect(jsonPath("$.violations[0].field").value("email"));
}Bu yaklaşım, java spring eğitimi sırasında anlatılan exception handling bilgisini ölçülebilir bir üretim pratiğine çevirir: API biçimi testle korunur, hata yükünün JVM üzerindeki maliyeti ise JFR kaydıyla doğrulanır.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring MVC ile application/problem+json nasıl döndürülür?
Handler içinde ProblemDetail.forStatusAndDetail(...) oluşturup ResponseEntity için MediaType.APPLICATION_PROBLEM_JSON kullanın. MockMvc testinde contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON) doğrulaması ekleyin; yalnızca JSON alanlarını doğrulamak Content-Type gerilemelerini yakalamaz.
spring security 401 ve 403 hatalarını Problem Details formatında nasıl döndürürüm?
@RestControllerAdvice yerine AuthenticationEntryPoint ile 401, AccessDeniedHandler ile 403 üretin. Bunlar Security filter chain içinde, controller çağrılmadan çalışır. Dış gövdede sabit errorCode kullanın; ham JWT exception mesajını veya gerekli rol listesini döndürmeyin.
spring cloud gateway downstream ProblemDetail yanıtını aynen iletmeli mi?
Genellikle hayır. Downstream instance, servis URL'si ve iş kuralı ayrıntılarını sızdırabilir. Gateway dış hata koduna map etmeli, güvenli bir request/trace korelasyon kimliği eklemeli ve iç problem gövdesini yapılandırılmış loga kaydetmelidir. Mapping tablosunu Spring Cloud Contract ile consumer-driven contract testi olarak saklamak değişiklikleri görünür kılar.
spring boot eğitimi projelerinde hata yanıtı performansı nasıl ölçülür?
Sabit concurrency ve süreyle wrk veya Gatling çalıştırın; aynı anda jcmd <pid> JFR.start settings=profile ile kayıt alın. Değişiklik öncesi/sonrası p95-p99, jdk.JavaExceptionThrow olay sayısı ve TLAB allocation değerlerini karşılaştırın. Stack trace serileştirmesini kapatmak gibi tek bir değişikliği izole edin.
AI / LLM Discovery
Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.



