¿Qué es un proxy con alcance en Spring?

21

Como sabemos, Spring usa proxies para agregar funcionalidad ( @Transactionaly @Scheduledpor ejemplo). Hay dos opciones: usar un proxy dinámico JDK (la clase tiene que implementar interfaces no vacías) o generar una clase secundaria usando el generador de código CGLIB. Siempre pensé que proxyMode me permite elegir entre un proxy dinámico JDK y CGLIB.

Pero pude crear un ejemplo que muestra que mi suposición es incorrecta:

Caso 1:

Único:

@Service
public class MyBeanA {
    @Autowired
    private MyBeanB myBeanB;

    public void foo() {
        System.out.println(myBeanB.getCounter());
    }

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototipo:

@Service
@Scope(value = "prototype")
public class MyBeanB {
    private static final AtomicLong COUNTER = new AtomicLong(0);

    private Long index;

    public MyBeanB() {
        index = COUNTER.getAndIncrement();
        System.out.println("constructor invocation:" + index);
    }

    @Transactional // just to force Spring to create a proxy
    public long getCounter() {
        return index;
    }
}

Principal:

MyBeanA beanA = context.getBean(MyBeanA.class);
beanA.foo();
beanA.foo();
MyBeanB myBeanB = beanA.getMyBeanB();
System.out.println("counter: " + myBeanB.getCounter() + ", class=" + myBeanB.getClass());

Salida:

constructor invocation:0
0
0
counter: 0, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$2f3d648e

Aquí podemos ver dos cosas:

  1. MyBeanBfue instanciado solo una vez .
  2. Para agregar la @Transactionalfuncionalidad para MyBeanB, Spring usó CGLIB.

Caso 2:

Déjame corregir la MyBeanBdefinición:

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

En este caso, la salida es:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$b06d71f2

Aquí podemos ver dos cosas:

  1. MyBeanBfue instanciado 3 veces.
  2. Para agregar la @Transactionalfuncionalidad para MyBeanB, Spring usó CGLIB.

¿Podría explicar lo que está pasando? ¿Cómo funciona realmente el modo proxy?

PD

He leído la documentación:

/**
 * Specifies whether a component should be configured as a scoped proxy
 * and if so, whether the proxy should be interface-based or subclass-based.
 * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
 * that no scoped proxy should be created unless a different default
 * has been configured at the component-scan instruction level.
 * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
 * @see ScopedProxyMode
 */

Pero no está claro para mí.

Actualizar

Caso 3:

Investigué un caso más, en el que extraje la interfaz de MyBeanB:

public interface MyBeanBInterface {
    long getCounter();
}



@Service
public class MyBeanA {
    @Autowired
    private MyBeanBInterface myBeanB;


@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class MyBeanB implements MyBeanBInterface {

y en este caso el resultado es:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class com.sun.proxy.$Proxy92

Aquí podemos ver dos cosas:

  1. MyBeanBfue instanciado 3 veces.
  2. Para agregar la @Transactionalfuncionalidad MyBeanB, Spring usó un proxy dinámico JDK.
gstackoverflow
fuente
Muéstrenos su configuración transaccional.
Sotirios Delimanolis
@SotiriosDelimanolis No tengo ninguna configuración especial
gstackoverflow 02 de
No sé acerca de beans de ámbito o cualquier otro tipo de magia de marco empresarial contenida en Spring o JEE. @SotiriosDelimanolis escribió una respuesta maravillosa sobre esas cosas, quiero comentar solo sobre los servidores proxy JDK vs. CGLIB: en los casos 1 y 2 su MyBeanBclase no extiende ninguna interfaz, por lo que no es sorprendente que el registro de la consola muestre instancias de proxy CGLIB. En el caso 3, introduce e implementa una interfaz, por lo tanto, obtiene un proxy JDK. Incluso lo describe en su texto introductorio.
kriegaex el
Entonces, para los tipos que no son de interfaz, realmente no tienes otra opción, tienen que ser proxies CGLIB porque los proxys JDK solo funcionan para tipos de interfaz. Sin embargo, puede aplicar proxies CGLIB incluso para tipos de interfaz cuando usa Spring AOP. Esto se configura mediante <aop:config proxy-target-class="true">o @EnableAspectJAutoProxy(proxyTargetClass = true), respectivamente.
kriegaex
@kriegaex ¿Quiere decir que Aspectj usa CGlib para la generación de proxy?
gstackoverflow

Respuestas:

10

El proxy generado para el @Transactionalcomportamiento tiene un propósito diferente que los proxies con ámbito.

El @Transactionalproxy es uno que envuelve el bean específico para agregar un comportamiento de administración de sesión. Todas las invocaciones de métodos realizarán la gestión de transacciones antes y después de delegar en el bean real.

Si lo ilustras, se vería como

main -> getCounter -> (cglib-proxy -> MyBeanB)

Para nuestros propósitos, esencialmente puede ignorar su comportamiento (eliminar @Transactionaly debería ver el mismo comportamiento, excepto que no tendrá el proxy cglib).

El @Scopeproxy se comporta de manera diferente. La documentación dice:

[...] necesita inyectar un objeto proxy que exponga la misma interfaz pública que el objeto de ámbito, pero que también pueda recuperar el objeto objetivo real del alcance relevante (como una solicitud HTTP) y delegar llamadas de método al objeto real .

Lo que Spring realmente está haciendo es crear una definición de bean singleton para un tipo de fábrica que representa el proxy. Sin embargo, el objeto proxy correspondiente consulta el contexto del bean real para cada invocación.

Si lo ilustras, se vería como

main -> getCounter -> (cglib-scoped-proxy -> context/bean-factory -> new MyBeanB)

Como MyBeanBes un bean prototipo, el contexto siempre devolverá una nueva instancia.

A los fines de esta respuesta, suponga que recuperó el MyBeanBdirectamente con

MyBeanB beanB = context.getBean(MyBeanB.class);

que es esencialmente lo que hace Spring para satisfacer un @Autowiredobjetivo de inyección.


En tu primer ejemplo,

@Service
@Scope(value = "prototype")
public class MyBeanB { 

Usted declara una definición de bean prototipo (a través de las anotaciones). @Scopetiene un proxyModeelemento que

Especifica si un componente debe configurarse como un proxy con ámbito y, en caso afirmativo, si el proxy debe estar basado en la interfaz o en una subclase.

El valor predeterminado es ScopedProxyMode.DEFAULT, que generalmente indica que no se debe crear un proxy con ámbito a menos que se haya configurado un valor predeterminado diferente en el nivel de instrucción de exploración de componentes.

Entonces Spring no está creando un proxy con alcance para el bean resultante. Recuperas ese frijol con

MyBeanB beanB = context.getBean(MyBeanB.class);

Ahora tiene una referencia a un nuevo MyBeanBobjeto creado por Spring. Esto es como cualquier otro objeto Java, las invocaciones de métodos irán directamente a la instancia referenciada.

Si lo usó getBean(MyBeanB.class)nuevamente, Spring devolvería una nueva instancia, ya que la definición de bean es para un bean prototipo . No está haciendo eso, por lo que todas las invocaciones de métodos van al mismo objeto.


En tu segundo ejemplo,

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

declara un proxy con ámbito que se implementa a través de cglib. Al solicitar un bean de este tipo a Spring con

MyBeanB beanB = context.getBean(MyBeanB.class);

Spring sabe que MyBeanBes un proxy con alcance y, por lo tanto, devuelve un objeto proxy que satisface la API de MyBeanB(es decir, implementa todos sus métodos públicos) que internamente sabe cómo recuperar un bean de tipo real MyBeanBpara cada invocación de método.

Intenta correr

System.out.println("singleton?: " + (context.getBean(MyBeanB.class) == context.getBean(MyBeanB.class)));

Esto devolverá una truepista al hecho de que Spring está devolviendo un objeto proxy singleton (no un bean prototipo).

En una invocación de método, dentro de la implementación de proxy, Spring usará una getBeanversión especial que sepa cómo distinguir entre la definición de proxy y la MyBeanBdefinición de bean real . Eso devolverá una nueva MyBeanBinstancia (ya que es un prototipo) y Spring le delegará la invocación del método a través de la reflexión (clásico Method.invoke).


Su tercer ejemplo es esencialmente el mismo que su segundo.

Sotirios Delimanolis
fuente
Entonces, para el segundo caso, tengo 2 proxies: scoped_proxy que envuelve transaccional_proxy que envuelve MyBeanB_bean natural ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
¿Es posible tener un proxy CGLIB para scoped_proxy y JDK_Dynamic_proxy para transactiona_proxy?
gstackoverflow
1
@gstackoverflow Cuando lo haces context.getBean(MyBeanB.class), en realidad no estás obteniendo el proxy, estás obteniendo el bean real. @Autowiredobtiene el proxy (de hecho, fallará si inyecta en MyBeanBlugar del tipo de interfaz). No sé por qué Spring te permite hacer getBean(MyBeanB.class)con INTERFACES.
Sotirios Delimanolis
1
@gstackoverflow Olvídate de @Transactional. Con @Autowired MyBeanBInterfaceproxies y con ámbito, Spring inyectará el objeto proxy. getBean(MyBeanB.class)Sin embargo, si solo lo hace , Spring no devolverá el proxy, devolverá el bean de destino.
Sotirios Delimanolis
1
Vale la pena señalar que esta es una implementación de patrón de delegación con respecto a los frijoles dentro de la primavera
Stephan