# Spring Boot Integration

Use KeyEnv with Spring Boot applications for secure secrets management.

Source: https://keyenv.dev/docs/sdks/spring-boot/

Integrate KeyEnv with your Spring Boot application to securely manage secrets like database credentials, API keys, and external service configurations.

## Features

- Auto-configuration with Spring Boot starter
- PropertySource integration for seamless `@Value` injection
- Environment-aware configuration via `spring.profiles.active`
- Type-safe `@ConfigurationProperties` binding
- Works with Spring Cloud Config
- Caching for optimal performance

## Installation

### Maven

Add the JitPack repository and dependency to your `pom.xml`:

```xml
<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependency>
    <groupId>com.github.keyenv</groupId>
    <artifactId>java-sdk</artifactId>
    <version>v1.1.0</version>
</dependency>
```

### Gradle

```groovy
repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.keyenv:java-sdk:v1.1.0'
}
```

## Quick Start

The simplest approach loads secrets into system properties at application startup:

```java
// Application.java
import dev.keyenv.KeyEnv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import jakarta.annotation.PostConstruct;

@SpringBootApplication
public class Application {

    @PostConstruct
    public void loadSecrets() {
        String token = System.getenv("KEYENV_TOKEN");
        String projectId = System.getenv("KEYENV_PROJECT");
        String environment = System.getenv("KEYENV_ENV");

        if (token != null && projectId != null) {
            KeyEnv client = KeyEnv.create(token);
            int count = client.loadEnv(projectId, environment != null ? environment : "development");
            System.out.println("Loaded " + count + " secrets from KeyEnv");
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

Now use secrets in your configuration:

```java
@Service
public class PaymentService {

    @Value("${STRIPE_SECRET_KEY}")
    private String stripeSecretKey;

    // Use the secret...
}
```

## Application Configuration

### application.properties

Configure your application using secrets loaded from KeyEnv:

```properties
# application.properties

# Database (values come from KeyEnv)
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}

# Redis
spring.data.redis.url=${REDIS_URL}

# External APIs
app.stripe.secret-key=${STRIPE_SECRET_KEY}
app.sendgrid.api-key=${SENDGRID_API_KEY}
```

### application.yml

```yaml
# application.yml

spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  data:
    redis:
      url: ${REDIS_URL}

app:
  stripe:
    secret-key: ${STRIPE_SECRET_KEY}
  sendgrid:
    api-key: ${SENDGRID_API_KEY}
```

## Environment-Aware Configuration

### Mapping Spring Profiles to KeyEnv Environments

Map `spring.profiles.active` to KeyEnv environments:

```java
import dev.keyenv.KeyEnv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import jakarta.annotation.PostConstruct;

@SpringBootApplication
public class Application {

    private final Environment springEnv;

    public Application(Environment springEnv) {
        this.springEnv = springEnv;
    }

    @PostConstruct
    public void loadSecrets() {
        String token = System.getenv("KEYENV_TOKEN");
        String projectId = System.getenv("KEYENV_PROJECT");

        if (token == null || projectId == null) {
            return; // Skip in local development
        }

        // Map Spring profile to KeyEnv environment
        String keyenvEnv = mapProfileToEnvironment();

        KeyEnv client = KeyEnv.create(token);
        client.loadEnv(projectId, keyenvEnv);
    }

    private String mapProfileToEnvironment() {
        String[] profiles = springEnv.getActiveProfiles();

        if (profiles.length == 0) {
            return "development";
        }

        String profile = profiles[0];
        return switch (profile) {
            case "prod", "production" -> "production";
            case "staging", "stage" -> "staging";
            case "test", "testing" -> "testing";
            default -> "development";
        };
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

### Profile-Specific Loading

Load different secrets based on Spring profiles:

```java
@Configuration
@Profile("!local") // Don't load from KeyEnv in local profile
public class KeyEnvConfig {

    @Value("${spring.profiles.active:development}")
    private String activeProfile;

    @PostConstruct
    public void loadSecrets() {
        String token = System.getenv("KEYENV_TOKEN");
        String projectId = System.getenv("KEYENV_PROJECT");

        if (token != null && projectId != null) {
            KeyEnv client = KeyEnv.create(token);
            client.loadEnv(projectId, activeProfile);
        }
    }
}
```

## PropertySource Integration

### Custom PropertySource

Create a custom PropertySource to integrate KeyEnv with Spring's property resolution:

```java
import dev.keyenv.KeyEnv;
import org.springframework.core.env.PropertySource;
import java.util.Map;

public class KeyEnvPropertySource extends PropertySource<Map<String, String>> {

    public KeyEnvPropertySource(String name, Map<String, String> source) {
        super(name, source);
    }

    @Override
    public Object getProperty(String name) {
        return source.get(name);
    }

    public static KeyEnvPropertySource create(KeyEnv client, String projectId, String environment) {
        Map<String, String> secrets = client.exportSecretsAsMap(projectId, environment);
        return new KeyEnvPropertySource("keyenv", secrets);
    }
}
```

### Register PropertySource Early

Register the PropertySource before Spring processes configuration:

```java
import dev.keyenv.KeyEnv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.addInitializers(new KeyEnvInitializer());
        app.run(args);
    }
}

class KeyEnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        String token = System.getenv("KEYENV_TOKEN");
        String projectId = System.getenv("KEYENV_PROJECT");
        String environment = System.getenv("KEYENV_ENV");

        if (token == null || projectId == null) {
            return;
        }

        KeyEnv client = KeyEnv.create(token);
        KeyEnvPropertySource propertySource = KeyEnvPropertySource.create(
            client,
            projectId,
            environment != null ? environment : "development"
        );

        ConfigurableEnvironment env = context.getEnvironment();
        env.getPropertySources().addFirst(propertySource);
    }
}
```

## ConfigurationProperties Binding

### Type-Safe Configuration

Use `@ConfigurationProperties` for type-safe secret binding:

```java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {

    private Stripe stripe = new Stripe();
    private Database database = new Database();
    private Redis redis = new Redis();
    private SendGrid sendgrid = new SendGrid();

    // Getters and setters...

    public static class Stripe {
        private String secretKey;
        private String webhookSecret;

        // Getters and setters...
        public String getSecretKey() { return secretKey; }
        public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
        public String getWebhookSecret() { return webhookSecret; }
        public void setWebhookSecret(String webhookSecret) { this.webhookSecret = webhookSecret; }
    }

    public static class Database {
        private String url;
        private String username;
        private String password;

        // Getters and setters...
        public String getUrl() { return url; }
        public void setUrl(String url) { this.url = url; }
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }

    public static class Redis {
        private String url;
        private String password;

        // Getters and setters...
        public String getUrl() { return url; }
        public void setUrl(String url) { this.url = url; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }

    public static class SendGrid {
        private String apiKey;

        public String getApiKey() { return apiKey; }
        public void setApiKey(String apiKey) { this.apiKey = apiKey; }
    }

    // Root getters and setters
    public Stripe getStripe() { return stripe; }
    public void setStripe(Stripe stripe) { this.stripe = stripe; }
    public Database getDatabase() { return database; }
    public void setDatabase(Database database) { this.database = database; }
    public Redis getRedis() { return redis; }
    public void setRedis(Redis redis) { this.redis = redis; }
    public SendGrid getSendgrid() { return sendgrid; }
    public void setSendgrid(SendGrid sendgrid) { this.sendgrid = sendgrid; }
}
```

### Mapping Secrets to Properties

In your `application.yml`, map KeyEnv secrets to configuration properties:

```yaml
# application.yml
app:
  stripe:
    secret-key: ${STRIPE_SECRET_KEY}
    webhook-secret: ${STRIPE_WEBHOOK_SECRET}
  database:
    url: ${DATABASE_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  redis:
    url: ${REDIS_URL}
    password: ${REDIS_PASSWORD:}
  sendgrid:
    api-key: ${SENDGRID_API_KEY}
```

### Using Configuration Properties

```java
@Service
public class PaymentService {

    private final AppProperties properties;

    public PaymentService(AppProperties properties) {
        this.properties = properties;
    }

    public void processPayment() {
        Stripe.apiKey = properties.getStripe().getSecretKey();
        // Process payment...
    }
}
```

## Complete Example

Here's a complete Spring Boot application with KeyEnv integration:

### Project Structure

```
src/main/java/com/example/
├── Application.java
├── config/
│   ├── KeyEnvConfig.java
│   ├── KeyEnvPropertySource.java
│   └── AppProperties.java
├── service/
│   └── UserService.java
└── controller/
    └── UserController.java
```

### Application.java

```java
package com.example;

import com.example.config.KeyEnvPropertySource;
import dev.keyenv.KeyEnv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

@SpringBootApplication
@EnableConfigurationProperties
public class Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.addInitializers(new KeyEnvInitializer());
        app.run(args);
    }

    static class KeyEnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext context) {
            String token = System.getenv("KEYENV_TOKEN");
            String projectId = System.getenv("KEYENV_PROJECT");

            if (token == null || projectId == null) {
                System.out.println("KeyEnv not configured, using local environment");
                return;
            }

            // Determine environment from Spring profile or KEYENV_ENV
            ConfigurableEnvironment env = context.getEnvironment();
            String keyenvEnv = System.getenv("KEYENV_ENV");
            if (keyenvEnv == null) {
                String[] profiles = env.getActiveProfiles();
                keyenvEnv = profiles.length > 0 ? mapProfile(profiles[0]) : "development";
            }

            try {
                KeyEnv client = KeyEnv.create(token);
                var secrets = client.exportSecretsAsMap(projectId, keyenvEnv);
                env.getPropertySources().addFirst(new KeyEnvPropertySource("keyenv", secrets));
                System.out.println("Loaded " + secrets.size() + " secrets from KeyEnv (" + keyenvEnv + ")");
            } catch (Exception e) {
                System.err.println("Failed to load secrets from KeyEnv: " + e.getMessage());
            }
        }

        private String mapProfile(String profile) {
            return switch (profile) {
                case "prod", "production" -> "production";
                case "staging", "stage" -> "staging";
                case "test" -> "testing";
                default -> "development";
            };
        }
    }
}
```

### application.yml

```yaml
# application.yml
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:development}

  # Database
  datasource:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/myapp}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5

  # JPA
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

  # Redis
  data:
    redis:
      url: ${REDIS_URL:redis://localhost:6379}

# Application secrets
app:
  stripe:
    secret-key: ${STRIPE_SECRET_KEY:}
    webhook-secret: ${STRIPE_WEBHOOK_SECRET:}
    publishable-key: ${STRIPE_PUBLISHABLE_KEY:}
  sendgrid:
    api-key: ${SENDGRID_API_KEY:}
  jwt:
    secret: ${JWT_SECRET:dev-secret-change-in-production}
    expiration: 86400000
```

### UserService.java

```java
package com.example.service;

import com.example.config.AppProperties;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final AppProperties properties;

    public UserService(AppProperties properties) {
        this.properties = properties;
    }

    public void sendWelcomeEmail(String email) {
        // Use SendGrid API key from KeyEnv
        String apiKey = properties.getSendgrid().getApiKey();
        // Send email...
    }
}
```

## Setting Up Your Deployment

### Required Environment Variables

Set these in your deployment platform (not in KeyEnv):

| Variable | Description |
|----------|-------------|
| `KEYENV_TOKEN` | Your KeyEnv service token |
| `KEYENV_PROJECT` | Your KeyEnv project ID |
| `KEYENV_ENV` | KeyEnv environment (optional, derived from Spring profile) |
| `SPRING_PROFILES_ACTIVE` | Spring profile (production, staging, etc.) |

### Secrets to Store in KeyEnv

Store these secrets in KeyEnv for each environment:

| Secret | Description | Example |
|--------|-------------|---------|
| `DATABASE_URL` | JDBC connection URL | `jdbc:postgresql://host:5432/db` |
| `DB_USER` | Database username | `myapp` |
| `DB_PASSWORD` | Database password | `secure-password` |
| `REDIS_URL` | Redis connection URL | `redis://localhost:6379` |
| `JWT_SECRET` | JWT signing secret | `your-256-bit-secret` |
| `STRIPE_SECRET_KEY` | Stripe API key | `sk_live_...` |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook secret | `whsec_...` |
| `SENDGRID_API_KEY` | SendGrid API key | `SG.xxx` |

## Docker Deployment

### Dockerfile

```dockerfile
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app
COPY target/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
```

### docker-compose.yml

```yaml
version: '3.8'

services:
  app:
    build: .
    environment:
      - KEYENV_TOKEN=${KEYENV_TOKEN}
      - KEYENV_PROJECT=${KEYENV_PROJECT}
      - SPRING_PROFILES_ACTIVE=production
    ports:
      - "8080:8080"
```

## Local Development

### Skip KeyEnv in Development

For local development, you can skip KeyEnv entirely:

```java
@PostConstruct
public void loadSecrets() {
    String token = System.getenv("KEYENV_TOKEN");
    if (token == null) {
        System.out.println("KEYENV_TOKEN not set, using local configuration");
        return;
    }
    // Load from KeyEnv...
}
```

### Use .env File Locally

Generate a `.env` file for local development:

```bash
keyenv pull --env development --output .env
```

Load it with a library like [dotenv-java](https://github.com/cdimascio/dotenv-java):

```java
import io.github.cdimascio.dotenv.Dotenv;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        // Load .env in development
        if (System.getenv("KEYENV_TOKEN") == null) {
            Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
            dotenv.entries().forEach(e ->
                System.setProperty(e.getKey(), e.getValue())
            );
        }
        SpringApplication.run(Application.class, args);
    }
}
```

## Testing

### Unit Tests

Mock the configuration properties in tests:

```java
@SpringBootTest
@TestPropertySource(properties = {
    "app.stripe.secret-key=sk_test_xxx",
    "app.jwt.secret=test-secret"
})
class PaymentServiceTest {

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldProcessPayment() {
        // Test with mocked properties
    }
}
```

### Integration Tests

Use test-specific configuration:

```yaml
# application-test.yml
app:
  stripe:
    secret-key: sk_test_xxx
  database:
    url: jdbc:h2:mem:testdb
```

```java
@SpringBootTest
@ActiveProfiles("test")
class IntegrationTest {
    // Tests run with test configuration, not KeyEnv
}
```

## Troubleshooting

### "KEYENV_TOKEN not set"

In production, ensure `KEYENV_TOKEN` is set in your deployment platform's environment variables.

### "Project not found"

Verify `KEYENV_PROJECT` matches your project ID in the KeyEnv dashboard.

### "Environment not found"

Check that the environment name (development, staging, production) exists in your KeyEnv project.

### Secrets not available in @Value

1. Ensure secrets are loaded before Spring processes `@Value` annotations (use `ApplicationContextInitializer`)
2. Check that the secret key matches exactly (case-sensitive)
3. Verify the service token has read access to the environment

### DataSource fails to initialize

Database properties must be available before Spring creates the DataSource. Use the `ApplicationContextInitializer` approach to load secrets early enough.

### Performance

Secrets are loaded once at startup and cached. There's no runtime API calls after initialization. For optimal performance in containerized environments, consider enabling caching:

```java
KeyEnv client = KeyEnv.builder()
    .token(token)
    .cacheTtl(Duration.ofMinutes(5))
    .build();
```
