8

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):

  • Event + Listener to indicate the beginning and end of the scope (these occur when message is received and just before response is sent); in listener, the scope is explicitly cleared which clears the entire map used by the thread local implementation (scope.clear()). Clearing scope does result in the next call to context.getBean() returning a new instance when handled manually in tests, but my bean which is autowired in a singleton class does not get a new bean--it uses the same bean over and over.

  • Listener which implements: BeanFactoryPostProcessor, BeanPostProcessor, BeanFactoryAware, DisposableBean and attempt to call destroy() on all Disposable bean instances; something like this but for my custom scope only. This seems to fail in that nothing is explicitly ending the lifecycle of the beans, despite the fact that I'm calling customScope.clear() when I receive the scope ending event; ending the scope doesn't seem to translate to "end all beans associated with this scope".

  • I've read Spring documentation extensively and it seems to be clear that Spring doesn't manage the lifecycle of these custom beans in that it doesn't know when or how they should be destroyed, which means that it must be told when and how to destroy them; I've tried to read and understand the Session and Request scopes as provided by Spring so that I can mimic this but am missing something (again, these are not available to me since this is not a web-aware application and I'm not using HttpRequests and it is a non-trivial change in our application's structure)

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");
    }
}
Andrew Cotton
  • 415
  • 3
  • 12
  • It would be good to see some code to get a better idea exactly why you need to create a custom scope. There's probably a different way to get a context specific for a particular invocation of a thread/request/servlet – stringy05 May 23 '18 at 03:16
  • @stringy05 thanks for the response. I updated the question with much more context than before. Starting at the time the message is routed to the "controller" it should be considered the start of this "request", and at the end the controller would formally send the event signifying the end of the "request". Everything in between would share the same bean (requestWrapper), and the next "request" which is received by said system would get a new instance of these beans. I hope this helps clarify the use cases a bit more. I apologize for not being able to be more concise. – Andrew Cotton May 23 '18 at 05:14
  • So request wrapper is meant to have some context about the request? Maybe source IP address / port or something? The problem with injecting it is exactly the problem you are facing - there's only going to be a single instance of your `@TransactionController` so everyone is potentially mutating the same object. Even with ThreadLocal, the properties are bound to the Thread lifecycle, not the request. If it's request context you are after I would inject it with the `@Header` annotation on the `ServiceActivator` method or the `@MessageMapping` which is pretty flexible. – stringy05 May 23 '18 at 07:31
  • @stringy05 the requestWrapper will contain the request body, which is a lot more than headers and context, it is the foundation for many calculations and decisions we have to make. My understanding was that by using ScopedProxyMode.TARGET_CLASS it would resolve as expected and that the only thing which is autowired is a proxy. – Andrew Cotton May 24 '18 at 16:54
  • I'm aware that it is all based on the thread (because it's a thread local); I will read about `@MessageMapping` and `@Header`. The most important part that I want to emphasize is that it is not a web-aware app, so I'm going to make sure that what you mentioned is an option for me. Thank you very much for your help so far. – Andrew Cotton May 24 '18 at 17:00
  • 1
    Yeah, web aware is not really the issue, it's the scope of the request - even If it's raw TCP there's probably some boundary (ie 2 \n's, some weird byte encoding) which you can use to leverage the out of the box spring integration tools. Scoping the proxy to the Thread should work as long as you can control the Thread lifecycle, but it may be inventing stuff that already exists in the framework. (I realise that I'm arguing about the implementation choice, rather than answering the question :) ) – stringy05 May 28 '18 at 04:25

1 Answers1

3

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:

Andrew Cotton
  • 415
  • 3
  • 12