mirror of
https://github.com/apache/struts.git
synced 2026-08-05 14:47:09 +00:00
WW-5668 Keep the localized-text providers deserializable across a version upgrade (#1824)
Follow-up to #1821. Pins serialVersionUID to the value implicitly computed for the Struts 7.2.1 class shape instead of 1L, so a session serialized by a 7.2.1 node still loads on a 7.3.0 one during a rolling upgrade rather than failing with InvalidClassException. Such a stream carries no value for the new cache settings, and field initialisers do not run during deserialization, so readObject restores their defaults before rebuilding the caches - without that guard it failed with a NullPointerException. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
// Pinned to the value implicitly computed for the Struts 7.2.1 class shape, so sessions serialized by
|
||||
// an older node still deserialize here during a rolling upgrade. The caches that became transient are
|
||||
// simply discarded from such a stream and rebuilt by readObject.
|
||||
private static final long serialVersionUID = -4455624669971032217L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class);
|
||||
|
||||
@@ -77,8 +80,10 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
// transient + reinitialised in readObject: a bare Object is not Serializable.
|
||||
private transient Object bundlesMapLock = new Object();
|
||||
|
||||
private static final int DEFAULT_I18N_CACHE_MAX_SIZE = 10000;
|
||||
|
||||
private volatile CacheType i18nCacheType = CacheType.WTLFU;
|
||||
private volatile int i18nCacheMaxSize = 10000;
|
||||
private volatile int i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
|
||||
|
||||
private <K, V> OgnlCache<K, V> buildI18nCache() {
|
||||
return new DefaultOgnlCacheFactory<K, V>(i18nCacheMaxSize, i18nCacheType).buildOgnlCache();
|
||||
@@ -473,6 +478,14 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
bundlesMapLock = new Object();
|
||||
// Field initialisers do not run during deserialization, so a stream written before these settings
|
||||
// existed (an older node in a rolling upgrade) leaves them at null/0. Restore the defaults.
|
||||
if (i18nCacheType == null) {
|
||||
i18nCacheType = CacheType.WTLFU;
|
||||
}
|
||||
if (i18nCacheMaxSize <= 0) {
|
||||
i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
|
||||
}
|
||||
rebuildI18nCaches();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
// Pinned to the value implicitly computed for the Struts 7.2.1 class shape, see AbstractLocalizedTextProvider.
|
||||
private static final long serialVersionUID = 3777960740495792359L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(GlobalLocalizedTextProvider.class);
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
// Pinned to the value implicitly computed for the Struts 7.2.1 class shape, see AbstractLocalizedTextProvider.
|
||||
private static final long serialVersionUID = 1939638936989370989L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class);
|
||||
private transient ReflectionProvider reflectionProvider;
|
||||
|
||||
@@ -812,6 +812,38 @@ public class StrutsLocalizedTextProviderTest extends XWorkTestCase {
|
||||
assertEquals("Static cached value", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* A stream written before the i18n cache settings existed carries no value for them, and field
|
||||
* initialisers do not run during deserialization, so they arrive as null/0. The provider must still
|
||||
* come back usable rather than failing while rebuilding its caches.
|
||||
*/
|
||||
public void testProviderIsUsableAfterDeserializingLegacyStream() throws Exception {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
|
||||
// Simulate the absent-field state an older stream produces.
|
||||
java.lang.reflect.Field cacheType = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheType");
|
||||
cacheType.setAccessible(true);
|
||||
cacheType.set(provider, null);
|
||||
java.lang.reflect.Field maxSize = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheMaxSize");
|
||||
maxSize.setAccessible(true);
|
||||
maxSize.setInt(provider, 0);
|
||||
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
try (java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos)) {
|
||||
oos.writeObject(provider);
|
||||
}
|
||||
Object restored;
|
||||
try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(baos.toByteArray()))) {
|
||||
restored = ois.readObject();
|
||||
}
|
||||
|
||||
TestStrutsLocalizedTextProvider deserialized = (TestStrutsLocalizedTextProvider) restored;
|
||||
String result = deserialized.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
assertEquals("Static cached value", result);
|
||||
}
|
||||
|
||||
public void testCacheTypeSelectionKeepsProviderWorking() {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
provider.setI18nCacheType("basic");
|
||||
|
||||
Reference in New Issue
Block a user