¿Por qué Spring's ApplicationContext.getBean se considera malo?

270

Hice una pregunta general de Spring: Auto-cast Spring Beans y varias personas respondieron que llamar a Spring's ApplicationContext.getBean()debería evitarse tanto como sea posible. ¿Porqué es eso?

¿De qué otra forma debería obtener acceso a los beans que configuré Spring para crear?

Estoy usando Spring en una aplicación que no es web y había planeado acceder a un ApplicationContextobjeto compartido como lo describe LiorH .

Enmienda

Acepto la respuesta a continuación, pero aquí hay una toma alternativa de Martin Fowler que analiza los méritos de la Inyección de dependencias frente al uso de un Localizador de servicios (que es esencialmente lo mismo que llamar a un envuelto ApplicationContext.getBean()).

En parte, Fowler afirma: " Con el localizador de servicios, la clase de aplicación lo solicita [el servicio] explícitamente mediante un mensaje al localizador. Con la inyección no hay una solicitud explícita, el servicio aparece en la clase de aplicación, de ahí la inversión de control. La inversión del control es una característica común de los marcos, pero es algo que tiene un precio. Tiende a ser difícil de entender y genera problemas cuando se trata de depurar. En general, prefiero evitarlo [Inversión de control ] a menos que lo necesite. Esto no quiere decir que sea algo malo, solo que creo que necesita justificarse sobre la alternativa más directa " .

Vinnie
fuente

Respuestas:

202

Mencioné esto en un comentario sobre la otra pregunta, pero la idea general de Inversion of Control es que ninguna de sus clases sepa o les importe cómo obtienen los objetos de los que dependen . Esto hace que sea fácil cambiar el tipo de implementación de una dependencia determinada que utiliza en cualquier momento. También hace que las clases sean fáciles de probar, ya que puede proporcionar implementaciones simuladas de dependencias. Finalmente, hace que las clases sean más simples y más centradas en su responsabilidad principal.

¡Llamar ApplicationContext.getBean()no es invertir el control! Si bien aún es fácil cambiar qué implementación está configurada para el nombre de bean dado, la clase ahora depende directamente de Spring para proporcionar esa dependencia y no puede obtenerla de otra manera. No puede simplemente hacer su propia implementación simulada en una clase de prueba y pasársela usted mismo. Esto básicamente derrota el propósito de Spring como un contenedor de inyección de dependencia.

En todas partes quieres decir:

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

en su lugar, debería, por ejemplo, declarar un método:

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

Y luego en su configuración:

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

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

Spring se inyectará automáticamente myClassen myOtherClass.

Declare todo de esta manera, y en el fondo todo tiene algo como:

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

MyApplicationes la clase más central, y depende al menos indirectamente de cualquier otro servicio en su programa. Al iniciar, en su mainmétodo, puede llamar, ¡ applicationContext.getBean("myApplication")pero no debería necesitar llamar a getBean()ningún otro lugar!

ColinD
fuente
3
¿Hay algo relacionado con esto que funcione solo con anotaciones al crear un new MyOtherClass()objeto? Yo sé de @Autowired, pero yo sólo he utilizado en los campos y se rompe en new MyOtherClass()..
Tim
71
No es cierto que ApplicationContext.getBean () no sea IoC. Niether es obligatorio tener todas tus clases instanciadas para la primavera. Ese es un dogma inapropiado. Si el ApplicationContext se inyecta, entonces está perfectamente bien pedirle que cree una instancia de un bean de esta manera, y los beans que crea pueden ser implementaciones diferentes basadas en el ApplicationContext inicialmente inyectado. Por ejemplo, tengo un escenario en el que creo dinámicamente nuevas instancias de bean basadas en un nombre de bean que es desconocido en el momento de la compilación pero que coincide con una de las implementaciones definidas en mi archivo spring.xml.
Alex Worden el
3
De acuerdo con Alex, tengo el mismo problema, donde una clase de fábrica solo sabrá qué bean o implementación usar en tiempo de ejecución a través de la interacción del usuario, creo que aquí es donde entra la interfaz ContextAware
Ben
3
@elbek: applicationContext.getBeanno es inyección de dependencia: está accediendo al marco directamente, usándolo como un localizador de servicios .
ColinD
66
@herman: No sé acerca de Spring porque no lo he usado en mucho tiempo, pero en JSR-330 / Guice / Dagger, harías esto inyectando un en Provider<Foo>lugar de a Fooy llamando provider.get()cada vez que necesites un nueva instancia. No se hace referencia al contenedor en sí, y puede crear fácilmente una Providerpara realizar pruebas.
ColinD
65

Las razones para preferir el Localizador de servicios a la Inversión de control (IoC) son:

  1. Service Locator es mucho, mucho más fácil para que otras personas lo sigan en su código. IoC es 'mágico', pero los programadores de mantenimiento deben comprender sus configuraciones complicadas de Spring y toda la miríada de ubicaciones para descubrir cómo conectar sus objetos.

  2. IoC es terrible para depurar problemas de configuración. En ciertas clases de aplicaciones, la aplicación no se iniciará cuando esté mal configurada y es posible que no tenga la oportunidad de pasar por lo que sucede con un depurador.

  3. IoC se basa principalmente en XML (las anotaciones mejoran las cosas, pero todavía hay mucho XML por ahí). Eso significa que los desarrolladores no pueden trabajar en su programa a menos que conozcan todas las etiquetas mágicas definidas por Spring. Ya no es suficiente conocer Java. Esto dificulta la experiencia de los programadores (es decir, en realidad es un diseño deficiente utilizar una solución más complicada cuando una solución más simple, como Service Locator, cumplirá los mismos requisitos). Además, el soporte para diagnosticar problemas XML es mucho más débil que el soporte para problemas Java.

  4. La inyección de dependencia es más adecuada para programas más grandes. La mayoría de las veces la complejidad adicional no vale la pena.

  5. A menudo se usa Spring en caso de que "desee cambiar la implementación más adelante". Hay otras formas de lograr esto sin la complejidad de Spring IoC.

  6. Para las aplicaciones web (Java EE WARs), el contexto de Spring está vinculado de manera efectiva en el momento de la compilación (a menos que desee que los operadores trabajen en torno al contexto en la guerra explosiva). Puede hacer que Spring use archivos de propiedades, pero con los archivos de propiedades de los servlets tendrá que estar en una ubicación predeterminada, lo que significa que no puede implementar varios servlets de la misma vez en el mismo cuadro. Puede usar Spring con JNDI para cambiar las propiedades en el momento del inicio del servlet, pero si está usando JNDI para parámetros modificables por el administrador, la necesidad de Spring disminuye (ya que JNDI es efectivamente un Localizador de servicios).

  7. Con Spring puede perder el control del programa si Spring está enviando sus métodos. Esto es conveniente y funciona para muchos tipos de aplicaciones, pero no para todas. Es posible que deba controlar el flujo del programa cuando necesite crear tareas (subprocesos, etc.) durante la inicialización o necesite recursos modificables que Spring no sabía cuando el contenido estaba vinculado a su WAR.

Spring es muy bueno para la gestión de transacciones y tiene algunas ventajas. Es solo que IoC puede ser una ingeniería excesiva en muchas situaciones e introducir una complejidad injustificada para los mantenedores. No use IoC automáticamente sin pensar en formas de no usarlo primero.

Moa
fuente
77
Además, su ServiceLocator siempre puede usar IoC de Spring, abstrayendo su código de ser dependiente de Spring, lleno de anotaciones de Spring y magik indescifrable. Recientemente porté un montón de código a GoogleAppEngine donde Spring no es compatible. ¡Desearía haber ocultado todo IoC detrás de una ServiceFactory en primer lugar!
Alex Worden el
IoC fomenta un modelo de dominio anémico, que desprecio. Los beans de entidad necesitan una forma de buscar sus servicios para poder implementar su propio comportamiento. En esa capa, realmente no puedes evitar necesitar un localizador de servicios.
Joel
44
Bizarro Uso Spring TODO el tiempo con anotaciones. Aunque de hecho está involucrada una cierta curva de aprendizaje, ahora no tengo ningún problema en mantenimiento, depuración, claridad, legibilidad ... Supongo que cómo estructuran las cosas es el truco.
Lawrence
25

Es cierto que incluir la clase en application-context.xml evita la necesidad de usar getBean. Sin embargo, incluso eso es realmente innecesario. Si está escribiendo una aplicación independiente y NO desea incluir su clase de controlador en application-context.xml, puede usar el siguiente código para que Spring conecte automáticamente las dependencias del controlador:

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

He necesitado hacer esto un par de veces cuando tengo algún tipo de clase independiente que necesita usar algún aspecto de mi aplicación (por ejemplo, para pruebas) pero no quiero incluirla en el contexto de la aplicación porque no en realidad parte de la aplicación. Tenga en cuenta también que esto evita la necesidad de buscar el bean usando un nombre de cadena, que siempre he pensado que es feo.

iftheshoefritz
fuente
Pude usar este método con éxito con la @Autowiredanotación también.
blong
21

Uno de los mejores beneficios de usar algo como Spring es que no tiene que conectar sus objetos entre sí. La cabeza de Zeus se abre y aparecen tus clases, completamente formadas con todas sus dependencias creadas y conectadas, según sea necesario. Es mágico y fantástico.

Cuanto más dices ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, menos magia obtienes. Menos código es casi siempre mejor. Si su clase realmente necesitaba un bean ClassINeed, ¿por qué no lo conectó?

Dicho esto, algo obviamente necesita crear el primer objeto. No hay nada de malo en que tu método principal adquiera un bean o dos a través de getBean (), pero debes evitarlo porque cada vez que lo usas, realmente no estás usando toda la magia de Spring.

Brandon Yarbrough
fuente
1
Pero el OP no dice "ClassINeed", está diciendo "BeanNameINeed", lo que permite que el contenedor IoC cree una instancia en cualquier clase configurada de cualquier manera. Quizás es más como el patrón de "localizador de servicios" que IoC, pero todavía resulta en un acoplamiento flojo.
HDave
16

La motivación es escribir código que no dependa explícitamente de Spring. De esa manera, si elige cambiar de contenedor, no tiene que volver a escribir ningún código.

Piense en el contenedor como algo invisible para su código, que cubre mágicamente sus necesidades, sin que se lo pidan.

La inyección de dependencia es un contrapunto al patrón del "localizador de servicios". Si va a buscar dependencias por nombre, también podría deshacerse del contenedor DI y usar algo como JNDI.

erickson
fuente
11

Usar @Autowiredo ApplicationContext.getBean()es realmente lo mismo. En ambos sentidos, obtiene el bean que está configurado en su contexto y en ambos sentidos su código depende de spring. Lo único que debe evitar es crear instancias de su ApplicationContext. ¡Haz esto solo una vez! En otras palabras, una línea como

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

solo debe usarse una vez en su aplicación.

easyplanner.cu.cc
fuente
No Algunas veces @Autowired o ApplicationContext.getBean () pueden producir beans totalmente diferentes. No estoy seguro de cómo sucedió, pero estoy luchando con este problema en este momento.
Oleksandr_DJ
4

La idea es que confíe en la inyección de dependencia ( inversión de control o IoC). Es decir, sus componentes están configurados con los componentes que necesitan. Estas dependencias se inyectan (a través del constructor o establecedores): no se obtiene usted mismo.

ApplicationContext.getBean()requiere que nombre un bean explícitamente dentro de su componente. En cambio, al usar IoC, su configuración puede determinar qué componente se usará.

Esto le permite volver a cablear su aplicación con diferentes implementaciones de componentes fácilmente, o configurar objetos para probar de manera sencilla al proporcionar variantes simuladas (por ejemplo, un DAO simulado para que no golpee una base de datos durante la prueba)

Brian Agnew
fuente
4

Otros han señalado el problema general (y son respuestas válidas), pero solo ofreceré un comentario adicional: no es que NUNCA debas hacerlo, sino que lo hagas lo menos posible.

Por lo general, esto significa que se hace exactamente una vez: durante el arranque. Y luego es solo para acceder al bean "raíz", a través del cual se pueden resolver otras dependencias. Este puede ser un código reutilizable, como el servlet base (si se desarrollan aplicaciones web).

StaxMan
fuente
4

Una de las premisas de Spring es evitar el acoplamiento . Defina y use Interfaces, DI, AOP y evite usar ApplicationContext.getBean () :-)

rebeldes
fuente
4

Una de las razones es la comprobabilidad. Digamos que tienes esta clase:

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

¿Cómo puedes probar este frijol? Por ejemplo, así:

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, verdad?

Si bien aún depende de Spring (debido a las anotaciones), puede eliminar su dependencia de Spring sin cambiar ningún código (solo las definiciones de las anotaciones) y el desarrollador de la prueba no necesita saber nada sobre cómo funciona Spring (tal vez debería de todos modos, pero permite revisar y probar el código por separado de lo que hace spring).

Todavía es posible hacer lo mismo cuando se utiliza el ApplicationContext. Sin embargo, debes burlarte de ApplicationContextque es una interfaz enorme. Necesita una implementación ficticia o puede usar un marco de imitación como 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);
    }
}

Esta es una gran posibilidad, pero creo que la mayoría de la gente estaría de acuerdo en que la primera opción es más elegante y simplifica la prueba.

La única opción que realmente es un problema es esta:

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

Probar esto requiere grandes esfuerzos o su bean intentará conectarse a stackoverflow en cada prueba. Y tan pronto como tenga una falla en la red (o los administradores en stackoverflow lo bloqueen debido a una tasa de acceso excesiva), tendrá pruebas de falla al azar.

Como conclusión, no diría que usar el ApplicationContextdirectamente es automáticamente incorrecto y debe evitarse a toda costa. Sin embargo, si hay mejores opciones (y las hay en la mayoría de los casos), utilice las mejores opciones.

yanqui
fuente
3

Solo he encontrado dos situaciones en las que se requería getBean ():

Otros han mencionado el uso de getBean () en main () para obtener el bean "principal" para un programa independiente.

Otro uso que he hecho de getBean () es en situaciones en las que una configuración interactiva del usuario determina la composición del bean para una situación particular. De modo que, por ejemplo, parte del sistema de arranque recorre una tabla de base de datos usando getBean () con una definición de bean scope = 'prototype' y luego configurando propiedades adicionales. Presumiblemente, hay una interfaz de usuario que ajusta la tabla de la base de datos que sería más amigable que intentar (re) escribir el XML de contexto de la aplicación.

nsayer
fuente
3

Hay otro momento en que usar getBean tiene sentido. Si está reconfigurando un sistema que ya existe, donde las dependencias no se invocan explícitamente en los archivos de contexto de Spring. Puede comenzar el proceso haciendo llamadas a getBean, para que no tenga que conectar todo de una vez. De esta manera, puede construir lentamente su configuración de resorte colocando cada pieza en su lugar con el tiempo y alineando las brocas correctamente. Las llamadas a getBean eventualmente serán reemplazadas, pero a medida que comprenda la estructura del código, o le falte, puede comenzar el proceso de cablear más y más beans y usar cada vez menos llamadas para getBean.

Tony Giaccone
fuente
2

sin embargo, todavía hay casos en los que necesita el patrón del localizador de servicios. por ejemplo, tengo un bean de controlador, este controlador puede tener algunos beans de servicio predeterminados, que pueden ser inyectados por la dependencia de la configuración. aunque también podría haber muchos servicios adicionales o nuevos que este controlador puede invocar ahora o más tarde, que luego necesitan el localizador de servicios para recuperar los beans de servicio.

lwpro2
fuente
0

Debe usar: ConfigurableApplicationContext en lugar de para ApplicationContext

Hai Nguyen Le
fuente