O que é um proxy com escopo no Spring?

21

Como sabemos, o Spring usa proxies para adicionar funcionalidade ( @Transactionale @Scheduledpor exemplo). Existem duas opções - usar um proxy dinâmico JDK (a classe precisa implementar interfaces não vazias) ou gerar uma classe filha usando o gerador de código CGLIB. Eu sempre pensei que proxyMode me permite escolher entre um proxy dinâmico JDK e CGLIB.

Mas pude criar um exemplo que mostra que minha suposição está errada:

Caso 1:

Singleton:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Protótipo:

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

A Principal:

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

Resultado:

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

Aqui podemos ver duas coisas:

  1. MyBeanBfoi instanciado apenas uma vez .
  2. Para adicionar a @Transactionalfuncionalidade MyBeanB, o Spring usou o CGLIB.

Caso 2:

Deixe-me corrigir a MyBeanBdefinição:

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

Nesse caso, a saída é:

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

Aqui podemos ver duas coisas:

  1. MyBeanBfoi instanciado 3 vezes.
  2. Para adicionar a @Transactionalfuncionalidade MyBeanB, o Spring usou o CGLIB.

Você poderia explicar o que está acontecendo? Como o modo proxy realmente funciona?

PS

Eu li a documentação:

/**
 * 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
 */

mas não está claro para mim.

Atualizar

Caso 3:

Investiguei mais um caso, no qual extraí a interface 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 {

e, neste caso, a saída é:

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

Aqui podemos ver duas coisas:

  1. MyBeanBfoi instanciado 3 vezes.
  2. Para adicionar a @Transactionalfuncionalidade MyBeanB, o Spring usou um proxy dinâmico JDK.
gstackoverflow
fonte
Por favor, mostre-nos sua configuração transacional.
Sotirios Delimanolis
@SotiriosDelimanolis Eu não tenho nenhuma configuração especial
gstackoverflow
Não sei sobre beans com escopo definido ou qualquer outro tipo de mágica da estrutura corporativa contida no Spring ou no JEE. O @SotiriosDelimanolis escreveu uma resposta maravilhosa sobre isso, quero comentar apenas nos proxies JDK vs. CGLIB: Nos casos 1 e 2, sua MyBeanBclasse não estende nenhuma interface, portanto, não é de surpreender que o log do console mostre instâncias de proxy CGLIB. No caso 3, você apresenta e implementa uma interface, consequentemente, obtém um proxy JDK. Você até descreve isso em seu texto introdutório.
Kriegaex 03/10/19
Portanto, para tipos que não são de interface, você realmente não tem escolha, eles precisam ser proxies CGLIB porque os proxies JDK funcionam apenas para tipos de interface. No entanto, você pode aplicar proxies CGLIB mesmo para tipos de interface ao usar o Spring AOP. Isso é configurado via <aop:config proxy-target-class="true">ou @EnableAspectJAutoProxy(proxyTargetClass = true), respectivamente.
Kriegaex 03/10/19
@kriegaex Deseja dizer que o Aspectj usa CGlib para geração de proxy?
Gstackoverflow 03/10/19

Respostas:

10

O proxy gerado para o @Transactionalcomportamento tem uma finalidade diferente dos proxies com escopo definido.

O @Transactionalproxy é aquele que agrupa o bean específico para adicionar o comportamento de gerenciamento de sessões. Todas as invocações de métodos executarão o gerenciamento de transações antes e depois da delegação ao bean real.

Se você ilustrar, pareceria

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

Para nossos propósitos, você pode essencialmente ignorar seu comportamento (remova @Transactionale você deverá ver o mesmo comportamento, exceto que não terá o proxy cglib).

O @Scopeproxy se comporta de maneira diferente. A documentação declara:

[...] você precisa injetar um objeto proxy que exponha a mesma interface pública que o objeto com escopo, mas que também possa recuperar o objeto de destino real do escopo relevante (como uma solicitação HTTP) e delegar chamadas de método ao objeto real .

O que o Spring realmente está fazendo é criar uma definição de bean singleton para um tipo de fábrica que representa o proxy. O objeto proxy correspondente, no entanto, consulta o contexto do bean real para cada chamada.

Se você ilustrar, pareceria

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

Como MyBeanBé um protótipo de bean, o contexto sempre retornará uma nova instância.

Para os fins desta resposta, suponha que você recuperou o MyBeanBdiretamente com

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

que é essencialmente o que o Spring faz para satisfazer um @Autowiredalvo de injeção.


No seu primeiro exemplo,

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

Você declara uma definição de bean de protótipo (por meio das anotações). @Scopetem um proxyModeelemento que

Especifica se um componente deve ser configurado como um proxy com escopo definido e, se sim, se o proxy deve ser baseado em interface ou em subclasse.

O padrão é o ScopedProxyMode.DEFAULTque normalmente indica que nenhum proxy com escopo definido deve ser criado, a menos que um padrão diferente tenha sido configurado no nível da instrução de varredura de componente.

Portanto, o Spring não está criando um proxy com escopo definido para o bean resultante. Você recupera esse bean com

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

Agora você tem uma referência a um novo MyBeanBobjeto criado pelo Spring. Assim como qualquer outro objeto Java, as invocações de métodos irão diretamente para a instância referenciada.

Se você usasse getBean(MyBeanB.class)novamente, o Spring retornaria uma nova instância, pois a definição de bean é para um protótipo de bean . Você não está fazendo isso, portanto, todas as invocações de métodos vão para o mesmo objeto.


No seu segundo exemplo,

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

você declara um proxy com escopo implementado por meio do cglib. Ao solicitar um bean desse tipo do Spring com

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

O Spring sabe que MyBeanBé um proxy com escopo definido e, portanto, retorna um objeto proxy que satisfaz a API de MyBeanB(ou seja, implementa todos os seus métodos públicos) que sabe internamente como recuperar um bean de tipo real MyBeanBpara cada chamada de método.

Tente correr

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

Isso retornará truesugerindo que o Spring está retornando um objeto proxy singleton (não um protótipo de bean).

Em uma chamada de método, dentro da implementação do proxy, o Spring usará uma getBeanversão especial que sabe como distinguir entre a definição de proxy e a MyBeanBdefinição de bean real . Isso retornará uma nova MyBeanBinstância (já que é um protótipo) e o Spring delegará a chamada do método a ela por meio de reflexão (clássica Method.invoke).


Seu terceiro exemplo é essencialmente o mesmo que o seu segundo.

Sotirios Delimanolis
fonte
Portanto, para o segundo caso, eu tenho 2 proxies: scoped_proxy, que envolve o transactional_proxy, que envolve o MyBeanB_bean natural ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
É possível ter o proxy CGLIB para scoped_proxy e JDK_Dynamic_proxy para transactiona_proxy?
Gtackoverflow 02/10/19
11
@gstackoverflow Quando você obtém context.getBean(MyBeanB.class), na verdade você não está obtendo o proxy, está obtendo o bean real. @Autowiredobtém o proxy (na verdade, falhará se você injetar em MyBeanBvez do tipo de interface). Não sei por que o Spring permite fazer getBean(MyBeanB.class)com INTERFACES.
Sotirios Delimanolis
11
@gstackoverflow Esqueça @Transactional. Com @Autowired MyBeanBInterfaceproxies e com escopo definido, o Spring injeta o objeto proxy. Se você apenas fizer getBean(MyBeanB.class)isso, o Spring não retornará o proxy, ele retornará o bean de destino.
Sotirios Delimanolis
11
É importante notar que este é um padrão de delegação de implementação em relação aos grãos de dentro da Primavera
Stephan