Este documento detalla los conceptos fundamentales de los Beans, la Inversión de Control (IoC) y la Inyección de Dependencias (DI) en el ecosistema de Spring, utilizando ejemplos modernos con Java 21.
En el contexto de Spring Framework, un Bean es un objeto que es instanciado, ensamblado y gestionado por el contenedor de Spring (IoC Container).
- Java Bean (Clásico): Una clase simple que sigue ciertas convenciones (serializable, constructor sin argumentos, getters/setters).
- Spring Bean: Cualquier objeto gestionado por el contenedor de Spring. No necesita seguir todas las reglas de un Java Bean clásico.
Es un principio de diseño donde el control del flujo del programa se invierte. En lugar de que tu código controle la creación y gestión de objetos, un framework (Spring) se encarga de ello.
- Sin IoC: Tú creas las instancias con
new Service(). - Con IoC: El contenedor crea la instancia y te la da cuando la necesitas.
Es el patrón de diseño que implementa IoC. Permite que los objetos definan sus dependencias (los otros objetos con los que trabajan) a través de argumentos de constructor o métodos, y el contenedor se encarga de "inyectar" esas dependencias cuando crea el bean.
Existen tres formas principales de inyectar dependencias. En Java moderno, la inyección por constructor es la práctica estándar.
Es la forma más segura y recomendada. Garantiza que el objeto no se pueda crear sin sus dependencias y permite que los campos sean final (inmutables).
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Spring inyecta automáticamente las dependencias aquí
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public void registerUser(String username) {
// Lógica de negocio
var user = userRepository.save(username);
emailService.sendWelcomeEmail(user);
}
}Tip
Desde Spring 4.3, si la clase tiene un único constructor, no es necesario anotar el constructor con @Autowired.
Útil para dependencias opcionales o que pueden cambiar en tiempo de ejecución. No permite campos final.
@Service
public class NotificationService {
private LoggerService logger;
@Autowired // Necesario en setters
public void setLogger(LoggerService logger) {
this.logger = logger;
}
}Aunque es muy común en tutoriales antiguos por su brevedad, se considera una mala práctica porque oculta dependencias, dificulta los tests unitarios y viola la inmutabilidad.
@Service
public class BadService {
@Autowired // Evitar esto
private UserRepository userRepository;
}Spring escanea el classpath buscando clases anotadas para crear Beans automáticamente.
@Component: Componente genérico.@Service: Capa de lógica de negocio.@Repository: Capa de acceso a datos (maneja excepciones de DB).@Controller/@RestController: Capa web (MVC o API REST).
Para librerías de terceros o configuraciones complejas donde no puedes anotar la clase directamente.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.thirdparty.PaymentGateway;
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() {
// Configuración manual del bean
var gateway = new PaymentGateway();
gateway.setApiKey("secret-key");
return gateway;
}
}El scope define cuántas instancias de un bean se crean y cuánto tiempo viven. Se define con @Scope.
| Scope | Descripción |
|---|---|
| Singleton (Default) | Una sola instancia por contenedor IoC. Es el valor por defecto. |
| Prototype | Se crea una nueva instancia cada vez que se solicita el bean. |
| Request | Una instancia por cada petición HTTP (Web). |
| Session | Una instancia por cada sesión HTTP (Web). |
| Application | Una instancia por ciclo de vida del ServletContext (Web). |
Puedes ejecutar lógica justo después de crear el bean o antes de destruirlo.
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class DatabaseConnection {
@PostConstruct
public void init() {
System.out.println("Bean creado y dependencias inyectadas. Abriendo conexión...");
}
@PreDestroy
public void cleanup() {
System.out.println("Cerrando conexión antes de destruir el bean...");
}
}Un ejemplo integrando un Controlador, Servicio y Repositorio simulado.
Repository:
@Repository
public class ProductRepository {
public List<String> findAll() {
return List.of("Laptop", "Mouse", "Keyboard"); // Java 9+ List.of
}
}Service:
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
public List<String> getProducts() {
// Uso de var (Java 10+)
var products = repository.findAll();
return products.stream()
.map(String::toUpperCase)
.toList(); // Java 16+ Stream.toList()
}
}Controller:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public List<String> listProducts() {
return service.getProducts();
}
}Spring Data JPA simplifica la capa de acceso a datos, eliminando la necesidad de escribir implementaciones repetitivas de DAO.
Clases POJO que representan tablas en la base de datos.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
// Getters, Setters, Constructors
}Interfaces que extienden JpaRepository. Spring genera la implementación en tiempo de ejecución.
public interface UserRepository extends JpaRepository<User, Long> {
// Derived Query Methods (Spring crea la query por el nombre del método)
Optional<User> findByEmail(String email);
List<User> findByAgeGreaterThan(int age);
// JPQL Custom Query
@Query("SELECT u FROM User u WHERE u.active = true")
List<User> findAllActiveUsers();
}Spring soporta la especificación JSR-380 (Bean Validation / Hibernate Validator) para validar objetos de entrada automáticamente.
@NotNull: No permite null.@NotBlank: No permite null ni vacíos (Strings).@Min/@Max: Rangos numéricos.@Email: Formato de email válido.@Pattern: Regex custom.
// DTO (Record de Java 16+)
public record CreateUserRequest(
@NotBlank(message = "El nombre es requerido")
String name,
@Email(message = "Email inválido")
String email
) {}
// Controller
@PostMapping
public void createUser(@Valid @RequestBody CreateUserRequest request) {
// Si la validación falla, Spring lanza MethodArgumentNotValidException automáticamente
// y no entra a este método.
}En lugar de try-catch en cada controlador, usamos un manejador global para capturar excepciones y devolver respuestas JSON estandarizadas.
@RestControllerAdvice
public class GlobalExceptionHandler {
// Manejo de errores de validación (@Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
// Manejo de excepciones de negocio personalizadas
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}Los perfiles permiten segregar partes de la configuración de la aplicación y hacer que solo estén disponibles en ciertos entornos (ej: dev, test, prod).
- Properties:
application-dev.properties,application-prod.properties. - Activación:
spring.profiles.active=dev(enapplication.propertieso variable de entorno).
Puedes condicionar la creación de Beans según el perfil activo.
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
public void send(String msg) {
System.out.println("Simulando envío de email: " + msg);
}
}
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
public void send(String msg) {
// Lógica real de envío SMTP
}
}