Java Spring Boot IoC
Módulo 3 — Java

Spring Boot: IoC & Beans

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.

O Problema: Dependências Hardcoded

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.

Inversão de Controlo (IoC)

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 e os Beans

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
Singleton por omissão Por omissão, cada bean é um singleton — o Spring cria uma única instância e partilha-a por toda a aplicação. Quando o 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.

Injecção de Dependências: Constructor vs Field

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 aviso do IntelliJ sobre Field Injection O IntelliJ IDEA avisa "Field injection is not recommended" precisamente pelas razões acima. O conselho é correcto — segue-o. Constructor injection é o padrão recomendado pela equipa do Spring desde a versão 4.

Os Estereótipos: @Component e Derivados

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çãoCamadaComportamento 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);
    }
}

@Bean e @Configuration

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 é singleton por design As classes @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.

Scopes: Singleton e Prototype

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();
    }
}
Singleton com estado mutable é um bug de concorrência Um bean singleton é partilhado por todos os threads que processam pedidos HTTP em simultâneo. Se tiver campos mutáveis, dois pedidos concorrentes podem corromper esse estado. A regra: beans singleton são stateless. Toda a informação por pedido vive nos parâmetros dos métodos, não em campos da classe.

@Value e @ConfigurationProperties

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; }
}
Segredos não pertencem ao application.properties O 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.

A Arquitectura em Camadas com IoC

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();
    }
}

IoC e Segurança: Beans do Spring Security

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"));
    }
}
O Spring Security é IoC aplicado à segurança Não chamas 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.

Checklist IoC & Beans