这篇带你熟悉 SpringBootRabbitMQ 方式收发消息

2020-09-13 16:01 java那些事


本篇会和SpringBoot做整合,采用自动配置的方式进行开发,我们只需要声明RabbitMQ地址就可以了,关于各种创建连接关闭连接的事都由Spring帮我们了~


交给Spring帮我们管理连接可以让我们专注于业务逻辑,就像声明式事务一样易用,方便又高效。


祝有好收获,先赞后看,快乐无限。


本文代码: 


  • https://gitee.com/he-erduo/spring-boot-learning-demo

  • https://github.com/he-erduo/spring-boot-learning-demo


1. 🔍环境配置


第一节我们先来搞一下环境的配置,上一篇中我们已经引入了自动配置的包,我们既然使用了自动配置的方式,那RabbitMQ的连接信息我们直接放在配置文件中就行了,就像我们需要用到JDBC连接的时候去配置一下DataSource一样。



如图所示,我们只需要指明一下连接的IP+端口号和用户名密码就行了,这里我用的是默认的用户名与密码,不写的话默认也都是guest,端口号也是默认5672。


主要我们需要看一下手动确认消息的配置,需要配置成manual才是手动确认,日后还会有其他的配置项,眼下我们配置这一个就可以了。

接下来我们要配置一个Queue,上一篇中我们往一个名叫erduo的队列中发送消息,当时是我们手动定义的此队列,这里我们也需要手动配置,声明一个Bean就可以了。


@Configuration
public class RabbitmqConfig {
    @Bean
    public Queue erduo() {
        // 其三个参数:durable exclusive autoDelete
        // 一般只设置一下持久化即可
        return new Queue("erduo",true);
    }

}


就这么简单声明一下就可以了,当然了RabbitMQ毕竟是一个独立的组件,如果你在RabbitMQ中通过其他方式已经创建过一个名叫erduo的队列了,你这里也可以不声明,这里起到的一个效果就是如果你没有这个队列,会按照你声明的方式帮你创建这个队列。


配置完环境之后,我们就可以以SpringBoot的方式来编写生产者和消费者了。


2. 📕生产者与RabbitTemplate


和上一篇的节奏一样,我们先来编写生产者,不过这次我要引入一个新的工具:RabbitTemplate。


听它的这个名字就知道,又是一个拿来即用的工具类,Spring家族这点就很舒服,什么东西都给你封装一遍,让你用起来更方便更顺手。

RabbitTemplate实现了标准AmqpTemplate接口,功能大致可以分为发送消息和接受消息。


我们这里是在生产者中来用,主要就是使用它的发送消息功能:send和convertAndSend方法。


// 发送消息到默认的Exchange,使用默认的routing key
void send(Message message) throws AmqpException;

// 使用指定的routing key发送消息到默认的exchange
void send(String routingKey, Message message) throws AmqpException;

// 使用指定的routing key发送消息到指定的exchange
void send(String exchange, String routingKey, Message message) throws AmqpException;


send方法是发送byte数组的数据的模式,这里代表消息内容的对象是Message对象,它的构造方法就是传入byte数组数据,所以我们需要把我们的数据转成byte数组然后构造成一个Message对象再进行发送。


// Object类型,可以传入POJO
void convertAndSend(Object message) throws AmqpException;

void convertAndSend(String routingKey, Object message) throws AmqpException;

void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException;


convertAndSend方法是可以传入POJO对象作为参数,底层是有一个MessageConverter帮我们自动将数据转换成byte类型或String或序列化类型。


所以这里支持的传入对象也只有三种:byte类型,String类型和实现了Serializable接口的POJO。


介绍完了,我们可以看一下代码:


@Slf4j
@Component("rabbitProduce")
public class RabbitProduce {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send() {
        String message = "Hello 我是作者和耳朵,欢迎关注我。" + LocalDateTime.now().toString();

        System.out.println("Message content : " + message);

        // 指定消息类型
        MessageProperties props = MessagePropertiesBuilder.newInstance()
                .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN).build();

        rabbitTemplate.send(Producer.QUEUE_NAME,new Message(message.getBytes(StandardCharsets.UTF_8),props));
        System.out.println("消息发送完毕。");
    }

    public void convertAndSend() {
        User user = new User();

        System.out.println("Message content : " + user);

        rabbitTemplate.convertAndSend(Producer.QUEUE_NAME,user);
        System.out.println("消息发送完毕。");
    }

}


这里我特意写明了两个例子,一个用来测试send,另一个用来测试convertAndSend。


send方法里我们看下来和之前的代码是几乎一样的,定义一个消息,然后直接send,但是这个构造消息的构造方法可能比我们想的要多一个参数,我们原来说的只要把数据转成二进制数组放进去即可,现在看来还要多放一个参数了。


MessageProperties,是的我们需要多放一个MessageProperties对象,从他的名字我们也可以看出它的功能就是附带一些参数,但是某些参数是少不了的,不带不行。


比如我的代码这里就是设置了一下消息的类型,消息的类型有很多种可以是二进制类型,文本类型,或者序列化类型,JSON类型,我这里设置的就是文本类型,指定类型是必须的,也可以为我们拿到消息之后要将消息转换成什么样的对象提供一个参考。


convertAndSend方法就要简单太多,这里我放了一个User对象拿来测试用,直接指定队列然后放入这个对象即可。


Tips:User必须实现Serializable接口,不然的话调用此方法的时候会抛出IllegalArgumentException异常。


代码完成之后我们就可以调用了,这里我写一个测试类进行调用:


@SpringBootTest
public class RabbitProduceTest {
    @Autowired
    private RabbitProduce rabbitProduce;

    @Test
    public void sendSimpleMessage() {
        rabbitProduce.send();
        rabbitProduce.convertAndSend();
    }
}


效果如下图~



同时在控制台使用命令rabbitmqctl.bat list_queues查看队列-erduo现在的情况:



如此一来,我们的生产者测试就算完成了,现在消息队列里两条消息了,而且消息类型肯定不一样,一个是我们设置的文本类型,一个是自动设置的序列化类型。


3. 📗消费者与RabbitListener


既然队列里面已经有消息了,接下来我们就要看我们该如何通过新的方式拿到消息并消费与确认了。


消费者这里我们要用到@RabbitListener来帮我们拿到指定队列消息,它的用法很简单也很复杂,我们可以先来说简单的方式,直接放到方法上,指定监听的队列就行了。


@Slf4j
@Component("rabbitConsumer")
public class RabbitConsumer {

    @RabbitListener(queues = Producer.QUEUE_NAME)
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        System.out.println("消息已确认");
    }

}


这段代码就代表onMessage方法会处理erduo(Producer.QUEUE_NAME是常量字符串"erduo")队列中的消息。


我们可以看到这个方法里面有两个参数,Message和Channel,如果用不到Channel可以不写此参数,但是Message消息一定是要的,它代表了消息本身。


我们可以想想,我们的程序从RabbitMQ之中拉回一条条消息之后,要以怎么样的方式展示给我们呢?


没错,就是封装为一个个Message对象,这里面放入了一条消息的所有信息,数据结构是什么样一会我一run你就能看到了。


同时这里我们使用Channel做一个消息确认的操作,这里的DeliveryTag代表的是这个消息在队列中的序号,这个信息存放在MessageProperties中。


4. 📖SpringBoot 启动!


编写完生产者和消费者,同时已经运行过生产者往消息队列里面放了两条信息,接下来我们可以直接启动消息,查看消费情况:



在我红色框线标记的地方可以看到,因为我们有了消费者所以项目启动后先和RabbitMQ建立了一个连接进行监听队列。


随后就开始消费我们队列中的两条消息:


第一条信息是contentType=text/plain类型,所以直接就在控制台上打印出了具体内容。


第二条信息是contentType=application/x-java-serialized-object,在打印的时候只打印了一个内存地址+字节大小。


不管怎么说,数据我们是拿到了,也就是代表我们的消费是没有问题的,同时也都进行了消息确认操作,从数据上看,整个消息可以分为两部分:body和MessageProperties。


我们可以单独使用一个注解拿到这个body的内容 - @Payload


@RabbitListener(queues = Producer.QUEUE_NAME)
public void onMessage(@Payload String body, Channel channel) throws Exception {
    System.out.println("Message content : " + body);
}


也可以单独使用一个注解拿到MessageProperties的headers属性,headers属性在截图里也可以看到,只不过是个空的 - @Headers。


@RabbitListener(queues = Producer.QUEUE_NAME)
public void onMessage(@Payload String body, @Headers Map<String,Object> headers) throws Exception {
    System.out.println("Message content : " + body);
    System.out.println("Message headers : " + headers);
}


这两个注解都算是扩展知识,我还是更喜欢直接拿到全部,全都要!!!


上面我们已经完成了消息的发送与消费,整个过程我们可以再次回想一下,一切都和我画的这张图上一样的轨迹:



只不过我们一直没有指定Exchage一直使用的默认路由,希望大家好好记住这张图。


5. 📘@RabbitListener与@RabbitHandler


下面再来补一些知识点,有关@RabbitListener与@RabbitHandler。


@RabbitListener上面我们已经简单的进行了使用,稍微扩展一下它其实是可以监听多个队列的,就像这样:


@RabbitListener(queues = { "queue1""queue2" })
public void onMessage(Message message, Channel channel) throws Exception {
    System.out.println("Message content : " + message);
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    System.out.println("消息已确认");
}


还有一些其他的特性如绑定之类的,这里不再赘述因为太硬编码了一般用不上。


下面来说说这节要主要讲的一个特性:@RabbitListener和@RabbitHandler的搭配使用。


前面我们没有提到,@RabbitListener注解其实是可以注解在类上的,这个注解在类上标志着这个类监听某个队列或某些队列。


这两个注解的搭配使用就要让@RabbitListener注解在类上,然后用@RabbitHandler注解在方法上,根据方法参数的不同自动识别并去消费,写个例子给大家看一看更直观一些。


@Slf4j
@Component("rabbitConsumer")
@RabbitListener(queues = Producer.QUEUE_NAME)
public class RabbitConsumer {

    @RabbitHandler
    public void onMessage(@Payload String message){
        System.out.println("Message content : " + message);
    }

    @RabbitHandler
    public void onMessage(@Payload User user) {
        System.out.println("Message content : " + user);
    }
}


大家可以看看这个例子,我们先用@RabbitListener监听erduo队列中的消息,然后使用@RabbitHandler注解了两个方法。


  • 第一个方法的body类型是String类型,这就代表着这个方法只能处理文本类型的消息。

  • 第二个方法的body类型是User类型,这就代表着这个方法只能处理序列化类型且为User类型的消息。


这两个方法正好对应着我们第二节中测试类会发送的两种消息,所以我们往RabbitMQ中发送两条测试消息,用来测试这段代码,看看效果:



都在控制台上如常打印了,如果@RabbitHandler注解的方法中没有一个的类型可以和你消息的类型对的上,比如消息都是byte数组类型,这里没有对应的方法去接收,系统就会在控制台不断的报错,如果你出现这个情况就证明你类型写的不正确。


假设你的erduo队列中会出现三种类型的消息:byte,文本和序列化,那你就必须要有对应的处理这三种消息的方法,不然消息发过来的时候就会因为无法正确转换而报错。


而且使用了@RabbitHandler注解之后就不能再和之前一样使用Message做接收类型。


@RabbitHandler
public void onMessage(Message message, Channel channel) throws Exception {
    System.out.println("Message content : " + message);
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    System.out.println("消息已确认");
}


这样写的话会报类型转换异常的,所以二者选其一。


同时上文我的@RabbitHandler没有进行消息确认,大家可以自己试一下进行消息确认。


6. 📙消息的序列化转换


通过上文我们已经知道,能被自动转换的对象只有byte[]、String、java序列化对象(实现了Serializable接口的对象),但是并不是所有的Java对象都会去实现Serializable接口,而且序列化的过程中使用的是JDK自带的序列化方法,效率低下。


所以我们更普遍的做法是:使用Jackson先将数据转换成JSON格式发送给RabbitMQ,再接收消息的时候再用Jackson将数据反序列化出来。


这样做可以完美解决上面的痛点:消息对象既不必再去实现Serializable接口,也有比较高的效率(Jackson序列化效率业界应该是最好的了)。


默认的消息转换方案是消息转换顶层接口-MessageConverter的一个子类:SimpleMessageConverter,我们如果要换到另一个消息转换器只需要替换掉这个转换器就行了。



上图是MessageConverter结构树的结构树,可以看到除了SimpleMessageConverter之外还有一个Jackson2JsonMessageConverter,我们只需要将它定义为Bean,就可以直接使用这个转换器了。


@Bean
    public MessageConverter jackson2JsonMessageConverter() {
        return new Jackson2JsonMessageConverter(jacksonObjectMapper);
    }


这样就可以了,这里的jacksonObjectMapper可以不传入,但是默认的ObjectMapper方案对JDK8的时间日期序列化会不太友好,具体可以参考我的上一篇文章:从LocalDateTime序列化探讨全局一致性序列化,总的来说就是定义了自己的ObjectMapper。


同时为了接下来测试方便,我又定义了一个专门测试JSON序列化的队列:


@Bean
public Queue erduoJson() {
    // 其三个参数:durable exclusive autoDelete
    // 一般只设置一下持久化即可
    return new Queue("erduo_json",true);
}


如此之后就可以进行测试了,先是生产者代码:


public void sendObject() {
        Client client = new Client();

        System.out.println("Message content : " + client);

        rabbitTemplate.convertAndSend(RabbitJsonConsumer.JSON_QUEUE,client);
        System.out.println("消息发送完毕。");
    }


我又重新定义了一个Client对象,它和之前测试使用的User对象成员变量都是一样的,不一样的是它没有实现Serializable接口。


同时为了保留之前的测试代码,我又新建了一个RabbitJsonConsumer,用于测试JSON序列化的相关消费代码,里面定义了一个静态变量:JSON_QUEUE = "erduo_json";


所以这段代码是将Client对象作为消息发送到"erduo_json"队列中去,随后我们在测试类中run一下进行一次发送。


紧着是消费者代码:


@Slf4j
@Component("rabbitJsonConsumer")
@RabbitListener(queues = RabbitJsonConsumer.JSON_QUEUE)
public class RabbitJsonConsumer {
    public static final String JSON_QUEUE = "erduo_json";

    @RabbitHandler
    public void onMessage(Client client, @Headers Map<String,Object> headers, Channel channel) throws Exception {
        System.out.println("Message content : " + client);
        System.out.println("Message headers : " + headers);
        channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG),false);
        System.out.println("消息已确认");
    }

}


有了上文的经验之后,这段代码理解起来也是很简单了吧,同时给出了上一节没写的如何在@RabbitHandler模式下进行消息签收。


我们直接来看看效果:



在打印的Headers里面,往后翻可以看到contentType=application/json,这个contentType是表明了消息的类型,这里正是说明我们新的消息转换器生效了,将所有消息都转换成了JSON类型。


本文来源:juejin.im/post/6859152029823008781


推荐程序员必备微信号 

程序员内参
微信号:

progrmer0001

推荐理由:
在这里,我们分享程序员相关技术,职场生活,行业热点资讯。不定期还会分享IT趣文和趣图。这里属于我们程序员自己的生活,工作和娱乐空间。
 ▼长按下方↓↓↓二维码识别关注

收外国男骗中国妹子的炮?天朝竟有这样一帮「女权组织」 2018-03-19 INSIGHT视界 From 酷玩实验室 微信号:coollabs 其实我读书的时候 也曾经想过做一个女权主义者 但是后来发生了一些事情 让我选择了放弃 简单来说是这么一个事情:我发现 女权对于一些中国人来说是信仰 但是对另一些中国人来说是生意 所谓的“伪女权”“女权癌” 大概就是这么回事 尽管早就有这样的思想准备 但让我没想到的是 这两天,知乎上曝光了一件大事 还是让我三观震碎 我没想到,这些“伪女权” 竟然已经形成了黑色产业链 让人细思恐极—— 国内竟然有一群人 打着“女权主义”的名号 从事着组织卖淫的事情 在中国女生不知情的情况下 把她们卖给外国男人!事情是这样的:根据知乎用户伊利丹·怒风的爆料 他在知乎和一个伪女权主义者 吵了起来 一开始,他可能以为这只是一个 脑子比较轴的伪女权主义者 所以两人就吵了一通 本来,他以为就是撕个逼而已 没想到的是 这个伪女权主义者 可不是什么好惹的主 这个自称为“玛丽女王”的人 竟然在半个月中 持续不断地骚扰他 而最夸张的是 玛丽女王声称 自己有能力 让伊利丹的QQ号 在5天之内被封掉 到这里为止 伊利丹一直以为 他不过是碰到了一个杠精 但是万万没想到 5天之后 他的QQ号竟然真的被永久封禁了!说真的,这就有点吓人了 这个不起眼的玛丽女王 竟然还能操控别人的QQ账号被封?难不成,她真的背后有人?伊利丹这才意识到 自己好像惹到了一个组织 他去扒了扒这个玛丽女王的QQ空间 这才发现 自己简直捅出一个马蜂窝:这个人平时干的 竟然是把中国女生 卖给外国男人的皮肉生意!真的,我本来以为 我是一个见过不少套路的人 但没想到 这一套操作 真的是惊为天人 简单来说是这样的 首先,玛丽女王自称是“女权主义者” 但是实际上她的言论 宣传的却是 中国男人配不上中国女人 她甚至恶意辱骂中国男人 恨不得中国男人全部死光 连自己的爸爸都不放过 但是,这么做对她有什么好处呢?很简单 骂完中国男人以后 接下来她就说—— 既然中国男人这么差劲 那就找外国男人吧!于是,她就经常发布外国男人的介绍 看起来是一个热心的媒婆 还在各种QQ和微信群里 散播此类信息 但是看到这里 我们不难发现有点问题 看看其中这些不堪入目的措辞 这并不是普通的介绍男友啊!这简直是在拉皮条啊!果然,伊利丹发现 玛丽女王真的在 拉皮条的过程中 收外国男人的钱!下面是聊天记录实锤:而且,请注意—— 在这个过程中 她会收外国男人的钱 但是钱不给中国女生 却落到了她自己的腰包 于是一个诡异的情况出现了:中国妹子 并不知道收钱这回事 还以为是正常交友 而外国男人 却都交了钱 很可能认为自己是在买春!额,也就是说 在中国女孩不知情的情况下 她们被“卖”给了外国男人 而好处费 却全都进了玛丽女王的腰包... 我真的是没见过这种操作 这说轻了是骗炮 说重了,已经可以算是卖淫了吧?我想请熟悉刑法的朋友们看看 这个玛丽女王 至少应该算是个 介绍组织卖淫罪吧?而且,从伊利丹曝光的资料看来 这个组织规模不小 玛丽女王甚至把外国男生的信息 建了一个完整的表格 有详细的个人资料、照片 可以说 是一条非常完整的产业链 那如果按照这样操作 外国男人都是来嫖的 中国女生却不知道 还以为是要跟他们谈恋爱 那双方难道不会穿帮吗?恩,在这方面 玛丽女王早有对策 根据知乎一位 从事过这个产业的匿名用户提供的信息 针对这种情况 玛丽女王们 还会手把手地教外国男人 怎么快速摆脱女生的纠缠 怎么调教中国女生 怎么让女生觉得自己很可爱 可以说 各种套路一应俱全 甚至还可以开发票!看到这里 她们背后的产业就非常清楚了 这个玛丽女王 她根本就不是什么女权主义者 而是打着女权主义的口号 贩卖中国女生的人贩子 一方面 她们通过辱骂中国男人 吸引对外国男人感兴趣的中国女生 另一方面 她们向外国男人收钱 然后把中国女生卖给他们!图片来源:知乎@渭水徐工 而可怜的中国妹子们 还以为自己是在 追求男女平权 其实,不过是沦为了 这些老鸨的赚钱工具 伊利丹把这整个事情 写出来以后 在知乎、微博引起了巨大的关注 关于其中提到的 伊利丹的QQ被永久封禁的问题 腾讯经过核查 目前也有了结果:经调查,是玛丽女王利用伪造证据 恶意举报了伊利丹的QQ号 目前,腾讯已经将伊利丹的QQ解封 同时封禁了玛丽女王等人的 两个QQ账号 警方也就此事立案侦查了 相信很快就会有结果 这个事情算是告一段落了 但是在我看来 却有一件事让我无法释怀:为什么“女权主义”竟然会和 辱骂中国男性等同起来?为什么“和外国男人交友” 竟然还能演变成 一个免费的陪睡组织?我想,这个玛丽女王 也许只是一个 发现了恶性赚钱模式的生意人 但是在这背后隐藏的 其实是一个很深的问题:为什么有不少中国女人 越来越看不上中国男人 甚至觉得嫁给外国男人 是一种时尚?这里面的原因可能非常复杂 我这里先提供一个思路 供大家讨论:我发现 现在中国很多大型的女权组织 背后都有着西方势力的影子 她们打着女权的名号 为自己谋取暴利 为西方国家从事破坏活动 而那些真正为女性平权而奔走的人 却得不到应有的帮助 我之所以这样说 并不是信口开河 而是有充足的证据 有一个非常有名的民间女权组织 叫做“女权之声” 它一再声称 自己只是一个自发的民间组织 致力于促进男女平等的 它所有的微博账号、微信账号 全部都是由一个 叫做妇女传媒监测网络的创办的 而这个妇女传媒监测网络 有这么多媒体产品 那它的钱都是哪里来的呢?从她们介绍的合作组织里 我们可以清楚地找到 她们的资助者—— 竟然有西方的福特基金会 有人也许会问 收了西方的钱怎么了?中国的组织不能收西方的钱吗?然而,她们不只是收了西方的钱而已 女权之声组织里 有一个人叫做郑楚然 她除了女权运动之外 没有任何其他工作 表面上,是一个全职的女权工作者 在2015年的时候 她还因为寻衅滋事 被警察拘留过30多天 甚至在她被拘留的时候 希拉里还借题发挥 指责中国侵犯人权、压制民主 一个中国的小小民间组织的首领 在互联网上的粉丝还没有我多 竟然能得到希拉里这个级别的关注?我真的是惊掉了下巴 这样看来 我离希拉里也不是很远了??而不止是希拉里 这样一个明明思想上毫无建树的人 却被西方媒体BBC评为了 全球百大思想家 图:郑楚然在王宝强事件中发表的言论 除此以外 更让人匪夷所思的 是她们平时就喜欢攻击政府 甚至于,她们还会试图分裂我们国家 比如,女权之声这个组织里 著名的女权斗士洪理达 就曾经转发著名的港独媒体 Hong Kong Free Press的言论 甚至曾公开发表过 支持藏独、港独、台独的言论 她也经常和郑楚然混在一起 我很想不通 如果她们真的只是单纯的女权主义者 为何要发表分裂国家的言论?为何要支持藏独、港独、台独?我只能说,这大概就叫 拿人家的手短,吃人家的嘴软吧 以前,我在接触中国的女权组织时 我就觉得很奇怪 她们都喜欢声称 自己是不盈利的非政府组织 但是她们无论是宣传 还是组织各类活动 都需要大量的钱 如果她们真的不盈利 那这些钱都是哪里来的呢?而这些外国的金主 他们也更加不可能是什么慈善组织 大发善心来给中国人投钱 每一分投出去的钱 一定都是要有回报的 那么,他们的回报是什么呢?他们给中国的“女权组织”投钱 能得到什么利益呢?联想到中国网络上 如火如荼的对中国男人的讨伐 我只能说,细思恐极 我绝不是危言耸听 因为我们就看不远的邻国日本 近些年来日本对于西方的崇拜 可谓深入骨髓 已经到了崇洋媚外的程度 而这其中 当然也包括对白人男性的崇拜 甚至在2016年一个瑞士白人 发了一个视频,赤裸裸的说 “在东京,只要你是白人, 做什么都可以” 视频里面他在日本便利店 随意的亲吻不认识的收银员女孩 在酒吧把不认识的日本女孩 按向自己的裤裆 而日本女孩回应的却是谄媚的笑容 我想,并不会有那么多中国人 真正被西方伪女权主义控制 但是,我们要警惕的是 别在你自己都没有察觉的时候 被别有用心的人洗了脑 更有甚者 别在你自己都不知道的情况下 被别人卖给了外国男人 还去帮他数钱 本文系授权发布,From 酷玩实验室,微信号:coollabs,欢迎分享到朋友圈,未经许可不得转载,INSIGHT视界 诚意推荐 Forwarded from Official Account 酷玩实验室 酷玩实验室 Learn More Scan QR Code via WeChat to follow Official Account 采集文章采集样式近似文章查看封面

本文章转载自公众号:csh624366188

首页 - java 相关的更多文章: