JoyLau's Blog

JoyLau 的技术学习与思考

JoyMedia - Beta - 预览图
JoyMedia - Beta - 系统结构

在线地址

JoyMedia - Beta 预览版

项目介绍

实现目的

  • 本人经常在写代码或者没事的时候会听一些音乐
  • 以前大部分会选择本地安装客户端
  • 其中最喜欢的认为做的比较好的音乐客户端实属网易云音乐了
  • 无论是从 Mac 版的客户端,还是 IOS 版的客户端,界面都非常优美,简直是极客和码农的必备
  • 最主要是的网易云的歌曲推荐功能,很强大,我一度认为2个人的歌单相似度超过90%,就可以在一起了,这样再也不怕找不到对象了,😆
  • 但也有些问题,网易云有一些版权音乐,是无法听到的,有时候昨天还在听得音乐,今天就听不了了
  • 这就很烦了

自己的想法

  • 最初想把这个版权音乐的 mp3 地址解析出来,这样就可以直接听了
  • 恩,想法很 nice
  • 那么,照着这个想法做吧

项目实现

总体架构

  • 正如上述系统结构所示
  • 我自己有2台云服务器,一台阿里云的,另一台是腾讯云的
  • 这2台服务器,我是这样分配的: 阿里云只提供 WEB 服务,腾讯云为 WEB 访问提供各种服务
  • 当然服务器上我还跑了其他服务

阿里云服务器

  • Nginx 主要负责了 JoyMedia 的 负载均衡,在该台服务器上,我用 部署了2个 spring-boot 项目,以权重的方式配置了负载均衡,这样我在更新项目的时候可以保证另一个服务的可用性
  • 当然 Nginx 还有个反向代理的作用, upstream 配置了其他项目的访问
  • 还有台 Redis 服务了,爬到的数据会存到 Redis 了,以供 WEB 服务迅速读取,当然在有些地方不会读取 Redis ,比如单曲歌曲播放的 mp3地址的获取
  • 在最开始的时候我会先在后台解析出来再存到 Redis 里,但是发现网易云的歌曲 mp3 地址失效太快了,有时会导致播放异常,不如实时解析来的实在
  • 在比如单首歌曲的评论的获取,这个得是实时解析的

腾讯云服务器

  • 提供网易云音乐解析的是一个 Node 服务,这个 Node 服务是如何解析地址的,这个需要单独再写一篇文章,先知道这个 Node 服务是干嘛的就好
  • 然后部署了3个spring-boot服务,分别提供了各自的服务,有定时爬去网易云音乐的推荐歌单,爬取歌单的歌曲列表,爬取歌单评论
  • 由于爬到的音乐信息很快就会失效,这个服务都要定时的爬取
  • 爬取到的数据的落地存储,我是存到的MongoDB中,在这篇文章中:重剑无锋,大巧不工 SpringBoot — 整合使用MongoDB , 我说明了为什么要选择 MongoDB
  • 这3个服务爬到的数据会实时存到 Redis 中,另一方面,会异步存到 MongoDB 中,我想着这些数据或许还能做什么数据分析之类的,😄

初版完成后

等我搭建完这个服务后,发现了问题

  • 有版权控制的音乐根本解析不到 mp3 的实际地址
  • 那么我想听的音乐,听不到还是听不到,突然变得很尴尬

又有了想法

  • 一般情况下,我们在一家音乐网站上找不到自己想要的音乐,就回去其他音乐网站上找
  • 恩,就这么干
  • 网易云找不到的音乐,我就去虾米音乐,去 QQ 音乐找
  • 这2个网站的音乐我都小试了下,都是可以的
  • 于是我现在把这些功能集中在页面的搜索框中,搜索这3个音乐网站的结果,然后实施解析来播放
  • 这是我下步要做的事情

有些地方还有 BUG

  • 有些地方还是有 BUG 的,需要修复

有些地方功能还没写好

  • 比如右上角的用户登录,现在的想法是使用第三方登录,比如 QQ, 微信…,但是是登录网易云音乐呢,还是登录网站呢?
  • 要是登录网易云音乐的话,估计账号安全是个问题,而且登录接口不能频繁调用
  • 要是登录网站的,好像没什么卵用
  • 再比如左下角的歌词界面,虽然能获取到歌词,但是怎么做到歌词随着歌曲的播放实时滚动,这个现在还没有头绪…

还在继续开发中…

前言

MongoDB 安装

  • yum install mongodb-server mongodb
  • systemctl start mongod
  • whereis mongo

MongoDB 配置文件

  • 修改 bind_ip为 0.0.0.0 即可外网可访问
  • 修改 fork 为 true 即可后台运行
  • 修改 auth为 true 即访问连接时需要认证
  • 修改 port 修改端口号

开始使用

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

配置文件

mongoDB配置
还有种配置url方式: spring.data.mongodb.uri=mongodb://name:pass@host:port/db_name

相比这种方式,我觉得第一种截图的方式要更直观一些

在 SpringBoot 项目中使用

  • 主要的一个接口MongoRepository<T,ID>,第一个是要存储的实体类,第二个参数是 ID 类型
  • 自定义一个接口实现上述接口
  • 定义实体类
  • 自定义实现类可直接注入使用
  • 默认的已经存在了增删改查的方法了,可以直接使用
  • 想要更多的功能可以在接口中实现更多的自定义
  • 下面截图所示:

自定义一个 DAO :
mongoDB-DAO

查看如何使用 :
mongoDB-method
有个 username 忘了配置了,得加上的

使用起来就是如此简单,感觉使用起来很像 mybatis 的 mapper 配置

有一些注解的配置

有时候使用起来会有一些问题

  • 在默认策略下, Java 实体类叫什么名字,生成后的表名就叫什么,但我们可能并不想这样
  • 同样的道理,有时属性名和字段也并不想一样的
  • 有时一些属性我们也并不想存到 MongoDB

注解解决这些问题

  • @Id : 标明表的 ID , 自带索引,无需维护
  • @Document : 解决第一个问题
  • @Field : 解决第二个问题
  • @Transient : 解决第三个问题

此外,还有其他的注解

可能并不常用,在此也说明下

  • @Indexed(unique = true) : 加在属性上,标明添加唯一索引
  • @CompoundIndex : 复合索引

预览

查看下刚爬的网易云官网的歌曲信息吧

![歌曲信息](//s3.joylau.cn:9000/blog/springboot-mongoDB-preview.gif)

前言

  • ZeroC Ice 的背景我就不介绍了
  • ZeroC Ice 环境安装搭建,概念原理,技术基础,这些网络上都有,再介绍的话就是copy过来了,没有多大意义,不再赘述了
  • 下面我们开始实战

开始动手

  • 首先我们需要几个ice接口文件,比如说这几个:
    Ice 文件展示
  • 我们来看一下其中一个ice文件定义的接口说明
    Ice接口文件说明
    文件里定义了5个接口,可以很明显的的看到是区间的增删改查接口
    刚好很适合我们对外提供增删改查的RESTFul API 接口
    这里在对外提供 RESTFul API 是可以很清楚的 使用 POST GET PUT DELETE
    可以说这里很好的提供了这样一个例子
  • 命令 slice2java xxx.ice 生成 java 的 client,server类
    生成的Java类
    生成的Java文件很多,这个不用管,更不必更改里面的代码内容
    你要是有兴趣的话,也可以将这些文件分为 client 和 server 分门别类的归纳好
    打开看一下,里面的代码很混乱,无论是代码风格,样式,变量命名,对于我来说,简直不忍直视
    生成的Java代码
  • 编写client类
    client类
    代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Data
@Component
@ConfigurationProperties(prefix = "ice")
public class Client {
private String adapterName;
private String host;
private int port;

private Logger _logger = LoggerFactory.getLogger(Client.class);

/**
* 执行操作
*
* @param command 命令体
* @return Result
*/
public Result execute(CommandBody command) {
Ice.Communicator ic = null;
try {
//初使化通信器
ic = Ice.Util.initialize();
//传入远程服务单元的名称、网络协议、IP及端口,获取接口的远程代理,这里使用的stringToProxy方式
Ice.ObjectPrx base = ic.stringToProxy(getStringProxy());
//通过checkedCast向下转换,获取接口的远程,并同时检测根据传入的名称获取的服务单元是否代理接口,如果不是则返回null对象
ZKRoadRangeAdminPrx interfacePrx = ZKRoadRangeAdminPrxHelper.checkedCast(base);
if (interfacePrx == null) {
return new Result(false, "Invalid proxy");
}
//把接口的方法传给服务端,让服务端执行
Result result = executeCommand(command, interfacePrx);
if (result == null) {
return new Result(false, "暂无此操作命令");
}
return result;
} catch (Exception e) {
_logger.info(e.getMessage(), e);
return new Result(false, "连接错误!" + e);
} finally {
if (ic != null) {
ic.destroy();
}
}
}

/**
* 执行操作命令
*
* @param command 命令体
* @param interfacePrx 接口
* @return ProgramResponse
*/
private Result executeCommand(CommandBody command, ZKRoadRangeAdminPrx interfacePrx) {
CommandType type = command.getCommandType();
if (type.equals(CommandType.addRange)) {
return returnMessage(interfacePrx.AddRange(command.getZkRoadRange()));
} else if (type.equals(CommandType.updateRange)) {
return returnMessage(interfacePrx.UpdateRange(command.getZkRoadRange()));
} else if (type.equals(CommandType.removeRange)) {
return returnMessage(interfacePrx.RemoveRange(command.getZkRoadRange().code));
} else if (type.equals(CommandType.getRange)) {
return new Result(true, JSONObject.toJSONString(interfacePrx.GetRange(command.getZkRoadRange().code)));
} else if (type.equals(CommandType.listRanges)) {
return new Result(true, JSONObject.toJSONString(interfacePrx.ListRanges()));
}
return null;
}


/**
* 获取配置的地址信息
*
* @return String
*/
private String getStringProxy() {
return adapterName + ":tcp -h " + host + " -p " + port;
}


private Result returnMessage(boolean result) {
return result ? new Result(true, "success") : new Result(false, "failure");
}

}
  • 需要三个配置: 适配器名,IP地址,端口号,配置在SpringBoot项目里,如下:
    ICE配置信息

再封装一下

  • 封装返回消息体
    ICE配置信息
  • 封装执行命令体
    ICE配置信息

重要

  • 调用 ice 里的接口方法:获取远程代理的 checkedCast
  • 获取远程接口的 interfacePrx 可直接调用 ice 文件里的方法
  • 服务端的 Ice 版本最好和 客户端的版本相同
  • 服务端提供服务时需要创建一个 servant ,一般的我们会在接口名后面加一个I,以此命名作为Java文件类名
  • 该servant继承 接口文件的Disp类,并重写接口中定义的方法,实现具体的业务逻辑
  • Server端创建一个适配器 adapter,将servant 放进去
  • 服务退出前,一直对请求持续监听

听首歌回忆下

实验步骤

  • 新建一个项目
  • 可先分别在码云和 GitHub 上建好仓库<可选>
  • 将项目提交的码云上
  • 项目提交到另一个仓库的时候重新 define remote <可选>
  • 之后每次先提交到本地仓库,可以根据每次提交到本地仓库的不同,来选择定义的 remote 来分别提交
  • 每次 pull 也可以选择仓库

遇到个问题

问题

  • 在我新建好码云的仓库后,提交项目,遇到 Git Pull Failed: fatal: refusing to merge unrelated histories

原因

  • 原因:git拒绝合并两个不相干的东西

解决

  • 此时在命令行输入 : git pull origin master –allow-unrelated-histories
  • 要求我输入提交信息
  • 输入完成后,按一下Esc,再输入:wq,然后回车就OK了
  • 再回来提交就可以了

系统工具

  • BetterZip : mac上面的最好的解压工具
  • CHM View : 查看chm类型的开发文档
  • Easy New File Free : 右击桌面,可以像win一样新建文件
  • Bartender 2 : 任务栏menu图标整理
  • iStat Menus : 系统网速、cpu、内存监控工具
  • SwitchResX : 外接显示器,调节DPI
  • Go2Shell : 在finder的任意文件夹下打开终端
  • Aria2GUI : 突破百度限速
  • Alfred 3 : 效率神器,谁用谁知道
  • PDF Expert : 查看pdf
  • 远程桌面连接 : mac电脑上远程连接windows,网址: https://rink.hockeyapp.net/apps/5e0c144289a51fca2d3bfa39ce7f2b06 (2017年10月26日加)

播放器

  • 网易云音乐 :这个必备啊
  • 优酷 :这个可以免费看1080P视频,没广告,有时候出抽风的时候还可以看会员视频
  • OBS : 视频直播、录制软件
  • Movist : 视频播放器,支持的格式很多

小工具

  • CleanMyMac 3 : 清理mac电脑垃圾
  • ShadowsocksX : 翻墙必备
  • TeamView : 桌面远程软件
  • MacDown : 开源的markdown编辑器
  • Path Finder : Finder增强版
  • Parallels Desktop : 虚拟机
  • FileZilla : ftp工具
  • Foxmail : 邮箱客户端
  • Folx : 下载工具

开发工具

  • FireFox : 火狐
  • Google Chrome : 必备
  • IntelliJ IDEA : 必备IDE
  • WebStorm : web开发必备
  • DataGrip : 数据库管理软件
  • Navicat Premium : 已经使用习惯的MySQL连接工具,也支持其他数据库
  • XShell : SSH远程连接工具,我还是比较喜欢终端下的ssh命令连接,虽然有一个家族的系列产品
  • Sublime Text3 : 文本编辑器
  • Beyond Compare : 文本比较工具
  • GitHub Desktop : github GUI客户端
  • rdm : redis可视化GUI界面
  • HBuilder : h5开发工具
  • iTerm : 终端

自己暂时使用的工具都已归纳出来,以后有新的好用的工具,会加上的,Mac下大部分工具都是收费的,你可以偷偷点一下 xclient.info

官方视频

开始拆箱

MacBook Pro

先来看一下刚拿到手的包装是什么样的

一台主机

我在官网订购了一个 USB-typeC 转 USB 的转接口

那个小盒子就是

MacBook Pro

打开主机纸盒

MacBook Pro

掰开这个直接就可以把里面的主机盒抽出来,很方便

两边都是这样设计的

MacBook Pro

就2样东西

都摆放好了

准备拿剪刀拆开

MacBook Pro

来一张侧面照

MacBook Pro

拆开盒子保护膜

打开镂空设计的上盖,看到我们的主机真容

MacBook Pro

这样一看,真的很薄,起码比我以前用过得笔记本都要薄多了

MacBook Pro

2端都是 USB-C 接口的充电线

适配器感觉好大啊

MacBook Pro

靠近点看下USB-C的充电线

MacBook Pro

然后就什么都没有了

底下的盒子也打不开

MacBook Pro

苹果的LOGO贴纸

说明书

三包凭证

MacBook Pro

开始正式拆开主机的包装纸

MacBook Pro

一睹真容
15.6寸的

进入系统

MacBook Pro

盖子一打开就开机了

屏幕与键盘之间隔了一张纸

让我们拿开他

MacBook Pro

很快就进入了系统

MacBook Pro

重新设计的蝴蝶键盘

键程很短

按键很紧凑

MacBook Pro

来一张键盘的整体照

上面是全新的 Multi-Touch Bar ,替换了以前的一排功能按键,许多mac内置的应用在Touch Bar上都有支持

MacBook Pro

触摸板的占比实在是太大了

看我一只手放上去,刚好差不多

手有点丑,请忽略

MacBook Pro

迫不及待的想进入系统尝试一下了

先来连接家里的WIFI

MacBook Pro

老套路了

都是下一步

再下一步

MacBook Pro

在电源键上提供了和iPhone上一个的指纹支持

MacBook Pro

来录入我自己的指纹

不知道用的是什么材料,在这个TouchBar上面滑来滑去很舒服,很有感觉

MacBook Pro

正在设置指纹

稍等一下

MacBook Pro

终于正式进入系统了

屏幕的显示效果很震撼

特效动画的帧数很高,给人感觉很流畅

系统信息

MacBook Pro

看一下系统信息

MacBook Pro

显示器信息

2G独立显存

个人感受

使用它也有一周多了,说一下自己的整体感受吧

  • Retina显示器的显示效果真的很好,真是惯坏了眼睛,现在再去看普通的显示器,就感觉有很强的颗粒感
  • macOS High Sierra字体渲染的很棒,系统中有很多适合编程的字体,在 IntelliJ IDEA 中编码很爽
  • 更大的分辨率能看到更多的内容
  • 系统安装软件什么的很方便,没有想Windows下那么碎片化
  • Multi-Touch Bar 有很多有意思的功能,除了官方宣传的和MacOS本身自带的,想滑动查看照片,添加emoji小表情,控制亮度。。。之类的,大量第三方的软件也进行了适配,网易云音乐,搜狗输入法就适配的很不错
  • 系统触摸板真的是Windows平台无法比拟的,有很多手势,编码什么的,完全可以不用鼠标
  • 键盘旁边2个喇叭的音质效果很震撼,而且声音特别大,看电影,听音乐很有感觉
  • 耗电也比Windows系统的笔记本少多了,充满电的话,就拿我平时工作情况来说,开多个IDEA,起多个服务,多个浏览器,多个编辑器。。。什么什么的,大概能撑个8,9个小时,上班一天不充电….
  • 颜值好,很符合现代化审美

缺点也还是有的

  • 太贵

前言

  • 使用很简单
  • 关注业务开发
  • 熟悉提供的注解

开始

引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

配置启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@SpringBootApplication
@EnableSwagger2
public class JoylauSwagger2Application {

public static void main(String[] args) {
SpringApplication.run(JoylauSwagger2Application.class, args);
}

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("cn.joylau.code"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot构建RESTful APIs")
.description("将每一个注解的@RestController和@ResponseBody的类和方法生成API,点击即可展开")
.termsOfServiceUrl("http://blog.joylau.cn")
.contact(new Contact("joylau","http://blog.joylau.cn","2587038142@qq.com"))
.license("The Apache License, Version 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0")
.build();
}
}

注解说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RestController
@RequestMapping(value="/users") // 通过这里配置使下面的映射都在/users下,可去除
public class UserController {

static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());

@ApiOperation(value="获取用户列表", notes="")
@RequestMapping(value={""}, method= RequestMethod.GET)
public List<User> getUserList() {
List<User> r = new ArrayList<User>(users.values());
return r;
}

@ApiOperation(value="创建用户", notes="根据User对象创建用户")
@ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User")
@RequestMapping(value="", method=RequestMethod.POST)
public String postUser(@RequestBody User user) {
users.put(user.getId(), user);
return "success";
}

@ApiOperation(value="获取用户详细信息", notes="根据url的id来获取用户详细信息")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public User getUser(@PathVariable Long id) {
return users.get(id);
}

@ApiOperation(value="更新用户详细信息", notes="根据url的id来指定更新对象,并根据传过来的user信息来更新用户详细信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long"),
@ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User")
})
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}

@ApiOperation(value="删除用户", notes="根据url的id来指定删除对象")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}

}

常见注解

  • @Api:修饰整个类,描述Controller的作用
  • @ApiOperation:描述一个类的一个方法,或者说一个接口
  • @ApiParam:单个参数描述
  • @ApiModel:用对象来接收参数
  • @ApiProperty:用对象接收参数时,描述对象的一个字段
  • @ApiResponse:HTTP响应其中1个描述
  • @ApiResponses:HTTP响应整体描述
  • @ApiIgnore:使用该注解忽略这个API
  • @ApiClass
  • @ApiError
  • @ApiErrors
  • @ApiParamImplicit
  • @ApiParamsImplicit

最后

注意

  • Swagger2默认将所有的Controller中的RequestMapping方法都会暴露,然而在实际开发中,我们并不一定需要把所有API都提现在文档中查看,这种情况下,使用注解@ApiIgnore来解决,如果应用在Controller范围上,则当前Controller中的所有方法都会被忽略,如果应用在方法上,则对应用的方法忽略暴露API

或者重写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Docket createRestApi() {
Predicate<RequestHandler> predicate = new Predicate<RequestHandler>() {
@Override
public boolean apply(RequestHandler input) {
Class<?> declaringClass = input.declaringClass();
if (declaringClass == BasicErrorController.class)// 排除
return false;
if(declaringClass.isAnnotationPresent(RestController.class)) // 被注解的类
return true;
if(input.isAnnotatedWith(ResponseBody.class)) // 被注解的方法
return true;
return false;
}
};

前言

本文说明

  • 使用之前rabbitMQ的介绍我就不说了,我认为你已经了解了
  • rabbitMQactiveMQ的对比区别我也不说了,我认为你已经查过资料了
  • rabbitMQ的安装,我也不说了,我认为你下载的时候已经看到了官网的安装说明,给一个Windows安装的链接:http://www.rabbitmq.com/install-windows.html
  • rabbitMQweb插件的启用,我也不说,我认为你已经会了
  • 那我们开始吧

入门使用

在使用之前先看一下rabbitMQ-client的使用

先引入依赖:

1
2
3
4
5
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.0</version>
</dependency>

在看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void product() throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ地址
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//创建一个新的连接
Connection connection = factory.newConnection();
//创建一个频道
Channel channel = connection.createChannel();
//声明一个队列 -- 在RabbitMQ中,队列声明是幂等性的(一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同),也就是说,如果不存在,就创建,如果存在,不会对已经存在的队列产生任何影响。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
//发送消息到队列中
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("P [x] Sent '" + message + "'");
//关闭频道和连接
channel.close();
connection.close();
}


public void consumer() throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ地址
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//创建一个新的连接
Connection connection = factory.newConnection();
//创建一个频道
Channel channel = connection.createChannel();
//声明要关注的队列 -- 在RabbitMQ中,队列声明是幂等性的(一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同),也就是说,如果不存在,就创建,如果存在,不会对已经存在的队列产生任何影响。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("C [*] Waiting for messages. To exit press CTRL+C");
//DefaultConsumer类实现了Consumer接口,通过传入一个频道,告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("C [x] Received '" + message + "'");
}
};
//自动回复队列应答 -- RabbitMQ中的消息确认机制
channel.basicConsume(QUEUE_NAME, true, consumer);
}

代码的注释很详细

SpringBoot中的使用

引入依赖

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>

配置文件

1
2
3
4
5
6
7
8
9
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
output:
ansi:
enabled: always

生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class Product {
@Autowired
private AmqpTemplate rabbitTemplate;

public void send() {
String context = "hello " + new Date();
System.out.println("生产者发送信息 : " + context);

new Queue("hello");
this.rabbitTemplate.convertAndSend("hello", context);
}
}

创建消息生产者Product。通过注入AmqpTemplate接口的实例来实现消息的发送,AmqpTemplate接口定义了一套针对AMQP协议的基础操作。在Spring Boot中会根据配置来注入其具体实现。在该生产者,我们会产生一个字符串,并发送到名为hello的队列中

消费者

1
2
3
4
5
6
7
8
@Component
@RabbitListener(queues = "hello")
public class Consumer {
@RabbitHandler
public void process(String hello) {
System.out.println("消费者接受信息 : " + hello);
}
}

创建消息消费者Consumer。通过@RabbitListener注解定义该类对hello队列的监听,并用@RabbitHandler注解来指定对消息的处理方法。所以,该消费者实现了对hello队列的消费,消费操作为输出消息的字符串内容。

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class JoylauSpringBootRabbitmqApplicationTests {

@Autowired
private Product product;

@Test
public void test() throws Exception {
product.send();
}

}

再来一张图

示例截图

exchange 多个消费者

当Exchange和RoutingKey相同、queue不同时,所有消费者都能消费同样的信息
Exchange和RoutingKey、queue都相同时,消费者中只有一个能消费信息,其他消费者都不能消费该信息。

下面示例的队列名称可以随意写个,启动时 @RabbitListener 的 bindings 会自动使用 key 绑定队列到exchange

1
2
3
4
5
6
7
8
9
10
@RabbitHandler
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "${spring.application.name}"),
exchange = @Exchange(value = "${spring.rabbitmq.template.exchange}"),
key = "${spring.rabbitmq.template.routing-key}")
)
public void listenerTrafficMessage(Message message){
System.out.println(message.getClass().getName());
}

消息返回队列

需要处理完消息后在将消息返回队列的话需要配置 spring.rabbitmq.listener.simple.acknowledge-mode: manual
之后注解@RabbitListener 到方法上
Channel channel 进行返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RabbitHandler
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "${spring.application.name}"),
exchange = @Exchange(value = "${spring.rabbitmq.template.exchange}"),
key = "${spring.rabbitmq.template.routing-key}")
)
public void listenerTrafficMessage(Message message, Channel channel){

System.out.println(message.getClass().getName());

try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
} catch (IOException e) {
e.printStackTrace();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
rabbitmq:
host: 192.168.10.224
port: 35672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual #设置消费端手动 ack
concurrency: 1 #消费者最小数量
max-concurrency: 1 #消费者最大数量
prefetch: 1 #在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
template:
exchange: SURVEY_CENTER
routing-key: trafficCongestionSituationBD

在属性配置文件里面开启了ACK确认 所以如果代码没有执行ACK确认 你在RabbitMQ的后台会看到消息会一直留在队列里面未消费掉 只要程序一启动开始接受该队列消息的时候 又会收到

1
2
3
// 告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了
// 否则消息服务器以为这条消息没处理掉 后续还会在发,true确认所有消费者获得的消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

丢弃消息

1
2
3
4
5
6
7
8
9
10
//最后一个参数是:是否重回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
//拒绝消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
//消息被丢失
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
//消息被重新发送
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
//多条消息被重新发送
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
0%