Ao gerar um relatório, o time percebeu que ele também incluía itens do relatório anterior. O componente responsável pela montagem guardava os itens em uma lista de instância e, por ser um Bean singleton, reutilizava essa mesma lista a cada geração.
A correção pareceu simples: colocar @Scope("prototype") na classe.
Afinal, se singleton reutiliza e prototype cria instâncias, cada chamada passaria a trabalhar com um objeto novo.
O código compilou. A aplicação subiu. O estado continuou sendo compartilhado.
O problema estava na expectativa: o componente era injetado diretamente no construtor de um service singleton. Mudar o escopo da dependência não fez aquele construtor executar novamente a cada operação.
Prototype não é um new melhor. É uma política de obtenção de Beans no container.
No post sobre singleton, a fronteira era a definição do Bean dentro do contexto. Aqui, a pergunta muda: em que momento a aplicação pede outra instância?
Uma instância nova quando alguém pede ao container
Uma definição de Bean informa ao Spring como montar um objeto. O escopo participa da decisão sobre quais instâncias o container entrega.
Para um Bean prototype, uma nova solicitação ao container produz uma nova instância. Isso não significa que qualquer chamada de método no objeto recebido volte ao Spring para pedir outra.
Considere este componente, com os imports usuais omitidos:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MontadorRelatorio {
private final List<String> linhas = new ArrayList<>();
public void adicionar(String linha) {
linhas.add(linha);
}
public String concluir() {
return String.join("\n", linhas);
}
}
O campo linhas pertence à instância. A anotação não limpa essa lista depois de concluir(), não identifica uma operação de negócio e não cria uma cópia do objeto em cada acesso.
Se o código obtiver duas instâncias do Bean, cada uma terá sua própria lista. Se guardar uma única referência e usá-la várias vezes, continuará alterando a mesma lista.
O Spring controla a entrega. O código que recebe o objeto controla como aquela referência será usada.
Injetar em singleton prende a referência
Agora veja quem usa o montador:
@Service
public class RelatorioService {
private final MontadorRelatorio montador;
public RelatorioService(MontadorRelatorio montador) {
this.montador = montador;
}
public String gerar(List<String> itens) {
itens.forEach(montador::adicionar);
return montador.concluir();
}
}
RelatorioService usa o escopo singleton padrão. Quando o Spring cria esse service, resolve sua dependência e entrega um montador.
Depois disso, o campo guarda aquela referência. Duas chamadas a gerar() usam o mesmo objeto, acumulando linhas de execuções anteriores. Chamadas concorrentes também podem disputar sua lista mutável.
O montador continua tendo uma definição prototype. A injeção direta aconteceu uma vez durante a criação do service.
Essa distinção explica por que trocar a anotação não resolveu o defeito. O requisito era ter um montador por relatório, mas o código só pedia um montador ao construir o consumidor.
Também explica por que adicionar um clear() ao final seria uma correção frágil. Além de exigir cuidado com exceções, a limpeza não impediria duas execuções simultâneas de usar o mesmo estado.
Peça a instância no começo da operação
Se existe motivo para o montador ser um Bean e ele precisa nascer a cada execução, o ponto de obtenção deve acompanhar essa execução.
Uma opção é receber ObjectProvider:
@Service
public class RelatorioService {
private final ObjectProvider<MontadorRelatorio> montadores;
public RelatorioService(
ObjectProvider<MontadorRelatorio> montadores) {
this.montadores = montadores;
}
public String gerar(List<String> itens) {
MontadorRelatorio montador = montadores.getObject();
itens.forEach(montador::adicionar);
return montador.concluir();
}
}
Agora cada execução faz uma solicitação. Como a definição é prototype, recebe um novo montador, mantido em variável local durante a montagem daquele relatório.
O provider explicita o momento da obtenção. Ele não transforma qualquer Bean em prototype: se a definição fosse singleton, chamadas a getObject() normalmente devolveriam a mesma instância compartilhada.
Também importa obter uma vez e usar essa referência ao longo da operação. Pedir outro montador para adicionar cada linha e mais um para concluir separaria o estado que deveria permanecer junto.
Um scoped proxy exige atenção semelhante. Para um alvo prototype, cada chamada pelo proxy obtém um novo alvo. Uma sequência de chamadas a adicionar() e concluir() não preservaria o montador entre elas. O mecanismo escolhido precisa respeitar a unidade de trabalho.
Antes do provider, pergunte se precisa ser Bean
O exemplo anterior corrige a obtenção, mas ainda deixa uma pergunta de desenho: por que esse montador está no container?
A classe mostrada só acumula strings. Não recebe infraestrutura nem precisa de configuração ou processamento do Spring. Criá-la localmente expressa bem sua duração:
public String gerar(List<String> itens) {
MontadorRelatorio montador = new MontadorRelatorio();
itens.forEach(montador::adicionar);
return montador.concluir();
}
Nessa alternativa, remova @Component e @Scope da classe. Ela passa a ser um objeto Java comum, criado para uma operação específica.
Se a construção exigir colaboradores, uma fábrica pode recebê-los por injeção e montar o objeto com new. A fábrica permanece Bean; o produto não precisa ser.
Usar new dentro dessa fábrica não faz o produto passar automaticamente pelos mecanismos do container. Se ele depende de processamento de Bean, essa diferença precisa ser considerada.
Prototype pode fazer sentido quando cada instância realmente precisa da montagem oferecida pelo Spring e possui estado de trabalho próprio. Não há ganho automático em registrar DTOs, valores ou todo objeto temporário como Bean.
Essa é a continuidade do post sobre o problema que um Bean resolve: a participação do container deve ter uma razão concreta.
Criar outra instância não resolve o ciclo de vida inteiro
Imagine que, além das linhas, o montador passe a manter um arquivo temporário aberto durante a geração.
Quem encerra esse recurso se a montagem falhar? E quem o remove quando o relatório terminar?
A resposta deve aparecer no contrato de uso: por exemplo, uma operação que fecha o recurso em finally, ou um objeto AutoCloseable usado com try-with-resources. Declarar a interface sozinho também não executa a limpeza.
O Spring injeta dependências e executa callbacks de inicialização, como @PostConstruct, nas instâncias prototype. Já callbacks de destruição configurados, como @PreDestroy, não são chamados automaticamente pelo container para essas instâncias. A limpeza fica com quem as utiliza.
Não deixe essa responsabilidade implícita na ideia de que o objeto é gerenciado. No caso de prototype, a participação do container na criação não equivale ao acompanhamento completo até o fim do uso.
O mesmo cuidado vale para concorrência. Instâncias distintas do montador ainda podem compartilhar um colaborador singleton, um cache ou um recurso externo. Separar o estado local ajuda, mas não torna toda a cadeia automaticamente segura.
Escopo não substitui o desenho da operação, a definição de responsabilidades nem o entendimento do estado compartilhado.
O ponto que vale fixar
Prototype responde a uma pergunta específica: como o container deve entregar instâncias daquela definição de Bean?
A aplicação ainda precisa decidir quando solicitar uma instância, por quanto tempo guardá-la e quem encerra os recursos usados por ela.
Se o objeto só organiza dados de uma execução, new ou uma fábrica podem deixar essa intenção mais clara. Se ele precisa da montagem do Spring, obtenha o prototype no ponto em que começa seu trabalho.
Trocar singleton por prototype sem rever quem segura a referência muda a configuração. Pode deixar o comportamento que causou o problema exatamente no mesmo lugar.
Referência técnica: escopos de Beans na documentação oficial do Spring.
