İçeriğe geç

Spring MVC — Bir İstek Controller'a Nasıl Ulaşır?

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: IoC ve Dependency Injection

30 saniyede özet

Her HTTP isteği önce filter'lardan geçer, sonra tek kapıdan, DispatcherServlet'ten girer. İçeride doğru metot bulunur, JSON nesneye çevrilir ve cevap yine JSON'a yazılır. Hatanın nerede çıktığı, onu kimin göreceğini belirler.

Büyük bir şirkete mektup gönderdiğini düşün. Mektup doğrudan ilgili kişinin masasına düşmez; önce kapıdan, sonra resepsiyondan geçer.

Her durağın tek bir işi var.
Adım adım oku
  1. Kapıdaki görevli mektubu kontrol eder ve içeri alır.
  2. Resepsiyon mektubun hangi masaya gideceğini bulur.
  3. Sekreter not alır, masadaki memur işi yapar.
  4. Tercüman cevabı gönderenin okuyacağı dile çevirir.
  1. Bayt: Denetim ekibi soruyor: bir havale isteği uygulamaya girince tam olarak nerelerden geçiyor?

  2. Sen: Controller'a gidiyor işte. Başka nereye gidecek?

  3. Bayt: Controller'dan önce ve sonra epey durak var bence...

  4. Bayt: Bir mektup gibi takip edelim. Durak durak.

Tek kapı: DispatcherServlet

Spring MVC uygulamasında bütün istekler aynı kapıdan girer: DispatcherServletSpring MVC uygulamasının tek giriş kapısı. Gelen her isteği handler mapping ile doğru controller metoduna yollar, dönen cevabı istemciye yazar ve hataları hata işleyicilere verir.Sözlükte gör →. Kendisi iş yapmaz; isteği doğru yere yollar.

Önce handler mapping’e sorar: “POST /transfers kimin işi?” Cevap bir controller metodudur. DispatcherServlet o metodu çağırır, dönen cevabı da istemciye geri yazar.

istek → filter → filter → DispatcherServlet → handler mapping → interceptor → controller
cevap ← filter ← filter ← DispatcherServlet ← JSON'a çevir ← interceptor ←
Kafam karıştı, daha basit anlat

Binaya tek kapıdan girilir. Resepsiyon mektubun hangi masaya gideceğini bulur ve oraya götürür.

Hızlı kontrolBaşlangıç

Spring MVC uygulamasına gelen bir HTTP isteğini doğru controller metoduna kim yönlendirir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kapının dışında, kapının içinde

Kapının dışında servlet filter’ları durur. Filter her isteği görür, ama hangi controller’ın çalışacağını bilmez; o karar henüz verilmedi. Spring Security de bir filter zinciridir.

İçeride ise HandlerInterceptorDispatcherServlet içinde, controller metodundan önce ve sonra çalışan kanca. Filter'dan farkı, hangi metodun çalışacağını bilmesidir.Sözlükte gör → çalışır. Handler mapping’den sonra devreye girdiği için hangi metodun çağrılacağını bilir. Üç anı vardır: preHandle metottan önce, postHandle metot başarıyla dönünce, afterCompletion her şey bitince.

Audit kaydını bir HandlerInterceptor'ın preHandle metodunda tutuyorsun. Token'ı olmayan biri istek atıyor ve Spring Security 401 dönüyor. Bu istek audit kaydında görünür mü? Cevabı göster

Görünmez. Spring Security bir filter; DispatcherServlet’ten önce çalışır ve isteği kapıda geri çevirir. Interceptor binanın içindedir, kapıda dönen mektubu hiç görmez.

Kural basit. Her isteği görmesi gereken iş filter’a gider: correlation id, token kontrolü. Hangi metodun çalıştığını bilmesi gereken iş interceptor’a gider: audit, metoda özel ek kontrol.

Kafam karıştı, daha basit anlat

Filter kapıdaki görevli: herkesi görür, kimin nereye gideceğini bilmez. Interceptor içerideki sekreter: yalnızca içeri girenleri görür ama hangi masaya gittiklerini bilir.

Hızlı kontrolOrta

Her isteğe bir correlation id vermek ve bunu loglara yazmak istiyorsun. Security'nin 401 ile geri çevirdiği istekler de dahil olmalı. Bu kodu nereye koyarsın?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her işi yapılacağı yere göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Servlet filter

Her isteği görmeli, controller'ı bilmesi gerekmez

    HandlerInterceptor

    Hangi controller metodunun çalışacağını bilmeli

      Satır satır: JSON’dan nesneye, nesneden JSON’a

      Controller JSON görmez, Java nesnesi görür. Aradaki çeviriyi bir HttpMessageConverterHTTP gövdesini Java nesnesine, Java nesnesini de gövdeye çeviren bileşen. @RequestBody ve @RestController dönüşleri bunu kullanır; JSON için varsayılanı Jackson'dır.Sözlükte gör → yapar; Spring Boot’ta varsayılanı Jackson kullanır. Aynı çevirmen dönüşte nesneyi JSON’a yazar.

      Bir havale isteğini, bakiye yetmediği gün izleyelim.

      Bir havale isteği ve bir hata

      TransferController.java
      1@RestController
      2@RequestMapping("/transfers")
      3class TransferController {
      4 private final TransferService transfers; // comes from the constructor
      5
      şu an çalışan satır @PostMapping
      7 ResponseEntity<TransferResponse> create(@Valid @RequestBody TransferRequest request) {
      8 TransferResponse done = transfers.send(request);
      9 return ResponseEntity.status(HttpStatus.CREATED).body(done);
      10 }
      11}
      12
      13@RestControllerAdvice
      14class ApiErrors {
      15
      16 @ExceptionHandler(InsufficientFundsException.class)
      17 ProblemDetail insufficient(InsufficientFundsException e) {
      18 return ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, "Bakiye yetersiz");
      19 }
      20}

      Debug

      Adım 1/6

      DispatcherServlet Handler mapping, POST /transfers için bu metodu buldu.

      istek
      = POST /transfers
      Java 21UTF-8LF6:1

      Sol/sağ ok tuşlarıyla da gezebilirsin.

      Hatayı yakalayan sınıf bir @RestControllerAdviceBütün controller'lardan çıkan istisnaları tek yerde HTTP cevabına çeviren sınıf. İçindeki @ExceptionHandler metotları hangi istisnanın hangi koda eşleneceğini söyler.Sözlükte gör →. Ayrıntıları Validation ve hata yönetimi dersinde gördün; burada önemli olan yeri: DispatcherServlet’in içi.

      Hızlı kontrolOrta

      GET /balance çağrıldı. Konsolda ne yazar?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
      Timing.java
      1class Timing implements HandlerInterceptor {
      2 public boolean preHandle(HttpServletRequest q, HttpServletResponse r, Object h) {
      3 System.out.println("pre");
      4 return true;
      5 }
      6 public void postHandle(HttpServletRequest q, HttpServletResponse r, Object h, ModelAndView mv) {
      7 System.out.println("post");
      8 }
      9 public void afterCompletion(HttpServletRequest q, HttpServletResponse r, Object h, Exception ex) {
      10 System.out.println("after");
      11 }
      12}
      13
      14@GetMapping("/balance")
      15BigDecimal balance() {
      16 throw new AccountFrozenException();
      17}
      18
      19@ExceptionHandler(AccountFrozenException.class)
      20ProblemDetail frozen() {
      21 System.out.println("advice");
      22 return ProblemDetail.forStatus(HttpStatus.LOCKED);
      23}
      Java 21UTF-8LF

      Bir @RestController metodu TransferResponse nesnesi döndürüyor. İstemciye giden JSON'u kim üretir?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Kendin gör

      Bir havale isteği yola çıkıyor. Neyin ters gideceğini ve advice olup olmadığını sen seçiyorsun.

      Bir istek controller'a nasıl ulaşır?

      Tohum 1
      1. Filter: correlation idbekliyor
      2. Filter: Spring Securitybekliyor
      3. DispatcherServletbekliyor
      4. Handler mappingiçeride · bekliyor
      5. Interceptor: preHandleiçeride · bekliyor
      6. JSON → nesne, @Validiçeride · bekliyor
      7. Controlleriçeride · bekliyor
      8. Nesne → JSONiçeride · bekliyor
      9. Interceptor: afterCompletioniçeride · bekliyor
      10. Hata işleyicibekliyor

      Oynat ya da adımla.

      Hız
      Adım 0

      Şu an ne oldu?

      Bir havale isteği yola çıktı

      POST /transfers geldi. Hangi duraklardan geçecek ve cevabı kim yazacak?

      Görevler0/3

      • Bir isteğin DispatcherServlet'e hiç ulaşmadan cevap almasını sağlaaçık

        İpucu

        Kapıdaki görevli token sorar.

      • Bir iş kuralı hatası 422 ve ProblemDetail ile dönsünaçık

        İpucu

        Bakiye yetmesin, advice açık olsun.

      • Advice açıkken bile 500 dönen bir hata bulaçık

        İpucu

        Advice yalnızca DispatcherServlet'in içini görür.

      Olay günlüğü (0)

      Henüz olay yok. Oynat veya adımla.

      1. Varsayılanı oynat. Geçerli istek bütün duraklardan geçer ve 201 alır. Audit kaydı var.
      2. “Token yok” seç. Yolculuk ikinci durakta biter. Audit kaydı yok, çünkü interceptor isteği hiç görmedi.
      3. “Bakiye yetersiz” seç ve advice’ı kapatıp aç. Kapalıyken 500, açıkken 422 ve ProblemDetail.
      4. “Bizim filtremiz hata fırlatıyor” seç. Advice açıkken bile 500. Neden?
      Hızlı kontrolOrta

      Uygulamada bütün hataları ProblemDetail'e çeviren bir @RestControllerAdvice var. Hangisi yine de Boot'un varsayılan 500 cevabıyla döner?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar

      Filter’daki hatayı advice görmez. Filter DispatcherServlet’ten önce çalışır. Orada fırlayan hata advice’a hiç ulaşmaz ve Boot’un varsayılan 500 cevabına düşer.

      postHandle, JSON cevabını değiştiremez. @RestController’da gövde, metot döner dönmez yazılır. postHandle geldiğinde cevap yola çıkmıştır; başlık eklemek için filter ya da ResponseBodyAdvice kullanılır.

      Filter’da gövdeyi okumak onu tüketir. İstek gövdesi bir kez okunabilen bir akıştır. Filter onu okursa controller boş bulur; ContentCachingRequestWrapper bir kopya tutar.

      Kafam karıştı, daha basit anlat

      Kapıdaki hatayı içerideki kimse görmez. Cevap bir kez gönderildikten sonra değiştirilemez. Mektubu kapıda açıp okursan içeriye boş zarf gider.

      Hızlı kontrolOrta

      Bu filtre eklendikten sonra POST /transfers istekleri 400 dönmeye başladı: "Required request body is missing". Sorun hangi satırda?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Hatalı satıra dokun, sonra kontrol et.

      BodyLoggingFilter.java
      Java 21UTF-8LF
      Derinleş · Bankacılık API'si: kapıda kimlik, içeride audit, tek yerde hata 5 dosya · ~94 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/web/ CorrelationIdFilter.java Filter: her isteğe bir correlation id verir ve cevaba da yazar. Security'den önce çalışır, reddedilen istekler de kimlik taşır. finally içinde MDC temizlenir; thread havuza döner ve kimlik bir sonraki isteğin loglarına sızmaz.

      src/main/java/com/bank/web/CorrelationIdFilter.java
      @Component
      @Order(Ordered.HIGHEST_PRECEDENCE)
      class CorrelationIdFilter extends OncePerRequestFilter {
      private static final String HEADER = "X-Correlation-Id";
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws ServletException, IOException {
      String id = Optional.ofNullable(request.getHeader(HEADER)).orElseGet(() -> UUID.randomUUID().toString());
      MDC.put("correlationId", id);
      response.setHeader(HEADER, id);
      try {
      chain.doFilter(request, response);
      } finally {
      // The thread goes back to the pool; a leftover id would leak into the next request's logs.
      MDC.remove("correlationId");
      }
      }
      }

      src/main/java/com/bank/web/ AuditInterceptor.java Interceptor: preHandle'da çağrılan metodun adını not alır. afterCompletion hata olsa da çalışır; audit kaydı bu yüzden orada yazılır.

      src/main/java/com/bank/web/AuditInterceptor.java
      @Component
      class AuditInterceptor implements HandlerInterceptor {
      private final AuditLog audit;
      AuditInterceptor(AuditLog audit) {
      this.audit = audit;
      }
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
      if (handler instanceof HandlerMethod method) {
      request.setAttribute("audit.action", method.getMethod().getName());
      }
      return true;
      }
      // Runs after success and after failure alike, as long as preHandle returned true.
      @Override
      public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
      audit.record(request.getUserPrincipal(), (String) request.getAttribute("audit.action"), response.getStatus());
      }
      }

      src/main/java/com/bank/web/ WebConfig.java Advice: iş hatası 422, geçersiz alanlar 400 döner. İkisi de ProblemDetail; istemci hangi alanın yanlış olduğunu cevaptan görür.

      src/main/java/com/bank/web/WebConfig.java
      @Configuration
      class WebConfig implements WebMvcConfigurer {
      private final AuditInterceptor audit;
      WebConfig(AuditInterceptor audit) {
      this.audit = audit;
      }
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(audit).addPathPatterns("/transfers/**", "/accounts/**");
      }
      }

      src/main/java/com/bank/transfer/ TransferController.java

      src/main/java/com/bank/transfer/TransferController.java
      @RestController
      @RequestMapping("/transfers")
      class TransferController {
      private final TransferService transfers;
      TransferController(TransferService transfers) {
      this.transfers = transfers;
      }
      @PostMapping
      ResponseEntity<TransferResponse> create(@Valid @RequestBody TransferRequest request) {
      TransferResponse done = transfers.send(request);
      return ResponseEntity.status(HttpStatus.CREATED).body(done);
      }
      }
      record TransferRequest(@NotBlank String fromIban, @NotBlank String toIban, @NotNull @Positive BigDecimal amount) {
      }

      src/main/java/com/bank/web/ ApiErrors.java

      src/main/java/com/bank/web/ApiErrors.java
      @RestControllerAdvice
      class ApiErrors {
      @ExceptionHandler(InsufficientFundsException.class)
      ProblemDetail insufficient(InsufficientFundsException e) {
      ProblemDetail body = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, "Bakiye yetersiz");
      body.setProperty("accountId", e.accountId());
      return body;
      }
      @ExceptionHandler(MethodArgumentNotValidException.class)
      ProblemDetail invalid(MethodArgumentNotValidException e) {
      ProblemDetail body = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "İstek alanları geçersiz");
      body.setProperty("fields", e.getBindingResult().getFieldErrors().stream()
      .collect(Collectors.toMap(FieldError::getField, f -> String.valueOf(f.getDefaultMessage()), (a, b) -> a)));
      return body;
      }
      }

      Kendini sına

      Önce hızlı bir ısınma: puan yok, kayıt yok. Sonra asıl sorular.

      Şimşek turu1/5

      Spring Security, DispatcherServlet'ten önce çalışan bir filter zinciridir.

      Soru 1/3İleri

      Bir HandlerInterceptor'ın postHandle metodunda response'a X-Processing-Time başlığı ekliyorsun. @RestController cevaplarında başlık istemciye hiç ulaşmıyor. Neden?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 Bütün istekler tek kapıdan, DispatcherServlet'ten girer. Handler mapping doğru metodu bulur, HttpMessageConverter JSON'u nesneye ve nesneyi JSON'a çevirir.
      2. 2 Filter kapının dışında durur ve her isteği görür; Spring Security de bir filter zinciridir. Interceptor içeridedir ve hangi metodun çalışacağını bilir.
      3. 3 @RestControllerAdvice yalnızca DispatcherServlet'in içinde çıkan hataları görür. Filter'da çıkan hata ona hiç ulaşmaz.
      Sonraki kapı Bir liste çektin, sonra her elemanın detayına baktın. Kaç sorgu çalıştı? JPA N+1 Problemi · 10 dk

      5 kart sonraki derste seni bekliyor

      0/4 kart bu dersten toplandı