From 780aeaccc934a8c31306ed2b6d7a5c0ac4aab7f6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 6 Feb 2024 13:37:19 -0500 Subject: [PATCH] GH-2609: Restore ClassLoader for SerMC & SimpleMC Fixes: #2609 The deserialization functionality must rely on the `ClassLoader` from the application context (at least, by default). * Fix `SimpleMessageConverter` to accept `BeanClassLoaderAware` and use it for `ConfigurableObjectInputStream` * Fix `SerializerMessageConverter` to accept `BeanClassLoaderAware` * Remove reflection for the `new DirectFieldAccessor(deserializer).getPropertyValue("classLoader")` from the `SerializerMessageConverter` since it is not this converter responsibility to interfere into provided `Deserializer` logic **Cherry-pick to `3.0.x`** (cherry picked from commit e7be534b547e4790640e74efdf3fdafa28cd2642) --- .../converter/SerializerMessageConverter.java | 69 ++++++++----------- .../converter/SimpleMessageConverter.java | 45 +++++++----- .../SerializerMessageConverterTests.java | 27 +------- 3 files changed, 58 insertions(+), 83 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index bdbe2bde0d..cfe8e923d5 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -26,13 +26,14 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ConfigurableObjectInputStream; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.DefaultSerializer; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings or native objects @@ -50,26 +51,26 @@ * @author Gary Russell * @author Artem Bilan */ -public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter { +public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter + implements BeanClassLoaderAware { public static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name(); - private volatile String defaultCharset = DEFAULT_CHARSET; + private String defaultCharset = DEFAULT_CHARSET; - private volatile Serializer serializer = new DefaultSerializer(); + private Serializer serializer = new DefaultSerializer(); - private volatile Deserializer deserializer = new DefaultDeserializer(); + private Deserializer deserializer = new DefaultDeserializer(); - private volatile boolean ignoreContentType = false; + private boolean ignoreContentType = false; - private volatile ClassLoader defaultDeserializerClassLoader; + private ClassLoader defaultDeserializerClassLoader = ClassUtils.getDefaultClassLoader(); - private volatile boolean usingDefaultDeserializer = true; + private boolean usingDefaultDeserializer = true; /** * Flag to signal that the content type should be ignored and the deserializer used irrespective if it is a text * message. Defaults to false, in which case the default encoding is used to convert a text message to a String. - * * @param ignoreContentType the flag value to set */ public void setIgnoreContentType(boolean ignoreContentType) { @@ -79,7 +80,6 @@ public void setIgnoreContentType(boolean ignoreContentType) { /** * Specify the default charset to use when converting to or from text-based Message body content. If not specified, * the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ public void setDefaultCharset(@Nullable String defaultCharset) { @@ -88,7 +88,6 @@ public void setDefaultCharset(@Nullable String defaultCharset) { /** * The serializer to use for converting Java objects to message bodies. - * * @param serializer the serializer to set */ public void setSerializer(Serializer serializer) { @@ -97,24 +96,16 @@ public void setSerializer(Serializer serializer) { /** * The deserializer to use for converting from message body to Java object. - * * @param deserializer the deserializer to set */ public void setDeserializer(Deserializer deserializer) { this.deserializer = deserializer; - if (this.deserializer.getClass().equals(DefaultDeserializer.class)) { - try { - this.defaultDeserializerClassLoader = (ClassLoader) new DirectFieldAccessor(deserializer) - .getPropertyValue("classLoader"); - } - catch (Exception e) { - // no-op - } - this.usingDefaultDeserializer = true; - } - else { - this.usingDefaultDeserializer = false; - } + this.usingDefaultDeserializer = false; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.defaultDeserializerClassLoader = classLoader; } /** @@ -170,17 +161,17 @@ private Object asString(Message message, MessageProperties properties) { private Object deserialize(ByteArrayInputStream inputStream) throws IOException { try (ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, - this.defaultDeserializerClassLoader) { - - @Override - protected Class resolveClass(ObjectStreamClass classDesc) - throws IOException, ClassNotFoundException { - Class clazz = super.resolveClass(classDesc); - checkAllowedList(clazz); - return clazz; - } + this.defaultDeserializerClassLoader) { + + @Override + protected Class resolveClass(ObjectStreamClass classDesc) + throws IOException, ClassNotFoundException { + Class clazz = super.resolveClass(classDesc); + checkAllowedList(clazz); + return clazz; + } - }) { + }) { return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { @@ -194,6 +185,7 @@ protected Class resolveClass(ObjectStreamClass classDesc) @Override protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + byte[] bytes; if (object instanceof String) { try { @@ -220,9 +212,8 @@ else if (object instanceof byte[]) { bytes = output.toByteArray(); messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } - if (bytes != null) { - messageProperties.setContentLength(bytes.length); - } + + messageProperties.setContentLength(bytes.length); return new Message(bytes, messageProperties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index d162147d0c..56897f80fb 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -27,6 +27,10 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.SerializationUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.ConfigurableObjectInputStream; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings, Serializable @@ -38,23 +42,30 @@ * @author Mark Fisher * @author Oleg Zhurakousky * @author Gary Russell + * @author Artem Bilan */ -public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter { +public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter implements BeanClassLoaderAware { public static final String DEFAULT_CHARSET = "UTF-8"; - private volatile String defaultCharset = DEFAULT_CHARSET; + private String defaultCharset = DEFAULT_CHARSET; + + private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Specify the default charset to use when converting to or from text-based * Message body content. If not specified, the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ - public void setDefaultCharset(String defaultCharset) { + public void setDefaultCharset(@Nullable String defaultCharset) { this.defaultCharset = (defaultCharset != null) ? defaultCharset : DEFAULT_CHARSET; } + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + /** * Converts from a AMQP Message to an Object. */ @@ -73,8 +84,7 @@ public Object fromMessage(Message message) throws MessageConversionException { content = new String(message.getBody(), encoding); } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert text-based Message content", e); + throw new MessageConversionException("failed to convert text-based Message content", e); } } else if (contentType != null && @@ -84,8 +94,7 @@ else if (contentType != null && createObjectInputStream(new ByteArrayInputStream(message.getBody()))); } catch (IOException | IllegalArgumentException | IllegalStateException e) { - throw new MessageConversionException( - "failed to convert serialized Message content", e); + throw new MessageConversionException("failed to convert serialized Message content", e); } } } @@ -99,7 +108,9 @@ else if (contentType != null && * Creates an AMQP Message from the provided Object. */ @Override - protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + protected Message createMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + byte[] bytes = null; if (object instanceof byte[]) { bytes = (byte[]) object; @@ -110,8 +121,7 @@ else if (object instanceof String) { bytes = ((String) object).getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert to Message content", e); + throw new MessageConversionException("failed to convert to Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); @@ -121,8 +131,7 @@ else if (object instanceof Serializable) { bytes = SerializationUtils.serialize(object); } catch (IllegalArgumentException e) { - throw new MessageConversionException( - "failed to convert to serialized Message content", e); + throw new MessageConversionException("failed to convert to serialized Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } @@ -135,15 +144,15 @@ else if (object instanceof Serializable) { } /** - * Create an ObjectInputStream for the given InputStream and codebase. The default - * implementation creates an ObjectInputStream. + * Create an ObjectInputStream for the given InputStream. The default + * implementation creates an {@link ConfigurableObjectInputStream} against configured {@link ClassLoader}. + * The class for object to deserialize is checked against {@code allowedListPatterns}. * @param is the InputStream to read from * @return the new ObjectInputStream instance to use * @throws IOException if creation of the ObjectInputStream failed */ - @SuppressWarnings("deprecation") protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { - return new ObjectInputStream(is) { + return new ConfigurableObjectInputStream(is, this.classLoader) { @Override protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java index 744adb5748..026fe7a308 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -18,25 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.core.serializer.DefaultDeserializer; -import org.springframework.core.serializer.Deserializer; /** * @author Mark Fisher @@ -151,24 +144,6 @@ public void serializedObjectToMessage() throws Exception { assertThat(deserializedObject).isEqualTo(testBean); } - @SuppressWarnings("unchecked") - @Test - public void testDefaultDeserializerClassLoader() throws Exception { - SerializerMessageConverter converter = new SerializerMessageConverter(); - ClassLoader loader = mock(ClassLoader.class); - Deserializer deserializer = new DefaultDeserializer(loader); - converter.setDeserializer(deserializer); - assertThat(TestUtils.getPropertyValue(converter, "defaultDeserializerClassLoader")).isSameAs(loader); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isTrue(); - Deserializer mock = mock(Deserializer.class); - converter.setDeserializer(mock); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isFalse(); - TestBean testBean = new TestBean("foo"); - Message message = converter.toMessage(testBean, new MessageProperties()); - converter.fromMessage(message); - verify(mock).deserialize(Mockito.any(InputStream.class)); - } - @Test public void messageConversionExceptionForClassNotFound() { SerializerMessageConverter converter = new SerializerMessageConverter();