diff --git a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java index df1100e471c..596b587fa29 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java @@ -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"); * 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 Arjen Poutsma * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLoaderAware, InitializingBean { @@ -187,7 +188,7 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo private ClassLoader beanClassLoader = new CompositeClassLoader(); @Nullable - private XStream xstream; + private volatile XStream xstream; /** @@ -616,12 +617,21 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo *

NOTE: This method has been marked as final as of Spring 4.0. * It can be used to access the fully configured XStream for marshalling * but not configuration purposes anymore. + *

As of Spring Framework 5.2.7, creation of the {@link XStream} instance + * returned by this method is thread safe. */ public final XStream getXStream() { - if (this.xstream == null) { - this.xstream = buildXStream(); + XStream xs = this.xstream; + if (xs == null) { + synchronized (this) { + xs = this.xstream; + if (xs == null) { + xs = buildXStream(); + this.xstream = xs; + } + } } - return this.xstream; + return xs; } diff --git a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java index 5d06efc4f4c..5f05936d751 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,10 @@ import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -69,28 +68,24 @@ import static org.mockito.Mockito.mock; * @author Arjen Poutsma * @author Sam Brannen */ -public class XStreamMarshallerTests { +class XStreamMarshallerTests { private static final String EXPECTED_STRING = "42"; - private XStreamMarshaller marshaller; + private final XStreamMarshaller marshaller = new XStreamMarshaller(); - private Flight flight; + private final Flight flight = new Flight(); @BeforeEach - public void createMarshaller() { - marshaller = new XStreamMarshaller(); - Map aliases = new HashMap<>(); - aliases.put("flight", Flight.class.getName()); - marshaller.setAliases(aliases); - flight = new Flight(); + void createMarshaller() { + marshaller.setAliases(Collections.singletonMap("flight", Flight.class.getName())); flight.setFlightNumber(42L); } @Test - public void marshalDOMResult() throws Exception { + void marshalDOMResult() throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); Document document = builder.newDocument(); @@ -108,7 +103,7 @@ public class XStreamMarshallerTests { // see SWS-392 @Test - public void marshalDOMResultToExistentDocument() throws Exception { + void marshalDOMResultToExistentDocument() throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); Document existent = builder.newDocument(); @@ -136,7 +131,7 @@ public class XStreamMarshallerTests { } @Test - public void marshalStreamResultWriter() throws Exception { + void marshalStreamResultWriter() throws Exception { StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); marshaller.marshal(flight, result); @@ -144,7 +139,7 @@ public class XStreamMarshallerTests { } @Test - public void marshalStreamResultOutputStream() throws Exception { + void marshalStreamResultOutputStream() throws Exception { ByteArrayOutputStream os = new ByteArrayOutputStream(); StreamResult result = new StreamResult(os); marshaller.marshal(flight, result); @@ -153,7 +148,7 @@ public class XStreamMarshallerTests { } @Test - public void marshalSaxResult() throws Exception { + void marshalSaxResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flight, result); @@ -168,7 +163,7 @@ public class XStreamMarshallerTests { } @Test - public void marshalStaxResultXMLStreamWriter() throws Exception { + void marshalStaxResultXMLStreamWriter() throws Exception { XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); StringWriter writer = new StringWriter(); XMLStreamWriter streamWriter = outputFactory.createXMLStreamWriter(writer); @@ -178,7 +173,7 @@ public class XStreamMarshallerTests { } @Test - public void marshalStaxResultXMLEventWriter() throws Exception { + void marshalStaxResultXMLEventWriter() throws Exception { XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); StringWriter writer = new StringWriter(); XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer); @@ -188,19 +183,29 @@ public class XStreamMarshallerTests { } @Test - public void converters() throws Exception { + void converters() throws Exception { marshaller.setConverters(new EncodedByteArrayConverter()); - byte[] buf = new byte[]{0x1, 0x2}; - Writer writer = new StringWriter(); - marshaller.marshal(buf, new StreamResult(writer)); - assertThat(XmlContent.from(writer)).isSimilarTo("AQI="); - Reader reader = new StringReader(writer.toString()); - byte[] bufResult = (byte[]) marshaller.unmarshal(new StreamSource(reader)); - assertThat(Arrays.equals(buf, bufResult)).as("Invalid result").isTrue(); + byte[] buf = {0x1, 0x2}; + + // Execute multiple times concurrently to ensure there are no concurrency issues. + // See https://github.com/spring-projects/spring-framework/issues/25017 + IntStream.rangeClosed(1, 100).parallel().forEach(n -> { + try { + Writer writer = new StringWriter(); + marshaller.marshal(buf, new StreamResult(writer)); + assertThat(XmlContent.from(writer)).isSimilarTo("AQI="); + 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 - public void useAttributesFor() throws Exception { + void useAttributesFor() throws Exception { marshaller.setUseAttributeForTypes(Long.TYPE); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -209,7 +214,7 @@ public class XStreamMarshallerTests { } @Test - public void useAttributesForStringClassMap() throws Exception { + void useAttributesForStringClassMap() throws Exception { marshaller.setUseAttributeFor(Collections.singletonMap("flightNumber", Long.TYPE)); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -218,7 +223,7 @@ public class XStreamMarshallerTests { } @Test - public void useAttributesForClassStringMap() throws Exception { + void useAttributesForClassStringMap() throws Exception { marshaller.setUseAttributeFor(Collections.singletonMap(Flight.class, "flightNumber")); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -227,7 +232,7 @@ public class XStreamMarshallerTests { } @Test - public void useAttributesForClassStringListMap() throws Exception { + void useAttributesForClassStringListMap() throws Exception { marshaller.setUseAttributeFor(Collections.singletonMap(Flight.class, Collections.singletonList("flightNumber"))); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -236,7 +241,7 @@ public class XStreamMarshallerTests { } @Test - public void aliasesByTypeStringClassMap() throws Exception { + void aliasesByTypeStringClassMap() throws Exception { Map> aliases = new HashMap<>(); aliases.put("flight", Flight.class); FlightSubclass flight = new FlightSubclass(); @@ -249,7 +254,7 @@ public class XStreamMarshallerTests { } @Test - public void aliasesByTypeStringStringMap() throws Exception { + void aliasesByTypeStringStringMap() throws Exception { Map aliases = new HashMap<>(); aliases.put("flight", Flight.class.getName()); FlightSubclass flight = new FlightSubclass(); @@ -262,7 +267,7 @@ public class XStreamMarshallerTests { } @Test - public void fieldAliases() throws Exception { + void fieldAliases() throws Exception { marshaller.setFieldAliases(Collections.singletonMap("org.springframework.oxm.xstream.Flight.flightNumber", "flightNo")); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -272,17 +277,17 @@ public class XStreamMarshallerTests { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) - public void omitFields() throws Exception { + void omitFields() throws Exception { Map omittedFieldsMap = Collections.singletonMap(Flight.class, "flightNumber"); marshaller.setOmittedFields(omittedFieldsMap); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); - assertXpathNotExists("/flight/flightNumber", writer.toString()); + assertXpathDoesNotExist("/flight/flightNumber", writer.toString()); } @Test @SuppressWarnings({ "rawtypes", "unchecked" }) - public void implicitCollections() throws Exception { + void implicitCollections() throws Exception { Flights flights = new Flights(); flights.getFlights().add(flight); flights.getStrings().add("42"); @@ -298,14 +303,14 @@ public class XStreamMarshallerTests { Writer writer = new StringWriter(); marshaller.marshal(flights, new StreamResult(writer)); String result = writer.toString(); - assertXpathNotExists("/flights/flights", result); + assertXpathDoesNotExist("/flights/flights", result); assertXpathExists("/flights/flight", result); - assertXpathNotExists("/flights/strings", result); + assertXpathDoesNotExist("/flights/strings", result); assertXpathExists("/flights/string", result); } @Test - public void jettisonDriver() throws Exception { + void jettisonDriver() throws Exception { marshaller.setStreamDriver(new JettisonMappedXmlDriver()); Writer writer = new StringWriter(); marshaller.marshal(flight, new StreamResult(writer)); @@ -318,7 +323,7 @@ public class XStreamMarshallerTests { } @Test - public void jsonDriver() throws Exception { + void jsonDriver() throws Exception { marshaller.setStreamDriver(new JsonHierarchicalStreamDriver() { @Override public HierarchicalStreamWriter createWriter(Writer writer) { @@ -334,7 +339,7 @@ public class XStreamMarshallerTests { } @Test - public void annotatedMarshalStreamResultWriter() throws Exception { + void annotatedMarshalStreamResultWriter() throws Exception { marshaller.setAnnotatedClasses(Flight.class); StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); @@ -349,20 +354,13 @@ public class XStreamMarshallerTests { private static void assertXpathExists(String xPathExpression, String inXMLString){ Source source = Input.fromString(inXMLString).build(); Iterable nodes = new JAXPXPathEngine().selectNodes(xPathExpression, source); - assertThat(count(nodes) > 0).as("Expecting to find matches for Xpath " + xPathExpression).isTrue(); + assertThat(nodes).as("Expecting to find matches for Xpath " + xPathExpression).hasSizeGreaterThan(0); } - private static void assertXpathNotExists(String xPathExpression, String inXMLString){ + private static void assertXpathDoesNotExist(String xPathExpression, String inXMLString){ Source source = Input.fromString(inXMLString).build(); Iterable nodes = new JAXPXPathEngine().selectNodes(xPathExpression, source); - assertThat(count(nodes)).as("Should be zero matches for Xpath " + xPathExpression).isEqualTo(0); - } - - private static int count(Iterable nodes) { - assertThat(nodes).isNotNull(); - AtomicInteger count = new AtomicInteger(); - nodes.forEach(n -> count.incrementAndGet()); - return count.get(); + assertThat(nodes).as("Should be zero matches for Xpath " + xPathExpression).isEmpty(); } }