Fix concurrency issues in XStreamMarshaller

Prior to this commit, if an instance of XStreamMarshaller was used
concurrently from multiple threads without having first invoked the
afterPropertiesSet() method, the private `xstream` field could be
initialized multiple times resulting in a ConcurrentModificationException
in XStream's internal DefaultConverterLookup.

This commit fixes these concurrency issues by making the `xstream` field
volatile and by implementing a double-checked locking algorithm in
getXStream() to avoid concurrent instance creation.

Closes gh-25017
This commit is contained in:
Sam Brannen 2020-05-06 18:46:43 +02:00
parent 740f0d766f
commit ce11fdfa80
2 changed files with 33 additions and 13 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -113,6 +113,7 @@ import org.springframework.util.xml.StaxUtils;
* @author Peter Meijer * @author Peter Meijer
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0 * @since 3.0
*/ */
public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLoaderAware, InitializingBean { public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLoaderAware, InitializingBean {
@ -187,7 +188,7 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo
private ClassLoader beanClassLoader = new CompositeClassLoader(); private ClassLoader beanClassLoader = new CompositeClassLoader();
@Nullable @Nullable
private XStream xstream; private volatile XStream xstream;
/** /**
@ -616,12 +617,21 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo
* <p><b>NOTE: This method has been marked as final as of Spring 4.0.</b> * <p><b>NOTE: This method has been marked as final as of Spring 4.0.</b>
* It can be used to access the fully configured XStream for marshalling * It can be used to access the fully configured XStream for marshalling
* but not configuration purposes anymore. * but not configuration purposes anymore.
* <p>As of Spring Framework 5.2.7, creation of the {@link XStream} instance
* returned by this method is thread safe.
*/ */
public final XStream getXStream() { public final XStream getXStream() {
if (this.xstream == null) { XStream xs = this.xstream;
this.xstream = buildXStream(); if (xs == null) {
synchronized (this) {
xs = this.xstream;
if (xs == null) {
xs = buildXStream();
this.xstream = xs;
}
}
} }
return this.xstream; return xs;
} }

View File

@ -21,10 +21,10 @@ import java.io.Reader;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.IntStream;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
@ -185,13 +185,23 @@ class XStreamMarshallerTests {
@Test @Test
void converters() throws Exception { void converters() throws Exception {
marshaller.setConverters(new EncodedByteArrayConverter()); marshaller.setConverters(new EncodedByteArrayConverter());
byte[] buf = new byte[]{0x1, 0x2}; byte[] buf = {0x1, 0x2};
Writer writer = new StringWriter();
marshaller.marshal(buf, new StreamResult(writer)); // Execute multiple times concurrently to ensure there are no concurrency issues.
assertThat(XmlContent.from(writer)).isSimilarTo("<byte-array>AQI=</byte-array>"); // See https://github.com/spring-projects/spring-framework/issues/25017
Reader reader = new StringReader(writer.toString()); IntStream.rangeClosed(1, 100).parallel().forEach(n -> {
byte[] bufResult = (byte[]) marshaller.unmarshal(new StreamSource(reader)); try {
assertThat(Arrays.equals(buf, bufResult)).as("Invalid result").isTrue(); Writer writer = new StringWriter();
marshaller.marshal(buf, new StreamResult(writer));
assertThat(XmlContent.from(writer)).isSimilarTo("<byte-array>AQI=</byte-array>");
Reader reader = new StringReader(writer.toString());
byte[] bufResult = (byte[]) marshaller.unmarshal(new StreamSource(reader));
assertThat(bufResult).as("Invalid result").isEqualTo(buf);
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
} }
@Test @Test