Merge branch '7.0.x'

This commit is contained in:
Juergen Hoeller
2026-04-21 20:36:27 +02:00
5 changed files with 175 additions and 104 deletions
@@ -102,7 +102,7 @@ public final class ConcurrentLruCache<K, V> {
if (this.capacity == 0) {
return this.generator.apply(key);
}
final Node<K, V> node = this.cache.get(key);
Node<K, V> node = this.cache.get(key);
if (node == null) {
V value = this.generator.apply(key);
put(key, value);
@@ -115,9 +115,9 @@ public final class ConcurrentLruCache<K, V> {
private void put(K key, V value) {
Assert.notNull(key, "key must not be null");
Assert.notNull(value, "value must not be null");
final CacheEntry<V> cacheEntry = new CacheEntry<>(value, CacheEntryState.ACTIVE);
final Node<K, V> node = new Node<>(key, cacheEntry);
final Node<K, V> prior = this.cache.putIfAbsent(node.key, node);
CacheEntry<V> cacheEntry = new CacheEntry<>(value, CacheEntryState.ACTIVE);
Node<K, V> node = new Node<>(key, cacheEntry);
Node<K, V> prior = this.cache.putIfAbsent(node.key, node);
if (prior == null) {
processWrite(new AddTask(node));
}
@@ -128,7 +128,7 @@ public final class ConcurrentLruCache<K, V> {
private void processRead(Node<K, V> node) {
boolean drainRequested = this.readOperations.recordRead(node);
final DrainStatus status = this.drainStatus.get();
DrainStatus status = this.drainStatus.get();
if (status.shouldDrainBuffers(drainRequested)) {
drainOperations();
}
@@ -228,7 +228,7 @@ public final class ConcurrentLruCache<K, V> {
* {@code false} if there was no matching key
*/
public boolean remove(K key) {
final Node<K, V> node = this.cache.remove(key);
Node<K, V> node = this.cache.remove(key);
if (node == null) {
return false;
}
@@ -237,27 +237,29 @@ public final class ConcurrentLruCache<K, V> {
return true;
}
/*
/**
* Transition the node from the {@code active} state to the {@code pending removal} state,
* if the transition is valid.
*/
private void markForRemoval(Node<K, V> node) {
for (; ; ) {
final CacheEntry<V> current = node.get();
while (true) {
CacheEntry<V> current = node.get();
if (!current.isActive()) {
return;
}
final CacheEntry<V> pendingRemoval = new CacheEntry<>(current.value, CacheEntryState.PENDING_REMOVAL);
CacheEntry<V> pendingRemoval = new CacheEntry<>(current.value, CacheEntryState.PENDING_REMOVAL);
if (node.compareAndSet(current, pendingRemoval)) {
return;
}
}
}
/**
* Write operation recorded when a new entry is added to the cache.
*/
private final class AddTask implements Runnable {
final Node<K, V> node;
AddTask(Node<K, V> node) {
@@ -275,7 +277,7 @@ public final class ConcurrentLruCache<K, V> {
private void evictEntries() {
while (currentSize.get() > capacity) {
final Node<K, V> node = evictionQueue.poll();
Node<K, V> node = evictionQueue.poll();
if (node == null) {
return;
}
@@ -283,7 +285,6 @@ public final class ConcurrentLruCache<K, V> {
markAsRemoved(node);
}
}
}
@@ -291,6 +292,7 @@ public final class ConcurrentLruCache<K, V> {
* Write operation recorded when an entry is removed to the cache.
*/
private final class RemovalTask implements Runnable {
final Node<K, V> node;
RemovalTask(Node<K, V> node) {
@@ -310,7 +312,7 @@ public final class ConcurrentLruCache<K, V> {
*/
private enum DrainStatus {
/*
/**
* No drain operation currently running.
*/
IDLE {
@@ -320,7 +322,7 @@ public final class ConcurrentLruCache<K, V> {
}
},
/*
/**
* A drain operation is required due to a pending write modification.
*/
REQUIRED {
@@ -330,7 +332,7 @@ public final class ConcurrentLruCache<K, V> {
}
},
/*
/**
* A drain operation is in progress.
*/
PROCESSING {
@@ -367,12 +369,6 @@ public final class ConcurrentLruCache<K, V> {
private static final int BUFFER_COUNT = detectNumberOfBuffers();
private static int detectNumberOfBuffers() {
int availableProcessors = Runtime.getRuntime().availableProcessors();
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
return Math.min(4, nextPowerOfTwo);
}
private static final int BUFFERS_MASK = BUFFER_COUNT - 1;
private static final int MAX_PENDING_OPERATIONS = 32;
@@ -383,19 +379,13 @@ public final class ConcurrentLruCache<K, V> {
private static final int BUFFER_INDEX_MASK = BUFFER_SIZE - 1;
/*
* Number of operations recorded, for each buffer
*/
// Number of operations recorded, for each buffer
private final AtomicLongArray recordedCount = new AtomicLongArray(BUFFER_COUNT);
/*
* Number of operations read, for each buffer
*/
// Number of operations read, for each buffer
private final long[] readCount = new long[BUFFER_COUNT];
/*
* Number of operations processed, for each buffer
*/
// Number of operations processed, for each buffer
private final AtomicLongArray processedCount = new AtomicLongArray(BUFFER_COUNT);
@SuppressWarnings("rawtypes")
@@ -403,10 +393,11 @@ public final class ConcurrentLruCache<K, V> {
private final EvictionQueue<K, V> evictionQueue;
@SuppressWarnings("rawtypes")
ReadOperations(EvictionQueue<K, V> evictionQueue) {
this.evictionQueue = evictionQueue;
for (int i = 0; i < BUFFER_COUNT; i++) {
this.buffers[i] = new AtomicReferenceArray<>(BUFFER_SIZE);
this.buffers[i] = new AtomicReferenceArray(BUFFER_SIZE);
}
}
@@ -458,6 +449,12 @@ public final class ConcurrentLruCache<K, V> {
}
this.processedCount.lazySet(bufferIndex, writeCount);
}
private static int detectNumberOfBuffers() {
int availableProcessors = Runtime.getRuntime().availableProcessors();
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
return Math.min(4, nextPowerOfTwo);
}
}
@@ -536,10 +533,9 @@ public final class ConcurrentLruCache<K, V> {
if (this.first == null) {
return null;
}
final Node<K, V> f = this.first;
final Node<K, V> next = f.getNext();
Node<K, V> f = this.first;
Node<K, V> next = f.getNext();
f.setNext(null);
this.first = next;
if (next == null) {
this.last = null;
@@ -558,13 +554,12 @@ public final class ConcurrentLruCache<K, V> {
}
private boolean contains(Node<K, V> e) {
return (e.getPrevious() != null) || (e.getNext() != null) || (e == this.first);
return (e.getPrevious() != null || e.getNext() != null || e == this.first);
}
private void linkLast(final Node<K, V> e) {
final Node<K, V> l = this.last;
private void linkLast(Node<K, V> e) {
Node<K, V> l = this.last;
this.last = e;
if (l == null) {
this.first = e;
}
@@ -575,8 +570,8 @@ public final class ConcurrentLruCache<K, V> {
}
private void unlink(Node<K, V> e) {
final Node<K, V> prev = e.getPrevious();
final Node<K, V> next = e.getNext();
Node<K, V> prev = e.getPrevious();
Node<K, V> next = e.getNext();
if (prev == null) {
this.first = next;
}
@@ -892,17 +892,29 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
// By default, Spring will prevent the processing of external entities.
// This is a mitigation against XXE attacks.
if (xmlReader == null) {
SAXParserFactory saxParserFactory = this.sourceParserFactory;
if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.sourceParserFactory = saxParserFactory;
SAXParserFactory factory = this.sourceParserFactory;
if (factory == null) {
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
this.sourceParserFactory = factory;
}
SAXParser saxParser = saxParserFactory.newSAXParser();
SAXParser saxParser = factory.newSAXParser();
xmlReader = saxParser.getXMLReader();
}
if (!isProcessExternalEntities()) {
@@ -910,8 +922,8 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
}
return new SAXSource(xmlReader, inputSource);
}
catch (SAXException | ParserConfigurationException ex) {
logger.info("Processing of external entities could not be disabled", ex);
catch (Exception ex) {
logger.warn("Processing of external entities could not be disabled", ex);
return source;
}
}
@@ -75,6 +75,7 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
(publicId, systemId) -> new InputSource(new StringReader(""));
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@@ -165,8 +166,22 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
factory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
return factory;
}
@@ -195,17 +210,29 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
* @throws ParserConfigurationException if thrown by JAXP methods
*/
protected XMLReader createXmlReader() throws SAXException, ParserConfigurationException {
SAXParserFactory parserFactory = this.saxParserFactory;
if (parserFactory == null) {
parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
parserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
parserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.saxParserFactory = parserFactory;
SAXParserFactory factory = this.saxParserFactory;
if (factory == null) {
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
this.saxParserFactory = factory;
}
SAXParser saxParser = parserFactory.newSAXParser();
SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
@@ -456,8 +483,7 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
catch (NullPointerException ex) {
if (!isSupportDtd()) {
throw new UnmarshallingFailureException("NPE while unmarshalling. " +
"This can happen on JDK 1.6 due to the presence of DTD " +
"declarations, which are disabled.");
"This can happen due to the presence of DTD declarations, which are disabled.");
}
throw ex;
}
@@ -19,7 +19,6 @@ package org.springframework.http.converter.xml;
import java.io.StringReader;
import java.nio.charset.Charset;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Result;
@@ -39,7 +38,6 @@ import jakarta.xml.bind.annotation.XmlType;
import org.jspecify.annotations.Nullable;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.springframework.core.annotation.AnnotationUtils;
@@ -68,6 +66,10 @@ import org.springframework.util.ClassUtils;
*/
public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
(publicId, systemId) -> new InputSource(new StringReader(""));
private boolean supportDtd = false;
private boolean processExternalEntities = false;
@@ -181,24 +183,36 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
try {
// By default, Spring will prevent the processing of external entities.
// This is a mitigation against XXE attacks.
SAXParserFactory saxParserFactory = this.sourceParserFactory;
if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.sourceParserFactory = saxParserFactory;
SAXParserFactory factory = this.sourceParserFactory;
if (factory == null) {
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
this.sourceParserFactory = factory;
}
SAXParser saxParser = saxParserFactory.newSAXParser();
SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
}
return new SAXSource(xmlReader, inputSource);
}
catch (SAXException | ParserConfigurationException ex) {
catch (Exception ex) {
logger.warn("Processing of external entities could not be disabled", ex);
return source;
}
@@ -245,8 +259,4 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
return true;
}
private static final EntityResolver NO_OP_ENTITY_RESOLVER =
(publicId, systemId) -> new InputSource(new StringReader(""));
}
@@ -181,17 +181,29 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
try {
// By default, Spring will prevent the processing of external entities.
// This is a mitigation against XXE attacks.
DocumentBuilderFactory builderFactory = this.documentBuilderFactory;
if (builderFactory == null) {
builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
builderFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
builderFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.documentBuilderFactory = builderFactory;
DocumentBuilderFactory factory = this.documentBuilderFactory;
if (factory == null) {
factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
this.documentBuilderFactory = factory;
}
DocumentBuilder builder = builderFactory.newDocumentBuilder();
DocumentBuilder builder = factory.newDocumentBuilder();
if (!isProcessExternalEntities()) {
builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
}
@@ -217,17 +229,29 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
try {
SAXParserFactory parserFactory = this.saxParserFactory;
if (parserFactory == null) {
parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
parserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
parserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.saxParserFactory = parserFactory;
SAXParserFactory factory = this.saxParserFactory;
if (factory == null) {
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try {
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
}
catch (Exception ex) {
// Xerces properties not recognized/supported - ignore
}
try {
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", isProcessExternalEntities());
}
catch (Exception ex) {
// SAX properties not recognized/supported - ignore
}
this.saxParserFactory = factory;
}
SAXParser saxParser = parserFactory.newSAXParser();
SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
@@ -235,7 +259,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
byte[] bytes = StreamUtils.copyToByteArray(body);
return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(bytes)));
}
catch (SAXException | ParserConfigurationException ex) {
catch (ParserConfigurationException ex) {
throw new HttpMessageNotReadableException(
"Could not set feature: " + ex.getMessage(), ex, inputMessage);
}
catch (SAXException ex) {
throw new HttpMessageNotReadableException(
"Could not parse document: " + ex.getMessage(), ex, inputMessage);
}