接口还是目标类:我应该选择哪种proxyMode?

49

我正在寻找一种存储对象的方法,似乎最好的方法是使用代理。我在网上找到了两个注解,我应该使用哪一个:

@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
或者
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS )

此外,代理是否比使用@Component @Scope("session")或使用@SessionAttributes更好?

3个回答

61
您需要了解每个注释的作用,以便自行选择。请参阅javadoc,这里。继续阅读以获取更详细的解释。

第一个

@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)

创建

使用JDK动态代理实现目标对象类所暴露的所有接口。

换句话说,代理将是目标对象类实现的接口的子类型,但不会是目标对象类本身的子类。

基本上,Spring执行以下操作

public class Example {
    public static void main(String[] args) throws Exception {
        Foo target = new Foo();
        InvocationHandler proxyHandler = ... // some proxy specific logic, likely referencing the `target`

        // works fine
        Printable proxy = (Printable) Proxy.newProxyInstance(Example.class.getClassLoader(),
                target.getClass().getInterfaces(), proxyHandler);

        // not possible, ClassCastException
        Foo foo = (Foo) proxy; 
    }

    public static class Foo implements Printable {
        @Override
        public void print() {
        }
    }

    public interface Printable {
        void print();
    }
}

代理返回的类型不会是Foo,因此您无法将其注入到该类型的任何目标中。例如,Spring将无法将其注入到类似于以下字段的字段中。
@Autowired
private Foo foo;

但是它将成功地将代理注入到类似字段中。
@Autowired
private Printable printable;

所有对代理的调用都将由InvocationHandler处理(通常执行一些特定于用例的逻辑,然后委托给目标对象)。
第二个注释
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS )

创建

基于类的代理(使用CGLIB)。

除了接口外,使用CGLIB,Spring还能够创建一个类是目标类子类的代理。本质上,它执行以下操作:

Foo target = new Foo();
net.sf.cglib.proxy.Enhancer enhancer = new net.sf.cglib.proxy.Enhancer();
enhancer.setInterfaces(target.getClass().getInterfaces());
enhancer.setSuperclass(target.getClass());
net.sf.cglib.proxy.MethodInterceptor interceptor = ... // some proxy specific logic, likely referencing the `target`
enhancer.setCallback(interceptor);

// works fine
Foo proxy = (Foo) enhancer.create();

CGLIB会创建一个Foo子类的新类并实例化它(调用Foo的构造函数)。所有对代理的调用都将被提供的回调拦截(通常执行一些用例特定逻辑,然后委托给目标对象)。
由于代理类扩展了Foo,因此Spring可以将代理注入到字段(或构造函数/方法参数)中。
@Autowired
private Foo injectMe;

所有这些都是为了说,如果你正在编程到接口,那么ScopedProxyMode.INTERFACES就足够了。如果不是,则使用ScopedProxyMode.TARGET_CLASS
关于使用@SessionAttributes,它并不是会话作用域bean的替代品。会话属性只是对象,它们不是bean。它们没有完整的生命周期、注入能力和代理行为,这些是bean可能具有的。

28
如果想要将整个Bean存储在会话中,请使用@Scope,否则请使用@SessionAttributes。在使用@Scope的情况下,如果类实现了一些接口,则使用INTERFACES代理模式,否则使用TARGET_CLASS。
通常,您的服务实现一个接口,这允许使用JDK代理(INTERFACES模式)。但如果不是这种情况,请使用TARGET_CLASS,它创建一个CGLIB代理。
尽可能使用INTERFACES,如果Bean没有实现接口,则使用TARGET作为最后的选择。

8

在查看上面评论中提供的博客文章时,我发现有评论指出了基于接口的代理的缺点。

在这篇文章中,用户Flemming Jønsson发表了以下评论:

小心使用基于接口的代理。如果您使用Spring Security或Spring Transactions,则在使用基于接口的代理时可能会遇到一些奇怪的问题。例如,如果您有一个名为T的bean,该bean具有两个被注释为@Transactional的方法a()和b()。直接从其他bean调用a()或b()将按照配置正确执行。但是,如果您引入了内部调用 - 其中a()调用b(),那么b的事务元数据将没有任何效果。原因是当您使用基于接口的代理时,内部调用将不会通过代理 - 因此事务拦截器将无法有机会启动新事务。对于安全性也是如此。如果方法a()仅需要USER角色,但调用需要ADMIN角色的b(),则来自a()到b()的内部调用将为任何USER执行而没有警告。与上述相同的原因,内部调用不会经过代理,因此安全拦截器无法对从a()到b()的调用进行处理。要解决这些问题,请使用targetClass。

12
这不仅适用于基于接口的代理,CGLIB代理也存在同样的问题。 - oskansavli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接