第四十八章:SpringBoot2.0新特性 - RabbitMQ信任package設置

在此次SpringBoot升級後,以前的系統內使用實體傳輸受到了限制,若是使用SpringBoot默認的序列化方式不會出現信任package的問題,之因此出現這個問題是由於項目使用fastjson方式進行類的序列化已經反序列化,在以前SpringBoot 1.5.10版本的時候 RabbitMQ依賴內的DefaultClassMapper類在構造函數內配置*,表示信任項目內的全部package,在SpringBoot 2.0.0版本時,DefaultClassMapper類源碼構造函數進行了修改,再也不信任所有package而是僅僅信任java.utiljava.langjava

本章目標

基於SpringBoot2.0使用RabbitMQ自定義MessageConverter配置信任指定package或者所有packagegit

SpringBoot 企業級核心技術學習專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術 講解SpringBoot一些企業級層面的核心組件
002 Spring Boot 核心技術章節源碼 Spring Boot 核心技術簡書每一篇文章碼雲對應源碼
003 Spring Cloud 核心技術 對Spring Cloud核心技術全面講解
004 Spring Cloud 核心技術章節源碼 Spring Cloud 核心技術簡書每一篇文章對應源碼
005 QueryDSL 核心技術 全面講解QueryDSL核心技術以及基於SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術 全面講解SpringDataJPA核心技術
007 SpringBoot核心技術學習目錄 SpringBoot系統的學習目錄,敬請關注點贊!!!

構建項目

建立項目添加對應依賴,pom.xml配置文件以下所示:web

<dependencies>
        <!--消息隊列依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--web相關依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--fastjson依賴-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>
        <!--lombok依賴-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--測試依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
複製代碼

消息隊列配置文件

咱們須要在application.properties配置文件內添加RabbitMQ相應的配置信息,以下所示:spring

spring.rabbitmq.host=localhost
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/hengyu
複製代碼

具體消息隊列的鏈接配置信息須要根據實際狀況填寫。json

隊列常量配置

咱們以前的文章都是採用的Enum方式來配置隊列相關的ExchangeNameRouteKey等相關的信息,使用枚舉有個弊端,沒法在註解內做爲屬性的值使用,因此咱們以前的Consumer類配置監聽的隊列時都是字符串的形式,這樣後期修改時還要修改多個地方(固然隊列信息不多變更),咱們本章使用Constants常量的形式進行配置,以下所示:數組

/**
 * 隊列常量配置
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/7
 * Time:下午10:10
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
public interface QueueConstants {
    /**
     * 消息交換
     */
    String MESSAGE_EXCHANGE = "message.direct.exchange";
    /**
     * 消息隊列名稱
     */
    String MESSAGE_QUEUE_NAME = "message.queue";
    /**
     * 消息路由鍵
     */
    String MESSAGE_ROUTE_KEY = "message.send";
}
複製代碼

示例消息隊列JavaConfig配置

本章是爲了設置信任package,因此這裏使用消息中心隊列來模擬,配置代碼以下所示:bash

/**
 * 消息隊列配置類
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/7
 * Time:下午10:07
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Configuration
public class MessageRabbitMqConfiguration {
    /**
     * 交換配置
     *
     * @return
     */
    @Bean
    public DirectExchange messageDirectExchange() {
        return (DirectExchange) ExchangeBuilder.directExchange(QueueConstants.MESSAGE_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 消息隊列聲明
     *
     * @return
     */
    @Bean
    public Queue messageQueue() {
        return QueueBuilder.durable(QueueConstants.MESSAGE_QUEUE_NAME)
                .build();
    }

    /**
     * 消息綁定
     *
     * @return
     */
    @Bean
    public Binding messageBinding() {
        return BindingBuilder.bind(messageQueue())
                .to(messageDirectExchange())
                .with(QueueConstants.MESSAGE_ROUTE_KEY);
    }
}
複製代碼

上面配置類內添加ExchangeQueueBinding等配置,將messageQueue使用message.send路由鍵與messageDirectExchange交換配置進行綁定。微信

咱們在以前說了只有傳遞實體類時纔會出現信任package問題,下面咱們須要建立一個簡單的消息傳輸實體,以下所示:app

/**
 * 消息實體
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/11
 * Time:下午5:18
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Data
public class MessageEntity implements Serializable {
    /**
     * 消息內容
     */
    private String content;
}
複製代碼

該實體類僅添加了一個content字段,這樣足夠模擬咱們的場景了,到這裏咱們的配置已經處理完,下面就是咱們的隊列的Provider以及Consumer的相關實體類編寫。框架

消息提供者

爲隊列message.queue添加Provider的代碼實現,以下所示:

/**
 * 消息隊列 - 消息提供者
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/11
 * Time:下午6:16
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Component
public class MessageProvider {
    /**
     * logger instance
     */
    static Logger logger = LoggerFactory.getLogger(MessageProvider.class);
    /**
     * 消息隊列模板
     */
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMessage(Object object) {
        logger.info("寫入消息隊列內容:{}", JSON.toJSONString(object));
        amqpTemplate.convertAndSend(QueueConstants.MESSAGE_EXCHANGE, QueueConstants.MESSAGE_ROUTE_KEY, object);
    }
}
複製代碼

消息消費者

固然咱們有了Provider必然要有對應的Consumer,消費者代碼實現以下所示:

/**
 * 消息隊列 - 消息消費者
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/11
 * Time:下午5:32
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Component
@RabbitListener(queues = QueueConstants.MESSAGE_QUEUE_NAME)
public class MessageConsumer {
    /**
     * logger instance
     */
    static Logger logger = LoggerFactory.getLogger(MessageConsumer.class);

    @RabbitHandler
    public void handler(@Payload MessageEntity messageEntity) {
        logger.info("消費內容:{}", JSON.toJSONString(messageEntity));
    }
}
複製代碼

建立測試控制器

咱們採用控制器發送Get請求的方式進行發送消息,建立名爲TestController的控制器,並添加測試方法,以下代碼所示:

/**
 * 測試控制器
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/11
 * Time:下午5:43
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@RestController
public class TestController {
    /**
     * 消息隊列 - 消息提供者 注入
     */
    @Autowired
    private MessageProvider messageProvider;

    /**
     * 測試發送消息隊列方法
     *
     * @param messageEntity 發送消息實體內容
     * @return
     */
    @RequestMapping(value = "/index")
    public String index(MessageEntity messageEntity) {
        // 將實體實例寫入消息隊列
        messageProvider.sendMessage(messageEntity);
        return "Success";
    }
}
複製代碼

測試RabbitMQ默認實體傳輸

下面咱們啓動項目,首先先來測試RabbitMQ默認的實體類方式,固然這種默認的方式不會產生信任package的狀況。

咱們爲了證明這一點,來訪問(http://localhost:8080/index?content=admin)[http://localhost:8080/index?content=admin],咱們傳遞content的值爲admin,訪問效果控制檯輸出內容以下:

2018-03-13 21:59:08.844  INFO 16047 --- [nio-8080-exec-1] c.h.chapter48.provider.MessageProvider   : 寫入消息隊列內容:{"content":"admin"}
2018-03-13 21:59:08.898  INFO 16047 --- [cTaskExecutor-1] c.h.chapter48.consumer.MessageConsumer   : 消費內容:{"content":"admin"}
複製代碼

能夠看到控制檯的輸出內容,直接完成了消息的消費,是沒有任何問題的,下面咱們對RabbitMQ添加自定義MessageConverter的配置,使用fastjson替代默認轉換方式。

MessageConverter

咱們先來建立一個轉換的實現類,只須要繼承抽象類AbstractMessageConverter並實現內部的createMessagefromMessage兩個方法就能夠完成實體類的序列化反序列化的轉換,代碼以下所示:

/**
 * 自定義消息轉換器
 * 採用FastJson完成消息轉換
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with Eclipse.
 * Date:2017/10/26
 * Time:19:28
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
public class RabbitMqFastJsonConverter
        extends AbstractMessageConverter {
    /**
     * 日誌對象實例
     */
    private Logger logger = LoggerFactory.getLogger(RabbitMqFastJsonConverter.class);
    /**
     * 消息類型映射對象
     */
    private static ClassMapper classMapper = new DefaultClassMapper();
    /**
     * 默認字符集
     */
    private static String DEFAULT_CHART_SET = "UTF-8";

    /**
     * 建立消息
     *
     * @param o                 消息對象
     * @param messageProperties 消息屬性
     * @return
     */
    @Override
    protected Message createMessage(Object o, MessageProperties messageProperties) {
        byte[] bytes = null;
        try {
            String jsonString = JSON.toJSONString(o);
            bytes = jsonString.getBytes(DEFAULT_CHART_SET);
        } catch (IOException e) {
            throw new MessageConversionException(
                    "Failed to convert Message content", e);
        }
        messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
        messageProperties.setContentEncoding(DEFAULT_CHART_SET);
        if (bytes != null) {
            messageProperties.setContentLength(bytes.length);
        }
        classMapper.fromClass(o.getClass(), messageProperties);
        return new Message(bytes, messageProperties);
    }

    /**
     * 轉換消息爲對象
     *
     * @param message 消息對象
     * @return
     * @throws MessageConversionException
     */
    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        Object content = null;
        MessageProperties properties = message.getMessageProperties();
        if (properties != null) {
            String contentType = properties.getContentType();
            if (contentType != null && contentType.contains("json")) {
                String encoding = properties.getContentEncoding();
                if (encoding == null) {
                    encoding = DEFAULT_CHART_SET;
                }
                try {
                    Class<?> targetClass = classMapper.toClass(
                            message.getMessageProperties());

                    content = convertBytesToObject(message.getBody(),
                            encoding, targetClass);
                } catch (IOException e) {
                    throw new MessageConversionException(
                            "Failed to convert Message content", e);
                }
            } else {
                logger.warn("Could not convert incoming message with content-type ["
                        + contentType + "]");
            }
        }
        if (content == null) {
            content = message.getBody();
        }
        return content;
    }

    /**
     * 將字節數組轉換成實例對象
     *
     * @param body     Message對象主體字節數組
     * @param encoding 字符集
     * @param clazz    類型
     * @return
     * @throws UnsupportedEncodingException
     */
    private Object convertBytesToObject(byte[] body, String encoding,
                                        Class<?> clazz) throws UnsupportedEncodingException {
        String contentAsString = new String(body, encoding);
        return JSON.parseObject(contentAsString, clazz);
    }
}
複製代碼

在該轉換類內咱們使用了DefaultClassMapper來做爲類的映射,咱們能夠先來看下該類相關信任package的源碼,以下所示:

......
public class DefaultClassMapper implements ClassMapper, InitializingBean {
    public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
    private static final String DEFAULT_HASHTABLE_TYPE_ID = "Hashtable";
    // 默認信任的package列表
    private static final List<String> TRUSTED_PACKAGES = Arrays.asList("java.util", "java.lang");
    private final Set<String> trustedPackages;
    private volatile Map<String, Class<?>> idClassMapping;
    private volatile Map<Class<?>, String> classIdMapping;
    private volatile Class<?> defaultMapClass;
    private volatile Class<?> defaultType;

    public DefaultClassMapper() {
        // 構造函數初始化信任的package爲默認的pakcage列表
        // 僅支持java.util、java.lang兩個package
        this.trustedPackages = new LinkedHashSet(TRUSTED_PACKAGES);
        this.idClassMapping = new HashMap();
        this.classIdMapping = new HashMap();
        this.defaultMapClass = LinkedHashMap.class;
        this.defaultType = LinkedHashMap.class;
    }
......
複製代碼

RabbitMqConfiguration

下面咱們須要將該轉換設置到RabbitTemplateSimpleRabbitListenerContainerFactory內,讓RabbitMQ支持自定義的消息轉換,以下所示:

/**
 * rabbitmq 相關配置
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/11
 * Time:下午5:42
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Configuration
public class RabbitMqConfiguration {


    /**
     * 配置消息隊列模版
     * 而且設置MessageConverter爲自定義FastJson轉換器
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(new RabbitMqFastJsonConverter());
        return template;
    }

    /**
     * 自定義隊列容器工廠
     * 而且設置MessageConverter爲自定義FastJson轉換器
     * @param connectionFactory
     * @return
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new RabbitMqFastJsonConverter());
        factory.setDefaultRequeueRejected(false);
        return factory;
    }

}
複製代碼

重啓測試

上面的代碼配置咱們已經把MessageConverter改爲了fastjson,重啓項目,再次訪問http://localhost:8080/index?content=admin路徑,看下控制檯輸出日誌內容以下所示:

Caused by: java.lang.IllegalArgumentException: The class 'com.hengyu.chapter48.entity.MessageEntity' is not in the trusted packages: [java.util, java.lang]. If you believe this class is safe to deserialize, please provide its name. If the serialization is only done by a trusted source, you can also enable trust all (*).
	at org.springframework.amqp.support.converter.DefaultClassMapper.toClass(DefaultClassMapper.java:211) ~[spring-amqp-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.support.converter.DefaultClassMapper.toClass(DefaultClassMapper.java:199) ~[spring-amqp-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at com.hengyu.chapter48.RabbitMqFastJsonConverter.fromMessage(RabbitMqFastJsonConverter.java:88) ~[classes/:na]
	at org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener.extractMessage(AbstractAdaptableMessageListener.java:246) ~[spring-rabbit-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter$MessagingMessageConverterAdapter.extractPayload(MessagingMessageListenerAdapter.java:266) ~[spring-rabbit-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.support.converter.MessagingMessageConverter.fromMessage(MessagingMessageConverter.java:118) ~[spring-amqp-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.toMessagingMessage(MessagingMessageListenerAdapter.java:168) ~[spring-rabbit-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:115) ~[spring-rabbit-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1414) ~[spring-rabbit-2.0.2.RELEASE.jar:2.0.2.RELEASE]
	... 8 common frames omitted

複製代碼

能夠看到控制檯已經輸出了不信任com.hengyu.chapter48.entity.MessageEntity實體的錯誤信息,也代表了僅信任java.utiljava.lang兩個package,下面咱們就須要繼承DefaultClassMapper來重寫構造函數完成信任指定的package

重寫DefaultClassMapper構造函數

建立一個名爲RabbitMqFastJsonClassMapper的類而且繼承DefaultClassMapper,以下所示:

/**
 * fastjson 轉換映射
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/13
 * Time:下午10:17
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
public class RabbitMqFastJsonClassMapper extends DefaultClassMapper {
    /**
     * 構造函數初始化信任全部pakcage
     */
    public RabbitMqFastJsonClassMapper() {
        super();
        setTrustedPackages("*");
    }
}
複製代碼

在上面構造函數內咱們設置了信任所有的package,添加了RabbitMqFastJsonClassMapper類後,須要讓MessageConverter使用該類做爲映射,修改RabbitMqFastJsonConverter部分代碼以下所示:

/**
 * 消息類型映射對象
 */
private static ClassMapper classMapper = new DefaultClassMapper();
>>> 修改成 >>>
/**
* 消息類型映射對象
*/
private static ClassMapper classMapper = new RabbitMqFastJsonClassMapper();
複製代碼

再次重啓測試

咱們再次重啓項目後,仍然訪問http://localhost:8080/index?content=admin路徑,查看控制檯日誌以下所示:

2018-03-13 22:23:35.414  INFO 16121 --- [nio-8080-exec-1] c.h.chapter48.provider.MessageProvider   : 寫入消息隊列內容:{"content":"admin"}
2018-03-13 22:23:35.493  INFO 16121 --- [cTaskExecutor-1] c.h.chapter48.consumer.MessageConsumer   : 消費內容:{"content":"admin"}
複製代碼

根據日誌輸出已經證實能夠正常的完成消息的消費。

總結

若是使用RabbitMQ默認的轉換方式,並不會涉及到本章遇到的信任package問題,若是想自定義消息轉換而且使用DefaultClassMapper做爲映射,確定會出現信任package的問題,因此若是須要自定義轉換的小夥伴,記住要設置trustedPackages

本章源碼已經上傳到碼雲: SpringBoot配套源碼地址:gitee.com/hengboy/spr… SpringCloud配套源碼地址:gitee.com/hengboy/spr… SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄 QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄 SpringDataJPA相關係列文章請訪問:目錄:SpringDataJPA學習目錄,感謝閱讀! 歡迎加入QQ技術交流羣,共同進步。

QQ技術交流羣

微信掃碼關注 - 專一分享
相關文章
相關標籤/搜索