Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot-dependency-injection/references/examples.md
2025-11-29 18:28:34 +08:00

14 KiB

Spring Boot Dependency Injection - Examples

Comprehensive examples demonstrating dependency injection patterns, from basic to advanced scenarios.

The preferred pattern for mandatory dependencies.

// With Lombok @RequiredArgsConstructor (RECOMMENDED)
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final PasswordEncoder passwordEncoder;

    public User registerUser(CreateUserRequest request) {
        log.info("Registering user: {}", request.getEmail());
        
        User user = User.builder()
            .email(request.getEmail())
            .name(request.getName())
            .password(passwordEncoder.encode(request.getPassword()))
            .build();
        
        User saved = userRepository.save(user);
        emailService.sendWelcomeEmail(saved.getEmail());
        
        return saved;
    }
}

// Without Lombok (Explicit)
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository,
                      EmailService emailService,
                      PasswordEncoder passwordEncoder) {
        this.userRepository = Objects.requireNonNull(userRepository);
        this.emailService = Objects.requireNonNull(emailService);
        this.passwordEncoder = Objects.requireNonNull(passwordEncoder);
    }

    public User registerUser(CreateUserRequest request) {
        // Implementation
    }
}

Test (Easy - No Spring Needed)

@Test
void shouldRegisterUserAndSendEmail() {
    // Arrange - Create mocks manually
    UserRepository mockRepository = mock(UserRepository.class);
    EmailService mockEmailService = mock(EmailService.class);
    PasswordEncoder mockEncoder = mock(PasswordEncoder.class);
    
    UserService service = new UserService(mockRepository, mockEmailService, mockEncoder);
    
    User user = User.builder().email("test@example.com").build();
    when(mockRepository.save(any())).thenReturn(user);
    when(mockEncoder.encode("password")).thenReturn("encoded");

    // Act
    User result = service.registerUser(new CreateUserRequest("test@example.com", "Test", "password"));

    // Assert
    assertThat(result.getEmail()).isEqualTo("test@example.com");
    verify(mockEmailService).sendWelcomeEmail("test@example.com");
}

Example 2: Setter Injection for Optional Dependencies

Use setter injection ONLY for optional dependencies with sensible defaults.

@Service
public class ReportService {
    private final ReportRepository reportRepository;
    private EmailService emailService;  // Optional
    private CacheService cacheService;   // Optional

    // Constructor for mandatory dependency
    public ReportService(ReportRepository reportRepository) {
        this.reportRepository = Objects.requireNonNull(reportRepository);
    }

    // Setters for optional dependencies
    @Autowired(required = false)
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }

    @Autowired(required = false)
    public void setCacheService(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    public Report generateReport(ReportRequest request) {
        Report report = reportRepository.create(request.getTitle());

        // Use optional services if available
        if (emailService != null) {
            emailService.sendReport(report);
        }

        if (cacheService != null) {
            cacheService.cache(report);
        }

        return report;
    }
}

Example 3: Configuration with Multiple Bean Definitions

@Configuration
public class AppConfig {

    // Bean 1: Database
    @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);
        
        return new HikariDataSource(config);
    }

    // Bean 2: Transaction Manager (depends on DataSource)
    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    // Bean 3: Repository (depends on DataSource via JPA)
    @Bean
    public UserRepository userRepository(UserJpaRepository jpaRepository) {
        return new UserRepositoryAdapter(jpaRepository);
    }

    // Bean 4: Service (depends on Repository)
    @Bean
    public UserService userService(UserRepository repository) {
        return new UserService(repository);
    }
}

Example 4: Resolving Ambiguities with @Qualifier

@Configuration
public class DataSourceConfig {
    
    @Bean(name = "primaryDB")
    public DataSource primaryDataSource() {
        return new HikariDataSource();
    }

    @Bean(name = "secondaryDB")
    public DataSource secondaryDataSource() {
        return new HikariDataSource();
    }
}

@Service
public class MultiDatabaseService {
    private final DataSource primaryDataSource;
    private final DataSource secondaryDataSource;

    // Using @Qualifier to resolve ambiguity
    public MultiDatabaseService(
            @Qualifier("primaryDB") DataSource primary,
            @Qualifier("secondaryDB") DataSource secondary) {
        this.primaryDataSource = primary;
        this.secondaryDataSource = secondary;
    }

    public void performOperation() {
        // Use primary for writes
        executeUpdate(primaryDataSource);
        
        // Use secondary for reads
        executeQuery(secondaryDataSource);
    }
}

// Alternative: Using @Primary
@Configuration
public class PrimaryDataSourceConfig {
    
    @Bean
    @Primary  // This bean is preferred when multiple exist
    public DataSource primaryDataSource() {
        return new HikariDataSource();
    }

    @Bean
    public DataSource secondaryDataSource() {
        return new HikariDataSource();
    }
}

Example 5: Conditional Bean Registration

@Configuration
public class OptionalFeatureConfig {

    // Only create if feature is enabled
    @Bean
    @ConditionalOnProperty(name = "feature.notifications.enabled", havingValue = "true")
    public NotificationService notificationService() {
        return new EmailNotificationService();
    }

    // Fallback if no other bean exists
    @Bean
    @ConditionalOnMissingBean(NotificationService.class)
    public NotificationService defaultNotificationService() {
        return new NoOpNotificationService();
    }

    // Only create if class is on classpath
    @Bean
    @ConditionalOnClass(RedisTemplate.class)
    public CacheService cacheService() {
        return new RedisCacheService();
    }
}

@Service
public class OrderService {
    private final NotificationService notificationService;

    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;  // Works regardless of implementation
    }

    public void createOrder(Order order) {
        // Always works, but behavior depends on enabled features
        notificationService.sendConfirmation(order);
    }
}

Example 6: Profiles and Environment-Specific Configuration

@Configuration
@Profile("production")
public class ProductionConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://prod-db:5432/production");
        config.setMaximumPoolSize(30);
        config.setMaxLifetime(1800000);  // 30 minutes
        return new HikariDataSource(config);
    }

    @Bean
    public SecurityService securityService() {
        return new StrictSecurityService();
    }
}

@Configuration
@Profile("test")
public class TestConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
    }

    @Bean
    public SecurityService securityService() {
        return new PermissiveSecurityService();
    }
}

@Configuration
@Profile("development")
public class DevelopmentConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/dev");
        return new HikariDataSource(config);
    }

    @Bean
    public SecurityService securityService() {
        return new DebugSecurityService();
    }
}

Usage:

export SPRING_PROFILES_ACTIVE=production
# or in application.properties:
# spring.profiles.active=production

Example 7: Lazy Initialization

@Configuration
public class ExpensiveResourceConfig {

    @Bean
    @Lazy  // Created only when first accessed
    public ExpensiveService expensiveService() {
        System.out.println("ExpensiveService initialized (lazy)");
        return new ExpensiveService();
    }

    @Bean
    public NormalService normalService(ExpensiveService expensive) {
        // ExpensiveService not created yet
        return new NormalService(expensive);  // Lazy proxy injected here
    }
}

@SpringBootTest
class LazyInitializationTest {
    @Test
    void shouldInitializeExpensiveServiceLazy() {
        ApplicationContext context = new AnnotationConfigApplicationContext(ExpensiveResourceConfig.class);
        
        // ExpensiveService not initialized yet
        assertThat(context.getBean(NormalService.class)).isNotNull();
        
        // Now ExpensiveService is initialized
        ExpensiveService service = context.getBean(ExpensiveService.class);
        assertThat(service).isNotNull();
    }
}

Example 8: Circular Dependency Resolution with Events

// ❌ BAD - Circular dependency
@Service
public class UserService {
    private final OrderService orderService;
    
    public UserService(OrderService orderService) {
        this.orderService = orderService;  // Circular!
    }
}

@Service
public class OrderService {
    private final UserService userService;
    
    public OrderService(UserService userService) {
        this.userService = userService;  // Circular!
    }
}

// ✅ GOOD - Use events to decouple
public class UserRegisteredEvent extends ApplicationEvent {
    private final String userId;
    
    public UserRegisteredEvent(Object source, String userId) {
        super(source);
        this.userId = userId;
    }
}

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;

    public User registerUser(CreateUserRequest request) {
        User user = userRepository.save(User.create(request));
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId()));
        return user;
    }
}

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;

    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        // Create welcome order when user registers
        Order welcomeOrder = Order.createWelcomeOrder(event.getUserId());
        orderRepository.save(welcomeOrder);
    }
}

Example 9: Component Scanning

@Configuration
@ComponentScan(basePackages = {
    "com.example.users",
    "com.example.products",
    "com.example.orders"
})
public class AppConfig {
}

// Alternative: Exclude packages
@Configuration
@ComponentScan(basePackages = "com.example",
    excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX,
        pattern = "com\\.example\\.internal\\..*"))
public class AppConfig {
}

// Auto-discovered by Spring Boot
@SpringBootApplication  // Implies @ComponentScan("package.of.main.class")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Example 10: Testing with Constructor Injection

// ❌ Service with field injection (hard to test)
@Service
public class BadUserService {
    @Autowired
    private UserRepository userRepository;
    
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

@Test
void testBadService() {
    // Must use Spring to test this
    UserService service = new BadUserService();
    // Can't inject mocks without reflection or Spring
}

// ✅ Service with constructor injection (easy to test)
@Service
@RequiredArgsConstructor
public class GoodUserService {
    private final UserRepository userRepository;
    
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

@Test
void testGoodService() {
    // Can test directly without Spring
    UserRepository mockRepository = mock(UserRepository.class);
    UserService service = new GoodUserService(mockRepository);
    
    User mockUser = new User(1L, "Test");
    when(mockRepository.findById(1L)).thenReturn(Optional.of(mockUser));
    
    User result = service.getUser(1L);
    assertThat(result.getName()).isEqualTo("Test");
}

// Integration test
@SpringBootTest
@ActiveProfiles("test")
class UserServiceIntegrationTest {
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFetchUserFromDatabase() {
        User user = User.create("test@example.com");
        userRepository.save(user);
        
        User retrieved = userService.getUser(user.getId());
        assertThat(retrieved.getEmail()).isEqualTo("test@example.com");
    }
}

These examples cover constructor injection (recommended), setter injection (optional dependencies), configuration, testing patterns, and common best practices for dependency injection in Spring Boot.