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