I want to turn off serialization in my Wicket app and store all page/session information in RAM. My application has a very small number of users (generally 1); I do not need
I can't comment about anything specific to Wicket, but speaking generally the entire point of an Http Session
is to store Serializable
state between requests (and in clustered environments, to allow that state to be replicated to multiple nodes in the cluster to provide redundancy in the event of a node failure). Putting something that is not Serializable
into it is generally considered an error, as shown by your stack trace. I'd be somewhat surprised if there is any sort of configuration option that would change this (though perhaps there is; as I said I can't really comment on the Wicket side of things).
A simple alternative, if you do not require true persistence and if the data is not exceptionally large/complex, is to just use hidden form fields on your page to keep track of the relevant state.
But if what you want is an in-memory cache, why not implement your own? It's simple enough to do:
public class SessionCache {
private static final Map<String, Map<String, Object>> CACHE = Collections.synchronizedMap(new HashMap<String, Map<String, Object>>());
public static Object getAttribute(String sessionId, String attribName) {
Map<String, Object> attribs = CACHE.get(sessionId);
if (attribs != null) {
synchronized(attribs) {
return attribs.get(attribName);
}
}
return null;
}
public static void setAttribute(String sessionId, String attribName, Object attribValue) {
Map<String, Object> attribs = CACHE.get(sessionId);
if (attribs == null) {
attribs = new HashMap<String, Object>();
CACHE.put(sessionId, attribs);
}
synchronized(attribs) {
attribs.put(attribName, attribValue);
}
}
public static void destroySession(String sessionId) {
CACHE.remove(sessionId);
}
public static void createSession(String sessionId, boolean force) {
if (force || ! CACHE.containsKey(sessionId)) {
CACHE.put(sessionId, new HashMap<String, Object>());
}
}
}
Note that you'll want to hook that into Wicket's session lifecycle so that old sessions are removed when they expire. Otherwise you'll have a gradual memory leak on your hands. From the docs it looks like you can accomplish this using registerUnboundListener()
on the HttpSessionStore class.
You can implement your own IPageStore that keeps pages in memory.
Here is the solution that I came up with, based on svenmeier's answers. I'm sure that this is not 100% correct, but it's working fine in my testing:
package com.prosc.wicket;
import org.apache.wicket.Application;
import org.apache.wicket.DefaultPageManagerProvider;
import org.apache.wicket.page.IManageablePage;
import org.apache.wicket.page.IPageManagerContext;
import org.apache.wicket.pageStore.IDataStore;
import org.apache.wicket.pageStore.IPageStore;
import org.apache.wicket.pageStore.memory.HttpSessionDataStore;
import org.apache.wicket.pageStore.memory.PageNumberEvictionStrategy;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* This class disables Wicket's serialization behavior, while still retaining session and page data in memory (so back button will work).
* This will run out of memory under heavy load; but it's very convenient for low volume web applications.
* To disable serialization in your application, call this code:
* <pre>
* setPageManagerProvider( new NoSerializePageManagerProvider( this, getPageManagerContext() ) );
* </pre>
*/
public class NoSerializePageManagerProvider extends DefaultPageManagerProvider {
private IPageManagerContext pageManagerContext;
public NoSerializePageManagerProvider( Application application, IPageManagerContext pageManagerContext ) {
super( application );
this.pageManagerContext = pageManagerContext;
}
@Override
protected IDataStore newDataStore() {
return new HttpSessionDataStore( pageManagerContext, new PageNumberEvictionStrategy( 20 ) );
}
@Override
protected IPageStore newPageStore( IDataStore dataStore ) {
return new IPageStore() {
Map<String,Map<Integer,IManageablePage>> cache = new HashMap<String, Map<Integer, IManageablePage>>();
public void destroy() {
cache = null;
}
public IManageablePage getPage( String sessionId, int pageId ) {
Map<Integer, IManageablePage> sessionCache = getSessionCache( sessionId, false );
IManageablePage page = sessionCache.get( pageId );
if( page == null ) {
throw new IllegalArgumentException( "Found this session, but there is no page with id " + pageId );
}
return page;
}
public void removePage( String sessionId, int pageId ) {
getSessionCache( sessionId, false ).remove( pageId );
}
public void storePage( String sessionId, IManageablePage page ) {
getSessionCache( sessionId, true ).put( page.getPageId(), page );
}
public void unbind( String sessionId ) {
cache.remove( sessionId );
}
public Serializable prepareForSerialization( String sessionId, Object page ) {
return null;
}
public Object restoreAfterSerialization( Serializable serializable ) {
return null;
}
public IManageablePage convertToPage( Object page ) {
return (IManageablePage)page;
}
private Map<Integer, IManageablePage> getSessionCache( String sessionId, boolean create ) {
Map<Integer, IManageablePage> sessionCache = cache.get( sessionId );
if( sessionCache == null ) {
if( create ) {
sessionCache = new HashMap<Integer, IManageablePage>();
cache.put( sessionId, sessionCache );
} else {
throw new IllegalArgumentException( "There are no pages stored for session id " + sessionId );
}
}
return sessionCache;
}
};
}
}
I want to improve on Johnny's answer who improves on Jesse's answer :)
IPageManagerProvider
, not only the IPageStore
store.unbind(sessionId)
when the session unbindspublic class NoSerializationButCachingPageManagerProvider implements IPageManagerProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(NoSerializationButCachingPageManagerProvider.class);
private final Application application;
public NoSerializationButCachingPageManagerProvider(final Application application) {
this.application = Args.notNull(application, "application");
LOGGER.info("Pages don't get serialized, but in-memory cached.");
}
@Override
public IPageManager get(IPageManagerContext pageManagerContext) {
final IPageStore store = new NoSerializationButCachingPageStore();
final IPageManager manager = new PageStoreManager(application.getName(), store, pageManagerContext);
/*
* session unbind must call store.unbind() to free memory (prevents memory leak)
*/
application.getSessionStore().registerUnboundListener((String sessionId) -> store.unbind(sessionId));
return manager;
}
}
class NoSerializationButCachingPageStore implements IPageStore {
private static final Logger LOGGER = LoggerFactory.getLogger(NoSerializationButCachingPageStore.class);
private static final int MEDIAN_OF_NUMBER_OF_SESSIONS = 100;
private final ConcurrentMap<String, CustomLinkedHashMap<Integer, IManageablePage>> cache = new ConcurrentHashMap<>(MEDIAN_OF_NUMBER_OF_SESSIONS);
@Override
public void destroy() {
cache.clear();
}
@Override
public IManageablePage getPage(final String sessionId, final int pageId) {
LOGGER.info("getPage. SessionId: {}, pageId: {}", sessionId, pageId);
final Map<Integer, IManageablePage> sessionCache = getSessionCache(sessionId);
final RequestCycle requestCycle = RequestCycle.get();
if (sessionCache == null) {
LOGGER.warn("Missing cache. SessionId: {}, pageId: {}, URL: {}", sessionId, pageId, requestCycle == null ? "" : requestCycle.getRequest().getUrl());
return null;
}
IManageablePage page;
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
page = sessionCache.get(pageId);
}
if (page == null && LOGGER.isDebugEnabled()) {
LOGGER.debug("Missed page. SessionId: {}, pageId: {}, URL: {}", sessionId, pageId, requestCycle == null ? "" : requestCycle.getRequest().getUrl());
}
return page;
}
@Override
public void removePage(final String sessionId, final int pageId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("removePage. SessionId: {}, pageId: {}", sessionId, pageId);
}
final Map<Integer, IManageablePage> sessionCache = getSessionCache(sessionId);
if (sessionCache != null) {
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
sessionCache.remove(pageId);
}
}
}
@Override
public void storePage(final String sessionId, final IManageablePage page) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("storePage. SessionId: {}, pageId: {}, cache-size: {}", sessionId, page.getPageId(), cache.size());
}
final LinkedHashMap<Integer, IManageablePage> sessionCache = getOrCreateSessionCache(sessionId);
final int pageId = page.getPageId();
// noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
if (sessionCache.containsKey(pageId)) {
// do this to change insertion order and update least inserted entry
sessionCache.remove(pageId);
sessionCache.put(pageId, page);
} else {
sessionCache.put(pageId, page);
}
}
}
/**
* @param sessionId
*/
@Override
public void unbind(final String sessionId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("unbind/cache-remove. SessionId: {}", sessionId);
}
cache.remove(sessionId);
}
@Override
public Serializable prepareForSerialization(final String sessionId, final Serializable page) {
return null;
}
@Override
public Object restoreAfterSerialization(final Serializable serializable) {
return null;
}
@Override
public IManageablePage convertToPage(final Object page) {
return (IManageablePage) page;
}
private Map<Integer, IManageablePage> getSessionCache(final String sessionId) {
return cache.get(sessionId);
}
private CustomLinkedHashMap<Integer, IManageablePage> getOrCreateSessionCache(final String sessionId) {
return cache.computeIfAbsent(sessionId, (final String s) -> new CustomLinkedHashMap<>());
}
/**
* Mimics "least recently inserted" cache
*/
private static class CustomLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1L;
/**
* use this parameter to control memory consumption and frequency of appearance of PageExpiredException
*/
private static final int MAX_PAGES_PER_SESSION = 3;
@Override
protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
return size() > MAX_PAGES_PER_SESSION;
}
}
}
I want to improve Jesse's answer. Below is a thread-safe implementation of IPageStore with internal "Least Recently Inserted" cache (keeps at most 5 recently accessed stateful pages per session):
public class CustomPageStore implements IPageStore {
private static final Logger logger = LoggerFactory.getLogger(CustomPageStore.class);
private static final int MEDIAN_OF_NUMBER_OF_SESSIONS = 6000;
private ConcurrentMap<String, CustomLinkedHashMap<Integer, IManageablePage>> cache = new ConcurrentHashMap<>(MEDIAN_OF_NUMBER_OF_SESSIONS);
@Override
public void destroy() {
cache.clear();
}
@Override
public IManageablePage getPage(final String sessionId, int pageId) {
final Map<Integer, IManageablePage> sessionCache = getSessionCache(sessionId);
final RequestCycle requestCycle = RequestCycle.get();
if (sessionCache == null) {
logger.warn("Missing cache. SessionId: {}, pageId: {}, URL: {}", sessionId, pageId, requestCycle == null ? StringUtils.EMPTY : requestCycle.getRequest().getUrl());
return null;
}
final IManageablePage page;
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
page = sessionCache.get(pageId);
}
if (page == null && logger.isDebugEnabled()) {
logger.debug("Missed page. SessionId: {}, pageId: {}, URL: {}", sessionId, pageId, requestCycle == null ? StringUtils.EMPTY : requestCycle.getRequest().getUrl());
}
return page;
}
@Override
public void removePage(final String sessionId, int pageId) {
final Map<Integer, IManageablePage> sessionCache = getSessionCache(sessionId);
if (sessionCache != null) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
sessionCache.remove(pageId);
}
}
}
@Override
public void storePage(final String sessionId, IManageablePage page) {
final LinkedHashMap<Integer, IManageablePage> sessionCache = getOrCreateSessionCache(sessionId);
final int pageId = page.getPageId();
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (sessionCache) {
if (sessionCache.containsKey(pageId)) {
// do this to change insertion order and update least inserted entry
sessionCache.remove(pageId);
sessionCache.put(pageId, page);
} else {
sessionCache.put(pageId, page);
}
}
}
@Override
public void unbind(final String sessionId) {
cache.remove(sessionId);
}
@Override
public Serializable prepareForSerialization(String sessionId, Object page) {
return null;
}
@Override
public Object restoreAfterSerialization(Serializable serializable) {
return null;
}
@Override
public IManageablePage convertToPage(final Object page) {
return (IManageablePage) page;
}
@Nullable
private Map<Integer, IManageablePage> getSessionCache(final String sessionId) {
return cache.get(sessionId);
}
@Nonnull
private CustomLinkedHashMap<Integer, IManageablePage> getOrCreateSessionCache(final String sessionId) {
return cache.computeIfAbsent(sessionId, s -> new CustomLinkedHashMap<>());
}
/** Mimics "least recently inserted" cache */
private static class CustomLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
/** use this parameter to control memory consumption and frequency of appearance of PageExpiredException */
private static final int MAX_PAGES_PER_SESSION = 5;
@Override
protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
return size() > MAX_PAGES_PER_SESSION;
}
}
}