Java ve Spring Boot, kurumsal ölçekli uygulamaların en yaygın teknoloji yığınlarından birini oluşturur. Güçlü tip sistemi, kapsamlı ekosistemi ve Spring'in otomatik konfigürasyonu ile hızlı ve güvenilir backend'ler geliştirilebilir.
Proje Kurulumu
Entity ve Repository
// JAVA //
// model/Makale.java
package com.blog.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "makaleler")
@Data
@NoArgsConstructor
public class Makale {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false, length = 300)
private String baslik;
@Column(nullable = false, unique = true, length = 350)
private String slug;
@Column(columnDefinition = "TEXT", nullable = false)
private String icerik;
@Column(nullable = false)
private boolean yayinlandi = false;
@Column(nullable = false)
private int gorunumler = 0;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant olusturuldu;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "yazar_id", nullable = false)
private Kullanici yazar;
@ManyToMany(cascade = CascadeType.MERGE)
@JoinTable(
name = "makale_etiketler",
joinColumns = @JoinColumn(name = "makale_id"),
inverseJoinColumns = @JoinColumn(name = "etiket_id")
)
private List<Etiket> etiketler = new ArrayList<>();
}
// repository/MakaleRepository.java
package com.blog.repository;
import com.blog.model.Makale;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
import java.util.UUID;
public interface MakaleRepository extends JpaRepository<Makale, UUID> {
Optional<Makale> findBySlugAndYayinlandiTrue(String slug);
Page<Makale> findByYayinlandiTrue(Pageable pageable);
@Query("""
SELECT m FROM Makale m
WHERE m.yayinlandi = true
AND (:kategori IS NULL OR :kategori MEMBER OF m.etiketler)
AND (LOWER(m.baslik) LIKE LOWER(CONCAT('%', :arama, '%'))
OR :arama IS NULL)
ORDER BY m.olusturuldu DESC
""")
Page<Makale> ara(String arama, String kategori, Pageable pageable);
}DTO ve MapStruct
// JAVA //
// dto/MakaleDto.java
package com.blog.dto;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
public record MakaleOnizleme(
UUID id,
String baslik,
String slug,
String excerpt,
Instant olusturuldu,
YazarOzet yazar,
List<String> etiketler
) {}
public record YazarOzet(UUID id, String ad, String avatar) {}
public record MakaleOlusturDto(
@NotBlank @Size(min = 5, max = 300) String baslik,
@NotBlank @Size(min = 100) String icerik,
boolean yayinla,
List<String> etiketler
) {}
// mapper/MakaleMapper.java
@Mapper(componentModel = "spring")
public interface MakaleMapper {
@Mapping(source = "yazar.ad", target = "yazar.ad")
@Mapping(source = "yazar.avatar", target = "yazar.avatar")
@Mapping(expression = "java(icerikExcerpt(m.getIcerik()))", target = "excerpt")
MakaleOnizleme toOnizleme(Makale m);
default String icerikExcerpt(String icerik) {
if (icerik == null || icerik.length() <= 160) return icerik;
return icerik.substring(0, 160) + "...";
}
}Service Katmanı
// JAVA //
// service/MakaleService.java
package com.blog.service;
import com.blog.dto.MakaleOnizleme;
import com.blog.dto.MakaleOlusturDto;
import com.blog.exception.NotFoundException;
import com.blog.mapper.MakaleMapper;
import com.blog.model.Makale;
import com.blog.repository.MakaleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MakaleService {
private final MakaleRepository makaleRepo;
private final MakaleMapper mapper;
private final SlugService slugService;
public Page<MakaleOnizleme> listele(int sayfa, int boyut, String arama, String kategori) {
var pageable = PageRequest.of(sayfa - 1, boyut, Sort.by("olusturuldu").descending());
return makaleRepo.ara(arama, kategori, pageable)
.map(mapper::toOnizleme);
}
public MakaleOnizleme slugIleGetir(String slug) {
return makaleRepo.findBySlugAndYayinlandiTrue(slug)
.map(mapper::toOnizleme)
.orElseThrow(() -> new NotFoundException("Makale bulunamadı: " + slug));
}
@Transactional
public MakaleOnizleme olustur(MakaleOlusturDto dto, UUID yazarId) {
var makale = new Makale();
makale.setBaslik(dto.baslik());
makale.setSlug(slugService.olustur(dto.baslik()));
makale.setIcerik(dto.icerik());
makale.setYayinlandi(dto.yayinla());
// yazar set et...
return mapper.toOnizleme(makaleRepo.save(makale));
}
}Controller
// JAVA //
// controller/MakaleController.java
@RestController
@RequestMapping("/api/makaleler")
@RequiredArgsConstructor
@Validated
public class MakaleController {
private final MakaleService service;
@GetMapping
public ResponseEntity<Page<MakaleOnizleme>> listele(
@RequestParam(defaultValue = "1") @Min(1) int sayfa,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int boyut,
@RequestParam(required = false) String arama,
@RequestParam(required = false) String kategori
) {
return ResponseEntity.ok(service.listele(sayfa, boyut, arama, kategori));
}
@GetMapping("/{slug}")
public ResponseEntity<MakaleOnizleme> getir(@PathVariable String slug) {
return ResponseEntity.ok(service.slugIleGetir(slug));
}
@PostMapping
@PreAuthorize("hasRole('EDITOR')")
public ResponseEntity<MakaleOnizleme> olustur(
@RequestBody @Valid MakaleOlusturDto dto,
@AuthenticationPrincipal KullaniciDetaylari detaylar
) {
var makale = service.olustur(dto, detaylar.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(makale);
}
}Global Exception Handler
// JAVA //
// exception/GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<HataCevap> notFound(NotFoundException ex) {
return ResponseEntity.status(404)
.body(new HataCevap(ex.getMessage(), "NOT_FOUND"));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationHataCevap> validasyon(MethodArgumentNotValidException ex) {
var hatalar = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Geçersiz değer"
));
return ResponseEntity.status(422)
.body(new ValidationHataCevap("Validasyon hatası", hatalar));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<HataCevap> genelHata(Exception ex, HttpServletRequest req) {
log.error("Beklenmedik hata: {} {}", req.getMethod(), req.getRequestURI(), ex);
return ResponseEntity.status(500)
.body(new HataCevap("Sunucu hatası oluştu.", "INTERNAL_ERROR"));
}
record HataCevap(String mesaj, String kod) {}
record ValidationHataCevap(String mesaj, Map<String, String> hatalar) {}
}application.yml
// YAML //
spring:
datasource:
url: ${DATABASE_URL}
username: ${DB_USER}
password: ${DB_PASS}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # Production'da validate, development'ta update
properties:
hibernate:
default_schema: public
format_sql: true
show-sql: false # Production'da false
security:
jwt:
secret: ${JWT_SECRET}
expire-min: 15
logging:
level:
com.blog: INFO
org.springframework.web: WARNSonuç
Spring Boot'un otomatik konfigürasyonu, JPA/Hibernate ile ORM ve Spring Security ile kimlik doğrulama; kurumsal Java geliştirmesinin temel sütunlarıdır. Lombok boilerplate'i, MapStruct dönüşüm kodunu ortadan kaldırır. Record DTO'lar ve @RestControllerAdvice ise temiz ve bakımı kolay API kodu yazmanızı sağlar. Bir sonraki derste Spring Security ile JWT kimlik doğrulama akışını detaylı inceleyeceğiz.