Andrew Cotton
Andrew Cotton

Reputation: 425

Spring Custom Scope Lifecycle Bean Termination

Question: How can I tell Spring that a set of beans with a custom scope should all be considered garbage, so that the next request on the same thread would not re-use their state?

What I've done: I've implemented a custom scope in Spring, to mimic the lifecycle of a request scope (HttpRequest) but for TcpRequests. It is very similar what is found here.

Many examples of custom scopes which I am finding are variants on prototype or singleton with no explicit termination of beans occurring, or, alternatively, they based around a thread local or ThreadScope but they do not describe telling Spring that the lifecycle has ended and that all beans should be destroyed.

Things I have tried (perhaps incorrectly):

Is anyone out there able to point me in the right direction?

I have the following code examples:

Xml Context Configuration:

<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 (handles request):

@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 (scope definition):

@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 (object we use throughout request lifecycle):

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

Upvotes: 9

Views: 4151

Answers (1)

Andrew Cotton
Andrew Cotton

Reputation: 425

After many months and a few more attempts, I finally stumbled across some articles which pointed me in the right direction. Specifically, references in David Winterfeldt's blog post helped me understand the SimpleThreadScope which I had previously read, and was well aware of the fact that Spring makes no attempt to clear the scope after its lifecycle is complete, however, his article demonstrated the missing link for all previous implementations I had seen.

Specifically, the missing links were static references to ThreadScopeContextHolder in ThreadScope class in his implementation (in my proposed implementation above I called mine TcpRequestScope; the rest of this answer uses David Winterfeldt's terms since his reference documentation will prove most useful, and he wrote it).

Upon closer inspection of the Custom Thread Scope Module I noticed I was missing the ThreadScopeContextHolder, which contained a static reference to a ThreadLocal, which contains a ThreadScopeAttributes object which is what holds in-scope objects.

Some minor differences between David's implementation and my final one were, after Spring Integration sends its response, I use a ChannelInterceptor to clear the thread scope, since I'm using Spring Integration. In his examples, he extended threads which included a call to the context holder as part of a finally block.

How I'm clearing the scope attributes / beans:

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

Additionally, I added a method in the ThreadScopeContextHolder which clears the 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();
    }

}

While I'm not absolutely certain that there will not be memory leaks due to the ThreadLocal usage, I believe this will work as expected since I am calling ThreadLocal.remove(), which will remove the only reference to the ThreadScopeAttributes object, and therefore open it up to garbage collection.

Any improvements are welcomed, especially in terms of usage of ThreadLocal and how this might cause problems down the road.

Sources:

Upvotes: 3

Related Questions