Spring自定义作用域生命周期Bean终止

8

问题: 我该如何告诉Spring一组具有自定义作用域的bean应该被视为垃圾,以便同一线程上的下一个请求不会重复使用它们的状态?

我的做法: 我在Spring中实现了一个自定义作用域,以模仿请求作用域(HttpRequest)的生命周期,但针对TcpRequests。它与这里找到的内容非常相似。

我发现的许多自定义作用域示例都是原型或单例的变体,没有显式终止bean的发生,或者基于线程本地或ThreadScope,但它们没有描述告诉Spring生命周期已经结束并且所有bean都应该被销毁。

我尝试过的事情(可能不正确):

  • 事件+监听器用于指示范围的开始和结束(这些发生在收到消息时和在响应发送之前);在监听器中,范围被明确清除,从而清除线程本地实现所使用的整个映射(scope.clear())。在测试中手动处理时,清除范围确实会导致下一次调用context.getBean()返回一个新实例,但我的bean在单例类中自动装配,不会得到新的bean-它一遍又一遍地使用相同的bean。

  • 实现BeanFactoryPostProcessor、BeanPostProcessor、BeanFactoryAware、DisposableBean的监听器,并尝试在所有可销毁的bean实例上调用destroy();类似于this,但仅适用于我的自定义作用域。这似乎失败了,因为没有什么明确地结束bean的生命周期,尽管我在接收范围结束事件时调用了customScope.clear();结束范围似乎并不意味着“结束与此范围相关的所有bean”。

  • 我已经广泛阅读了Spring文档,似乎很清楚Spring不管理这些自定义bean的生命周期,因为它不知道何时或如何销毁它们,这意味着必须告诉它何时以及如何销毁它们;我已经尝试阅读和理解Spring提供的会话和请求范围,以便我可以模拟这个过程,但我缺少一些东西(再次强调,由于这不是一个Web感知应用程序,并且我没有使用HttpRequests,因此这些对我不可用,这是我们应用程序结构的非平凡变化)。

请问有没有人能帮我指明正确的方向?

我有以下的代码示例:

XML上下文配置

<int-ip:tcp-connection-factory id="serverConnectionFactory" type="server" port="19000" 
    serializer="javaSerializer" deserializer="javaDeserializer"/>

<int-ip:tcp-inbound-gateway id="inGateway" connection-factory="serverConnectionFactory"
    request-channel="incomingServerChannel" error-channel="errorChannel"/>

<int:channel id="incomingServerChannel" />

<int:chain input-channel="incomingServerChannel">
    <int:service-activator ref="transactionController"/>
</int:chain>

TransactionController(处理请求)

@Component("transactionController")
public class TransactionController {

    @Autowired
    private RequestWrapper requestWrapper;

    @ServiceActivator
    public String handle(final Message<?> requestMessage) {

        // object is passed around through various phases of application
        // object is changed, things are added, and finally, a response is generated based upon this data

        tcpRequestCompletePublisher.publishEvent(requestWrapper, "Request lifecycle complete.");

        return response;
    }
}

TcpRequestScope(范围定义)

@Component
public class TcpRequestScope implements Scope {

    private final ThreadLocal<ConcurrentHashMap<String, Object>> scopedObjects =
        new InheritableThreadLocal<ConcurrentHashMap<String, Object>>({

            @Override
            protected ConcurrentHashMap<String, Object> initialValue(){

                return new ConcurrentHashMap<>();
            }
        };

    private final Map<String, Runnable> destructionCallbacks =
        Collections.synchronizedMap(new HashMap<String, Runnable>());

    @Override
    public Object get(final String name, final ObjectFactory<?> objectFactory) {

        final Map<String, Object> scope = this.scopedObjects.get();
        Object object = scope.get(name);
        if (object == null) {
            object = objectFactory.getObject();
            scope.put(name, object);
        }
        return object;
    }

    @Override
    public Object remove(final String name) {

        final Map<String, Object> scope = this.scopedObjects.get();

        return scope.remove(name);
    }

    @Override
    public void registerDestructionCallback(final String name, final Runnable callback) {

        destructionCallbacks.put(name, callback);
    }

    @Override
    public Object resolveContextualObject(final String key) {

        return null;
    }

    @Override
    public String getConversationId() {

        return String.valueOf(Thread.currentThread().getId());
    }

    public void clear() {

        final Map<String, Object> scope = this.scopedObjects.get();

        scope.clear();

    }

}

TcpRequestCompleteListener

@Component
public class TcpRequestCompleteListener implements ApplicationListener<TcpRequestCompleteEvent> {

    @Autowired
    private TcpRequestScope tcpRequestScope;

    @Override
    public void onApplicationEvent(final TcpRequestCompleteEvent event) {

        // do some processing

        // clear all scope related data (so next thread gets clean slate)
        tcpRequestScope.clear();
    }

}

RequestWrapper(我们在请求生命周期中使用的对象)

@Component
@Scope(scopeName = "tcpRequestScope", proxyMode = 
ScopedProxyMode.TARGET_CLASS)
public class RequestWrapper implements Serializable, DisposableBean {


    // we have many fields here which we add to and build up during processing of request
    // actual request message contents will be placed into this class and used throughout processing

    @Override
    public void destroy() throws Exception {

        System.out.print("Destroying RequestWrapper bean");
    }
}

能够看到一些代码会更好,以便更好地了解为什么需要创建自定义作用域。可能有一种不同的方法来获取特定于线程/请求/Servlet调用的上下文。 - stringy05
@stringy05 感谢您的回复。我已经更新了问题,提供了比之前更多的上下文。从消息路由到“控制器”的时间开始,应该被视为这个“请求”的开始,在结束时,控制器将正式发送事件,表示“请求”的结束。在此期间,所有内容都将共享同一个bean(requestWrapper),而接收到该系统的下一个“请求”将获得这些bean的新实例。我希望这有助于更清楚地阐明用例。很抱歉我不能更简洁。 - Andrew Cotton
那么请求包装器应该有一些关于请求的上下文信息?也许是源IP地址/端口或其他什么?注入它的问题正是你面临的问题 - 只会有一个@TransactionController实例,因此每个人都可能改变同一个对象。即使使用ThreadLocal,属性也绑定到线程生命周期,而不是请求。如果您想要请求上下文,我建议在ServiceActivator方法或@MessageMapping上使用@Header注释进行注入,这非常灵活。 - stringy05
@stringy05,requestWrapper将包含请求正文,它不仅包括头和上下文,还是我们必须做出许多计算和决策的基础。我的理解是,通过使用ScopedProxyMode.TARGET_CLASS,它将按预期解析,并且唯一自动装配的是代理。 - Andrew Cotton
我知道这一切都是基于线程的(因为它是线程本地的);我会阅读关于@MessageMapping@Header的内容。我想强调的最重要的部分是,这不是一个面向Web的应用程序,所以我会确保你提到的选项适用于我。非常感谢你迄今为止的帮助。 - Andrew Cotton
1
是的,Web感知并不是真正的问题,问题在于请求的范围 - 即使是原始TCP,也可能存在某些边界(例如2个\n,一些奇怪的字节编码),您可以利用现成的Spring集成工具。将代理限定在线程范围内应该可以工作,只要您可以控制线程生命周期,但这可能是在框架中已经存在的东西。 (我意识到我正在讨论实现选择,而不是回答问题 :)) - stringy05
1个回答

3
经过数月的尝试,我最终找到了一些文章,指引我朝着正确的方向前进。具体来说,David Winterfeldt的博客文章中的参考文献帮助我理解了SimpleThreadScope,我之前曾阅读过它,并且很清楚Spring在其生命周期结束后不会尝试清除范围,然而,他的文章展示了我之前看到的所有实现中缺失的关键点。
具体而言,这些缺失的关键点是他的实现中ThreadScope类中对ThreadScopeContextHolder的静态引用(在我上面提出的实现中,我称其为TcpRequestScope;本答案的其余部分使用David Winterfeldt的术语,因为他的参考文献将最有用,而且他撰写了它)。
仔细检查自定义线程作用域模块时,我注意到我缺少ThreadScopeContextHolder,它包含对ThreadLocal的静态引用,该ThreadLocal包含ThreadScopeAttributes对象,该对象保存在作用域内的对象。
David的实现和我的最终实现之间有一些小差异,Spring Integration发送响应后,我使用ChannelInterceptor来清除线程范围,因为我正在使用Spring Integration。在他的示例中,他扩展了线程,其中包括在finally块中调用上下文持有者。
我是如何清除作用域属性/bean的:
public class ThreadScopeInterceptor extends ChannelInterceptorAdapter {

@Override
public void afterSendCompletion(final Message<?> message, final MessageChannel channel, final boolean sent,
        @Nullable final Exception exception) {

    // explicitly clear scope variables
    ThreadScopeContextHolder.clearThreadScopeState();
}

此外,我在ThreadScopeContextHolder中添加了一个方法来清除ThreadLocal:
public class ThreadScopeContextHolder {

    // see: reference document for complete ThreadScopeContextHolder class

    /**
     * Clears all tcpRequest scoped beans which are stored on the current thread's ThreadLocal instance by calling
     * {@link ThreadLocal#remove()}.
     */
    public static void clearThreadScopeState() {

        threadScopeAttributesHolder.remove();
    }

}

虽然我不能完全确定由于ThreadLocal的使用不会出现内存泄漏,但是我相信这将按预期工作,因为我调用了ThreadLocal.remove(),它将删除对ThreadScopeAttributes对象的唯一引用,从而使其可以进行垃圾回收。
欢迎任何改进意见,特别是在使用ThreadLocal方面以及如何在未来可能导致问题方面。
来源:
- David Winterfeldt的自定义线程范围模块 - Spring By Example Custom Thread Scope Module github(请参见上述David Winterfeldt的示例) - jyore的Spring scopes(具体来说,是线程范围) - David Noel的(Devbury)Spring Boot Starter Thread Scope

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