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?
| Durum | Tercih | Neden |
|---|---|---|
| Kendi yazdığınız sınıf | @Component / @Service / @Repository | Sınıfa annotation ekleyebilirsiniz |
| 3rd-party kütüphane sınıfı | @Bean | Kaynak koda erişiminiz yok |
| Karmaşık initialization mantığı | @Bean | Constructor'dan fazlası gerekiyor |
| Koşullu bean oluşturma | @Bean | If/else, profil, environment kontrolü |
| Basit servis, controller | @Component / @Service | En 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:
objectMapper()metodu birObjectMappernesnesi oluşturur, konfigüre eder ve döndürürSpring bu nesneyi container'a
objectMapperadıyla bean olarak kaydederArtı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ştirilebilirFull Mode (
@Configuration): CGLIB proxy ile singleton garanti — her zaman bunu kullanınLite Mode (
@Component): Proxy yok,@Beanmetotları arası çağrıda singleton kırılırKendi sınıflarınız için
@Component/@Service, 3rd-party için@Beankullanın@Beanmetotları başka bean'lere parametre ile bağımlılık alabilir@ConditionalOnProperty,@ConditionalOnMissingBeanile 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 configurationHer 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.
AI Asistan
Sorularını yanıtlamaya hazır