Spring RestController: RESTful Web Servisleri Geliştirmede Temel Taş
Giriş: REST ve Spring Boot Ekosisteminde Yeri
Günümüzün dağıtık sistem mimarilerinde, farklı uygulamaların birbirleriyle etkileşim kurması kaçınılmaz bir gerekliliktir. Bu etkileşimi sağlamanın en yaygın ve etkili yollarından biri de REST (Representational State Transfer) mimari stilini kullanan web servisleridir. REST, web’in temel prensiplerine dayanarak, kaynakların (resources) HTTP metodları (GET, POST, PUT, DELETE vb.) ve durum kodları (200 OK, 404 Not Found vb.) aracılığıyla yönetilmesini sağlayan hafif, esnek ve ölçeklenebilir bir yaklaşımdır. Mikroservis mimarilerinin yükselişiyle birlikte RESTful servisler, modern yazılım geliştirmede adeta bir standart haline gelmiştir.
Java ekosisteminde RESTful servisler geliştirmek için birçok framework ve kütüphane bulunsa da, Spring Framework ve özellikle Spring Boot, bu alandaki en popüler ve güçlü araçlardan biridir. Spring Boot, geliştiricilerin hızlıca bağımsız, üretime hazır Spring tabanlı uygulamalar oluşturmasına olanak tanır. “Convention over configuration” (yapılandırma yerine kural) felsefesiyle, karmaşık yapılandırmaları basitleştirerek geliştirme sürecini hızlandırır. Spring Boot’un RESTful servis geliştirme yeteneklerinin kalbinde ise @RestController anotasyonu yer alır. Bu anotasyon, Spring MVC’nin tüm gücünü REST’in basitliğiyle birleştirerek, Java geliştiricilerinin güçlü ve ölçeklenebilir API’ler oluşturmasını son derece kolaylaştırır. Bu makalede, @RestController‘ın ne olduğunu, nasıl çalıştığını, temel özelliklerini ve RESTful servisler geliştirirken dikkat edilmesi gereken en iyi uygulamaları detaylı bir şekilde inceleyeceğiz.
@RestController Anotasyonu: Temeller ve İşleyiş
Spring Framework’te, gelen HTTP isteklerini işlemek ve uygun yanıtları döndürmek için Controller bileşenleri kullanılır. Geleneksel Spring MVC uygulamalarında, HTML sayfaları döndüren bir kontrolcü @Controller anotasyonu ile işaretlenirken, bir RESTful serviste veri (genellikle JSON veya XML) döndüren bir kontrolcüye ihtiyaç duyarız. İşte bu noktada @RestController devreye girer.
@RestController aslında iki temel Spring anotasyonunun birleşimidir:
1. @Controller: Bu anotasyon, bir sınıfı Spring MVC’nin bir kontrolcü bileşeni olarak işaretler. Spring’in bileşen taraması (component scanning) sırasında bu sınıfı bulmasını ve istek işleme yetenekleri için kaydetmesini sağlar.
2. @ResponseBody: Bu anotasyon, bir metodun dönüş değerinin doğrudan HTTP yanıt gövdesine yazılması gerektiğini belirtir. Varsayılan olarak, Spring bu dönüş değerini uygun bir HTTP mesaj dönüştürücüsü (HTTP Message Converter) kullanarak JSON veya XML gibi bir formata dönüştürür.
Dolayısıyla, bir sınıfı @RestController ile işaretlediğinizde, Spring bu sınıfın hem bir kontrolcü olduğunu anlar hem de bu sınıftaki tüm metodların dönüş değerlerinin otomatik olarak HTTP yanıt gövdesine dönüştürülüp gönderileceğini varsayar. Bu, her metoda ayrı ayrı @ResponseBody anotasyonu ekleme ihtiyacını ortadan kaldırarak kodu daha temiz ve okunabilir hale getirir.
Örnek:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MerhabaController {
@GetMapping("/merhaba")
public String merhabaDunya() {
return "Merhaba Dünya!";
}
@GetMapping("/api/kullanici")
public Kullanici getKullanici() {
return new Kullanici(1L, "Ali Can", "ali.can@example.com");
}
}
class Kullanici {
private Long id;
private String adSoyad;
private String email;
public Kullanici(Long id, String adSoyad, String email) {
this.id = id;
this.adSoyad = adSoyad;
this.email = email;
}
// Getter ve Setter metodları
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getAdSoyad() { return adSoyad; }
public void setAdSoyad(String adSoyad) { this.adSoyad = adSoyad; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
Yukarıdaki örnekte, /merhaba adresine yapılan bir GET isteği “Merhaba Dünya!” metnini, /api/kullanici adresine yapılan bir GET isteği ise Kullanici nesnesini JSON formatında döndürecektir (eğer projenizde Jackson veya Gson gibi bir JSON kütüphanesi varsa). Bu otomatik dönüşüm, geliştiricilerin veri serileştirme/deserileştirme detaylarıyla uğraşmak zorunda kalmamasını sağlar.
HTTP Metodları ve @RequestMapping
RESTful servislerin temelini, kaynaklar üzerindeki işlemleri temsil eden HTTP metodları oluşturur. @RestController sınıfları içinde, belirli HTTP isteklerini belirli metodlara yönlendirmek için @RequestMapping anotasyonu veya onun kısaltılmış versiyonları kullanılır.
@RequestMapping Anotasyonu
@RequestMapping anotasyonu, bir isteğin URI’sını (Uniform Resource Identifier) ve HTTP metodunu bir kontrolcü metoduna eşleştirmek için kullanılır. Hem sınıf seviyesinde hem de metod seviyesinde uygulanabilir.
* value veya path özniteliği: İsteğin eşleşeceği URI yolunu belirtir.
* method özniteliği: İsteğin eşleşeceği HTTP metodunu belirtir (örn. RequestMethod.GET, RequestMethod.POST).
* consumes özniteliği: İsteğin HTTP gövdesinde kabul edilebilir medya tiplerini belirtir (örn. application/json).
* produces özniteliği: Yanıtın HTTP gövdesinde üretilebilecek medya tiplerini belirtir (örn. application/xml).
Örnek:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/urunler") // Sınıf seviyesinde temel URI
public class UrunController {
@RequestMapping(method = RequestMethod.GET) // GET /api/urunler
public String getAllProducts() {
return "Tüm ürünler listeleniyor...";
}
@RequestMapping(method = RequestMethod.POST, consumes = "application/json") // POST /api/urunler
public String createProduct() {
return "Yeni ürün oluşturuluyor...";
}
}
Kısaltma Anotasyonları
@RequestMapping‘in HTTP metoduna özel kısaltmaları, kodu daha okunaklı ve özlü hale getirir. Bunlar, @RequestMapping(method = RequestMethod.GET) yerine doğrudan kullanılabilecek anotasyonlardır:
* @GetMapping: HTTP GET istekleri için.
* @PostMapping: HTTP POST istekleri için.
* @PutMapping: HTTP PUT istekleri için.
* @DeleteMapping: HTTP DELETE istekleri için.
* @PatchMapping: HTTP PATCH istekleri için.
Bu kısaltmalar, modern Spring Boot uygulamalarında en çok tercih edilen yöntemdir.
Örnek:
import org.springframework.web.bind.annotation.*; // Tüm anotasyonları içe aktarır
@RestController
@RequestMapping("/api/urunler")
public class UrunControllerModern {
@GetMapping // GET /api/urunler
public String getAllProducts() {
return "Tüm ürünler listeleniyor (modern)...";
}
@PostMapping(consumes = "application/json") // POST /api/urunler
public String createProduct() {
return "Yeni ürün oluşturuluyor (modern)...";
}
@PutMapping("/{id}") // PUT /api/urunler/{id}
public String updateProduct(@PathVariable Long id) {
return id + " numaralı ürün güncelleniyor...";
}
@DeleteMapping("/{id}") // DELETE /api/urunler/{id}
public String deleteProduct(@PathVariable Long id) {
return id + " numaralı ürün siliniyor...";
}
}
Bu kısaltmalar, kodun amacını anında belli ettiği için okunabilirliği büyük ölçüde artırır.
Parametre İşleme: İstemciden Gelen Verileri Yakalama
RESTful servisler, istemciden çeşitli yollarla veri alabilir. Spring @RestController‘lar, bu verileri yakalamak ve Java metod parametrelerine bağlamak için güçlü anotasyonlar sunar.
@PathVariable
Bu anotasyon, URI’nın bir parçası olarak gönderilen değişkenleri (path variables) yakalamak için kullanılır. Özellikle bir kaynağın kimliğini belirtmek için idealdir.
Örnek:
@GetMapping("/{id}") // GET /api/urunler/123
public String getProductById(@PathVariable Long id) {
return "ID'si " + id + " olan ürün getiriliyor.";
}
@GetMapping("/{kategori}/{id}") // GET /api/urunler/elektronik/456
public String getProductByCategoryAndId(@PathVariable String kategori, @PathVariable Long id) {
return kategori + " kategorisinden ID'si " + id + " olan ürün getiriliyor.";
}
@RequestParam
Bu anotasyon, sorgu parametrelerini (query parameters) yakalamak için kullanılır. Genellikle filtreleme, sıralama veya sayfalama gibi opsiyonel parametreler için tercih edilir.
Örnek:
@GetMapping
public String getProductsFiltered(
@RequestParam(required = false) String ad, // ad parametresi opsiyonel
@RequestParam(defaultValue = "0") int sayfa
) {
String response = "Ürünler listeleniyor.";
if (ad != null) {
response += " Adı: " + ad;
}
response += " Sayfa: " + sayfa;
return response;
}
// Örnek istekler: /api/urunler?ad=kalem, /api/urunler?sayfa=2, /api/urunler?ad=defter&sayfa=1
@RequestBody
Bu anotasyon, HTTP isteğinin gövdesini (request body) bir Java nesnesine dönüştürmek için kullanılır. Genellikle POST ve PUT isteklerinde, istemciden JSON veya XML formatında gönderilen verileri yakalamak için kullanılır. Spring, otomatik olarak uygun HTTP mesaj dönüştürücüsünü kullanarak bu serileştirmeyi/deserileştirmeyi yapar.
Örnek:
@PostMapping
public Urun createProduct(@RequestBody Urun yeniUrun) {
// yeniUrun nesnesi, gelen JSON verisinden otomatik olarak oluşturulur.
System.out.println("Yeni ürün oluşturuldu: " + yeniUrun.getAd());
// Ürünü kaydetme mantığı burada olurdu.
yeniUrun.setId(999L); // Örnek ID ataması
return yeniUrun;
}
// Urun sınıfı (POJO)
class Urun {
private Long id;
private String ad;
private double fiyat;
// Constructor, Getter ve Setter metodları
public Urun() {}
public Urun(Long id, String ad, double fiyat) {
this.id = id;
this.ad = ad;
this.fiyat = fiyat;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getAd() { return ad; }
public void setAd(String ad) { this.ad = ad; }
public double getFiyat() { return fiyat; }
public void setFiyat(double fiyat) { this.fiyat = fiyat; }
}
@RequestHeader
Bu anotasyon, HTTP isteği başlıklarındaki (request headers) belirli değerleri yakalamak için kullanılır. Örneğin, kimlik doğrulama tokenlarını veya dil tercihlerini okumak için faydalıdır.
Örnek:
@GetMapping("/basliklar")
public String getHeaders(@RequestHeader("User-Agent") String userAgent,
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
String response = "User-Agent: " + userAgent;
if (authorizationHeader != null) {
response += ", Authorization: " + authorizationHeader;
}
return response;
}
Cevap Dönüşü: İstemciye Veri Gönderme
RESTful servisler sadece istekleri almakla kalmaz, aynı zamanda istemcilere anlamlı ve yapılandırılmış yanıtlar da gönderir. @RestController metodları, çeşitli yollarla yanıt üretebilir ve HTTP yanıtının tüm yönleri üzerinde kontrol sağlayabilir.
Basit Java Nesneleri (POJO) Döndürme
Daha önce de belirtildiği gibi, @RestController metodları bir Java nesnesi (POJO) döndürdüğünde, Spring bu nesneyi otomatik olarak (genellikle JSON formatına) serileştirir ve HTTP yanıt gövdesine yazar. Bu, en basit ve en yaygın kullanım senaryosudur.
Örnek:
@GetMapping("/detay/{id}")
public Urun getProductDetails(@PathVariable Long id) {
// Veritabanından ürünü getirme simülasyonu
return new Urun(id, "Örnek Ürün", 19.99);
}
Bu durumda, Spring varsayılan olarak 200 OK HTTP durum kodunu kullanır.
ResponseEntity ile Tam Kontrol
ResponseEntity sınıfı, geliştiricilere HTTP yanıtı üzerinde tam kontrol sağlar. Yanıtın gövdesini, HTTP durum kodunu ve HTTP başlıklarını belirlemenize olanak tanır. Bu, özellikle başarılı bir işlemde 201 Created veya kaynak bulunamadığında 404 Not Found gibi özel durum kodları döndürmeniz gerektiğinde çok kullanışlıdır.
Örnek:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@PostMapping("/olustur")
public ResponseEntity createNewProduct(@RequestBody Urun yeniUrun) {
// Ürünü kaydetme mantığı
yeniUrun.setId(100L); // Örnek ID
return new ResponseEntity<>(yeniUrun, HttpStatus.CREATED); // 201 Created döndürür
}
@GetMapping("/ara/{id}")
public ResponseEntity findProduct(@PathVariable Long id) {
// Ürünü bulma mantığı
if (id == 1L) { // Örnek kontrol
Urun bulunanUrun = new Urun(1L, "Kitap", 25.50);
return ResponseEntity.ok(bulunanUrun); // 200 OK döndürür
} else {
return ResponseEntity.notFound().build(); // 404 Not Found döndürür
}
}
@DeleteMapping("/sil/{id}")
public ResponseEntity deleteProduct(@PathVariable Long id) {
// Ürünü silme mantığı
boolean deleted = true; // Simülasyon
if (deleted) {
return ResponseEntity.noContent().build(); // 204 No Content döndürür
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); // 400 Bad Request döndürür
}
}
ResponseEntity kullanmak, API’nizin daha anlamlı ve standartlara uygun HTTP yanıtları vermesini sağlar.
HTTP Durum Kodlarının Önemi
HTTP durum kodları, istemciye bir isteğin sonucunu bildiren standartlaştırılmış sinyallerdir. Doğru durum kodlarını kullanmak, API’nizin anlaşılabilirliğini ve kullanılabilirliğini artırır.
* 2xx (Başarılı): İstek başarıyla alındı, anlaşıldı ve kabul edildi.
* 200 OK: Standart başarılı yanıt.
* 201 Created: Yeni bir kaynak başarıyla oluşturuldu. POST metodunda sıkça kullanılır.
* 204 No Content: İstek başarıyla işlendi, ancak yanıt gövdesinde döndürülecek içerik yok (örn. DELETE isteği).
* 4xx (İstemci Hatası): İstek geçersiz sözdizimi nedeniyle veya başka bir nedenden dolayı yerine getirilemedi.
* 400 Bad Request: İstemci tarafından gönderilen istek geçersiz veya hatalı.
* 401 Unauthorized: Kimlik doğrulama gereklidir ve sağlanmamıştır veya geçersizdir.
* 403 Forbidden: İstemcinin kaynağa erişim izni yok.
* 404 Not Found: İstenen kaynak sunucuda bulunamadı.
* 405 Method Not Allowed: İstek yapılan HTTP metodu, kaynağa uygulanamaz.
* 409 Conflict: Kaynakta bir çakışma nedeniyle istek tamamlanamadı (örn. aynı ID ile tekrar oluşturma).
* 5xx (Sunucu Hatası): Sunucu, geçerli bir isteği yerine getirirken beklenmeyen bir durumla karşılaştı.
* 500 Internal Server Error: Sunucuda beklenmeyen bir hata oluştu.
@ResponseStatus
Bazen bir metodun her zaman belirli bir HTTP durum kodu döndürmesi gerektiğini belirtmek isteyebilirsiniz. Bu durumda @ResponseStatus anotasyonu kullanılabilir.
Örnek:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@PostMapping("/kaydet")
@ResponseStatus(HttpStatus.CREATED) // Her zaman 201 Created döndürür
public Urun saveProduct(@RequestBody Urun urun) {
// Ürünü kaydetme mantığı
urun.setId(200L);
return urun;
}
Ancak, @ResponseStatus sadece başarılı durumlar veya belirli hata durumları için sabit bir kod atamak istediğinizde kullanışlıdır. Dinamik durum kodları için ResponseEntity daha esnektir.
Veri Doğrulama (Validation)
Gelen istemci verilerinin geçerliliğini kontrol etmek, sağlam bir API geliştirmenin kritik bir parçasıdır. Spring Boot, Bean Validation API’si (JSR 380) ve Hibernate Validator gibi referans uygulamalarla entegrasyon sayesinde kolay ve güçlü veri doğrulama imkanları sunar.
@Valid Anotasyonu
Bir @RequestBody veya @ModelAttribute parametresiyle gelen veriyi doğrulamak için, parametrenin önüne @Valid anotasyonunu eklemeniz yeterlidir. Spring, bu nesne üzerindeki kısıtlamaları (constraints) otomatik olarak kontrol eder.
Örnek:
import jakarta.validation.Valid;
import jakarta.validation.constraints.*; // Jakarta EE 9+ için
// import javax.validation.Valid;
// import javax.validation.constraints.*; // Java EE 8 için
// Urun sınıfı güncelleniyor
class Urun {
private Long id;
@NotBlank(message = "Ürün adı boş olamaz.")
@Size(min = 3, max = 50, message = "Ürün adı 3 ile 50 karakter arasında olmalıdır.")
private String ad;
@Min(value = 0, message = "Fiyat negatif olamaz.")
@NotNull(message = "Fiyat boş olamaz.")
private Double fiyat;
// ... Constructor, Getter, Setter
}
@RestController
@RequestMapping("/api/urunler")
public class UrunValidationController {
@PostMapping("/valid")
public ResponseEntity createValidProduct(@Valid @RequestBody Urun yeniUrun) {
// Eğer doğrulama başarısız olursa, bu metoda gelinmez.
// Hata yakalama mekanizması devreye girer.
yeniUrun.setId(300L);
return new ResponseEntity<>(yeniUrun, HttpStatus.CREATED);
}
}
Bu örnekte, Urun sınıfındaki @NotBlank, @Size, @Min ve @NotNull gibi anotasyonlar, gelen yeniUrun nesnesinin belirli kurallara uymasını sağlar. Eğer bu kurallar ihlal edilirse, Spring otomatik olarak bir MethodArgumentNotValidException fırlatır.
BindingResult ve Hata Mesajları
Doğrulama hatalarını daha detaylı yönetmek isterseniz, @Valid anotasyonlu parametrenin hemen arkasına bir BindingResult parametresi ekleyebilirsiniz. Bu, hataları manuel olarak kontrol etmenizi ve özel hata yanıtları oluşturmanızı sağlar.
Örnek:
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
@PostMapping("/valid-manual")
public ResponseEntity> createProductManualValidation(@Valid @RequestBody Urun yeniUrun, BindingResult result) {
if (result.hasErrors()) {
Map errors = new HashMap<>();
for (FieldError error : result.getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); // 400 Bad Request
}
yeniUrun.setId(301L);
return new ResponseEntity<>(yeniUrun, HttpStatus.CREATED);
}
Genellikle, BindingResult‘ı her metodda manuel olarak kontrol etmek yerine, global hata yönetimi mekanizmaları (bir sonraki bölümde açıklanacak) kullanılarak bu hatalar merkezi olarak ele alınır.
Hata Yönetimi ve İstisna İşleme
API’lerin güvenilirliği için hata yönetimi hayati öneme sahiptir. İstisnaların doğru bir şekilde yakalanması ve istemciye anlamlı hata yanıtları döndürülmesi, API’nin kullanılabilirliğini artırır. Spring, hata yönetimini kolaylaştırmak için çeşitli mekanizmalar sunar.
@ExceptionHandler
@ExceptionHandler anotasyonu, bir kontrolcü sınıfı içinde belirli istisna tiplerini yakalamak için kullanılır. Bir metodun üzerine @ExceptionHandler ekleyerek, o metodun sadece belirli bir kontrolcü içindeki belirli istisnaları ele almasını sağlayabilirsiniz.
Örnek:
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
class KaynakBulunamadiException extends RuntimeException {
public KaynakBulunamadiException(String message) {
super(message);
}
}
@RestController
@RequestMapping("/api/ornek")
public class HataOrnekController {
@GetMapping("/kaynak/{id}")
public String getResource(@PathVariable Long id) {
if (id == 0) {
throw new KaynakBulunamadiException("ID'si 0 olan kaynak bulunamadı.");
}
return "Kaynak bulundu: " + id;
}
@ExceptionHandler(KaynakBulunamadiException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 404 Not Found döndürür
public String handleResourceNotFound(KaynakBulunamadiException ex) {
return ex.getMessage();
}
}
Bu yaklaşım, kontrolcüye özgü istisnalar için kullanışlıdır. Ancak, birçok kontrolcüde aynı istisna tipini yakalamanız gerekiyorsa, kodu tekrarlamanıza neden olabilir.
@ControllerAdvice / @RestControllerAdvice
Tüm uygulama genelindeki istisnaları merkezi bir yerden ele almak için @ControllerAdvice veya @RestControllerAdvice anotasyonları kullanılır. Bu, istisna yönetimini DRY (Don’t Repeat Yourself) prensibine uygun hale getirir. @RestControllerAdvice, @ControllerAdvice‘ın @ResponseBody içeren bir versiyonudur, yani hata yanıtları otomatik olarak JSON/XML’e dönüştürülür.
Örnek:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(KaynakBulunamadiException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map handleKaynakBulunamadi(KaynakBulunamadiException ex) {
Map errorResponse = new HashMap<>();
errorResponse.put("error", "Kaynak Bulunamadı");
errorResponse.put("message", ex.getMessage());
return errorResponse;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map handleValidationExceptions(MethodArgumentNotValidException ex) {
Map errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((org.springframework.validation.FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map handleGeneralExceptions(Exception ex) {
Map errorResponse = new HashMap<>();
errorResponse.put("error", "Sunucu Hatası");
errorResponse.put("message", "Beklenmeyen bir hata oluştu: " + ex.getMessage());
return errorResponse;
}
}
Bu global hata işleyici, KaynakBulunamadiException‘ları 404 olarak, @Valid doğrulaması başarısız olduğunda fırlatılan MethodArgumentNotValidException‘ları 400 olarak ve diğer tüm istisnaları 500 olarak ele alır. Bu sayede, kontrolcü metodları sadece iş mantığına odaklanabilir.
ProblemDetail (Spring 6+)
Spring Framework 6 ile birlikte, RFC 7807 standardına uygun hata yanıtları oluşturmak için ProblemDetail sınıfı tanıtılmıştır. Bu, hata yanıtlarını daha standart ve istemciler tarafından daha kolay işlenebilir hale getirir.
Örnek (Spring 6+):
// Sadece örnek amaçlı, ProblemDetail kullanımı Spring 6+ gerektirir.
// import org.springframework.http.ProblemDetail;
// import java.net.URI;
//
// @RestControllerAdvice
// public class GlobalExceptionHandlerProblemDetail {
//
// @ExceptionHandler(KaynakBulunamadiException.class)
// public ProblemDetail handleKaynakBulunamadi(KaynakBulunamadiException ex) {
// ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
// pd.setTitle("Kaynak Bulunamadı");
// pd.setType(URI.create("https://example.com/errors/resource-not-found"));
// return pd;
// }
// }
ProblemDetail kullanımı, API’nizin hata yanıtlarını daha profesyonel ve standartlara uygun hale getirir.
RESTful Servis Tasarımında En İyi Uygulamalar
@RestController ile güçlü API’ler geliştirmek, sadece anotasyonları doğru kullanmakla kalmaz, aynı zamanda RESTful tasarım prensiplerine uymayı da gerektirir.
1. Kaynak Odaklı URI’lar: URI’lar, işlenen kaynakları temsil etmeli ve isim (çoğul) içermelidir, fiil değil.
* Kötü: /getAllProducts, /createProduct
* İyi: /products
2. HTTP Metodlarını Doğru Kullanma: Her HTTP metodu (GET, POST, PUT, DELETE, PATCH) belirli bir anlamsal amaca hizmet eder. Bu amaçlara uygun kullanılmalıdır.
* GET: Kaynakları almak için (idempotent ve safe).
* POST: Yeni kaynak oluşturmak için (non-idempotent).
* PUT: Mevcut bir kaynağı tamamen güncellemek veya yoksa oluşturmak için (idempotent).
* PATCH: Mevcut bir kaynağın kısmi güncellemesi için (non-idempotent).
* DELETE: Kaynak silmek için (idempotent).
3. Durumsuzluk (Statelessness): Her istek, sunucunun isteği işlemek için ihtiyaç duyduğu tüm bilgiyi içermelidir. Sunucu, istemciden gelen önceki isteklerle ilgili herhangi bir “durum” bilgisini saklamamalıdır. Bu, API’nin ölçeklenebilirliğini ve güvenilirliğini artırır.
4. HTTP Durum Kodlarını Anlamlı Kullanma: İstemciye işlemin sonucunu bildirmek için doğru HTTP durum kodları kullanılmalıdır (2xx, 4xx, 5xx).
5. Versiyonlama: API’niz zamanla değişeceği için versiyonlama önemlidir. URI versiyonlama (/v1/products), header versiyonlama veya sorgu parametresi versiyonlama (/products?version=1) gibi yöntemler kullanılabilir.
6. HATEOAS (Hypermedia as the Engine of Application State): Kaynak yanıtlarına, ilgili kaynaklara veya işlemlere yönelik bağlantılar ekleyerek istemcinin API’yi dinamik olarak keşfetmesine olanak tanır. Spring HATEOAS kütüphanesi bu konuda yardımcı olur. (Daha ileri seviye bir konudur.)
7. Güvenlik: API’ler, kimlik doğrulama (Authentication) ve yetkilendirme (Authorization) mekanizmalarıyla korunmalıdır (örn. OAuth2, JWT).
Örnek Uygulama: Basit Bir Ürün API’si
Şimdiye kadar öğrendiklerimizi bir araya getirerek, basit bir CRUD (Create, Read, Update, Delete) operasyonlarını destekleyen bir Ürün API’si oluşturalım.
// src/main/java/com/example/demo/model/Product.java
package com.example.demo.model;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class Product {
private Long id;
@NotBlank(message = "Product name cannot be empty")
@Size(min = 3, max = 50, message = "Product name must be between 3 and 50 characters")
private String name;
@Min(value = 0, message = "Price cannot be negative")
@NotNull(message = "Price cannot be null")
private Double price;
public Product() {}
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
// src/main/java/com/example/demo/service/ProductService.java
package com.example.demo.service;
import com.example.demo.model.Product;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ProductService {
private final List products = new ArrayList<>();
private final AtomicLong counter = new AtomicLong();
public ProductService() {
products.add(new Product(counter.incrementAndGet(), "Laptop", 1200.0));
products.add(new Product(counter.incrementAndGet(), "Mouse", 25.0));
products.add(new Product(counter.incrementAndGet(), "Keyboard", 75.0));
}
public List findAll() {
return new ArrayList<>(products);
}
public Optional findById(Long id) {
return products.stream().filter(p -> p.getId().equals(id)).findFirst();
}
public Product save(Product product) {
product.setId(counter.incrementAndGet());
products.add(product);
return product;
}
public Optional update(Long id, Product updatedProduct) {
return findById(id).map(existingProduct -> {
existingProduct.setName(updatedProduct.getName());
existingProduct.setPrice(updatedProduct.getPrice());
return existingProduct;
});
}
public boolean deleteById(Long id) {
return products.removeIf(p -> p.getId().equals(id));
}
}
// src/main/java/com/example/demo/controller/ProductController.java
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// GET /api/v1/products - Tüm ürünleri getir
@GetMapping
public ResponseEntity> getAllProducts() {
return ResponseEntity.ok(productService.findAll());
}
// GET /api/v1/products/{id} - Belirli bir ürünü getir
@GetMapping("/{id}")
public ResponseEntity getProductById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/v1/products - Yeni ürün oluştur
@PostMapping
public ResponseEntity createProduct(@Valid @RequestBody Product product) {
Product savedProduct = productService.save(product);
return new ResponseEntity<>(savedProduct, HttpStatus.CREATED);
}
// PUT /api/v1/products/{id} - Mevcut ürünü güncelle
@PutMapping("/{id}")
public ResponseEntity updateProduct(@PathVariable Long id, @Valid @RequestBody Product product) {
return productService.update(id, product)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/v1/products/{id} - Ürünü sil
@DeleteMapping("/{id}")
public ResponseEntity deleteProduct(@PathVariable Long id) {
if (productService.deleteById(id)) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
}
}
Bu örnek, @RestController‘ın temel CRUD operasyonları için nasıl kullanılacağını, @Valid ile veri doğrulamasını ve ResponseEntity ile HTTP durum kodlarının nasıl yönetileceğini göstermektedir. ProductService sınıfı basit bir List kullanarak veritabanı işlemlerini simüle eder.
Sonuç ve Gelecek Trendler
@RestController anotasyonu, Spring Framework ve Spring Boot’un RESTful web servisleri geliştirme konusundaki merkezi ve en güçlü araçlarından biridir. Geliştiricilere, HTTP metodları, URI eşleştirmeleri, parametre bağlama, otomatik serileştirme/deserileştirme, veri doğrulama ve kapsamlı hata yönetimi gibi alanlarda zengin bir özellik seti sunar. Bu sayede, Spring geliştiricileri hızlı, verimli ve standartlara uygun API’ler oluşturabilirler.
Spring Boot’un @RestController ile sunduğu bu kolaylıklar, mikroservis mimarilerinin yaygınlaşmasında önemli bir rol oynamıştır. Geliştiricilerin, altyapı detaylarıyla uğraşmak yerine iş mantığına odaklanmasını sağlayarak üretkenliği artırır.
Gelecekte, REST’in tahtını sallayan GraphQL veya gRPC gibi alternatifler ortaya çıksa da, REST’in basitliği, yaygınlığı ve HTTP’nin temel prensiplerine olan bağlılığı sayesinde hala modern web servislerinin önemli bir parçası olmaya devam edecektir. Spring ekosistemi de bu gelişmelere ayak uydurarak, @RestController‘ı Spring WebFlux ile reaktif programlama paradigmalarına uyarlamış ve ProblemDetail gibi yeni özelliklerle hata yönetimini daha da standartlaştırmıştır.
Sonuç olarak, Spring @RestController, Java dünyasında RESTful API geliştirmek isteyen her geliştiricinin araç kutusunda bulunması gereken vazgeçilmez bir bileşendir. Onun sunduğu kolaylıklar ve entegrasyonlar sayesinde, karmaşık sistemler için bile sağlam ve ölçeklenebilir API’ler oluşturmak artık çok daha erişilebilir hale gelmiştir.
