Quando uma aplicação Spring Boot arranca, não és tu que instancias as classes — é o Spring. Este princípio chama-se Inversão de Controlo e é a fundação de tudo o que o Spring Boot faz: gestão do ciclo de vida dos objectos, injecção de dependências, configuração por convenção, e integração com segurança. Perceber como o container IoC funciona é perceber como o Spring Boot funciona.
Em C, quando uma função precisa de outra, chama-a directamente — o linker resolve o endereço em tempo de compilação. Em Java orientado a objectos sem IoC, o padrão equivalente é instanciar as dependências dentro da própria classe:
// Sem IoC — a classe constrói as suas próprias dependências
public class DocumentService {
// hardcoded: DocumentService sabe exactamente como criar o repositório
private DocumentRepository repository = new DocumentRepository();
private AuditService audit = new AuditService(new AuditRepository());
public Document findById(Long id) {
audit.log("find", id);
return repository.findById(id);
}
}
// Para usar o serviço, tens de instanciar toda a árvore manualmente:
AuditRepository auditRepo = new AuditRepository();
AuditService audit = new AuditService(auditRepo);
DocumentRepository docRepo = new DocumentRepository();
DocumentService service = new DocumentService();
Este padrão tem três problemas sérios. Primeiro, acoplamento rígido:
o DocumentService está amarrado a implementações concretas —
não consegues substituir o repositório por uma versão de teste sem alterar
o código do serviço. Segundo, gestão manual do ciclo de vida:
em 50 classes com dependências cruzadas, a ordem de instanciação torna-se
um problema complexo. Terceiro, impossibilidade de substituição:
não consegues injectar um mock em testes unitários sem refactoring extenso.
O princípio de Inversão de Controlo inverte quem controla a criação das dependências. Em vez de a classe criar as suas dependências, ela declara de que precisa — e um agente externo (o container IoC) trata de as fornecer.
O nome reflecte exactamente isto: o controlo do fluxo de criação de objectos
passa da classe para o container. A classe já não diz "vou criar um
DocumentRepository" — diz "preciso de algo que implemente
DocumentRepository, fornece-mo tu".
// Com IoC — a classe declara dependências, não as cria
@Service
public class DocumentService {
private final DocumentRepository repository;
private final AuditService audit;
// o Spring injeta as implementações concretas aqui
public DocumentService(DocumentRepository repository, AuditService audit) {
this.repository = repository;
this.audit = audit;
}
public Document findById(Long id) {
audit.log("find", id);
return repository.findById(id);
}
}
// O Spring Boot trata de tudo ao arrancar — tu não instancias nada
O container IoC do Spring é o ApplicationContext —
um registo central de todos os objectos geridos pelo Spring. Estes objectos
chamam-se beans. Quando a aplicação arranca, o Spring varre
os packages do projecto, encontra as classes anotadas, instancia-as pela
ordem correcta, injeta as dependências, e mantém-nas em memória durante
o tempo de vida da aplicação.
// O ciclo de vida de um bean ao arrancar a aplicação:
// 1. Spring varre pt.randomt.autoflow e sub-packages
// 2. Encontra @Service, @Repository, @Controller, @Component
// 3. Resolve a árvore de dependências (quem precisa de quem)
// 4. Instancia pela ordem correcta:
// → DocumentRepository (sem dependências)
// → AuditRepository (sem dependências)
// → AuditService (precisa de AuditRepository)
// → DocumentService (precisa de DocumentRepository e AuditService)
// 5. Regista todos no ApplicationContext
// 6. A aplicação está pronta para receber pedidos HTTP
DocumentController
e o AuditController pedem um DocumentService, recebem
a mesma instância. Isto é eficiente mas implica que os beans não devem ter estado
mutável — veremos os scopes mais à frente.
Existem três formas de injectar dependências em Spring. Apenas uma é correcta para produção.
// ❌ Field Injection — não usar
// O Spring injeta o campo directamente via reflexão após construção.
// Problemas: não funciona fora do container (testes), permite estado nulo,
// oculta dependências obrigatórias, impossibilita campos final.
@Service
public class DocumentService {
@Autowired
private DocumentRepository repository; // nulo fora do Spring container
}
// ❌ Setter Injection — evitar
// Dependências são opcionais por natureza — implica que o objecto pode existir
// sem elas, o que raramente é o que queres.
@Service
public class DocumentService {
private DocumentRepository repository;
@Autowired
public void setRepository(DocumentRepository repository) {
this.repository = repository;
}
}
// ✅ Constructor Injection — usar sempre
// Dependências são explícitas, obrigatórias, e imutáveis (final).
// Funciona fora do container — podes testar sem Spring.
// Se uma dependência não existir, o Spring falha ao arrancar — fail-fast.
@Service
public class DocumentService {
private final DocumentRepository repository;
private final AuditService audit;
// @Autowired é opcional quando há um único constructor (Spring 4.3+)
public DocumentService(DocumentRepository repository, AuditService audit) {
this.repository = repository;
this.audit = audit;
}
}
O Spring usa anotações para identificar classes que devem ser geridas pelo container.
Todas derivam de @Component — mas cada uma tem semântica própria
que comunica a responsabilidade da classe e activa comportamentos adicionais.
| Anotação | Camada | Comportamento adicional |
|---|---|---|
@Component |
Genérico | Bean base. Usar quando nenhum outro estereótipo se aplica. |
@Service |
Lógica de negócio | Semanticamente indica serviço de negócio. Comunica intenção claramente. |
@Repository |
Acesso a dados | Activa tradução automática de excepções JDBC/JPA para DataAccessException. Essencial na camada de dados. |
@Controller |
HTTP (MVC) | Mapeia pedidos HTTP. Métodos retornam nomes de vistas (Thymeleaf, etc.). |
@RestController |
HTTP (REST API) | Atalho para @Controller + @ResponseBody. Métodos retornam JSON directamente. |
// Camada de dados — @Repository
@Repository
public class DocumentRepository {
private final JdbcTemplate jdbc;
public DocumentRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public Optional<Document> findByIdAndOwnerId(Long id, Long ownerId) {
// WHERE id = ? AND owner_id = ? — isolamento lógico anti-IDOR
return jdbc.query(
"SELECT * FROM documents WHERE id = ? AND owner_id = ?",
new DocumentRowMapper(), id, ownerId
).stream().findFirst();
}
}
// Camada de negócio — @Service
@Service
public class DocumentService {
private final DocumentRepository repository;
public DocumentService(DocumentRepository repository) {
this.repository = repository;
}
public Document getDocument(Long id, Long authenticatedUserId) {
return repository.findByIdAndOwnerId(id, authenticatedUserId)
.orElseThrow(() -> new ResourceNotFoundException("Document not found"));
}
}
// Camada HTTP — @RestController
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
private final DocumentService service;
public DocumentController(DocumentService service) {
this.service = service;
}
@GetMapping("/{id}")
public Document getDocument(@PathVariable Long id,
@AuthenticationPrincipal UserDetails user) {
Long userId = ((AppUserDetails) user).getId();
return service.getDocument(id, userId);
}
}
Os estereótipos funcionam para as tuas próprias classes. Mas quando precisas
de registar no container uma classe de uma biblioteca externa — que não podes
anotar — usas @Configuration e @Bean.
Uma classe @Configuration é um ficheiro de configuração explícita.
Os seus métodos @Bean são executados pelo Spring ao arrancar,
e o objecto retornado é registado no container.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
// BCryptPasswordEncoder não tem @Service — é uma classe externa
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
@Configuration são proxied pelo Spring — chamadas
repetidas ao mesmo método @Bean retornam sempre a mesma instância,
não criam objectos novos. Isto garante que as dependências entre beans são
resolvidas correctamente mesmo quando um bean depende de outro definido
na mesma classe de configuração.
Por omissão, todos os beans são singleton — uma instância partilhada por toda a aplicação. Para a maioria dos beans este é o comportamento correcto. Mas existem casos em que precisas de uma nova instância por cada utilização.
// Singleton (default) — uma instância para toda a aplicação
// NÃO deve ter estado mutável — partilhado por todos os threads
@Service
public class DocumentService {
private final DocumentRepository repository;
public DocumentService(DocumentRepository repository) {
this.repository = repository;
}
}
// Prototype — nova instância cada vez que é pedida
// Correcto para objectos com estado mutável por operação
@Component
@Scope("prototype")
public class ReportBuilder {
private final List<ReportEntry> entries = new ArrayList<>();
public void addEntry(ReportEntry entry) { entries.add(entry); }
public Report build() { return new Report(entries); }
}
// Para injectar um prototype num singleton, usar ObjectProvider
// (injecção directa daria sempre a mesma instância)
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> builderProvider;
public ReportService(ObjectProvider<ReportBuilder> builderProvider) {
this.builderProvider = builderProvider;
}
public Report generateReport(List<ReportEntry> entries) {
ReportBuilder builder = builderProvider.getObject(); // nova instância
entries.forEach(builder::addEntry);
return builder.build();
}
}
Configuração que muda entre ambientes — segredos JWT, URLs, timeouts — não deve
estar hardcoded. O Spring Boot permite injectar valores do
application.properties directamente nos beans.
# application.properties
app.jwt.secret=segredo-muito-longo-e-aleatorio-para-producao
app.jwt.expiration-ms=900000
app.cors.allowed-origins=https://autoflow.randomt.pt
// @Value — injecção de propriedade individual
@Component
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration-ms}")
private long expirationMs;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
}
// @ConfigurationProperties — preferível para grupos de propriedades relacionadas
// Type-safe, validação automática, suporte a listas e mapas
@ConfigurationProperties(prefix = "app.jwt")
@Component
public class JwtProperties {
@NotBlank
private String secret;
@Positive
private long expirationMs;
public String getSecret() { return secret; }
public void setSecret(String secret) { this.secret = secret; }
public long getExpirationMs() { return expirationMs; }
public void setExpirationMs(long ms) { this.expirationMs = ms; }
}
application.properties entra no repositório Git.
Segredos como app.jwt.secret e credenciais de base de dados
devem ser injectados via variáveis de ambiente em produção.
O Spring Boot suporta isto nativamente: a variável de ambiente
APP_JWT_SECRET=valor sobrepõe-se a app.jwt.secret
no ficheiro de propriedades.
O padrão que o Spring Boot incentiva — e que emerge naturalmente do IoC — é uma arquitectura em três camadas com responsabilidades bem definidas. O container gere as dependências entre camadas; cada camada conhece apenas a camada imediatamente abaixo.
// Controller → Service → Repository → Base de dados
//
// O controller NÃO conhece o repository directamente.
// O repository NÃO conhece o controller.
// Cada camada é testável independentemente das outras.
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService service;
public AccountController(AccountService service) { this.service = service; }
@GetMapping("/{id}")
public Account getAccount(@PathVariable Long id,
@AuthenticationPrincipal UserDetails user) {
Long ownerId = ((AppUserDetails) user).getId();
return service.getAccount(id, ownerId);
}
}
@Service
public class AccountService {
private final AccountRepository repository;
public AccountService(AccountRepository repository) { this.repository = repository; }
public Account getAccount(Long accountId, Long authenticatedUserId) {
return repository.findByIdAndOwnerId(accountId, authenticatedUserId)
.orElseThrow(() -> new ResourceNotFoundException("Account not found"));
}
}
@Repository
public class AccountRepository {
private final JdbcTemplate jdbc;
public AccountRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
public Optional<Account> findByIdAndOwnerId(Long id, Long ownerId) {
// A query usa SEMPRE dois critérios: id + owner_id
// Mesmo que o atacante envie id=999, se owner_id não corresponder → vazio
return jdbc.query(
"SELECT * FROM accounts WHERE id = ? AND owner_id = ?",
new AccountRowMapper(), id, ownerId
).stream().findFirst();
}
}
A arquitectura de segurança deste projecto — JWT em HttpOnly cookie, filtro de autenticação, isolamento lógico — materializa-se inteiramente através de beans geridos pelo container IoC. Cada peça é um bean que o Spring instancia, configura, e injeta onde é necessário.
// JwtService — gera e valida tokens JWT
@Component
public class JwtService { /* ... */ }
// JwtAuthFilter — intercepta cada pedido, lê o cookie, valida o JWT
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService,
UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. Ler o JWT do HttpOnly cookie (nunca do header — protecção XSS)
String jwt = Arrays.stream(
Optional.ofNullable(request.getCookies()).orElse(new Cookie[0])
)
.filter(c -> "access_token".equals(c.getName()))
.map(Cookie::getValue)
.findFirst().orElse(null);
if (jwt == null) { filterChain.doFilter(request, response); return; }
// 2. Validar o JWT e extrair o username
String username = jwtService.extractUsername(jwt);
if (username == null || !jwtService.isTokenValid(jwt)) {
filterChain.doFilter(request, response); return;
}
// 3. Carregar utilizador e definir contexto de autenticação
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
}
// UserDetailsService — carrega o utilizador da base de dados
@Service
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
return userRepository.findByEmail(username)
.map(AppUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
jwtAuthFilter.doFilter() manualmente em lado nenhum —
o Spring Security regista o filtro na chain HTTP e invoca-o automaticamente
em cada pedido. O container IoC gere todo o ciclo de vida e a orquestração.
@Autowired.final — imutabilidade e clareza de intenção.@Repository nos dados, @Service na lógica, @RestController no HTTP.@Configuration + @Bean.application.properties commitado.@ConfigurationProperties em vez de múltiplos @Value.