Spring Profiles
Giriş
Bir uygulamayı geliştirirken bilgisayarınızda H2 in-memory veritabanı kullanırsınız. Test sunucusunda MySQL'e bağlanırsınız. Production'da ise PostgreSQL cluster'ınıza bağlanmanız gerekir. Aynı kod, üç farklı ortamda üç farklı konfigürasyonla çalışmalıdır. Peki bunu nasıl yönetirsiniz? Her deploy'dan önce application.properties'i elle mi değiştirirsiniz? Hayır! Spring Profiles tam olarak bu sorunu çözer.
Gerçek Dünya Analojisi
Bir arabanın sürüş modlarını düşünün: Eco modda düşük güç tüketimi, Sport modda yüksek performans, Comfort modda dengeli sürüş. Araba aynı araba, motor aynı motor — ama "profil" değiştirdiğinizde davranış değişir. Spring Profiles de uygulamanızın sürüş modudur: dev profilinde debug loglar açık ve H2 veritabanı, prod profilinde loglar minimum ve PostgreSQL.
Neden Profiles Kullanmalıyız?
Kod değişikliği yapmadan ortam ayarlarını değiştirmek
Güvenlik: Production şifrelerini development ortamına sızdırmamak
Farklı implementasyonlar: dev'de yerel dosya sistemi, prod'da AWS S3
12-Factor App: "Store config in the environment" prensibi
CI/CD uyumu: Pipeline'da profil belirterek otomatik deployment
@Profile Annotation — Bean Bazında Kontrol
@Profile annotation'ı, bir bean'in sadece belirli profil aktifken oluşturulmasını sağlar. Aktif olmayan profildeki bean'ler Spring container'a hiç eklenmez.
Temel Kullanım
// StorageService interface'i — ortak sözleşme
public interface StorageService {
String store(String filename, byte[] data);
byte[] retrieve(String filename);
void delete(String filename);
}
// === Development ortamı: Yerel dosya sistemi ===
@Service
@Profile("dev")
public class LocalStorageService implements StorageService {
private final Path uploadDir = Paths.get("/tmp/uploads");
@PostConstruct
public void init() throws IOException {
Files.createDirectories(uploadDir);
System.out.println("📁 Local storage initialized: " + uploadDir);
}
@Override
public String store(String filename, byte[] data) {
try {
Path filePath = uploadDir.resolve(filename);
Files.write(filePath, data);
return filePath.toString();
} catch (IOException e) {
throw new StorageException("Failed to store file locally", e);
}
}
@Override
public byte[] retrieve(String filename) {
try {
return Files.readAllBytes(uploadDir.resolve(filename));
} catch (IOException e) {
throw new StorageException("File not found: " + filename, e);
}
}
@Override
public void delete(String filename) {
try {
Files.deleteIfExists(uploadDir.resolve(filename));
} catch (IOException e) {
throw new StorageException("Failed to delete: " + filename, e);
}
}
}
// === Production ortamı: AWS S3 ===
@Service
@Profile("prod")
public class S3StorageService implements StorageService {
private final AmazonS3 s3Client;
private final String bucketName;
public S3StorageService(AmazonS3 s3Client,
@Value("${aws.s3.bucket}") String bucketName) {
this.s3Client = s3Client;
this.bucketName = bucketName;
}
@Override
public String store(String filename, byte[] data) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(data.length);
s3Client.putObject(bucketName, filename,
new ByteArrayInputStream(data), metadata);
return "s3://" + bucketName + "/" + filename;
}
@Override
public byte[] retrieve(String filename) {
S3Object object = s3Client.getObject(bucketName, filename);
try {
return object.getObjectContent().readAllBytes();
} catch (IOException e) {
throw new StorageException("Failed to read from S3: " + filename, e);
}
}
@Override
public void delete(String filename) {
s3Client.deleteObject(bucketName, filename);
}
}Bu örnekte:
devprofili aktifken →LocalStorageServicebean oluşturulur, S3 oluşturulmazprodprofili aktifken →S3StorageServicebean oluşturulur, local oluşturulmazDiğer service'ler sadece
StorageServiceinterface'ine bağımlı — hangi implementasyon geldiğini bilmez
@Profile ile @Configuration
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public DataSource dataSource() {
// H2 in-memory veritabanı
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("test-data.sql")
.build();
}
@Bean
public JavaMailSender mailSender() {
// Fake mail sender — gerçek mail göndermez
return new JavaMailSenderImpl(); // localhost:25 (MailHog)
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public DataSource dataSource(
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
return new HikariDataSource(config);
}
}Profile-Specific Properties — Dosya Bazında Konfigürasyon
Her profil için ayrı properties/yml dosyası oluşturabilirsiniz. Spring Boot aktif profile göre doğru dosyayı otomatik yükler:
src/main/resources/
├── application.properties # Ortak ayarlar (tüm profillerde geçerli)
├── application-dev.properties # dev profili ayarları
├── application-test.properties # test profili ayarları
├── application-staging.properties # staging profili ayarları
└── application-prod.properties # prod profili ayarlarıOrtak Ayarlar — application.properties
# Tüm ortamlarda geçerli
app.name=MyApplication
app.version=1.0.0
server.port=8080
# Varsayılan profil
spring.profiles.active=devDevelopment — application-dev.properties
# Veritabanı: H2 in-memory
spring.datasource.url=jdbc:h2:mem:devdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Loglama: Detaylı
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.hibernate.SQL=DEBUG
# Özel ayarlar
app.feature.email-verification=false
app.storage.type=local
app.cors.allowed-origins=http://localhost:3000Production — application-prod.properties
# Veritabanı: PostgreSQL
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
# Loglama: Minimum
logging.level.root=WARN
logging.level.com.example=INFO
# Güvenlik
app.feature.email-verification=true
app.storage.type=s3
app.cors.allowed-origins=https://myapp.com,https://www.myapp.com
# Performans
server.tomcat.max-threads=200
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5YAML Alternatifi
Tek dosyada çoklu profil tanımı (Spring Boot 2.4+):
# application.yml
app:
name: MyApplication
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
---
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL}
jpa:
show-sql: false
hibernate:
ddl-auto: validate⚠️ Dikkat: Tek dosyada çoklu profil tanımı (--- ayracı ile) okunabilirliği düşürür. Büyük projelerde ayrı dosyalar tercih edin.
Profil Aktifleştirme Yolları
1. application.properties ile (Geliştirme için)
spring.profiles.active=dev2. JVM Argument ile (IDE ve jar çalıştırma)
# IDE'de VM Options'a ekleyin
-Dspring.profiles.active=prod
# JAR çalıştırırken
java -jar myapp.jar --spring.profiles.active=prod
# Alternatif
java -Dspring.profiles.active=prod -jar myapp.jar3. Ortam Değişkeni ile (Docker ve CI/CD için ideal)
# Linux/Mac
export SPRING_PROFILES_ACTIVE=prod
java -jar myapp.jar
# Docker
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest
# Docker Compose
services:
app:
image: myapp:latest
environment:
- SPRING_PROFILES_ACTIVE=prod
- DATABASE_URL=jdbc:postgresql://db:5432/proddb4. Maven ile (Build sırasında)
mvn spring-boot:run -Dspring-boot.run.profiles=dev5. Programmatik Olarak
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApp.class);
app.setAdditionalProfiles("dev", "swagger");
app.run(args);
}Öncelik Sırası (Yüksekten düşüğe)
--spring.profiles.active=prod(command line argument)SPRING_PROFILES_ACTIVE=prod(environment variable)-Dspring.profiles.active=prod(JVM system property)spring.profiles.active=prod(application.properties)@ActiveProfiles("test")(test annotation)
💡 İpucu: Production'da asla application.properties içinde profil belirtmeyin. Ortam değişkeni veya command line argument kullanın. Böylece aynı JAR dosyası her ortamda çalışır.
Birden Fazla Profil ve Profile Groups
Çoklu Profil Aktifleştirme
# Virgülle ayırarak birden fazla profil aktifleştirin
spring.profiles.active=dev,metrics,swagger// Bu bean, "metrics" profili aktifken oluşturulur
@Configuration
@Profile("metrics")
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "myapp");
}
}Profile Groups (Spring Boot 2.4+)
Birden fazla profili gruplayarak tek bir profil adıyla aktifleştirebilirsiniz:
# application.properties
spring.profiles.group.development=dev,swagger,debug,h2
spring.profiles.group.production=prod,metrics,security
spring.profiles.group.staging=staging,metrics
# Kullanım:
# spring.profiles.active=development → dev + swagger + debug + h2 hepsi aktif olur
# spring.profiles.active=production → prod + metrics + security hepsi aktif olurProfile Negation ve Çoklu Profile Eşleştirme
// "prod" profili aktif DEĞİLKEN — geliştirme araçları
@Configuration
@Profile("!prod")
public class DevToolsConfig {
@Bean
public CommandLineRunner seedDatabase(UserRepository repo) {
return args -> {
repo.save(new User("Test User", "test@example.com"));
repo.save(new User("Admin", "admin@example.com"));
System.out.println("🌱 Test verileri yüklendi");
};
}
}
// "dev" VEYA "test" profilinde
@Service
@Profile({"dev", "test"})
public class MockPaymentService implements PaymentService {
@Override
public PaymentResult charge(PaymentRequest request) {
System.out.println("💳 Mock ödeme: " + request.getAmount());
return PaymentResult.success("MOCK-TXN-" + System.currentTimeMillis());
}
}
// "prod" VE "eu-west" profillerinin ikisi de aktifken
// NOT: @Profile tek annotation ile AND yapamaz
// Çözüm: @Conditional veya iç içe configuration kullanın
@Configuration
@Profile("prod")
public class ProdConfig {
@Configuration
@Profile("eu-west")
static class EuWestConfig {
@Bean
public DataSource euWestDataSource() {
// EU-West bölgesine özgü DataSource
return createDataSource("eu-west-db.example.com");
}
}
}Default Profile
Hiçbir profil aktif değilken default profili otomatik aktiftir:
@Service
@Profile("default")
public class DefaultStorageService implements StorageService {
// Hiçbir profil belirtilmemişse bu bean oluşturulur
@Override
public String store(String filename, byte[] data) {
System.out.println("⚠️ Default storage — hiçbir profil aktif değil!");
// Basit yerel depolama
return "/tmp/" + filename;
}
}⚠️ Dikkat: spring.profiles.active=dev belirlerseniz, default profili devre dışı kalır. @Profile("default") bean'leri sadece hiçbir profil aktif olmadığında oluşturulur.
Test'lerde Profile Kullanımı
// @ActiveProfiles ile test profilini aktifleştirin
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {
@Autowired
private UserService userService; // Test profili bean'leri kullanılır
@Test
void shouldCreateUser() {
// test profili aktif → H2 veritabanı, mock mail service...
}
}
// Integration test için farklı profil
@SpringBootTest
@ActiveProfiles({"test", "integration"})
class IntegrationTest { ... }# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
app.feature.email-verification=falseYaygın Hatalar ve Çözümleri
Hata 1: Profile Oluşturmayı Unutmak
// ❌ @Profile("dev") ve @Profile("prod") var ama "test" profili için implementasyon yok
// Test'lerde: NoSuchBeanDefinitionException!
// ✅ Her profil için bean olduğundan emin olun veya @Profile("!prod") kullanın
@Service
@Profile({"dev", "test"}) // Hem dev hem test'te çalışır
public class LocalStorageService implements StorageService { ... }Hata 2: application-profil.properties Dosya Adını Yanlış Yazmak
// ❌ YANLIŞ dosya adları
application.dev.properties // Tire eksik
application-Dev.properties // Büyük harf (case-sensitive!)
dev-application.properties // Yanlış sıra
// ✅ DOĞRU format
application-dev.propertiesHata 3: Prod Şifreleri application.properties'e Yazmak
# ❌ TEHLİKELİ — Git'e commit edilir!
spring.datasource.password=SuperSecretPassword123
# ✅ GÜVENLİ — Environment variable kullanın
spring.datasource.password=${DB_PASSWORD}Hata 4: Profil Test'te Kontrol Etmemek
// ❌ Profilin gerçekten aktif olduğunu test etmeyin → sessiz hata
// ✅ @ActiveProfiles mutlaka belirtin ve kontrol edin
@SpringBootTest
@ActiveProfiles("test")
class MyTest {
@Autowired
private Environment env;
@Test
void profileShouldBeActive() {
assertTrue(Arrays.asList(env.getActiveProfiles()).contains("test"));
}
}Özet
Spring Profiles aynı kodu farklı ortamlarda farklı konfigürasyonlarla çalıştırır
`@Profile("dev")` ile bean bazında, `application-dev.properties` ile konfigürasyon bazında ortam ayrımı
Profil aktifleştirme: environment variable (
SPRING_PROFILES_ACTIVE) production için en güvenli yoldurProfile groups ile birden fazla profili tek isimle aktifleştirebilirsiniz
`!prod` negation ile "production hariç tüm ortamlarda" mantığı kurabilirsiniz
Test'lerde
@ActiveProfiles("test")kullanınProduction şifrelerini asla properties dosyasına yazmayın — environment variable veya secret manager kullanın
Bütünleşik Gerçek Dünya Örneği: Bildirim Servisi
Farklı ortamlarda farklı bildirim stratejileri kullanan tam bir örnek:
// === Interface ===
public interface NotificationService {
void sendNotification(String recipient, String message);
}
// === Development: Konsola yazdır ===
@Service
@Profile("dev")
@Slf4j
public class ConsoleNotificationService implements NotificationService {
@Override
public void sendNotification(String recipient, String message) {
log.info("📧 [DEV] Bildirim → {}: {}", recipient, message);
// Gerçek e-posta göndermez, sadece loglar
}
}
// === Test: Hafızada sakla (doğrulama için) ===
@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {
private final List<Notification> sentNotifications = new ArrayList<>();
@Override
public void sendNotification(String recipient, String message) {
sentNotifications.add(new Notification(recipient, message, Instant.now()));
}
// Test'lerde kullanmak için
public List<Notification> getSentNotifications() {
return Collections.unmodifiableList(sentNotifications);
}
public void clear() {
sentNotifications.clear();
}
public record Notification(String recipient, String message, Instant sentAt) {}
}
// === Production: Gerçek e-posta gönder ===
@Service
@Profile("prod")
@RequiredArgsConstructor
@Slf4j
public class EmailNotificationService implements NotificationService {
private final JavaMailSender mailSender;
@Value("${app.email.from}")
private String fromAddress;
@Override
public void sendNotification(String recipient, String message) {
try {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setFrom(fromAddress);
mail.setTo(recipient);
mail.setSubject("Bildirim");
mail.setText(message);
mailSender.send(mail);
log.info("✅ E-posta gönderildi: {}", recipient);
} catch (Exception e) {
log.error("❌ E-posta gönderilemedi: {}", recipient, e);
throw new NotificationException("Failed to send email", e);
}
}
}// Bu service hangi profil aktif olursa olsun aynı şekilde çalışır
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final NotificationService notificationService; // Profil'e göre farklı impl
@Transactional
public Order placeOrder(OrderRequest request) {
Order order = new Order(request);
orderRepository.save(order);
// Dev'de konsola yazdırır, prod'da e-posta gönderir
notificationService.sendNotification(
request.getEmail(),
"Siparişiniz alındı: " + order.getOrderNumber()
);
return order;
}
}Bu pattern'in güzelliği: OrderService hiçbir zaman hangi notification implementasyonunun kullanıldığını bilmez. Profile göre doğru bean otomatik enjekte edilir. Single Responsibility ve Open/Closed prensipleri mükemmel uygulanmış olur.
Profil Yönetimi Best Practices
Profil adlarını kısa ve tutarlı tutun:
dev,test,staging,prodOrtak ayarları
application.properties'te, ortama özgü ayarları profil dosyalarında tutunProfile group kullanarak ilgili profilleri gruplayın:
development = dev,swagger,debugHassas bilgileri (şifre, API key) environment variable ile verin
Her profili test edin — "prod'da çalışır dev'de çalışmaz" en tehlikeli durumlardan biridir
Profil sayısını sınırlayın — 4-5 profil yeterli, fazlası yönetilemez karmaşıklık yaratır
AI Asistan
Sorularını yanıtlamaya hazır