- Bir Spring Boot projesi oluşturmak ve çalıştırmak
@RestController,@Service,@Repositorykatmanlarını ve bağımlılık enjeksiyonunu açıklamak- Doğru HTTP durum kodlarıyla bir CRUD API yazmak ve onu curl ile test etmek
Bir mobil uygulama, örneğin elektronik bir okul karnesi, notları bir sunucudan alır: uygulama bir HTTP isteği gönderir (GET /api/students), sunucu da JSON döndürür. Böyle bir sunucuya REST API denir. Java dünyasında bunu yazmanın en popüler yolu Spring Boot'tur: birkaç ek açıklamayla sıradan sınıflar bir web sunucusuna dönüşür; JSON, veritabanları ve yapılandırma otomatik olarak bağlanır.
Projeyi oluşturmak
- 1start.spring.io sitesini aç
Bu, resmî proje oluşturucudur (IntelliJ IDEA Ultimate'te de aynı sihirbaz vardır). Project için Maven ya da Gradle'ı, Language için Java'yı, Java için 21'i seç.
- 2Üst verileri doldur
Group
com.educora, Artifactschool, NameSchoolApi. Paketcom.educora.school, ana sınıf iseSchoolApiApplicationolur. - 3Bağımlılıkları ekle
ADD DEPENDENCIES düğmesine bas ve Spring Web'i seç. Gerçek projelerde genellikle Spring Data JPA, bir veritabanı sürücüsü ve Validation da eklenir.
- 4Oluştur ve aç
GENERATE bir ZIP arşivi indirir. Onu aç, klasörü IDE'de aç ve ana sınıfı çalıştır.
package com.educora.school;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SchoolApiApplication {
public static void main(String[] args) {
SpringApplication.run(SchoolApiApplication.class, args);
}
}@SpringBootApplication ve tek satırlık main@SpringBootApplication üç şeyi birleştirir: sınıfı bir yapılandırma kaynağı yapar, paketteki bileşen aramasını (component scan) ve otomatik yapılandırmayı açar. Sonuncusu classpath'e bakar: Spring Web oradaysa gömülü Tomcat sunucusu 8080 portunda başlar ve JSON için Jackson bağlanır. Ayrı bir sunucu kurmana gerek yok; uygulama, sıradan bir main metodu olan bir JAR'dır.
./mvnw spring-boot:run... Tomcat started on port 8080 (http) with context path '/' ... Started SchoolApiApplication in 1.9 seconds (process running for 2.2)
./gradlew bootRunKatmanlar ve bağımlılık enjeksiyonu
| Katman | Ek açıklama | Görevi |
|---|---|---|
| Denetleyici | @RestController | HTTP isteklerini alır, JSON döndürür |
| Servis | @Service | iş kuralları: doğrulama, hesaplamalar |
| Depo | @Repository | veriyi saklar ve okur |
Bu ek açıklamalarla işaretlenen sınıfları Spring kendisi oluşturur; bunlara bean denir ve IoC kapsayıcısında tutulur (genellikle her sınıftan tek bir örnek). Bir sınıfın yapıcısı başka bir bean'e ihtiyaç duyarsa Spring onu otomatik olarak verir: buna bağımlılık enjeksiyonu (dependency injection) denir. Sonuçta sınıflar birbirini new ile oluşturmaz ve testte bir bağımlılığı mock ile değiştirmek kolaylaşır; tıpkı JUnit dersindeki gibi.
@RestController
public class StudentController {
// the controller builds its own dependencies:
// hard to test, and every class gets its own copy
private final StudentService service =
new StudentService(new StudentRepository(), 100);
}@RestController
public class StudentController {
private final StudentService service;
// Spring creates StudentService once and passes it in
public StudentController(StudentService service) {
this.service = service;
}
}Öğrenciler için CRUD API
Şimdi öğrenciler için tam bir CRUD API kuralım: oluşturma (Create), okuma (Read), güncelleme (Update), silme (Delete). Basitlik için depo veriyi bellekte, bir ConcurrentHashMap içinde tutar: istekler paralel iş parçacıklarında geldiği için iş parçacığı güvenli bir koleksiyon seçildi.
public record Student(Long id, String name, int score) {}
@Repository
public class StudentRepository {
private final Map<Long, Student> students = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public List<Student> findAll() {
return students.values().stream().sorted(Comparator.comparing(Student::id)).toList();
}
public Optional<Student> findById(long id) {
return Optional.ofNullable(students.get(id));
}
public Student save(Student s) {
long id = s.id() != null ? s.id() : nextId.getAndIncrement();
Student saved = new Student(id, s.name(), s.score());
students.put(id, saved);
return saved;
}
public boolean deleteById(long id) {
return students.remove(id) != null;
}
}@Service
public class StudentService {
private final StudentRepository repository;
private final int maxScore;
public StudentService(StudentRepository repository,
@Value("${school.max-score:100}") int maxScore) {
this.repository = repository;
this.maxScore = maxScore;
}
public List<Student> all() { return repository.findAll(); }
public Student get(long id) {
return repository.findById(id).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "Student " + id + " not found"));
}
public Student save(Long id, Student s) {
if (s.score() < 0 || s.score() > maxScore)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Score must be 0-" + maxScore);
if (id != null) get(id); // 404 if we update a missing student
return repository.save(new Student(id, s.name(), s.score()));
}
public void delete(long id) {
if (!repository.deleteById(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}application.properties'ten gelen school.max-score@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService service;
public StudentController(StudentService service) {
this.service = service;
}
@GetMapping
public List<Student> all() { return service.all(); }
@GetMapping("/{id}")
public Student one(@PathVariable long id) { return service.get(id); }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Student create(@RequestBody Student student) { return service.save(null, student); }
@PutMapping("/{id}")
public Student update(@PathVariable long id, @RequestBody Student student) {
return service.save(id, student);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable long id) { service.delete(id); }
}| İstek | Ne yapar | Başarılı yanıt |
|---|---|---|
GET /api/students | tüm öğrenciler | 200 OK |
GET /api/students/{id} | tek öğrenci; yoksa 404 | 200 OK |
POST /api/students | öğrenci oluşturur | 201 Created |
PUT /api/students/{id} | öğrenciyi günceller | 200 OK |
DELETE /api/students/{id} | öğrenciyi siler | 204 No Content |
Tabloda REST'in temel kuralları görülüyor: URL bir kaynağı çoğul bir isimle adlandırır (/api/students), ne yapılacağını ise HTTP metodu söyler. Bu yüzden /api/getStudents ya da /api/deleteStudent?id=2 gibi yollar kötü üslup sayılır. GET asla veriyi değiştirmemeli, PUT kaynağı bütünüyle değiştirir, sonucu ise durum kodu bildirir; istemci yanıt metnini okumadan ne olduğunu anlar.
@RequestMapping, tüm denetleyici için ortak yolu belirler. @PathVariable URL'deki {id} değerini bir parametreye, @RequestBody ise isteğin JSON gövdesini bir Student nesnesine çevirir; yanıt nesnesini JSON'a Jackson kütüphanesi dönüştürür. ResponseStatusException, her yerden doğru HTTP durumunu (örneğin 404 ya da 400) döndürmeyi sağlar. @Value("${school.max-score:100}") ayarı yapılandırmadan okur; iki noktadan sonraki 100 varsayılan değerdir.
spring.application.name=school-api
server.port=8080
school.max-score=100src/main/resources/application.properties — uygulamanın ayarlarıAyarlar koda dokunmadan değiştirilebilir: java -jar school.jar --server.port=9090 ya da SERVER_PORT ortam değişkeniyle. Farklı ortamlar için profiller vardır: örneğin application-dev.properties dosyası --spring.profiles.active=dev ile etkinleşir.
curl ile test
Sunucu çalışırken ikinci bir terminal aç. curl, HTTP istekleri gönderen bir komut satırı aracıdır: -X metodu, -H bir başlığı, -d ise gövdeyi belirler. Önce iki öğrenci oluşturup listeyi okuyalım:
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Aysel","score":95}'
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Murad","score":78}'
curl localhost:8080/api/students
curl localhost:8080/api/students/1{"id":1,"name":"Aysel","score":95}
{"id":2,"name":"Murad","score":78}
[{"id":1,"name":"Aysel","score":95},{"id":2,"name":"Murad","score":78}]
{"id":1,"name":"Aysel","score":95}id değerini sunucu verirŞimdi güncelleme, silme ve hata durumları. -w '%{http_code}' yalnızca durum kodunu yazdırır, -s -o /dev/null ise gövdeyi ve ilerleme bilgisini gizler:
curl -X PUT localhost:8080/api/students/2 -H 'Content-Type: application/json' -d '{"name":"Murad","score":84}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/students \
-H 'Content-Type: application/json' -d '{"name":"Elvin","score":120}'{"id":2,"name":"Murad","score":84}
204
404
400@SpringBootTest
@AutoConfigureMockMvc
class StudentControllerTest {
@Autowired
MockMvc mvc;
@Test
void createsStudent() throws Exception {
mvc.perform(post("/api/students")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Aysel\",\"score\":95}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Aysel"));
}
}MockMvc, gerçek sunucuyu başlatmadan bir HTTP isteğini taklit eder (içe aktarmalar gösterilmedi)Önemli noktalar
- Spring Boot:
@SpringBootApplication+ otomatik yapılandırma + gömülü Tomcat; çalıştırmak için./mvnw spring-boot:run. - Katmanlar:
@RestController→@Service→@Repository; Spring bean'leri oluşturur ve yapıcılara verir. @GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PathVariable,@RequestBody; JSON dönüşümünü Jackson yapar.- Doğru durum kodları: 200, 201, 204, 400, 404;
@ResponseStatusveResponseStatusExceptionile. - Ayarlar
application.propertiesiçinde tutulur ve@Valueile okunur; API curl ya daMockMvcile test edilir.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.