第四十八章:SpringBoot2.0新特性 - RabbitMQ信任package设置本章目标SpringBoot 企业级核心技术学习专题构建项目总结

在这次SpringBoot升级后,之前的系统内使用实体传输受到了限制,如果使用SpringBoot默认的序列化方式不会出现信任package的问题,之所以出现这个问题是因为项目使用fastjson方式进行类的序列化已经反序列化,在之前SpringBoot 1.5.10版本的时候 RabbitMQ依赖内的DefaultClassMapper类在构造函数内配置*,表示信任项目内的所有package,在SpringBoot 2.0.0版本时,DefaultClassMapper类源码构造函数进行了修改,不再信任全部package而是仅仅信任java.utiljava.lang

本章目标

基于SpringBoot2.0使用RabbitMQ自定义MessageConverter配置信任指定package或者全部package

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核心技术

构建项目

创建项目添加对应依赖,pom.xml配置文件如下所示:

<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.rabbitmq.host=localhost
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/hengyu

具体消息队列的连接配置信息需要根据实际情况填写。

队列常量配置

我们之前的文章都是采用的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,所以这里使用消息中心队列来模拟,配置代码如下所示:

/**
 * 消息队列配置类
 *
 * @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问题,下面我们需要创建一个简单的消息传输实体,如下所示:

/**
 * 消息实体
 *
 * @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配套源码地址:https://gitee.com/hengboy/spring-boot-chapter SpringCloud配套源码地址:https://gitee.com/hengboy/spring-cloud-chapter SpringBoot相关系列文章请访问:目录:SpringBoot学习目录 QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录 SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录,感谢阅读!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阿杜的世界

Spring实战5-基于Spring构建Web应用主要内容

写在前面:关于Java Web,首先推荐一篇文章——写给java web一年左右工作经验的人,这篇文章的作者用精练的话语勾勒除了各种Java框架的缘由和最基本的...

922
来自专栏java系列博客

Java面试通关要点汇总集之框架篇参考答案

2074
来自专栏微信公众号:Java团长

Java面试中常问的Spring方面问题

Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编...

982
来自专栏菩提树下的杨过

spring cloud 学习(4) - hystrix 服务熔断处理

hystrix 是一个专用于服务熔断处理的开源项目,当依赖的服务方出现故障不可用时,hystrix有一个所谓的断路器,一但打开,就会直接拦截掉对故障服务的调用,...

2015
来自专栏Java架构师学习

Spring5都有那些新特性与增强,需要了解的Java程序员来看一看

Spring FrameWork 5.0新的功能 JDK 8+和Java EE7+以上版本 整个框架的代码基于java8 通过使用泛型等特性提高可读性 对j...

3827
来自专栏菩提树下的杨过

spring 3.2.x + struts2 + mybatis 3.x + logback 整合配置

与前面的一篇mybatis 3.2.7 与 spring mvc 3.x、logback整合 相比,只是web层的MVC前端框架,从spring mvc转换成s...

3705
来自专栏CSDN技术头条

Spring Data REST 与 Spring RestTemplate 实战详解

这篇分为两部分内容进行介绍(Spring Data REST 和 Spring RestTemplate)。我之前有一篇文章完整的介绍了 HTTP 协议的内容,...

8285
来自专栏李家的小酒馆

Spring MVC面试整理

Spring MVC执行过程 1. 客户端的请求提交到dispatcherServlet 2. DispatcherServ...

2340
来自专栏静默虚空的博客

Spring 面试问题 TOP 50

Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编...

982
来自专栏JAVA高级架构

Spring 知识点提炼

1. Spring框架的作用 轻量:Spring是轻量级的,基本的版本大小为2MB 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,...

3789

扫码关注云+社区