← Kursa Dön
📄 Text · 18 min

Bean Configuration — @Configuration & @Bean

Giriş

Önceki derslerde @Component, @Service, @Repository gibi stereotype annotation'larla kendi yazdığımız sınıfları bean olarak tanımladık. Peki ya bir 3rd-party kütüphaneden gelen sınıfı bean yapmak istiyorsak? Jackson'ın ObjectMapper'ını özelleştirmek, RestTemplate konfigüre etmek, ya da PasswordEncoder oluşturmak istiyorsak? Bu sınıfların kaynak koduna erişimimiz yok — annotation ekleyemeyiz. İşte @Configuration ve @Bean tam olarak bu ihtiyacı karşılar.

Gerçek Dünya Analojisi

Bir fabrikayı düşünün. Fabrikanın kendi ürettiği ürünlere etiket yapıştırmak kolaydır (@Component). Ama dışarıdan satın aldığınız hammaddelere de fabrikanızın etiketini yapıştırmanız ve onları da sisteminize entegre etmeniz gerekir. @Configuration sınıfı fabrika planınız, @Bean metotları ise bu hammaddeleri işleyip etiketleyip sisteme dahil eden üretim hatlarınızdır.

@Component vs @Bean — Ne Zaman Hangisi?

DurumTercihNeden
Kendi yazdığınız sınıf@Component / @Service / @RepositorySınıfa annotation ekleyebilirsiniz
3rd-party kütüphane sınıfı@BeanKaynak koda erişiminiz yok
Karmaşık initialization mantığı@BeanConstructor'dan fazlası gerekiyor
Koşullu bean oluşturma@BeanIf/else, profil, environment kontrolü
Basit servis, controller@Component / @ServiceEn temiz ve basit yol

@Configuration Sınıfı

@Configuration ile işaretlenen sınıf, Spring'e "bu sınıf bean tanımları içeriyor" mesajını verir. İçindeki @Bean metotları, döndürdükleri nesneleri Spring container'a bean olarak kaydeder.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        // Java 8 tarih/saat desteği
        mapper.registerModule(new JavaTimeModule());
        // Tarihleri timestamp yerine ISO-8601 formatında yaz
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // null alanları JSON'a dahil etme
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // Bilinmeyen alanlarla karşılaşınca hata fırlatma
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        return mapper;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(10))
                .build();
    }
}

Bu örnekte:

  1. objectMapper() metodu bir ObjectMapper nesnesi oluşturur, konfigüre eder ve döndürür

  2. Spring bu nesneyi container'a objectMapper adıyla bean olarak kaydeder

  3. Artık herhangi bir sınıf ObjectMapper'ı inject edebilir:

@Service
public class JsonService {
    private final ObjectMapper objectMapper; // Konfigüre edilmiş ObjectMapper gelir

    public JsonService(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
}

@Bean Metotları Detaylı

Bean İsimlendirme

Varsayılan bean adı metot adıdır. Özelleştirmek için:

// Varsayılan: metot adı = "passwordEncoder"
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

// Özel ad: "customEncoder"
@Bean("customEncoder")
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

// Birden fazla isim (alias)
@Bean({"mainDataSource", "primaryDS", "ds"})
public DataSource dataSource() {
    return DataSourceBuilder.create()
            .url("jdbc:mysql://localhost:3306/mydb")
            .build();
}

Bean'ler Arası Bağımlılık

Bir @Bean metodu başka bir bean'e ihtiyaç duyarsa, onu metot parametresi olarak alabilir:

@Configuration
public class ServiceConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    // passwordEncoder bean'i parametre olarak enjekte edilir
    @Bean
    public UserService userService(UserRepository userRepository,
                                    PasswordEncoder passwordEncoder) {
        return new UserService(userRepository, passwordEncoder);
    }
}

Alternatif olarak, aynı @Configuration sınıfındaki @Bean metotlarını doğrudan çağırabilirsiniz:

@Configuration
public class ServiceConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public UserService userService(UserRepository userRepository) {
        // passwordEncoder() çağrısı → aynı singleton bean döner (CGLIB proxy sayesinde)
        return new UserService(userRepository, passwordEncoder());
    }
}

3rd-Party Kütüphane Bean'leri — Gerçek Dünya Örnekleri

HTTP Client Konfigürasyonu

@Configuration
public class HttpClientConfig {

    @Bean
    public HttpClient httpClient() {
        return HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .version(HttpClient.Version.HTTP_2)
                .build();
    }

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("https://api.example.com")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .filter(ExchangeFilterFunctions.basicAuthentication("user", "pass"))
                .codecs(configurer -> configurer
                        .defaultCodecs()
                        .maxInMemorySize(10 * 1024 * 1024)) // 10 MB
                .build();
    }
}

Security Konfigürasyonu

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://myapp.com", "https://admin.myapp.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

AWS S3 Client

@Configuration
public class AwsConfig {

    @Bean
    public AmazonS3 amazonS3Client(
            @Value("${aws.access-key}") String accessKey,
            @Value("${aws.secret-key}") String secretKey,
            @Value("${aws.region}") String region) {

        var credentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}

Full Mode vs Lite Mode — Kritik Fark

Bu bölüm, Spring'in en az bilinen ama en önemli davranışlarından birini açıklar.

Full Mode — @Configuration (CGLIB Proxy)

@Configuration // CGLIB proxy oluşturulur
public class AppConfig {

    @Bean
    public CommonDependency commonDependency() {
        System.out.println("CommonDependency oluşturuluyor...");
        return new CommonDependency();
    }

    @Bean
    public ServiceA serviceA() {
        return new ServiceA(commonDependency()); // Aynı singleton instance!
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB(commonDependency()); // Aynı singleton instance!
    }
}

Çıktı:

CommonDependency oluşturuluyor...

commonDependency() metodu bir kez çağrılır. serviceA() ve serviceB() aynı instance'ı paylaşır. Bu, @Configuration sınıfının CGLIB proxy ile sarılması sayesinde olur. Proxy, metot çağrısını yakalayıp "bu bean zaten var mı?" kontrol eder.

Lite Mode — @Component (Proxy Yok!)

@Component // ⚠️ @Configuration değil, @Component!
public class AppConfig {

    @Bean
    public CommonDependency commonDependency() {
        System.out.println("CommonDependency oluşturuluyor...");
        return new CommonDependency();
    }

    @Bean
    public ServiceA serviceA() {
        return new ServiceA(commonDependency()); // YENİ instance!
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB(commonDependency()); // YENİ instance!
    }
}

Çıktı:

CommonDependency oluşturuluyor...
CommonDependency oluşturuluyor...
CommonDependency oluşturuluyor...

commonDependency() üç kez çağrılır! Her çağrı yeni bir nesne oluşturur. ServiceA ve ServiceB farklı CommonDependency instance'ları kullanır. Bu genellikle istenmeyen bir davranıştır.

⚠️ Dikkat: @Bean metotlarını her zaman @Configuration sınıfında tanımlayın! @Component içinde @Bean tanımlamak (Lite Mode) singleton garantisini bozar.

Spring Boot 3.x'te proxyBeanMethods

// Spring Boot 3.x'te proxy'yi kapatabilirsiniz
@Configuration(proxyBeanMethods = false) // Lite mode davranışı
public class LiteConfig {
    // Bean'ler arası metot çağrısı YAPMIYORSANIZ performans avantajı sağlar
    // Ama bean'ler arası çağrı yaparsanız singleton kırılır!
}

@Import — Konfigürasyonları Birleştirme

// Alt konfigürasyonları ana konfigürasyona dahil etmek
@Configuration
@Import({SecurityConfig.class, CacheConfig.class, AwsConfig.class})
public class AppConfig { }

// Spring Boot'ta genellikle gerek yoktur çünkü
// @ComponentScan tüm @Configuration sınıflarını otomatik bulur

@Conditional ile Koşullu Bean Oluşturma

@Configuration
public class ConditionalConfig {

    // Sadece "cache.enabled=true" ise bean oluştur
    @Bean
    @ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
    public CacheManager cacheManager() {
        return new CaffeineCacheManager();
    }

    // Redis classpath'te varsa
    @Bean
    @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
    public RedisTemplate<String, Object> redisTemplate() {
        return new RedisTemplate<>();
    }

    // Kullanıcı kendi bean'ini tanımlamamışsa
    @Bean
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper defaultObjectMapper() {
        return new ObjectMapper();
    }
}

Yaygın Hatalar ve Çözümleri

Hata 1: @Component İçinde @Bean (Lite Mode Tuzağı)

// ❌ @Component içinde @Bean → singleton kırılabilir
@Component
public class MyConfig {
    @Bean
    public DataSource dataSource() { ... } // Lite mode!
}

// ✅ @Configuration kullanın
@Configuration
public class MyConfig {
    @Bean
    public DataSource dataSource() { ... } // Full mode — singleton garanti
}

Hata 2: @Bean Metodunu Manuel Çağırmak

// ❌ @Configuration dışından @Bean metodunu çağırmak
@Service
public class SomeService {
    @Autowired AppConfig config;

    public void doSomething() {
        ObjectMapper mapper = config.objectMapper(); // Her çağrıda yeni nesne!
    }
}

// ✅ Bean'i inject edin
@Service
public class SomeService {
    private final ObjectMapper objectMapper;

    public SomeService(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper; // Singleton
    }
}

Hata 3: @Bean Metodunda void Döndürmek

// ❌ YANLIŞ — @Bean metodu bir nesne döndürmeli
@Bean
public void configure() {
    // Bir şeyler yapıyorum...
}

// ✅ DOĞRU — Bean olacak nesneyi döndürün
@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper();
}

Hata 4: Circular Bean Dependencies

// ❌ A, B'ye bağımlı → B, A'ya bağımlı → Döngü!
@Bean
public ServiceA serviceA(ServiceB b) { return new ServiceA(b); }

@Bean
public ServiceB serviceB(ServiceA a) { return new ServiceB(a); }
// BeanCurrentlyInCreationException!

// ✅ Tasarımı yeniden düşünün — ortak bağımlılığı çıkarın
@Bean
public CommonService commonService() { return new CommonService(); }

@Bean
public ServiceA serviceA(CommonService c) { return new ServiceA(c); }

@Bean
public ServiceB serviceB(CommonService c) { return new ServiceB(c); }

Bütünleşik Örnek: E-Ticaret Konfigürasyonu

@Configuration
public class ECommerceConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper;
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(10))
                .additionalInterceptors(new LoggingInterceptor())
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    @Profile("prod")
    public JavaMailSender mailSender(
            @Value("${mail.host}") String host,
            @Value("${mail.port}") int port,
            @Value("${mail.username}") String username,
            @Value("${mail.password}") String password) {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();
        sender.setHost(host);
        sender.setPort(port);
        sender.setUsername(username);
        sender.setPassword(password);
        Properties props = sender.getJavaMailProperties();
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");
        return sender;
    }
}

Özet

  • `@Configuration` + `@Bean` ile 3rd-party kütüphane sınıflarını Spring container'a entegre edin

  • Bean adı varsayılan olarak metot adıdır@Bean("customName") ile özelleştirilebilir

  • Full Mode (@Configuration): CGLIB proxy ile singleton garanti — her zaman bunu kullanın

  • Lite Mode (@Component): Proxy yok, @Bean metotları arası çağrıda singleton kırılır

  • Kendi sınıflarınız için @Component/@Service, 3rd-party için @Bean kullanın

  • @Bean metotları başka bean'lere parametre ile bağımlılık alabilir

  • @ConditionalOnProperty, @ConditionalOnMissingBean ile koşullu bean oluşturma yapılabilir


Programmatic Bean Registration

Nadir durumlarda bean'leri programmatik olarak kaydedebilirsiniz:

@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        var ctx = new SpringApplicationBuilder(MyApp.class)
                .initializers(context -> {
                    context.getBeanFactory()
                            .registerSingleton("myCustomBean", new MyCustomService("config"));
                })
                .run(args);
    }
}

BeanDefinitionRegistryPostProcessor ile Dinamik Bean Kayıt

@Component
public class DynamicBeanRegistrar implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // Runtime'da bean tanımları ekleyebilirsiniz
        GenericBeanDefinition beanDef = new GenericBeanDefinition();
        beanDef.setBeanClass(DynamicService.class);
        beanDef.setScope("singleton");
        registry.registerBeanDefinition("dynamicService", beanDef);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // Bean factory'yi customize edebilirsiniz
    }
}

Bu yöntemler plugin sistemi, multi-tenant mimari veya dinamik konfigürasyon gerektiren ileri seviye senaryolarda kullanılır. Günlük geliştirmede nadiren ihtiyaç duyarsınız.


@Configuration Sınıfı Organizasyonu — Best Practices

Büyük projelerde konfigürasyon sınıflarını işlevsel olarak ayırın:

config/
├── WebConfig.java          // CORS, interceptors, message converters
├── SecurityConfig.java     // Authentication, authorization
├── DataConfig.java         // DataSource, JPA, transaction
├── CacheConfig.java        // Redis, Caffeine cache
├── MailConfig.java         // JavaMailSender
├── AwsConfig.java          // S3, SQS, SNS clients
├── SwaggerConfig.java      // API documentation
└── AsyncConfig.java        // Thread pool, @Async configuration

Her config sınıfı tek bir sorumluluğa sahip olmalı (Single Responsibility). 20+ @Bean metodu olan dev bir AppConfig sınıfı yerine, her biri 3-5 bean tanımlayan küçük config sınıfları tercih edin.

💡 İpucu: Config sınıflarını config paketinde toplayın. Böylece proje yapısına bakan herkes konfigürasyonları kolayca bulur. Ayrıca @Configuration(proxyBeanMethods = false) kullanarak lite mode aktif edebilirsiniz — bu, CGLIB proxy'sini atlar ve startup süresini kısaltır. Ancak lite mode'da aynı @Configuration sınıfı içinde bir @Bean metodundan diğerini çağırmak yeni bir instance oluşturur (singleton garantisi kaybolur). Bu yüzden lite mode sadece bean'ler arası method çağrısı yapmadığınız sınıflarda güvenlidir.