← Kursa Dön
📄 Text · 18 min

Request Mapping

Spring MVC'de bir controller metodunun hangi URL'e ve hangi HTTP metoduna karşılık geleceğini belirlemek için @RequestMapping ve onun türevi annotation'lar kullanılır. Bu annotation'lar, URL pattern'leri ile Java metotları arasında köprü kurar ve Spring MVC'nin en temel yapı taşlarından biridir.

Bir an için posta sistemini düşünün. Zarfın üzerindeki adres (URL), mektubun türü (HTTP metodu — mektup mu, koli mi, taahhütlü mü) ve hangi departmana gideceği (controller metodu) belirlidir. @RequestMapping tam da bu adresleme işini yapar — gelen HTTP isteğini doğru Java metoduyla eşleştirir.

@RequestMapping — Genel Amaçlı Eşleştirme

@RequestMapping, Spring MVC'deki en genel ve en esnek URL eşleştirme annotation'ıdır. Hem sınıf hem de metot seviyesinde kullanılabilir:

@RestController
@RequestMapping("/api/products") // Sınıf seviyesi — tüm metotlar için prefix
public class ProductController {

    @RequestMapping(method = RequestMethod.GET)
    public List<Product> getAll() {
        return productService.findAll();
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public Product getById(@PathVariable Long id) {
        return productService.findById(id);
    }

    @RequestMapping(value = "", method = RequestMethod.POST,
        consumes = "application/json",
        produces = "application/json")
    public Product create(@RequestBody Product product) {
        return productService.save(product);
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void delete(@PathVariable Long id) {
        productService.delete(id);
    }
}

@RequestMapping'in temel özellikleri:

ÖzellikAçıklamaÖrnek
value / pathURL pattern"/products", "/products/{id}"
methodHTTP metoduRequestMethod.GET, RequestMethod.POST
producesYanıt content-type"application/json", "application/xml"
consumesİstek content-typeSadece belirtilen content-type kabul eder
headersHeader koşullarıheaders = "X-API-VERSION=2"
paramsQuery parameter koşullarıparams = "type=active"

⚠️ Dikkat: @RequestMapping doğrudan kullanımı modern Spring uygulamalarında artık nadir tercih edilir. Bunun yerine HTTP metot kısa yolları (@GetMapping, @PostMapping vb.) kullanılır. Ancak sınıf seviyesinde prefix tanımlamak için hâlâ @RequestMapping kullanılır.

HTTP Method Shortcut Annotation'ları

@RequestMapping(method = RequestMethod.GET) yazmak uzun ve tekrarlayıcıdır. Spring 4.3 ile birlikte kısa yol (shortcut) annotation'ları eklendi. Bu shortcut'lar kodu çok daha okunabilir hale getirir:

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // GET /api/users
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    // GET /api/users/5
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.findById(id);
    }

    // POST /api/users
    @PostMapping
    public User createUser(@RequestBody @Valid CreateUserRequest request) {
        return userService.save(request);
    }

    // PUT /api/users/5 (Tüm kaynağı günceller)
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest request) {
        return userService.update(id, request);
    }

    // PATCH /api/users/5 (Kısmi güncelleme)
    @PatchMapping("/{id}")
    public User partialUpdate(@PathVariable Long id,
                               @RequestBody Map<String, Object> updates) {
        return userService.partialUpdate(id, updates);
    }

    // DELETE /api/users/5
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

Her shortcut annotation'ın karşılığı:

ShortcutKarşılık
@GetMapping@RequestMapping(method = RequestMethod.GET)
@PostMapping@RequestMapping(method = RequestMethod.POST)
@PutMapping@RequestMapping(method = RequestMethod.PUT)
@DeleteMapping@RequestMapping(method = RequestMethod.DELETE)
@PatchMapping@RequestMapping(method = RequestMethod.PATCH)

PUT vs PATCH — Tam Güncelleme vs Kısmi Güncelleme

REST API'lerde güncelleme işlemleri için iki HTTP metodu kullanılır. Aradaki fark önemlidir ve genellikle yanlış anlaşılır.

PUT — Tüm kaynağı değiştirir. Gönderilmeyen alanlar varsayılan değere döner veya null olur. Bir resmin tamamını silip yenisini çizmek gibidir.

PATCH — Sadece belirtilen alanları günceller. Diğer alanlar olduğu gibi kalır. Resmin sadece bir köşesini boyamak gibidir.

// PUT — Tüm alanlar gönderilmeli
// PUT /api/users/5
// Body: {"name": "Ahmet", "email": "ahmet@mail.com", "age": 30, "phone": "555-1234"}
// Eğer phone gönderilmezse, phone null olur!

@PutMapping("/{id}")
public User fullUpdate(@PathVariable Long id, @RequestBody UserUpdateRequest request) {
    User user = userService.findById(id);
    user.setName(request.getName());
    user.setEmail(request.getEmail());
    user.setAge(request.getAge());
    user.setPhone(request.getPhone()); // null gelirse phone silinir
    return userService.save(user);
}

// PATCH — Sadece değişen alanlar
// PATCH /api/users/5
// Body: {"email": "yeni@mail.com"}
// Sadece email güncellenir, diğer alanlar olduğu gibi kalır

@PatchMapping("/{id}")
public User partialUpdate(@PathVariable Long id,
                           @RequestBody Map<String, Object> updates) {
    User user = userService.findById(id);
    
    updates.forEach((key, value) -> {
        switch (key) {
            case "name" -> user.setName((String) value);
            case "email" -> user.setEmail((String) value);
            case "age" -> user.setAge((Integer) value);
            case "phone" -> user.setPhone((String) value);
        }
    });
    
    return userService.save(user);
}

💡 İpucu: Gerçek projelerde PATCH implementasyonu Map<String, Object> yerine JSON Patch (RFC 6902) veya JSON Merge Patch (RFC 7396) standartları ile yapılabilir. Ancak basit uygulamalarda Map yaklaşımı yeterlidir.

URL Path Patterns — Gelişmiş Eşleştirme

Spring MVC, URL eşleştirmede güçlü pattern desteği sunar. Bu pattern'ler, esnek ve temiz URL yapıları oluşturmanızı sağlar.

1. Exact Match (Tam Eşleşme)

@GetMapping("/products/featured")  // Sadece /products/featured ile eşleşir
public List<Product> getFeatured() {
    return productService.findFeatured();
}

2. Path Variable (Yol Değişkeni)

@GetMapping("/products/{id}")                    // /products/5, /products/abc
@GetMapping("/users/{userId}/orders/{orderId}")  // Çoklu değişken
@GetMapping("/files/{directory}/{filename}")      // /files/docs/report.pdf

3. Wildcard Patterns

@GetMapping("/files/*")    // /files/a ile eşleşir, /files/a/b ile eşleşmez
@GetMapping("/files/**")   // /files/a, /files/a/b, /files/a/b/c ile eşleşir
@GetMapping("/docs/{path}/**")  // /docs/spring/mvc/intro ile eşleşir

Tek yıldız (*) sadece bir path segmentini, çift yıldız (**) birden fazla segmenti karşılar. Bir apartmanın katları gibi düşünün: * sadece bir kat, ** tüm katlar.

4. Regex in Path Variables

Regex ile path variable'ların formatını kısıtlayabilirsiniz. Bu, hatalı istekleri erken yakalamak için çok güçlü bir mekanizmadır:

// Sadece sayısal id kabul et
@GetMapping("/users/{id:\\d+}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

// Sadece harf kabul et
@GetMapping("/categories/{name:[a-zA-Z]+}")
public Category getCategory(@PathVariable String name) {
    return categoryService.findByName(name);
}

// Tarih formatı (YYYY-MM-DD)
@GetMapping("/events/{date:\\d{4}-\\d{2}-\\d{2}}")
public List<Event> getEventsByDate(@PathVariable String date) {
    return eventService.findByDate(LocalDate.parse(date));
}

// Slug formatı (küçük harf, tire ile ayrılmış)
@GetMapping("/posts/{slug:[a-z0-9-]+}")
public Post getBySlug(@PathVariable String slug) {
    return postService.findBySlug(slug);
}

// UUID formatı
@GetMapping("/orders/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}")
public Order getByUuid(@PathVariable String uuid) {
    return orderService.findByUuid(UUID.fromString(uuid));
}

⚠️ Dikkat: Regex pattern'lerinde \d yerine \\d yazmalısınız — Java string'lerinde backslash escape edilir.

5. Content Type ile Ayrıştırma

// produces ile content type belirtin
@GetMapping(value = "/report", produces = MediaType.APPLICATION_PDF_VALUE)
public byte[] downloadReport() {
    return reportService.generatePdf();
}

@GetMapping(value = "/report", produces = MediaType.APPLICATION_JSON_VALUE)
public ReportData getReportData() {
    return reportService.getData();
}

Sınıf Seviyesi vs Metot Seviyesi Mapping

Sınıf seviyesinde tanımlanan @RequestMapping, tüm metot mapping'lerine prefix olarak eklenir. Bu, URL yapısını DRY (Don't Repeat Yourself) prensibine uygun hale getirir:

@RestController
@RequestMapping("/api/v1/products") // Sınıf prefix'i
public class ProductController {

    @GetMapping                  // → GET /api/v1/products
    public List<Product> getAll() { ... }

    @GetMapping("/{id}")         // → GET /api/v1/products/{id}
    public Product getById() { ... }

    @GetMapping("/search")       // → GET /api/v1/products/search
    public List<Product> search() { ... }

    @GetMapping("/categories/{cat}") // → GET /api/v1/products/categories/electronics
    public List<Product> byCategory() { ... }

    @PostMapping                 // → POST /api/v1/products
    public Product create() { ... }

    @PutMapping("/{id}")         // → PUT /api/v1/products/{id}
    public Product update() { ... }

    @DeleteMapping("/{id}")      // → DELETE /api/v1/products/{id}
    public void delete() { ... }
}

API versiyonlama bu yapıyla çok doğal olur:

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { /* ... */ }

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { /* ... */ }

Produces ve Consumes — Content Type Kontrolü

produces ve consumes özellikleri, controller metodunun hangi content type'ları üretip tükettiğini belirler. Bu, API'nizin kabul ettiği ve döndürdüğü formatları açıkça tanımlar:

@RestController
@RequestMapping(value = "/api/data",
    produces = MediaType.APPLICATION_JSON_VALUE)
public class DataController {

    // Sadece application/json kabul eder, application/json döndürür
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public DataResponse processJson(@RequestBody DataRequest request) {
        return dataService.process(request);
    }

    // application/xml döndürür (Jackson XML modülü gerekli)
    @GetMapping(value = "/xml", produces = MediaType.APPLICATION_XML_VALUE)
    public DataResponse getAsXml() {
        return dataService.getData();
    }

    // Birden fazla content type destekle
    @GetMapping(value = "/multi",
        produces = {MediaType.APPLICATION_JSON_VALUE,
                    MediaType.APPLICATION_XML_VALUE})
    public DataResponse getMultiFormat() {
        return dataService.getData();
    }

    // Form verisi kabul et
    @PostMapping(value = "/form",
        consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public DataResponse processForm(@RequestParam String name,
                                     @RequestParam String email) {
        return dataService.processForm(name, email);
    }
}

Eğer istemci yanlış content type gönderirse:

  • Content-Type uyumsuzluğu → 415 Unsupported Media Type

  • Accept uyumsuzluğu → 406 Not Acceptable

Çakışan Mapping'ler ve Öncelik Sırası

Birden fazla mapping aynı URL'le eşleştiğinde Spring, en spesifik olanı seçer:

@GetMapping("/products/{id}")       // /products/5 ile eşleşir
@GetMapping("/products/featured")   // /products/featured ile eşleşir

// Spring /products/featured isteği için ikincisini seçer
// çünkü exact match, path variable'dan daha spesifiktir.

Öncelik sırası (en yüksekten en düşüğe):

  1. Exact match: /products/featured

  2. Longest path match: /products/electronics/laptops

  3. Path variable: /products/{id}

  4. Regex path variable: /products/{id:\\d+}

  5. Wildcard `*`: /products/*

  6. Double wildcard `:** /products/**`

💡 İpucu: Çakışma durumlarında Spring IllegalStateException fırlatabilir. Aynı HTTP metodu ve path için iki metot tanımlamayın. Regex kullanarak path variable'ları daraltmak çakışmayı önler: /products/{id:\\d+} sadece sayıları, /products/{slug:[a-z-]+} sadece slug'ları eşleştirir.

Yaygın Hatalar ve Çözümleri

Hata 1: Trailing Slash Sorunu

// /api/users ve /api/users/ farklı URL'lerdir!
// Spring Boot 3.0+ varsayılan olarak trailing slash'ı eşleştirmez

// Çözüm: İki path tanımlayın
@GetMapping(value = {"", "/"})
public List<User> getUsers() { ... }

// Veya global ayar yapın
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseTrailingSlashMatch(true);
    }
}

Hata 2: Çoklu Path Variable Çakışması

// ❌ YANLIŞ — Spring hangisini seçeceğini bilemez
@GetMapping("/{id}")        // /products/5
@GetMapping("/{slug}")      // /products/laptop-xyz

// ✅ DOĞRU — Regex ile ayır
@GetMapping("/{id:\\d+}")           // Sadece sayısal: /products/5
@GetMapping("/{slug:[a-z0-9-]+}")   // Slug format: /products/laptop-xyz

Hata 3: Sınıf ve Metot Mapping Birleşim Karışıklığı

@RestController
@RequestMapping("/api")
public class ApiController {

    // DİKKAT: Bu endpoint /api olur, /api/ değil!
    @GetMapping("")
    public String root() { return "API Root"; }

    // Bu endpoint /api/status olur
    @GetMapping("/status")
    public String status() { return "OK"; }

    // ❌ YANLIŞ — Çift slash: /api//health
    @GetMapping("//health")
    public String health() { return "Healthy"; }
}

Gerçek Dünya Örneği: Blog API

Tüm mapping konseptlerini birleştiren gerçek bir blog API'si:

@RestController
@RequestMapping("/api/v1")
public class BlogController {

    private final PostService postService;
    private final CommentService commentService;

    public BlogController(PostService postService, CommentService commentService) {
        this.postService = postService;
        this.commentService = commentService;
    }

    // GET /api/v1/posts?page=0&size=10&sort=createdAt,desc
    @GetMapping("/posts")
    public Page<PostDto> listPosts(Pageable pageable) {
        return postService.findAll(pageable);
    }

    // GET /api/v1/posts/spring-boot-giris (slug ile)
    @GetMapping("/posts/{slug:[a-z0-9-]+}")
    public PostDto getBySlug(@PathVariable String slug) {
        return postService.findBySlug(slug);
    }

    // GET /api/v1/posts/42 (ID ile)
    @GetMapping("/posts/{id:\\d+}")
    public PostDto getById(@PathVariable Long id) {
        return postService.findById(id);
    }

    // POST /api/v1/posts
    @PostMapping(value = "/posts",
        consumes = MediaType.APPLICATION_JSON_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<PostDto> createPost(@RequestBody @Valid CreatePostRequest request) {
        PostDto created = postService.create(request);
        URI location = URI.create("/api/v1/posts/" + created.getSlug());
        return ResponseEntity.created(location).body(created);
    }

    // GET /api/v1/posts/42/comments
    @GetMapping("/posts/{postId:\\d+}/comments")
    public List<CommentDto> getComments(@PathVariable Long postId) {
        return commentService.findByPostId(postId);
    }

    // POST /api/v1/posts/42/comments
    @PostMapping("/posts/{postId:\\d+}/comments")
    public ResponseEntity<CommentDto> addComment(
            @PathVariable Long postId,
            @RequestBody @Valid CreateCommentRequest request) {
        CommentDto created = commentService.create(postId, request);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    // GET /api/v1/categories/teknoloji/posts
    @GetMapping("/categories/{category}/posts")
    public List<PostDto> getByCategory(@PathVariable String category,
                                        @RequestParam(defaultValue = "date") String sort) {
        return postService.findByCategory(category, sort);
    }

    // GET /api/v1/search?q=spring+boot&page=0
    @GetMapping("/search")
    public Page<PostDto> search(@RequestParam("q") String query, Pageable pageable) {
        return postService.search(query, pageable);
    }

    // GET /api/v1/archive/2024/03
    @GetMapping("/archive/{year:\\d{4}}/{month:\\d{2}}")
    public List<PostDto> getArchive(@PathVariable int year, @PathVariable int month) {
        return postService.findByMonth(year, month);
    }
}

Bu controller'da farklı pattern teknikleri bir arada kullanılıyor:

  • Regex path variable: {id:\\d+} ve {slug:[a-z0-9-]+} ile çakışma önleniyor

  • Nested resource: /posts/{postId}/comments iç içe kaynak erişimi

  • Query parameter: /search?q=... ile arama

  • Path hierarchy: /archive/2024/03 ile tarih bazlı erişim

  • produces/consumes: Content type kontrolü

RESTful URL Tasarım Best Practice'leri

# İyi URL tasarımı — isim kullan, fiil kullanma
GET    /api/users          → Tüm kullanıcıları listele
GET    /api/users/42       → Tek kullanıcı getir
POST   /api/users          → Yeni kullanıcı oluştur
PUT    /api/users/42       → Kullanıcıyı güncelle (tam)
PATCH  /api/users/42       → Kullanıcıyı güncelle (kısmi)
DELETE /api/users/42       → Kullanıcıyı sil

# ❌ Kötü URL tasarımı — fiil kullanma
GET    /api/getUsers
POST   /api/createUser
POST   /api/deleteUser/42

# İlişkili kaynaklar
GET    /api/users/42/orders          → Kullanıcının siparişleri
GET    /api/users/42/orders/7        → Belirli sipariş
POST   /api/users/42/orders          → Yeni sipariş oluştur

# Filtreleme ve arama
GET    /api/products?category=electronics&minPrice=100&sort=price,asc
GET    /api/users?role=admin&active=true&page=0&size=20

Özet

  • `@GetMapping`, `@PostMapping` gibi shortcut'lar kullanarak temiz ve okunabilir controller'lar yazın. @RequestMapping sadece sınıf seviyesi prefix için kullanılır.

  • Sınıf seviyesinde ortak prefix tanımlayın: @RequestMapping("/api/v1/products"). Bu, URL yapısını tek noktadan yönetmenizi sağlar.

  • URL pattern'lerinde regex ile girdi doğrulama yapın: {id:\\d+} çakışmaları önler ve hatalı istekleri erken yakalar.

  • `produces/consumes` ile content type kontrolü sağlayın. API'nizin ne kabul ettiğini ve ne döndürdüğünü açıkça belirtin.

  • PUT vs PATCH farkını bilin: PUT tüm kaynağı, PATCH sadece belirtilen alanları günceller.

  • RESTful URL tasarımı kurallarına uyun: isim kullanın (users, products), fiil kullanmayın (getUsers, deleteProduct). HTTP metodu zaten fiili belirtir.