Spring Boot Auto-Configuration ve Properties
Component Scanning dersinde bean'lerin container tarafından nasıl bulunduğunu, Spring
IoC Container dersinde ise bean'lerin nasıl tanımlandığını ve yaşam döngüsünün nasıl
işlediğini gördük. Bu son derste, Spring Boot'un bu ikisinin üzerine kattığı üçüncü
katmana bakıyoruz: @SpringBootApplication ve auto-configuration'ın perde arkasında
nasıl çalıştığına, application.yml'den @Value/@ConfigurationProperties ile
property okumaya, @Profile ile ortama özel yapılandırmaya, ve ApplicationEvent ile
container'ın kendi kendine haber vermesine. Bu dersin sonunda, projenin kendi
application.yml dosyalarının ve LearningPlatformApplication'daki tek bir
@SpringBootApplication satırının aslında neyi temsil ettiğini tam olarak
anlayacaksın.
Spring Boot Auto-Configuration Nedir?
Auto-configuration, classpath'te hangi kütüphanelerin bulunduğuna bakarak Spring
Boot'un -- sen hiçbir @Bean metodu yazmadan -- senin yerine bean'ler kaydetmesidir.
Örneğin bu projede spring-boot-starter-data-jpa ve postgresql bağımlılığı olduğu
için, Spring Boot bir DataSource bean'i, bir EntityManagerFactory bean'i ve bir
JPA TransactionManager bean'i otomatik olarak kurar -- hiçbirini WebConfig gibi bir
@Configuration sınıfında elle tanımlamadık:
// Elle yazsaydık (asla yazmıyoruz, Spring Boot bizim yerimize yapıyor):
@Configuration
class ManualDataSourceConfig {
@Bean
DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://localhost:5433/learning");
ds.setUsername("learning");
ds.setPassword("learning");
return ds;
}
}
application.yml'deki spring.datasource.* anahtarlarını yazman dışında, yukarıdaki
gibi bir sınıfı hiç görmedin -- çünkü auto-configuration, classpath'te
org.postgresql.Driver ve spring-boot-starter-data-jpa'yı görüp bu bean'i senin
yerine kaydediyor.
Neden Var?
Component Scanning dersinde Java Config'in tekrarlayıcı olduğunu, component
scanning'in bunu sınıfın kendi üzerine taşıyarak azalttığını görmüştük. Ama component
scanning yalnızca senin kendi sınıfların için işe yarar -- DataSource,
EntityManagerFactory, RequestMappingHandlerMapping gibi bean'ler senin yazmadığın,
üçüncü parti kütüphanelerin sınıflarıdır; bunlara @Component ekleyemezsin (bkz.
Component Scanning dersindeki "Component Scanning vs Java Config: Ne Zaman Hangisi?").
Auto-configuration olmasaydı, her yeni Spring Boot projesinde yukarıdaki gibi
düzinelerce @Bean metodunu -- DataSource, TransactionManager,
RequestMappingHandlerMapping, ViewResolver, ObjectMapper, ve daha fazlasını --
elle yazman gerekirdi. Auto-configuration, "classpath'te şu kütüphane varsa, muhtemelen
şu bean'lere ihtiyacın vardır" varsayımını framework'ün kendisine taşır -- sen yalnızca
application.yml'de birkaç property ile bu varsayılanları özelleştirirsin.
Tarihçe
Spring Boot, 2014'te 1.0 sürümüyle çıktı -- o zamana kadar bir Spring uygulaması
kurmak, XML tabanlı yapılandırma (Spring IoC Container dersindeki "Tarihçe"
bölümünde bahsettiğimiz ClassPathXmlApplicationContext dönemi) ya da onlarca elle
yazılmış @Bean metoduyla saatler sürebiliyordu. Spring Boot'un temel vaadi "convention
over configuration" idi: makul varsayılanlarla başla, yalnızca varsayılandan
sapmak istediğinde bir şey yaz.
@EnableAutoConfiguration (ve onu saran @SpringBootApplication) bu vaadin teknik
temelidir. Başlangıçta META-INF/spring.factories dosyasında listelenen auto-configuration
sınıflarını okuyordu; Spring Boot 2.7'de (2022) bu mekanizma, daha hızlı ve daha açık
olan META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
dosyasına taşındı -- bu proje Spring Boot 4.1 kullandığı için (bkz. pom.xml), yeni
mekanizmayı kullanıyor. @ConditionalOnClass, @ConditionalOnMissingBean gibi
@Conditional türevleri de 1.0'dan beri auto-configuration'ın temelini oluşturuyor.
@SpringBootApplication: Üç Anotasyonun Birleşimi
LearningPlatformApplication sınıfının üzerindeki tek @SpringBootApplication
annotation'ı aslında üç ayrı annotation'ın birleşimidir -- ikisini zaten önceki
derslerden tanıyoruz:
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
// @SpringBootApplication is a convenience annotation: it is itself meta-annotated
// with three annotations we can already recognize. This file proves that
// composition with reflection, the same way StereotypeAnnotationsExample (in the
// Component Scanning lesson) proved @Service carries @Component underneath.
@SpringBootApplication
class DemoApplication {
}
class SpringBootApplicationExample {
public static void main(String[] args) {
boolean carriesSpringBootConfiguration =
AnnotationUtils.findAnnotation(DemoApplication.class, SpringBootConfiguration.class) != null;
boolean carriesEnableAutoConfiguration =
AnnotationUtils.findAnnotation(DemoApplication.class, EnableAutoConfiguration.class) != null;
boolean carriesComponentScan =
AnnotationUtils.findAnnotation(DemoApplication.class, ComponentScan.class) != null;
System.out.println("Carries @SpringBootConfiguration: " + carriesSpringBootConfiguration);
// Carries @SpringBootConfiguration: true
System.out.println("Carries @EnableAutoConfiguration: " + carriesEnableAutoConfiguration);
// Carries @EnableAutoConfiguration: true
System.out.println("Carries @ComponentScan: " + carriesComponentScan);
// Carries @ComponentScan: true
// @SpringBootConfiguration is itself meta-annotated with @Configuration --
// that's exactly why a @SpringBootApplication-annotated class (like this
// project's own LearningPlatformApplication) can be passed directly to
// an ApplicationContext, wherever a @Configuration class is expected.
boolean springBootConfigurationIsConfiguration =
AnnotationUtils.findAnnotation(SpringBootConfiguration.class, Configuration.class) != null;
System.out.println("@SpringBootConfiguration carries @Configuration: " + springBootConfigurationIsConfiguration);
// @SpringBootConfiguration carries @Configuration: true
}
}
@SpringBootConfiguration, @Configuration'ın (Spring IoC Container dersi) özel bir
türevidir. @ComponentScan, Component Scanning dersinde gördüğümüz, argümansız
kullanıldığında kendi paketini (ve alt paketlerini) tarayan annotation'ın ta kendisi --
bu yüzden com.cdurgun.learning altındaki her @Controller/@Service elle
kaydedilmeden bulunuyor. Üçüncüsü, bu dersin asıl konusu olan @EnableAutoConfiguration.
@Conditional Ailesi ve Auto-Configuration Mekanizması
Auto-configuration'ın kalbinde @Conditional ailesi yatar: bir bean'in ya da tüm bir
@Configuration sınıfının, belirli bir koşul sağlandığında (ya da sağlanmadığında)
kaydedilmesini sağlayan annotation'lar. @EnableAutoConfiguration işleme girdiğinde,
Spring Boot'un kendi spring-boot-autoconfigure modülündeki yüzlerce
@Configuration sınıfını (DataSourceAutoConfiguration,
JpaRepositoriesAutoConfiguration, ThymeleafAutoConfiguration gibi) sırayla dener --
her biri kendi @Conditional annotation'larıyla korunur, ve koşulu sağlamayan hiçbir
şey kaydedilmez. En sık kullanılan iki türevi -- @ConditionalOnClass ve
@ConditionalOnMissingBean -- sonraki iki bölümde kendi ellerimizle kullanacağız.
@ConditionalOnClass: Sınıf Classpath'te Varsa
@ConditionalOnClass, "bu bean'i yalnızca belirtilen sınıf classpath'te varsa
kaydet" der -- gerçek DataSourceAutoConfiguration'ın, yalnızca bir JDBC sürücüsü
projenin bağımlılıklarında varsa devreye girmesiyle aynı mekanizma:
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// @ConditionalOnClass is the annotation Spring Boot's own auto-configuration
// classes use dozens of times over: "only register this bean if a given class
// is present on the classpath." Here we use it directly on our own @Bean
// methods, with one class we know for certain IS on the classpath and one
// that is NOT, to see both branches.
@Configuration
class JsonSupportConfig {
// com.fasterxml.jackson.databind.ObjectMapper really is on the classpath --
// spring-boot-starter-web brings Jackson in transitively. This bean IS
// registered.
@Bean
@ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
String jacksonSupportMarker() {
return "Jackson support enabled";
}
// No such class exists anywhere on the classpath -- this bean is silently
// skipped, exactly like a real auto-configuration class skips registering
// (say) a DataSource bean when no JDBC driver is present at all.
@Bean
@ConditionalOnClass(name = "com.example.NoSuchLibraryEverInstalled")
String missingLibrarySupportMarker() {
return "This should never print";
}
}
class ConditionalOnClassExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JsonSupportConfig.class);
System.out.println(context.containsBean("jacksonSupportMarker"));
// true
System.out.println(context.containsBean("missingLibrarySupportMarker"));
// false
context.close();
}
}
com.fasterxml.jackson.databind.ObjectMapper gerçekten classpath'te olduğu için
(Jackson, spring-boot-starter-web üzerinden dolaylı olarak geliyor) ilk bean
kaydediliyor; uydurma bir sınıf adı verdiğimiz ikinci bean ise sessizce atlanıyor --
hiçbir hata fırlatılmıyor, bean sadece hiç var olmamış gibi davranıyor.
@ConditionalOnMissingBean: Kullanıcı Kendi Bean'ini Tanımladıysa
@ConditionalOnMissingBean, kütüphanelerin "makul bir varsayılan sunuyorum, ama sen
kendi bean'ini tanımlarsan onu kullan" demesini sağlar -- gerçek Spring Boot'ta
ObjectMapper, RestTemplateBuilder gibi birçok bean tam olarak bu şekilde
davranır:
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
interface MessageFormatter {
String format(String message);
}
// Simulates the "library default, application override" pattern used
// everywhere in real Spring Boot auto-configuration: a library ships a
// sensible default bean, marked @ConditionalOnMissingBean, so any bean the
// application itself defines of the same type silently takes priority.
@Configuration
class LibraryDefaultsConfig {
@Bean
@ConditionalOnMissingBean
MessageFormatter messageFormatter() {
return message -> "[default] " + message;
}
}
@Configuration
class UserOverrideConfig {
@Bean
MessageFormatter messageFormatter() {
return message -> "[custom] " + message;
}
}
class ConditionalOnMissingBeanExample {
public static void main(String[] args) {
// Case 1: only the library's config is present -- its default wins.
AnnotationConfigApplicationContext withoutOverride =
new AnnotationConfigApplicationContext(LibraryDefaultsConfig.class);
System.out.println(withoutOverride.getBean(MessageFormatter.class).format("hello"));
// [default] hello
withoutOverride.close();
// Case 2: the application also registers its own bean. Order matters:
// UserOverrideConfig is given first, so its bean definition already
// exists by the time @ConditionalOnMissingBean is evaluated for
// LibraryDefaultsConfig -- exactly why real auto-configuration classes
// are always processed after the application's own @Configuration
// classes.
AnnotationConfigApplicationContext withOverride =
new AnnotationConfigApplicationContext(UserOverrideConfig.class, LibraryDefaultsConfig.class);
System.out.println(withOverride.getBean(MessageFormatter.class).format("hello"));
// [custom] hello
withOverride.close();
}
}
Sıralama burada kritik: uygulamanın kendi @Configuration sınıfı, kütüphanenin
varsayılanını tanımlayan sınıftan önce işlenmeli -- gerçek Spring Boot'ta bu,
auto-configuration sınıflarının her zaman uygulamanın kendi @Configuration
sınıflarından sonra işlenmesiyle garanti edilir, tam olarak bu yüzden kendi
tanımladığın bir bean her zaman auto-configuration'ın varsayılanının önüne geçer.
Kendi Auto-Configuration'ımızı Yazmak
Gerçek bir Spring Boot starter'ının nasıl göründüğünü küçük ölçekte kendimiz
yazarak görelim -- @ConditionalOnProperty, bir özelliğin tamamen application.yml'den
açılıp kapatılmasını sağlar:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
interface CacheWarmer {
void warmUp();
}
// A hand-written stand-in for what a real Spring Boot "starter" auto-configuration
// class looks like: @ConditionalOnProperty lets the application turn a whole
// feature on/off from application.yml, with a safe default (off) if the
// property is never set at all.
@Configuration
class CacheWarmerAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.cache-warmer.enabled", havingValue = "true", matchIfMissing = false)
CacheWarmer cacheWarmer() {
return () -> System.out.println("Cache warmed up.");
}
}
class CustomAutoConfigurationExample {
public static void main(String[] args) {
// Case 1: the property is set to true -- the bean is registered.
AnnotationConfigApplicationContext enabledContext = new AnnotationConfigApplicationContext();
addProperty(enabledContext, "app.cache-warmer.enabled", "true");
enabledContext.register(CacheWarmerAutoConfiguration.class);
enabledContext.refresh();
System.out.println(enabledContext.containsBean("cacheWarmer"));
// true
enabledContext.close();
// Case 2: the property is never set -- matchIfMissing = false means
// the bean is skipped, exactly like an optional Spring Boot feature
// that stays off until the application opts in.
AnnotationConfigApplicationContext disabledContext = new AnnotationConfigApplicationContext();
disabledContext.register(CacheWarmerAutoConfiguration.class);
disabledContext.refresh();
System.out.println(disabledContext.containsBean("cacheWarmer"));
// false
disabledContext.close();
}
private static void addProperty(AnnotationConfigApplicationContext context, String key, String value) {
ConfigurableEnvironment environment = context.getEnvironment();
environment.getPropertySources().addFirst(new MapPropertySource("test", Map.of(key, value)));
}
}
matchIfMissing = false sayesinde, property hiç tanımlanmamışsa bean varsayılan olarak
kapalı kalıyor -- gerçek Spring Boot'taki birçok isteğe bağlı özelliğin
(spring.cache.type, management.endpoints.web.exposure.include gibi) davranışıyla
aynı: sen açıkça istemeden devreye girmiyor.
application.properties ve application.yml
Spring Boot iki eşdeğer dosya formatını destekler: düz key=value satırlarından
oluşan application.properties, ve iç içe geçmiş yapıyı girintiyle ifade eden
application.yml. Bu proje YAML'ı tercih ediyor -- kendi application.yml
dosyasından bir parça:
spring:
application:
name: learning-platform
profiles:
active: dev
thymeleaf:
cache: false
server:
port: 8080
Aynı ayarlar .properties formatında şöyle görünürdü: spring.application.name=learning-platform,
spring.profiles.active=dev, spring.thymeleaf.cache=false, server.port=8080. İkisi
de aynı düz nokta-ayrılmış property anahtarlarına (spring.thymeleaf.cache gibi)
çözümlenir -- YAML sadece bunu iç içe girintilerle daha az tekrarlı yazmanı sağlar.
Sonraki bölümlerde bu anahtarları @Value ve @ConfigurationProperties ile Java
tarafında nasıl okuyacağımızı göreceğiz.
@Value ile Tekil Property Enjeksiyonu
@Value, application.yml'den tek bir property'yi doğrudan bir alana ya da
constructor parametresine enjekte eder -- en basit okuma yöntemi, ama hiçbir
gruplama sunmaz:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
// @Value pulls a single property value into a field or constructor parameter --
// the simplest way to read application.yml/application.properties, but with
// no grouping and no type validation beyond the target field's own type.
class GreetingService {
@Value("${app.greeting.prefix:Hello}")
private String prefix;
// ${...} placeholders are resolved first, and only then is the resulting
// string evaluated as a SpEL expression (#{...}) -- so this becomes
// "#{'Hello'.toUpperCase()}" before it is ever evaluated.
@Value("#{'${app.greeting.prefix:Hello}'.toUpperCase()}")
private String shoutedPrefix;
String greet(String name) {
return prefix + ", " + name + "!";
}
String shoutedGreet(String name) {
return shoutedPrefix + ", " + name + "!";
}
}
@Configuration
class GreetingConfig {
// Outside Spring Boot, ${...} placeholders in @Value are NOT resolved
// automatically -- this bean is what actually makes them work. It must
// be `static`, so the container can run it very early, before other
// @Configuration classes are even fully processed. In a real Spring Boot
// app you never write this yourself: PropertyPlaceholderAutoConfiguration
// (triggered by @EnableAutoConfiguration) registers it for you -- exactly
// the kind of boilerplate auto-configuration exists to remove (see "Why
// Does It Exist?").
@Bean
static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
GreetingService greetingService() {
return new GreetingService();
}
}
class ValueInjectionExample {
public static void main(String[] args) {
// Case 1: the property is set explicitly (simulated here with a
// MapPropertySource, standing in for application.yml).
AnnotationConfigApplicationContext withProperty = new AnnotationConfigApplicationContext();
ConfigurableEnvironment env1 = withProperty.getEnvironment();
env1.getPropertySources().addFirst(new MapPropertySource("test", Map.of("app.greeting.prefix", "Merhaba")));
withProperty.register(GreetingConfig.class);
withProperty.refresh();
System.out.println(withProperty.getBean(GreetingService.class).greet("Ayse"));
// Merhaba, Ayse!
withProperty.close();
// Case 2: the property is never set -- the ":Hello" default after the
// colon kicks in, instead of a startup failure.
AnnotationConfigApplicationContext withoutProperty = new AnnotationConfigApplicationContext();
withoutProperty.register(GreetingConfig.class);
withoutProperty.refresh();
System.out.println(withoutProperty.getBean(GreetingService.class).greet("Ayse"));
// Hello, Ayse!
System.out.println(withoutProperty.getBean(GreetingService.class).shoutedGreet("Ayse"));
// HELLO, Ayse!
withoutProperty.close();
}
}
${app.greeting.prefix:Hello} ifadesindeki :Hello kısmı, property hiç
tanımlanmamışsa kullanılacak varsayılan değeri belirtir -- property zorunlu değilse
uygulamanın çökmesini önler. Kod örneğindeki yorumda da belirtildiği gibi, saf Spring
IoC Container'da (Spring Boot olmadan) ${...} yer tutucularının çalışması için
PropertySourcesPlaceholderConfigurer bean'ini elle tanımlaman gerekir -- Spring
Boot'ta bunu hiç yazmazsın, çünkü @EnableAutoConfiguration bunu senin için otomatik
kaydeder. Bu, "Neden Var?" bölümünde bahsettiğimiz tam olarak o türden bir
tekrarı ortadan kaldırma örneği.
@ConfigurationProperties ile Gruplanmış Property'ler
@Value'nun aksine, @ConfigurationProperties aynı önekle (prefix) başlayan bütün
bir property ailesini tek, tipli bir nesneye bağlar:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
// @ConfigurationProperties groups a whole family of related settings into one
// typed object, bound from a common prefix -- unlike @Value, which reads one
// property at a time with no structure of its own.
@ConfigurationProperties(prefix = "app.mail")
class MailProperties {
private String host = "localhost";
private int port = 25;
private boolean tlsEnabled = false;
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public boolean isTlsEnabled() { return tlsEnabled; }
public void setTlsEnabled(boolean tlsEnabled) { this.tlsEnabled = tlsEnabled; }
@Override
public String toString() {
return "MailProperties{host='" + host + "', port=" + port + ", tlsEnabled=" + tlsEnabled + "}";
}
}
@Configuration
@EnableConfigurationProperties(MailProperties.class)
class MailConfig {
// Note: no PropertySourcesPlaceholderConfigurer needed here, unlike the
// @Value example -- @ConfigurationProperties binds directly from the
// Environment's property sources, it does not go through the ${...}
// embedded value resolver mechanism @Value relies on.
}
class ConfigurationPropertiesExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment environment = context.getEnvironment();
// "tls-enabled" (kebab-case, as it would appear in application.yml)
// binds to the "tlsEnabled" field automatically -- Spring Boot's
// relaxed binding rules treat the two as the same property.
environment.getPropertySources().addFirst(new MapPropertySource("test", Map.of(
"app.mail.host", "smtp.example.com",
"app.mail.port", "587",
"app.mail.tls-enabled", "true"
)));
context.register(MailConfig.class);
context.refresh();
System.out.println(context.getBean(MailProperties.class));
// MailProperties{host='smtp.example.com', port=587, tlsEnabled=true}
context.close();
}
}
app.mail.tls-enabled (YAML'da kullanılacağı gibi kebab-case) otomatik olarak
tlsEnabled alanına bağlanıyor -- Spring Boot'un "relaxed binding" (esnek bağlama)
kuralları, kebab-case, camelCase ve UPPER_SNAKE_CASE'i (ortam değişkenleri için) aynı
property olarak kabul eder. Bu proje henüz kendi @ConfigurationProperties sınıfını
tanımlamıyor -- "Bu Projenin Kendi application.yml ve Config Sınıfları" bölümünde buna
tekrar döneceğiz.
@ConfigurationProperties Validasyonu
Gerçek projelerde @ConfigurationProperties, jakarta.validation annotation'ları
(@NotBlank, @Min gibi) ve @Validated ile doğrulanır -- bu, spring-boot-starter-validation
bağımlılığını gerektirir, ki bu projede yok (bkz. pom.xml). Aynı güvenliği elle,
@PostConstruct ile kuruyoruz:
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
// Real Spring Boot projects validate @ConfigurationProperties with
// jakarta.validation annotations (@NotBlank, @Min...) plus @Validated --
// that needs the spring-boot-starter-validation dependency, which this
// project doesn't have. We get the same safety net by hand instead, with a
// @PostConstruct check that fails fast at startup instead of silently
// running with a broken configuration.
@ConfigurationProperties(prefix = "app.retry")
class RetryProperties {
private int maxAttempts = 3;
private long backoffMillis = 500;
public int getMaxAttempts() { return maxAttempts; }
public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; }
public long getBackoffMillis() { return backoffMillis; }
public void setBackoffMillis(long backoffMillis) { this.backoffMillis = backoffMillis; }
@PostConstruct
void validate() {
if (maxAttempts < 1) {
throw new IllegalStateException("app.retry.max-attempts must be at least 1, was " + maxAttempts);
}
if (backoffMillis < 0) {
throw new IllegalStateException("app.retry.backoff-millis cannot be negative, was " + backoffMillis);
}
}
}
@Configuration
@EnableConfigurationProperties(RetryProperties.class)
class RetryConfig {
}
class ConfigurationPropertiesValidationExample {
public static void main(String[] args) {
// Case 1: a valid configuration -- starts up normally.
AnnotationConfigApplicationContext validContext = new AnnotationConfigApplicationContext();
ConfigurableEnvironment validEnv = validContext.getEnvironment();
validEnv.getPropertySources().addFirst(new MapPropertySource("test", Map.of("app.retry.max-attempts", "5")));
validContext.register(RetryConfig.class);
validContext.refresh();
System.out.println(validContext.getBean(RetryProperties.class).getMaxAttempts());
// 5
validContext.close();
// Case 2: an invalid configuration -- @PostConstruct fails fast at
// startup instead of the application running with a nonsensical
// "0 retries" setting. Spring wraps the exception our own code threw
// inside a BeanCreationException, the same as it would for any other
// failing @PostConstruct method (see the Spring IoC Container lesson).
AnnotationConfigApplicationContext invalidContext = new AnnotationConfigApplicationContext();
ConfigurableEnvironment invalidEnv = invalidContext.getEnvironment();
invalidEnv.getPropertySources().addFirst(new MapPropertySource("test", Map.of("app.retry.max-attempts", "0")));
invalidContext.register(RetryConfig.class);
try {
invalidContext.refresh();
} catch (BeanCreationException e) {
System.out.println("Startup failed: " + e.getRootCause().getMessage());
// Startup failed: app.retry.max-attempts must be at least 1, was 0
}
}
}
Geçersiz bir max-attempts değeri, uygulamanın "0 deneme hakkı" gibi anlamsız bir
durumla sessizce çalışmaya devam etmesi yerine, başlangıçta (context.refresh()
sırasında) açıkça başarısız oluyor -- Spring IoC Container dersindeki
@PostConstruct/@PreDestroy bölümünde gördüğümüz yaşam döngüsü kancasının,
burada "fail fast" (erken başarısız ol) için kullanılmış hâli.
Profiles: @Profile ile Ortama Özel Bean'ler
@Profile, aynı arayüzün birbirinden tamamen farklı iki implementasyonunun kaynak
kodda yan yana durmasını, ama yalnızca birinin -- aktif profile göre -- gerçekten
kaydedilmesini sağlar:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
interface PaymentGateway {
void charge(double amount);
}
// @Profile lets two completely different bean implementations exist side by
// side in the source code, with only one of them ever actually registered --
// chosen by which profile(s) are active. This is exactly how this project
// switches between application-dev.yml, application-test.yml, and
// application-prod.yml.
@Configuration
class PaymentConfig {
@Bean
@Profile("dev")
PaymentGateway sandboxPaymentGateway() {
return amount -> System.out.println("[sandbox] Pretending to charge $" + amount);
}
@Bean
@Profile("prod")
PaymentGateway realPaymentGateway() {
return amount -> System.out.println("[real] Charging $" + amount + " via the payment provider");
}
}
class ProfileExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext devContext = new AnnotationConfigApplicationContext();
devContext.getEnvironment().setActiveProfiles("dev");
devContext.register(PaymentConfig.class);
devContext.refresh();
devContext.getBean(PaymentGateway.class).charge(49.99);
// [sandbox] Pretending to charge $49.99
devContext.close();
AnnotationConfigApplicationContext prodContext = new AnnotationConfigApplicationContext();
prodContext.getEnvironment().setActiveProfiles("prod");
prodContext.register(PaymentConfig.class);
prodContext.refresh();
prodContext.getBean(PaymentGateway.class).charge(49.99);
// [real] Charging $49.99 via the payment provider
prodContext.close();
}
}
Bu, tam olarak bu projenin application-dev.yml, application-test.yml ve
application-prod.yml arasında geçiş yaparken kullandığı mekanizma -- yalnızca
property değerleri değil, bean'lerin kendisi bile ortama göre değişebilir.
Profile'a Özel application-{profile}.yml Dosyaları
Bu projenin dört application*.yml dosyası var: temel ayarları içeren
application.yml, ve üç profile özel dosya. application.yml'deki
spring.profiles.active: dev satırı, hangi profilin varsayılan olarak aktif
olacağını belirler:
# application-dev.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5433/learning
jpa:
show-sql: true
# application-prod.yml
spring:
datasource:
url: ${DB_URL}
jpa:
show-sql: false
application-prod.yml'de ${DB_URL} gibi ifadeler, "External Configuration: Property
Kaynaklarının Öncelik Sırası" bölümünde göreceğimiz ortam değişkenlerinden okunuyor --
gizli bilgiler (veritabanı
şifresi gibi) hiçbir zaman repoya yazılmıyor. Aktif profil spring.profiles.active
ile (ya da bir ortam değişkeniyle) değiştirildiğinde, Spring Boot ilgili
application-{profile}.yml dosyasını temel application.yml'in üzerine katman
katman uygular.
External Configuration: Property Kaynaklarının Öncelik Sırası
Bir property birden fazla kaynakta tanımlıysa (mesela hem application.yml'de hem
bir ortam değişkeninde), Spring Boot hangisinin kazanacağına sıkı bir öncelik
sırasıyla karar verir. En yüksek öncelikliden en düşüğe doğru başlıca kaynaklar: komut
satırı argümanları, ortam değişkenleri, application-{profile}.yml, ve en altta temel
application.yml. Bu sıralamayı kendi elimizle simüle edelim:
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.StandardEnvironment;
import java.util.Map;
// Spring Boot reads configuration from many places at once -- command-line
// arguments, environment variables, application-{profile}.yml,
// application.yml, and more -- and needs a strict priority order to pick a
// winner when more than one source defines the same key. We simulate three
// of those sources by hand here, added in *reverse* priority order, to watch
// the highest-priority one win.
class PropertySourceOrderExample {
public static void main(String[] args) {
ConfigurableEnvironment environment = new StandardEnvironment();
MutablePropertySources sources = environment.getPropertySources();
// Lowest priority: the base application.yml.
sources.addLast(new MapPropertySource("application.yml", Map.of("server.port", "8080")));
// Higher priority: a profile-specific application-prod.yml.
sources.addBefore("application.yml", new MapPropertySource("application-prod.yml", Map.of("server.port", "9090")));
// Highest priority in this example: an environment variable (in a
// real deployment this would come from the OS itself, via
// StandardEnvironment's own built-in "systemEnvironment" source).
sources.addFirst(new MapPropertySource("systemEnvironment", Map.of("server.port", "443")));
System.out.println(environment.getProperty("server.port"));
// 443
// Remove the environment variable to see the next source in line win.
sources.remove("systemEnvironment");
System.out.println(environment.getProperty("server.port"));
// 9090
sources.remove("application-prod.yml");
System.out.println(environment.getProperty("server.port"));
// 8080
}
}
Bu, tam olarak "Profile'a Özel application-{profile}.yml Dosyaları" bölümünde
gördüğümüz ${DB_URL} ifadesinin neden işe yaradığını açıklıyor: prodüksiyonda
gerçek bir ortam değişkeni, application-prod.yml'deki yer tutucunun üzerine
katmanlanıyor.
Ortam Değişkenleri ve Komut Satırı Argümanları
Property kaynaklarının en yüksek öncelikli ikisi -- ortam değişkenleri ve komut
satırı argümanları -- koddan tamamen bağımsız, dağıtım zamanında belirlenir. Bir
Spring Boot uygulaması java -jar app.jar --server.port=9090 şeklinde başlatılırsa,
bu değer application.yml'deki her şeyin önüne geçer; aynı şekilde
SERVER_PORT=9090 ortam değişkeni de (Spring Boot, SERVER_PORT'u otomatik olarak
server.port'a çevirir) aynı etkiyi yapar. Bu, sırrı (veritabanı şifresi gibi) hiç
repoya yazmadan, sadece dağıtım ortamında enjekte etmenin standart yoludur --
application-prod.yml'deki ${DB_URL}, ${DB_USERNAME}, ${DB_PASSWORD} tam olarak
bunu yapıyor.
ApplicationEvent ve @EventListener
Container, kendi yaşam döngüsü boyunca event'ler yayınlar, ve senin kendi sınıfların da kendi event'lerini yayınlayıp dinleyebilir -- yayınlayan ile dinleyen arasında hiçbir doğrudan bağımlılık olmadan:
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
// A custom application event -- any object extending ApplicationEvent (or,
// since Spring 4.2, any arbitrary object at all) can be published and picked
// up by listeners, completely decoupling the publisher from whoever reacts
// to it.
class OrderPlacedEvent extends ApplicationEvent {
private final String orderId;
OrderPlacedEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
String getOrderId() {
return orderId;
}
}
@Component
class OrderService {
private final ApplicationEventPublisher publisher;
OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
void placeOrder(String orderId) {
System.out.println("Order placed: " + orderId);
publisher.publishEvent(new OrderPlacedEvent(this, orderId));
}
}
@Component
class OrderNotificationListener {
// @EventListener is the modern, annotation-based alternative to
// implementing ApplicationListener<OrderPlacedEvent> directly -- both
// work, this one needs no interface at all.
@EventListener
void onOrderPlaced(OrderPlacedEvent event) {
System.out.println("Sending confirmation email for order " + event.getOrderId());
}
// The container itself publishes events too -- ContextRefreshedEvent
// fires once the ApplicationContext has finished starting up. In a full
// Spring Boot app, ApplicationReadyEvent is the equivalent "everything is
// completely ready" signal, fired after ContextRefreshedEvent, once the
// embedded server has also started (see "Spring Boot's Own Events").
@EventListener
void onContextRefreshed(ContextRefreshedEvent event) {
System.out.println("Application context is ready.");
}
}
@Configuration
@ComponentScan
class AppConfig {
}
class ApplicationEventExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Application context is ready.
context.getBean(OrderService.class).placeOrder("ORD-1001");
// Order placed: ORD-1001
// Sending confirmation email for order ORD-1001
context.close();
}
}
@EventListener, ApplicationListener<T> arayüzünü implemente etmenin modern,
annotation tabanlı alternatifi -- hiçbir arayüz gerekmiyor, metot imzasındaki
parametre tipi hangi event'in dinleneceğini belirliyor. ContextRefreshedEvent,
container'ın kendi yayınladığı event'lerden biri; bir sonraki bölümde Spring Boot'un
bunun üzerine eklediği kendi event'lerine bakıyoruz.
Spring Boot'un Kendi Event'leri (Kısa Bakış)
Saf Spring IoC Container'ın ContextRefreshedEvent'ine ek olarak, Spring Boot
SpringApplication.run(...) sırasında kendi event zincirini de yayınlar:
ApplicationStartingEvent (en başta), ApplicationEnvironmentPreparedEvent
(Environment hazırlandığında, ama context henüz oluşmadan), ApplicationContextInitializedEvent,
ApplicationPreparedEvent, ardından container'ın kendi ContextRefreshedEvent'i, ve
en sonda ApplicationReadyEvent -- "her şey, gömülü sunucu (embedded server) da dahil,
tamamen hazır" sinyali. Bu event'ler yalnızca gerçek SpringApplication.run(...) ile
başlatılan bir uygulamada oluşur -- bu dersteki örneklerin kullandığı sade
AnnotationConfigApplicationContext bunları tetiklemez, bu yüzden burada ayrı bir kod
örneği yok. Pratikte en sık kullanılan ikisi ApplicationReadyEvent (arka plan
işlerini başlatmak için) ve ApplicationFailedEvent'tir (başlatma başarısız
olduğunda temizlik yapmak için).
Bu Projenin Kendi application.yml ve Config Sınıfları
Bu projenin application.yml'i, auto-configuration'ın gerçek hayatta nasıl
kullanıldığının iyi bir örneği: spring.datasource.*, spring.jpa.*,
spring.thymeleaf.*, spring.flyway.* anahtarlarının hiçbiri elle yazılmış bir
@Bean metoduna karşılık gelmiyor -- hepsi, ilgili auto-configuration sınıflarının
(DataSourceAutoConfiguration, JpaBaseConfiguration, ThymeleafAutoConfiguration,
FlywayAutoConfiguration) okuduğu, önceden tanımlı property'ler. Projenin kendi
yazdığı tek @Configuration sınıfı WebConfig (Spring IoC Container dersinde
gördüğümüz), ve o da bir LocaleResolver bean'i tanımlıyor -- Spring Boot'un kendi
LocaleResolver auto-configuration'ının yerine geçiyor, çünkü
WebMvcAutoConfiguration'ın kendi localeResolver bean'i tam olarak
@ConditionalOnMissingBean ile korunuyor (bkz.
"@ConditionalOnMissingBean: Kullanıcı Kendi Bean'ini Tanımladıysa"). Projede henüz
hiçbir @Value ya da @ConfigurationProperties kullanılmıyor -- tüm ayarlar,
Spring Boot'un kendi auto-configuration sınıflarının doğrudan okuduğu standart
spring.*/server.* anahtarları.
Best Practices
- Auto-configuration'ı önce anla, sonra güven -- hangi bean'in neden kaydedildiğini bilmeden "sihir" gibi görmek, bir şey beklenmedik çalıştığında hata ayıklamayı imkânsız hâle getirir (bkz. "@Conditional Ailesi ve Auto-Configuration Mekanizması").
- Property gruplarını
@ConfigurationPropertiesile, tekil değerleri@Valueile oku -- birbiriyle ilişkili birden fazla ayar varsa, tek tek@Valueyerine gruplanmış bir sınıf çok daha bakımı kolay bir yaklaşımdır (bkz. "@ConfigurationProperties ile Gruplanmış Property'ler"). - Sırları (şifre, API anahtarı) asla
application.yml'e yazma, ortam değişkenlerinden oku -- bu projeninapplication-prod.yml'i tam olarak bunu yapıyor (bkz. "Ortam Değişkenleri ve Komut Satırı Argümanları"). @ConditionalOnMissingBeanile korunan varsayılanları geçersiz kılmak için, aynı tipte kendi bean'ini tanımlamak yeterlidir -- ekstra bir "kapat" anahtarı aramana gerek yok (bkz. "@ConditionalOnMissingBean: Kullanıcı Kendi Bean'ini Tanımladıysa").@ConfigurationPropertiesile gelen ayarları başlangıçta doğrula, çalışma zamanında değil -- geçersiz bir ayarla sessizce çalışmak yerine erken ve açıkça başarısız olmak, hatayı üretimde değil başlangıçta yakalar (bkz. "@ConfigurationProperties Validasyonu").
Yaygın Hatalar
1. @Value("${...}")'in saf Spring IoC Container'da (Spring Boot olmadan) otomatik
çalışacağını sanmak. PropertySourcesPlaceholderConfigurer bean'i elle
tanımlanmadan ${...} yer tutucuları hiç çözümlenmez (bkz. "@Value ile Tekil Property
Enjeksiyonu").
2. @ConfigurationProperties sınıfını yazıp @EnableConfigurationProperties (ya da
@ConfigurationPropertiesScan) eklemeyi unutmak. Sınıfın kendisi @Component
değildir -- container'a "bunu bağla" demeden hiçbir bean oluşmaz (bkz.
"@ConfigurationProperties ile Gruplanmış Property'ler").
3. @ConditionalOnProperty'de matchIfMissing'i unutmak. Varsayılan davranış
(matchIfMissing = false) property hiç tanımlanmamışsa bean'i kaydetmemektir --
"varsayılan olarak açık" bir özellik istiyorsan bunu açıkça belirtmen gerekir (bkz.
"Kendi Auto-Configuration'ımızı Yazmak").
4. @Profile ile korunan bir bean'i, o profil aktif değilken getBean(...) ile
almaya çalışmak. Bean hiç kaydedilmediği için bu, NoSuchBeanDefinitionException
ile sonuçlanır -- Component Scanning dersindeki @Component eklenmemiş bir sınıfla
aynı sonuç (bkz. "Profiles: @Profile ile Ortama Özel Bean'ler").
5. Property kaynaklarının önceliğini yanlış hatırlamak, ve "neden ortam değişkenim
application.yml'i geçersiz kılmıyor" diye şaşırmak. Ortam değişkenleri
application.yml'den her zaman daha yüksek öncelikli olmalı -- eğer geçersiz kılmıyorsa,
muhtemelen değişken adı yanlış yazılmıştır (bkz. "External Configuration: Property
Kaynaklarının Öncelik Sırası").
6. ApplicationReadyEvent gibi Spring Boot'a özel bir event'i, sade bir
AnnotationConfigApplicationContext ile test etmeye çalışmak. Bu event'ler yalnızca
gerçek SpringApplication.run(...) ile tetiklenir -- ContextRefreshedEvent ile
karıştırılmamalı (bkz. "Spring Boot'un Kendi Event'leri (Kısa Bakış)").
Özet, Cheat Sheet ve Terimler Sözlüğü
Auto-configuration, Spring Boot'un classpath'teki kütüphanelere bakarak senin yerine
bean kaydetmesidir; @Value ve @ConfigurationProperties, application.yml'deki
ayarları Java koduna taşımanın iki yolu; @Profile ortama göre farklı bean'ler
seçmeyi, ApplicationEvent/@EventListener ise container'ın (ve senin kendi
kodunun) birbirine gevşek bağlı şekilde haber vermesini sağlar. Önemli noktalar:
@SpringBootApplication=@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan@ConditionalOnClass/@ConditionalOnMissingBean/@ConditionalOnProperty: auto-configuration'ın bean kaydedip kaydetmeyeceğine karar verdiği koşullar@Value("${key:default}"): tekil property, isteğe bağlı varsayılan değerle@ConfigurationProperties(prefix = "...")+@EnableConfigurationProperties: gruplanmış, tipli property ailesi@Profile("name"): yalnızca belirtilen profil aktifken kaydedilen bean- Property kaynak önceliği (yüksekten düşüğe): komut satırı argümanları > ortam
değişkenleri >
application-{profile}.yml>application.yml ApplicationEvent+ApplicationEventPublisher+@EventListener: yayıncı ile dinleyici arasında doğrudan bağımlılık olmadan haberleşme
Hızlı referans:
@SpringBootApplication // = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
class MyApplication { }
@Configuration
class MyAutoConfiguration {
@Bean
@ConditionalOnClass(name = "some.library.Class")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true", matchIfMissing = false)
MyBean myBean() { return new MyBean(); }
}
class MyService {
@Value("${app.setting:default}")
private String setting;
}
@ConfigurationProperties(prefix = "app.settings")
class MySettings {
private String name;
// getter/setter
}
@Configuration
@EnableConfigurationProperties(MySettings.class)
class SettingsConfig {
@Bean
@Profile("prod")
MyBean prodBean() { return new MyBean(); }
}
@Component
class MyListener {
@EventListener
void onEvent(MyEvent event) { }
}
Terimler Sözlüğü
Auto-configuration — Spring Boot'un, classpath'teki kütüphanelere bakarak senin yerine bean kaydetmesi.
@SpringBootApplication — @SpringBootConfiguration, @EnableAutoConfiguration
ve @ComponentScan'i tek bir annotation'da birleştiren kolaylık annotation'ı.
@Conditional — Bir bean'in ya da @Configuration sınıfının, belirli bir koşul
sağlandığında (ya da sağlanmadığında) kaydedilmesini sağlayan annotation ailesinin
temeli.
@ConditionalOnClass — Belirtilen sınıf classpath'te varsa bean'i kaydeden koşul.
@ConditionalOnMissingBean — Belirtilen tipte başka bir bean henüz yoksa
kaydeden koşul; kütüphane varsayılanlarının kullanıcı tanımlı bean'lere yenilmesini
sağlar.
@ConditionalOnProperty — Belirtilen property belirli bir değere sahipse (ya da
hiç yoksa matchIfMissing'e göre) bean'i kaydeden koşul.
@Value — application.yml'den tek bir property'yi bir alana/parametreye
enjekte eden annotation.
@ConfigurationProperties — Ortak bir önekle başlayan bir property ailesini
tek, tipli bir nesneye bağlayan annotation.
@Profile — Bir bean'in yalnızca belirtilen profil(ler) aktifken kaydedilmesini
sağlayan annotation.
ApplicationEvent — Container tarafından ya da uygulama kodu tarafından
yayınlanabilen, @EventListener/ApplicationListener ile dinlenebilen olay nesnesi.
Ek: Mini Proje — Feature Toggle Sistemi
Bu mini proje, @ConfigurationProperties (bir feature flag ailesi) ile
@ConditionalOnProperty (tek bir flag'in bütün bir bean'in var olup olmayacağına
karar vermesi) ve Component Scanning dersindeki @Primary'yi bir araya getiriyor:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.HashMap;
import java.util.Map;
// Mini project: a small feature-toggle system, tying together
// @ConfigurationProperties (a whole family of on/off switches, grouped under
// one prefix) with @ConditionalOnProperty (one specific feature deciding, at
// startup, whether an entire bean should exist at all).
@ConfigurationProperties(prefix = "app.features")
class FeatureToggles {
private Map<String, Boolean> flags = new HashMap<>();
public Map<String, Boolean> getFlags() { return flags; }
public void setFlags(Map<String, Boolean> flags) { this.flags = flags; }
boolean isEnabled(String feature) {
return flags.getOrDefault(feature, false);
}
}
interface RecommendationEngine {
String recommend(String userId);
}
@Configuration
@EnableConfigurationProperties(FeatureToggles.class)
class FeatureToggleConfig {
// Registered unconditionally -- always available, whatever the feature
// flags say.
@Bean
RecommendationEngine basicRecommendationEngine() {
return userId -> "Popular items for you, " + userId;
}
// Registered only when the property is explicitly turned on. @Primary
// (from the Component Scanning lesson) resolves the ambiguity when both
// beans exist: the AI engine wins any plain-type injection whenever it's
// present at all.
@Bean
@Primary
@ConditionalOnProperty(name = "app.features.ai-recommendations", havingValue = "true")
RecommendationEngine aiRecommendationEngine() {
return userId -> "AI-personalized picks for you, " + userId;
}
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
class FeatureToggleDemo {
public static void main(String[] args) {
// Two independent property paths under the same "app.features" prefix:
// "flags.ai-recommendations" binds into FeatureToggles' Map field (for
// the application's own bookkeeping/UI), while the flat
// "ai-recommendations" key separately drives @ConditionalOnProperty
// on the bean itself. They happen to carry the same value here, but
// they are two different mechanisms answering two different questions.
// Case 1: AI recommendations turned off -- only the basic engine exists.
AnnotationConfigApplicationContext offContext = new AnnotationConfigApplicationContext();
ConfigurableEnvironment offEnv = offContext.getEnvironment();
offEnv.getPropertySources().addFirst(new MapPropertySource("test", Map.of(
"app.features.flags.ai-recommendations", "false",
"app.features.ai-recommendations", "false"
)));
offContext.register(FeatureToggleConfig.class);
offContext.refresh();
System.out.println(offContext.getBean(FeatureToggles.class).isEnabled("ai-recommendations"));
// false
System.out.println(offContext.getBean(RecommendationEngine.class).recommend("user-42"));
// Popular items for you, user-42
offContext.close();
// Case 2: AI recommendations turned on -- both beans exist, @Primary
// decides which one wins the ambiguous injection.
AnnotationConfigApplicationContext onContext = new AnnotationConfigApplicationContext();
ConfigurableEnvironment onEnv = onContext.getEnvironment();
onEnv.getPropertySources().addFirst(new MapPropertySource("test", Map.of(
"app.features.flags.ai-recommendations", "true",
"app.features.ai-recommendations", "true"
)));
onContext.register(FeatureToggleConfig.class);
onContext.refresh();
System.out.println(onContext.getBean(FeatureToggles.class).isEnabled("ai-recommendations"));
// true
System.out.println(onContext.getBean(RecommendationEngine.class).recommend("user-42"));
// AI-personalized picks for you, user-42
onContext.close();
}
}
app.features.flags.* altındaki her anahtar FeatureToggles bean'inin flags
map'ine bağlanırken, app.features.ai-recommendations bambaşka bir mekanizmayla --
@ConditionalOnProperty ile -- bir bean'in var olup olmayacağına karar veriyor. İkisi
aynı prefix'i paylaşsa da, birbirinden tamamen bağımsız iki yol: biri bir Java
nesnesine bağlanan veri, diğeri container'ın kendisine "bu bean'i hiç oluşturma"
diyen bir koşul.
aiRecommendationEngine() üzerindeki @Primary, tam olarak Component Scanning
dersindeki "@Primary: Varsayılan Aday Belirlemek" bölümünde gördüğümüz mekanizma --
iki bean aynı anda var olduğunda, hangisinin varsayılan olarak kazanacağını
belirliyor.
Ek: Mini Proje — Bildirim Ayarları Yöneticisi
Son mini proje, bu dersin neredeyse tüm konularını bir araya getiriyor:
@ConfigurationProperties ile gruplanmış ayarlar, @Profile ile ortama özel
davranış, ve ayarlar yüklendiğinde yayınlanan bir ApplicationEvent:
import jakarta.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
// Mini project: a notification settings manager that ties together most of
// this lesson at once -- grouped settings via @ConfigurationProperties,
// environment-specific overrides via @Profile, and an event published once
// the settings are loaded, so other beans can react without depending on
// this one directly.
@ConfigurationProperties(prefix = "app.notifications")
class NotificationSettings {
private int retryAttempts = 3;
private long timeoutMillis = 2000;
public int getRetryAttempts() { return retryAttempts; }
public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; }
public long getTimeoutMillis() { return timeoutMillis; }
public void setTimeoutMillis(long timeoutMillis) { this.timeoutMillis = timeoutMillis; }
@Override
public String toString() {
return "NotificationSettings{retryAttempts=" + retryAttempts + ", timeoutMillis=" + timeoutMillis + "}";
}
}
class SettingsLoadedEvent extends ApplicationEvent {
private final NotificationSettings settings;
SettingsLoadedEvent(Object source, NotificationSettings settings) {
super(source);
this.settings = settings;
}
NotificationSettings getSettings() {
return settings;
}
}
@Component
class SettingsLoader {
private final NotificationSettings settings;
private final ApplicationEventPublisher publisher;
SettingsLoader(NotificationSettings settings, ApplicationEventPublisher publisher) {
this.settings = settings;
this.publisher = publisher;
}
@PostConstruct
void publishOnceLoaded() {
publisher.publishEvent(new SettingsLoadedEvent(this, settings));
}
}
@Component
class SettingsAuditListener {
@EventListener
void onSettingsLoaded(SettingsLoadedEvent event) {
System.out.println("Settings loaded: " + event.getSettings());
}
}
@Configuration
@EnableConfigurationProperties(NotificationSettings.class)
@ComponentScan
class NotificationSettingsConfig {
// A more patient retry policy only in production, layered on top of the
// defaults from application.yml -- @Profile deciding between two
// completely different Runnable strategies, the same idea as
// PaymentConfig earlier in this lesson.
@Bean
@Profile("prod")
Runnable slowRetryWarning() {
return () -> System.out.println("Production mode: retries will be slower and more patient.");
}
@Bean
@Profile("!prod")
Runnable fastRetryWarning() {
return () -> System.out.println("Non-production mode: retries are fast, for quicker feedback.");
}
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
class NotificationSettingsDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment environment = context.getEnvironment();
environment.getPropertySources().addFirst(new MapPropertySource("test", Map.of(
"app.notifications.retry-attempts", "5",
"app.notifications.timeout-millis", "5000"
)));
environment.setActiveProfiles("prod");
context.register(NotificationSettingsConfig.class);
context.refresh();
// Settings loaded: NotificationSettings{retryAttempts=5, timeoutMillis=5000}
context.getBean(Runnable.class).run();
// Production mode: retries will be slower and more patient.
context.close();
}
}
SettingsLoader, @PostConstruct ile (Spring IoC Container dersindeki
"@PostConstruct ve @PreDestroy" bölümü) ayarlar enjekte edildikten hemen sonra bir
SettingsLoadedEvent yayınlıyor -- SettingsAuditListener bu event'i, SettingsLoader
sınıfının varlığından bile haberdar olmadan dinliyor. prod profili aktifken
slowRetryWarning bean'i, başka herhangi bir profilde (!prod) ise
fastRetryWarning bean'i kaydediliyor -- ikisi asla aynı anda var olmuyor.
@Profile("!prod") gibi olumsuzlama ifadeleri kullanışlıdır, ama dikkatli
kullanılmalı: dev, test, ya da hiçbir profil aktif değilken bile !prod koşulu
sağlanır -- "prod olmayan her durum" ile "yalnızca dev" aynı şey değildir, ve bu
ikisini karıştırmak yanlış bean'in yanlış ortamda kaydedilmesine yol açabilir.