← Kursa Dön
📄 Text · 15 min

Stereotype Annotations

Giriş

Spring'de bir sınıfın IoC container tarafından yönetilen bir bean olabilmesi için işaretlenmesi gerekir. Stereotype annotation'ları bu işaretleme işini yapar ve aynı zamanda sınıfın katmanlardaki rolünü belirtir. Tıpkı bir şirketteki çalışanların yaka kartlarında "Mühendislik", "Pazarlama", "İnsan Kaynakları" yazması gibi — herkes aynı şirkette çalışır ama rolü farklıdır.

Gerçek Dünya Analojisi

Bir hastaneyi düşünün. Doktor, hemşire, eczacı ve resepsiyonist — hepsi "hastane çalışanı"dır (@Component). Ama her birinin farklı bir uzmanlık alanı var:

  • Doktor → teşhis ve tedavi (@Service — iş mantığı)

  • Eczacı → ilaç depolama ve yönetimi (@Repository — veri erişim)

  • Resepsiyonist → hasta kabul ve yönlendirme (@Controller — dış dünya iletişimi)

Her biri "çalışan" olsa da, rollerine göre farklı yetkiler, sorumluluklar ve kurallar uygulanır. Stereotype annotation'lar da tam olarak budur.

Neden Farklı Annotation'lar?

Teknik olarak @Component ile tüm sınıfları işaretleyebilirsiniz — hepsi bean olur. Ama farklı annotation'lar kullanmak:

  1. Okunabilirlik: Kodun amacını anlatır (bu sınıf ne yapıyor?)

  2. Semantik anlam: Ekip arkadaşlarınıza ve IDE'ye ipucu verir

  3. Framework davranışı: @Repository exception translation, @Controller request mapping sağlar

  4. AOP hedefleme: Belirli katmanlara aspekt uygulayabilirsiniz


@Component — Genel Amaçlı Bean

@Component, bir sınıfı Spring bean'i olarak işaretleyen en genel annotation'dır. Diğer tüm stereotype annotation'lar @Component'in özelleştirilmiş versiyonlarıdır:

@Component
public class EmailValidator {

    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");

    public boolean isValid(String email) {
        if (email == null || email.isBlank()) return false;
        return EMAIL_PATTERN.matcher(email).matches();
    }
}

@Component
public class SlugGenerator {
    public String generate(String title) {
        return title.toLowerCase()
                .replaceAll("[^a-z0-9\\s-]", "")
                .replaceAll("\\s+", "-")
                .replaceAll("-+", "-");
    }
}

Ne zaman kullanılır? Herhangi bir katmana (service, repository, controller) net olarak ait olmayan yardımcı sınıflar: validator, converter, formatter, utility bean'ler.

@Component'in Anatomisi

// @Service'in kaynak kodu — @Component'i içerir!
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component  // ← @Service aslında bir @Component
public @interface Service {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

Bu, @Service, @Repository ve @Controller'ın hepsinin aslında `@Component` olduğu anlamına gelir. Fark sadece ek yetenekler ve semantik anlamdadır.


@Service — İş Mantığı Katmanı

@Service, business logic (iş mantığı) içeren sınıfları işaretler. Validasyon, hesaplama, koordinasyon, iş kuralları burada yaşar:

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentGateway gateway;
    private final OrderRepository orderRepo;
    private final TransactionRepository txRepo;

    @Transactional
    public PaymentResult processPayment(PaymentRequest request) {
        // 1. İş kuralı: Validasyon
        validateAmount(request.getAmount());
        validateCurrency(request.getCurrency());

        // 2. Dış servis çağrısı (koordinasyon)
        var result = gateway.charge(request);

        // 3. İş kuralı: Sonuca göre aksiyon
        if (result.isSuccess()) {
            orderRepo.updatePaymentStatus(request.getOrderId(), PaymentStatus.PAID);
            txRepo.save(new Transaction(request, result));
        } else {
            orderRepo.updatePaymentStatus(request.getOrderId(), PaymentStatus.FAILED);
        }

        return result;
    }

    private void validateAmount(BigDecimal amount) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidAmountException("Tutar pozitif olmalıdır");
        }
        if (amount.compareTo(new BigDecimal("50000")) > 0) {
            throw new AmountExceededException("Tek seferde max 50.000 TL");
        }
    }

    private void validateCurrency(String currency) {
        if (!Set.of("TRY", "USD", "EUR").contains(currency)) {
            throw new UnsupportedCurrencyException(currency);
        }
    }
}

Ne zaman kullanılır? İş kuralları, validasyon, hesaplama, birden fazla repository/servis koordinasyonu, transaction yönetimi.

⚠️ Dikkat: @Service annotation'ı tek başına ekstra bir teknik yetenek sağlamaz (exception translation gibi). Ama @Transactional gibi annotation'larla birlikte kullanıldığında, AOP proxy'si sayesinde transaction yönetimi devreye girer.


@Repository — Veri Erişim Katmanı

@Repository, veritabanı veya herhangi bir veri deposuyla iletişim kuran sınıfları işaretler. Özel yeteneği: Exception Translation.

@Repository
@RequiredArgsConstructor
public class JdbcUserRepository implements UserRepository {
    private final JdbcTemplate jdbcTemplate;

    @Override
    public Optional<User> findById(Long id) {
        try {
            User user = jdbcTemplate.queryForObject(
                "SELECT * FROM users WHERE id = ?",
                new UserRowMapper(), id
            );
            return Optional.ofNullable(user);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    @Override
    public User save(User user) {
        if (user.getId() == null) {
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(con -> {
                var ps = con.prepareStatement(
                    "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
                    Statement.RETURN_GENERATED_KEYS
                );
                ps.setString(1, user.getName());
                ps.setString(2, user.getEmail());
                ps.setInt(3, user.getAge());
                return ps;
            }, keyHolder);
            user.setId(keyHolder.getKey().longValue());
        }
        return user;
    }
}

Exception Translation — @Repository'nin Süper Gücü

@Repository ile işaretlenen sınıflardaki veritabanı exception'ları, Spring'in genel DataAccessException hiyerarşisine otomatik çevrilir:

// OLMADAN:
// MySQL → java.sql.SQLIntegrityConstraintViolationException
// PostgreSQL → org.postgresql.util.PSQLException
// Oracle → java.sql.SQLException (ORA-00001)
// Her veritabanı farklı exception fırlatır!

// @Repository İLE:
// Hepsi → org.springframework.dao.DataIntegrityViolationException
// Veritabanı bağımsız exception handling!

try {
    userRepository.save(user);
} catch (DataIntegrityViolationException e) {
    // MySQL, PostgreSQL, Oracle — hepsi aynı exception!
    throw new DuplicateEmailException("Bu email zaten kayıtlı: " + user.getEmail());
} catch (DataAccessException e) {
    // Genel veritabanı hatası
    throw new DatabaseException("Veritabanı hatası", e);
}

💡 İpucu: Spring Data JPA kullandığınızda JpaRepository interface'lerini @Repository ile işaretlemenize gerek yoktur — Spring Data bunu otomatik yapar. Ama kendi JDBC veya JdbcTemplate tabanlı repository'lerinizde @Repository kullanın.


@Controller ve @RestController — Web Katmanı

@Controller — HTML Template Döndürür

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("message", "Hoş Geldiniz!");
        model.addAttribute("currentTime", LocalDateTime.now());
        return "home"; // → templates/home.html (Thymeleaf) render edilir
    }

    @GetMapping("/about")
    public String about() {
        return "about"; // → templates/about.html
    }
}

@RestController — JSON/XML Döndürür

@RestController // = @Controller + @ResponseBody
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserApiController {
    private final UserService userService;

    @GetMapping
    public List<UserDto> getAll() {
        return userService.findAll().stream()
                .map(this::toDto)
                .toList();
        // Otomatik olarak JSON'a serialize edilir
    }

    @GetMapping("/{id}")
    public UserDto getById(@PathVariable Long id) {
        User user = userService.findById(id);
        return toDto(user); // JSON olarak döner
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@RequestBody @Valid CreateUserRequest request) {
        User user = userService.createUser(request);
        return toDto(user);
    }

    private UserDto toDto(User user) {
        return new UserDto(user.getId(), user.getName(), user.getEmail());
    }
}

@Controller vs @RestController:

Özellik@Controller@RestController
Dönüş değeriView adı (template)Nesne (JSON/XML)
@ResponseBodyGerekli (her metotta)Otomatik (sınıf seviyesinde)
KullanımMVC web uygulamasıREST API

Component Scanning — Bean'ler Nasıl Bulunur?

Spring Boot, @SpringBootApplication sınıfının bulunduğu paketi ve tüm alt paketleri otomatik tarar:

com.example.demo/          ← @SpringBootApplication burada
├── controller/
│   └── UserController.java    ← ✅ Taranır, bean olur
├── service/
│   └── UserService.java       ← ✅ Taranır, bean olur
├── repository/
│   └── UserRepository.java    ← ✅ Taranır, bean olur
└── util/
    └── EmailValidator.java    ← ✅ Taranır, bean olur

com.example.other/             ← ❌ TARANMAZ!
└── SomeService.java           ← Bu bean OLMAZ

Farklı paketleri taramak için:

@SpringBootApplication
@ComponentScan(basePackages = {
    "com.example.demo",     // Ana paket
    "com.example.other",    // Ek paket
    "com.example.shared"    // Paylaşılan paket
})
public class DemoApplication { }

⚠️ Dikkat: @SpringBootApplication sınıfını root package'a koyun. Böylece @ComponentScan tüm alt paketleri otomatik tarar, ekstra konfigürasyon gerekmez.


Hangisini Kullanmalıyım? — Karar Ağacı

Sınıfın rolü ne?
│
├─ İş mantığı? (validasyon, hesaplama, koordinasyon)
│   └─ @Service
│
├─ Veri erişimi? (veritabanı, dosya, API)
│   └─ @Repository
│
├─ HTTP endpoint? (REST API, web sayfası)
│   ├─ JSON döndürüyor → @RestController
│   └─ HTML döndürüyor → @Controller
│
├─ Konfigürasyon? (bean tanımları)
│   └─ @Configuration + @Bean
│
└─ Hiçbiri? (utility, helper, validator)
    └─ @Component

Yaygın Hatalar ve Çözümleri

Hata 1: Yanlış Annotation Kullanmak

// ❌ YANLIŞ — Repository'yi @Service ile işaretleme
@Service // Semantik olarak yanlış
public class UserRepositoryImpl {
    public User findById(Long id) { ... }
}
// Exception translation çalışmaz!

// ✅ DOĞRU
@Repository
public class UserRepositoryImpl {
    public User findById(Long id) { ... }
}

Hata 2: Bir Sınıfa Birden Fazla Stereotype

// ❌ YANLIŞ — İki stereotype
@Service
@Repository
public class UserService { }
// Karmaşık, anlaşılmaz, beklenmeyen davranış

// ✅ DOĞRU — Tek stereotype
@Service
public class UserService { }

Hata 3: @Component Scan Dışında Kalan Bean'ler

// Ana uygulama: com.example.demo
// Bean sınıfı: com.other.package.MyService
// Sonuç: MyService bean OLMAZ!

// Çözüm 1: @ComponentScan genişletin
// Çözüm 2: Sınıfı doğru pakete taşıyın
// Çözüm 3: @Import ile dahil edin

Hata 4: Abstract Sınıfı Bean Yapmak

// ❌ YANLIŞ — abstract sınıf bean olamaz
@Component
public abstract class BaseService { }
// Spring bu sınıfı instantiate edemez!

// ✅ DOĞRU — Concrete sınıfları bean yapın
@Service
public class UserService extends BaseService { }

Özet

  • `@Component` → genel amaçlı bean (utility, helper, validator)

  • `@Service` → iş mantığı katmanı (validasyon, hesaplama, koordinasyon)

  • `@Repository` → veri erişim katmanı + exception translation (veritabanı bağımsız hata yönetimi)

  • `@Controller` → web katmanı, HTML template döndürür

  • `@RestController` → REST API, JSON/XML döndürür (@Controller + @ResponseBody)

  • Hepsi @Component'in özelleştirilmiş halidir — doğru annotation'ı seçmek okunabilirlik ve framework davranışı için kritiktir

  • @SpringBootApplication sınıfını root package'a koyun — alt paketler otomatik taranır


Custom Stereotype Annotation Oluşturma

Kendi stereotype annotation'ınızı oluşturabilirsiniz:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service // @Component'i dolaylı olarak içerir
@Transactional // İş mantığı otomatik transactional olsun
public @interface UseCase {
    @AliasFor(annotation = Service.class)
    String value() default "";
}

// Kullanım
@UseCase
public class CreateOrderUseCase {
    private final OrderRepository orderRepo;
    private final PaymentService paymentService;

    public CreateOrderUseCase(OrderRepository orderRepo, PaymentService paymentService) {
        this.orderRepo = orderRepo;
        this.paymentService = paymentService;
    }

    // @Transactional otomatik — @UseCase annotation'ı sayesinde
    public Order execute(OrderRequest request) {
        paymentService.charge(request.getAmount());
        return orderRepo.save(new Order(request));
    }
}

Bu pattern, Clean Architecture veya Hexagonal Architecture uygulayan projelerde yaygındır. Her use case kendi sınıfında yaşar ve @UseCase annotation'ı ile işaretlenir.


Bütünleşik Gerçek Dünya Örneği: Katmanlı Mimari

Tüm stereotype annotation'ları bir arada gösteren e-ticaret uygulaması:

// === ENTITY ===
@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;
    private int stock;

    protected Product() {}
    public Product(String name, BigDecimal price, int stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }
    // Getter/Setter...
}

// === REPOSITORY (Veri Erişim) ===
// Spring Data JPA — interface yeterli, @Repository otomatik
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByPriceLessThan(BigDecimal maxPrice);
    boolean existsByName(String name);
}

// === SERVICE (İş Mantığı) ===
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProductService {
    private final ProductRepository productRepository;
    private final PriceCalculator priceCalculator; // @Component

    @Transactional
    public Product createProduct(CreateProductRequest request) {
        if (productRepository.existsByName(request.name())) {
            throw new DuplicateProductException(request.name());
        }

        BigDecimal finalPrice = priceCalculator.calculateWithTax(request.price());
        Product product = new Product(request.name(), finalPrice, request.stock());
        return productRepository.save(product);
    }

    public ProductDto getProduct(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
        return new ProductDto(product.getId(), product.getName(), product.getPrice());
    }

    public List<ProductDto> getAffordableProducts(BigDecimal maxPrice) {
        return productRepository.findByPriceLessThan(maxPrice).stream()
            .map(p -> new ProductDto(p.getId(), p.getName(), p.getPrice()))
            .toList();
    }
}

// === COMPONENT (Utility) ===
@Component
public class PriceCalculator {
    private static final BigDecimal TAX_RATE = new BigDecimal("0.20"); // %20 KDV

    public BigDecimal calculateWithTax(BigDecimal basePrice) {
        return basePrice.add(basePrice.multiply(TAX_RATE))
            .setScale(2, RoundingMode.HALF_UP);
    }
}

// === REST CONTROLLER (Web Katmanı) ===
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ProductDto create(@RequestBody @Valid CreateProductRequest request) {
        Product product = productService.createProduct(request);
        return new ProductDto(product.getId(), product.getName(), product.getPrice());
    }

    @GetMapping("/{id}")
    public ProductDto getById(@PathVariable Long id) {
        return productService.getProduct(id);
    }

    @GetMapping("/affordable")
    public List<ProductDto> getAffordable(
            @RequestParam(defaultValue = "100") BigDecimal maxPrice) {
        return productService.getAffordableProducts(maxPrice);
    }
}

// === DTO ===
public record CreateProductRequest(
    @NotBlank String name,
    @NotNull @Positive BigDecimal price,
    @Min(0) int stock
) {}

public record ProductDto(Long id, String name, BigDecimal price) {}

Bu örnekte her katman kendi sorumluluğuna odaklanır:

  • `@RestController` → HTTP request/response yönetimi

  • `@Service` → iş kuralları ve validasyon

  • `@Component` → tekrar kullanılabilir utility mantığı

  • Repository interface → veri erişim (Spring Data JPA otomatik implement eder)

Her katman bir alttaki katmana bağımlıdır, ama üst katmana asla bağımlı değildir. Controller → Service → Repository.