Por que o ApplicationContext.getBean do Spring é considerado ruim?

270

Fiz uma pergunta geral da Spring: criei o Spring Beans automaticamente e várias pessoas responderam que ligar para o Spring ApplicationContext.getBean()deveria ser evitado o máximo possível. Por que é que?

De que outra forma devo obter acesso aos beans que configurei o Spring para criar?

Estou usando o Spring em um aplicativo não-web e planejava acessar um ApplicationContextobjeto compartilhado, conforme descrito por LiorH .

Alteração

Aceito a resposta abaixo, mas aqui está um exemplo alternativo de Martin Fowler, que discute os méritos da injeção de dependência versus o uso de um localizador de serviço (que é essencialmente o mesmo que chamar de embrulhado ApplicationContext.getBean()).

Em parte, Fowler declara: " Com o localizador de serviço, a classe do aplicativo solicita explicitamente [o serviço] por uma mensagem ao localizador. Com a injeção, não há solicitação explícita, o serviço aparece na classe do aplicativo - daí a inversão de controle. A inversão do controle é um recurso comum das estruturas, mas é algo que tem um preço.Tende a ser difícil de entender e leva a problemas quando você está tentando depurar.Portanto, no geral, prefiro evitá-lo [Inversão do controle ], a menos que eu precise. Isso não quer dizer que seja uma coisa ruim, só que acho que precisa justificar-se sobre a alternativa mais direta " .

Vinnie
fonte

Respostas:

202

Mencionei isso em um comentário sobre a outra questão, mas a idéia toda da Inversão de controle é que nenhuma de suas classes saiba ou se importe com a maneira como elas obtêm os objetos dos quais dependem . Isso facilita a alteração do tipo de implementação de uma determinada dependência que você usa a qualquer momento. Também facilita o teste das classes, pois você pode fornecer implementações simuladas de dependências. Por fim, torna as aulas mais simples e mais focadas em sua responsabilidade principal.

Chamar ApplicationContext.getBean()não é Inversão de Controle! Embora ainda seja fácil alterar qual implementação está configurada para o nome de bean especificado, a classe agora depende diretamente do Spring para fornecer essa dependência e não pode obtê-la de outra maneira. Você não pode simplesmente fazer sua própria implementação simulada em uma classe de teste e passar isso para ela. Isso basicamente derrota o objetivo do Spring como um contêiner de injeção de dependência.

Em todos os lugares que você deseja dizer:

MyClass myClass = applicationContext.getBean("myClass");

você deve, por exemplo, declarar um método:

public void setMyClass(MyClass myClass) {
   this.myClass = myClass;
}

E então na sua configuração:

<bean id="myClass" class="MyClass">...</bean>

<bean id="myOtherClass" class="MyOtherClass">
   <property name="myClass" ref="myClass"/>
</bean>

O Spring será injetado automaticamente myClassno myOtherClass.

Declare tudo dessa maneira e, na raiz, tudo tem algo como:

<bean id="myApplication" class="MyApplication">
   <property name="myCentralClass" ref="myCentralClass"/>
   <property name="myOtherCentralClass" ref="myOtherCentralClass"/>
</bean>

MyApplicationé a classe mais central e depende pelo menos indiretamente de todos os outros serviços do seu programa. Ao iniciar, no seu mainmétodo, você pode chamar, applicationContext.getBean("myApplication")mas não precisa chamar em getBean()nenhum outro lugar!

ColinD
fonte
3
Existe algo relacionado a isso que funciona apenas com anotações ao criar um new MyOtherClass()objeto? Eu sei sobre @Autowired, mas eu só já é usado em campos e quebra em new MyOtherClass()..
Tim
71
Não é verdade que ApplicationContext.getBean () não seja IoC. Além disso, é obrigatório ter todas as suas aulas instanciadas pelo Spring. Isso é dogma inadequado. Se o ApplicationContext for injetado, é perfeitamente bom solicitar que ele instancie um bean dessa maneira - e os beans que ele cria podem ser implementações diferentes com base no ApplicationContext injetado inicialmente. Por exemplo, eu tenho um cenário em que crio dinamicamente novas instâncias de bean com base em um nome de bean desconhecido no momento da compilação, mas que corresponde a uma das implementações definidas no meu arquivo spring.xml.
Alex Worden
3
Concordo com Alex, eu tenho o mesmo problema, onde uma classe de fábrica só vai saber qual bean ou implementação de usar em tempo de execução por meio de interação do usuário, eu acho que isso é onde a interface ContextAware vem em
Ben
3
@elbek: applicationContext.getBeannão é injeção de dependência: está acessando a estrutura diretamente, usando-a como localizador de serviço .
colind
6
@herman: Eu não conheço o Spring porque não o uso há muito tempo, mas no JSR-330 / Guice / Dagger, você faria isso injetando um em Provider<Foo>vez de um Fooe ligando provider.get()toda vez que precisar de um nova instância. Nenhuma referência ao próprio contêiner, e você pode facilmente criar um Providerpara teste.
colind
64

Os motivos para preferir o Localizador de Serviço em vez da Inversão de Controle (IoC) são:

  1. O Localizador de serviço é muito, muito mais fácil para outras pessoas seguirem seu código. A IoC é 'mágica', mas os programadores de manutenção devem entender suas configurações complicadas do Spring e toda a miríade de locais para descobrir como você conectou seus objetos.

  2. A IoC é terrível para depurar problemas de configuração. Em certas classes de aplicativos, o aplicativo não inicia quando configurado incorretamente e você pode não ter a chance de percorrer o que está acontecendo com um depurador.

  3. A IoC é baseada principalmente em XML (as anotações melhoram as coisas, mas ainda há muito XML por aí). Isso significa que os desenvolvedores não podem trabalhar no seu programa a menos que conheçam todas as tags mágicas definidas pelo Spring. Não é bom o suficiente para conhecer Java. Isso dificulta os programadores com menos experiência (ou seja, é realmente um projeto ruim usar uma solução mais complicada quando uma solução mais simples, como o Localizador de Serviços, atende aos mesmos requisitos). Além disso, o suporte ao diagnóstico de problemas XML é muito mais fraco que o suporte a problemas Java.

  4. A injeção de dependência é mais adequada para programas maiores. Na maioria das vezes, a complexidade adicional não vale a pena.

  5. Frequentemente, o Spring é usado caso você "queira alterar a implementação posteriormente". Existem outras maneiras de conseguir isso sem a complexidade do Spring IoC.

  6. Para aplicativos da Web (Java EE WARs), o contexto do Spring é efetivamente vinculado no tempo de compilação (a menos que você queira que os operadores agrupem o contexto na guerra explodida). Você pode fazer com que o Spring use arquivos de propriedades, mas com os servlets, os arquivos de propriedades precisarão estar em um local predeterminado, o que significa que você não pode implantar vários servlets ao mesmo tempo na mesma caixa. É possível usar o Spring com JNDI para alterar as propriedades no momento da inicialização do servlet, mas se você estiver usando o JNDI para parâmetros modificáveis ​​pelo administrador, a necessidade do Spring diminui (já que o JNDI é efetivamente um Localizador de Serviço).

  7. Com o Spring, você pode perder o programa Control se o Spring estiver despachando para seus métodos. Isso é conveniente e funciona para muitos tipos de aplicativos, mas não para todos. Você pode precisar controlar o fluxo do programa quando precisar criar tarefas (threads etc.) durante a inicialização ou precisar de recursos modificáveis ​​que o Spring não sabia quando o conteúdo estava vinculado ao seu WAR.

O Spring é muito bom para gerenciamento de transações e tem algumas vantagens. Só que a IoC pode ter engenharia excessiva em muitas situações e introduzir complexidade injustificada para os mantenedores. Não use a IoC automaticamente sem pensar em maneiras de não usá-la primeiro.

Moa
fonte
7
Além disso - seu ServiceLocator sempre pode usar a IoC da Spring, abstraindo seu código de ser dependente da Spring, repleto de anotações da Spring e magik indecifrável. Recentemente, enviei um monte de código para o GoogleAppEngine, onde o Spring não é suportado. Eu gostaria de ter escondido toda a IoC atrás de uma ServiceFactory em primeiro lugar!
Alex Worden
A IoC incentiva um modelo de domínio anêmico, que desprezo. Os beans de entidade precisam de uma maneira de procurar seus serviços para que eles possam implementar seu próprio comportamento. Nessa camada, você não pode realmente precisar de um localizador de serviço.
Joel
4
Bizar. Eu uso o Spring o tempo todo com anotações. Embora de fato uma certa curva de aprendizado esteja envolvida, agora, não tenho nenhum problema em manutenção, depuração, clareza, legibilidade ... Acho que como você estrutura as coisas é o truque.
Lawrence
25

É verdade que incluir a classe no application-context.xml evita a necessidade de usar getBean. No entanto, mesmo isso é realmente desnecessário. Se você estiver escrevendo um aplicativo independente e NÃO desejar incluir sua classe de driver no application-context.xml, poderá usar o código a seguir para que o Spring controle automaticamente as dependências do driver:

public class AutowireThisDriver {

    private MySpringBean mySpringBean;    

    public static void main(String[] args) {
       AutowireThisDriver atd = new AutowireThisDriver(); //get instance

       ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                  "/WEB-INF/applicationContext.xml"); //get Spring context 

       //the magic: auto-wire the instance with all its dependencies:
       ctx.getAutowireCapableBeanFactory().autowireBeanProperties(atd,
                  AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);        

       // code that uses mySpringBean ...
       mySpringBean.doStuff() // no need to instantiate - thanks to Spring
    }

    public void setMySpringBean(MySpringBean bean) {
       this.mySpringBean = bean;    
    }
}

Preciso fazer isso algumas vezes quando tenho algum tipo de classe autônoma que precisa usar algum aspecto do meu aplicativo (por exemplo, para teste), mas não quero incluí-lo no contexto do aplicativo porque não é na verdade parte do aplicativo. Observe também que isso evita a necessidade de procurar o bean usando um nome String, que eu sempre achei feio.

iftheshoefritz
fonte
Consegui usar esse método com sucesso também com a @Autowiredanotação.
BLONG
21

Um dos benefícios mais legais de usar algo como o Spring é que você não precisa conectar seus objetos. A cabeça de Zeus se abre e suas classes aparecem, totalmente formadas com todas as dependências criadas e conectadas, conforme necessário. É mágico e fantástico.

Quanto mais você diz ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, menos mágica você recebe. Menos código é quase sempre melhor. Se a sua turma realmente precisava de um bean ClassINeed, por que você não o conectou?

Dito isto, algo obviamente precisa criar o primeiro objeto. Não há nada errado com o método principal de adquirir um bean ou dois via getBean (), mas você deve evitá-lo, porque sempre que o usa, você realmente não está usando toda a magia do Spring.

Brandon Yarbrough
fonte
1
Mas o OP não está dizendo "ClassINeed", ele está dizendo "BeanNameINeed" - o que permite que o contêiner de IoC crie uma instância em qualquer classe configurada de qualquer maneira. Talvez seja mais parecido com o padrão "localizador de serviço" do que a IoC, mas ainda resulta em acoplamento frouxo.
HDave
16

A motivação é escrever um código que não dependa explicitamente do Spring. Dessa forma, se você optar por trocar de contêiner, não precisará reescrever nenhum código.

Pense no contêiner como algo invisível ao seu código, suprindo magicamente suas necessidades, sem ser solicitado.

A injeção de dependência é um contraponto ao padrão "localizador de serviço". Se você estiver procurando dependências por nome, é melhor livrar-se do contêiner de DI e usar algo como JNDI.

erickson
fonte
11

Usar @Autowiredou ApplicationContext.getBean()é realmente a mesma coisa. De ambos os modos, você obtém o bean configurado no seu contexto e, de ambos os modos, seu código depende da primavera. A única coisa que você deve evitar é instanciar seu ApplicationContext. Faça isso apenas uma vez! Em outras palavras, uma linha como

ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml");

deve ser usado apenas uma vez no seu aplicativo.

easyplanner.cu.cc
fonte
Não. Às vezes, @Autowired ou ApplicationContext.getBean () poderia produzir totalmente diferentes beans. Não sei ao certo como aconteceu, mas estou enfrentando problemas com esse problema agora.
26618 Oleksandr_DJ
4

A idéia é que você confie na injeção de dependência ( inversão de controle ou IoC). Ou seja, seus componentes estão configurados com os componentes de que precisam. Essas dependências são injetadas (por meio do construtor ou dos setters) - você não recebe por conta própria.

ApplicationContext.getBean()requer que você nomeie um bean explicitamente em seu componente. Em vez disso, usando o IoC, sua configuração pode determinar qual componente será usado.

Isso permite reconectar seu aplicativo com diferentes implementações de componentes com facilidade ou configurar objetos para teste de maneira direta, fornecendo variantes simuladas (por exemplo, um DAO simulado para que você não acesse um banco de dados durante o teste)

Brian Agnew
fonte
4

Outros apontaram para o problema geral (e são respostas válidas), mas vou apenas fazer um comentário adicional: não é que você NUNCA deva fazê-lo, mas sim o mínimo possível.

Normalmente, isso significa que é feito exatamente uma vez: durante a inicialização. E é apenas para acessar o bean "raiz", através do qual outras dependências podem ser resolvidas. Pode ser um código reutilizável, como servlet base (se estiver desenvolvendo aplicativos da web).

StaxMan
fonte
4

Uma das instalações da Spring é evitar o acoplamento . Defina e use Interfaces, DI, AOP e evite usar ApplicationContext.getBean () :-)

sourcerebels
fonte
4

Uma das razões é a testabilidade. Digamos que você tenha esta classe:

interface HttpLoader {
    String load(String url);
}
interface StringOutput {
    void print(String txt);
}
@Component
class MyBean {
    @Autowired
    MyBean(HttpLoader loader, StringOutput out) {
        out.print(loader.load("http://stackoverflow.com"));
    }
}

Como você pode testar esse bean? Por exemplo:

class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();

        // execution
        new MyBean(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get, result::append);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

Fácil né?

Enquanto você ainda depende do Spring (devido às anotações), você pode remover sua dependência do Spring sem alterar nenhum código (apenas as definições de anotação) e o desenvolvedor de teste não precisa saber nada sobre como o Spring funciona (talvez ele deva, de qualquer maneira, mas permite revisar e testar o código separadamente do que a primavera faz).

Ainda é possível fazer o mesmo ao usar o ApplicationContext. No entanto, você precisa zombar de ApplicationContextqual é uma interface enorme. Você precisa de uma implementação fictícia ou pode usar uma estrutura de simulação como o Mockito:

@Component
class MyBean {
    @Autowired
    MyBean(ApplicationContext context) {
        HttpLoader loader = context.getBean(HttpLoader.class);
        StringOutput out = context.getBean(StringOutput.class);

        out.print(loader.load("http://stackoverflow.com"));
    }
}
class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();
        ApplicationContext context = Mockito.mock(ApplicationContext.class);
        Mockito.when(context.getBean(HttpLoader.class))
            .thenReturn(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get);
        Mockito.when(context.getBean(StringOutput.class)).thenReturn(result::append);

        // execution
        new MyBean(context);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

Essa é uma possibilidade, mas acho que a maioria das pessoas concorda que a primeira opção é mais elegante e simplifica o teste.

A única opção que realmente é um problema é esta:

@Component
class MyBean {
    @Autowired
    MyBean(StringOutput out) {
        out.print(new HttpLoader().load("http://stackoverflow.com"));
    }
}

Testar isso exige grandes esforços ou seu bean tentará se conectar ao stackoverflow em cada teste. E assim que houver uma falha na rede (ou os administradores do stackoverflow o bloquearem devido à taxa de acesso excessiva), você terá testes aleatórios com falha.

Portanto, como conclusão, não diria que o uso ApplicationContextdireto é automaticamente errado e deve ser evitado a todo custo. No entanto, se houver melhores opções (e na maioria dos casos), use as melhores opções.

ianque
fonte
3

Eu encontrei apenas duas situações em que getBean () era necessário:

Outros mencionaram o uso de getBean () em main () para buscar o bean "main" em um programa independente.

Outro uso que fiz de getBean () está em situações em que uma configuração interativa do usuário determina a composição do bean para uma situação específica. Para que, por exemplo, parte do sistema de inicialização faça um loop através de uma tabela de banco de dados usando getBean () com uma definição de bean scope = 'prototype' e, em seguida, defina propriedades adicionais. Presumivelmente, existe uma interface do usuário que ajusta a tabela do banco de dados que seria mais amigável do que tentar (re) escrever o XML do contexto do aplicativo.

nsayer
fonte
3

Há outro momento em que o uso do getBean faz sentido. Se você estiver reconfigurando um sistema que já existe, em que as dependências não são explicitamente chamadas nos arquivos de contexto do Spring. Você pode iniciar o processo fazendo chamadas para getBean, para não precisar conectar tudo de uma vez. Dessa forma, você pode construir lentamente sua configuração de mola, colocando cada peça no lugar ao longo do tempo e alinhando as peças corretamente. As chamadas para getBean acabarão sendo substituídas, mas, como você entende a estrutura do código, ou a falta dela, é possível iniciar o processo de conectar mais e mais beans e usar cada vez menos chamadas para getBean.

Tony Giaccone
fonte
2

No entanto, ainda existem casos em que você precisa do padrão do localizador de serviço. por exemplo, eu tenho um bean de controlador, esse controlador pode ter alguns beans de serviço padrão, que podem ser injetados na dependência pela configuração. embora também possa haver muitos serviços adicionais ou novos que esse controlador pode chamar agora ou mais tarde, que precisa do localizador de serviço para recuperar os beans de serviço.

lwpro2
fonte
0

Você deve usar: ConfigurableApplicationContext em vez de para ApplicationContext

Hai Nguyen Le
fonte