开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot应用篇基于Redis实现延时队列

发表于 2021-08-06

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

SpringBoot应用篇基于Redis实现延时队列

延时队列,相信各位小伙伴并不会陌生,jdk原生提供了延时队列的使用,当然我们这里介绍的不是这种;在实际的项目中,如果我们有延时队列的场景,可以怎样去实现呢

举一个简单的例子,如下单15分钟内,若没有支付,则自动取消订单

本文将介绍一种非常非常简单的实现方式

I. 方案设计

要实现15分钟后自动取消订单,这个也太简单了,来给出一段神级代码

1
2
3
4
5
java复制代码new Thread(() -> {
// 休眠十五分钟,执行取消订单
Thread.sleep(15 * 60 * 1000);
cancelOrder();
}).start();

好的,本文就此结束(开玩笑….)

忽略上面的段子,接下来想一想,如果让我们来实现一个延时队列,可以怎么整?

  • 单机:
    • DelayQueue
    • 定时任务
  • 分布式:
    • Quartz定时任务
    • rabbitmq延时队列
    • redis zset
    • redis 过期回调
    • 时间轮

首先我们这里排除掉单机版,至于原因,现在单体单实例应用实在不多见了,直接来看多实例的情况吧

在上面的几种方案中,重心放在redis上,两种case,下面分别介绍一下

1. redis过期时间

我们知道,在使用redis做缓存时,可以设置失效时间,借助redis的失效事件,我们可以来实现延时队列的场景

比如,现在一个订单,我们在redis中新加入一个订单id,失效时间设置为15分钟;当支付成功之后,主动删除这个缓存;若一直没有付钱,则15分钟后,触发一个过期事件,然后订阅这个事件,来执行取消订单

上面这种实现,有两个问题

  • key失效监听,可能存在大量的无效信息
  • 广播方式消费事件,多实例接收到这个事件,怎么防并发?或者没有一个实例接收到这个事件,那么这个取消订单就会漏掉

显然上面的第二点,漏消息是不能接受的

2. redis zset

zset属于redis提供的几个基本数据结构中的一种,它的特点是有 value + score

如果我们想使用zset拉实现演示队列,那么一个可选的方案就是将score设置为触发的时间戳,value为业务值

然后写一个定时任务,不断的从zset中,取出score小于当前时间戳的数据,任务它们都是已经到期可以执行的

借助这个方案,可以相对简单的实现一个演示队列了

II. redis演示队列实现

1. 环境配置

接下来我们将以redis的zset来实现延时队列,本文借助SpringBoot来搭建一个演示工程,使用的基本配置如下

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

核心依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 下面这里两个非必须,主要是后面的实现演示使用到了 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

redis使用默认的配置,本机 localhost + 6379

2. 核心实现

借助redis zset来实现延时队列,具体的实现代码就很简单了,无非是从zset中取出score小于当前时间戳的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private static final Long DELETE_SUCCESS = 1L;
@Autowired
private StringRedisTemplate redisTemplate;

public String fetchOne(String key) {
Set<String> sets = redisTemplate.opsForZSet().rangeByScore(key, 0, System.currentTimeMillis(), 0, 3);
if (CollectionUtils.isEmpty(sets)) {
return null;
}

for (String val : sets) {
if (DELETE_SUCCESS.equals(redisTemplate.opsForZSet().remove(key, val))) {
// 删除成功,表示抢占到
return val;
}
}
return null;
}

注意上面的实现,有一个点需要说一下

zset:每次查询时取了三个数据,然后遍历获取到的数据,依次尝试去删除,若删除成功,则表示当前实例抢占到了这个消息

为什么这样设计?

这里有两个点,先解释第一个,为啥先查后删

如果我们按照正常的实现流程,每次从zset中取一个,但是无法保证这个时候就只有我一个人拿到了这个数据,在多实例的场景下,可能存在多个实例同时拿到了它,那么如何才能表示只有我一个人霸占了她呢(忽然进入言情的世界😓)

借助redis的单线程机制,只可能有一个实例会删除成功,所以拿到并删除成功的那个小伙伴,就是最终的幸运儿;

因此实现细节就是先查,后删,若删除成功,表示获取成功;否则表示被其他的实例捷足先登

接下来再看第二个,为啥一次拿三个

从上面的分析可以看出,如果我一次只拿一个,那么我抢占到的几率并不太大,特别是当实例比较多时,可能会做多次的无效操作;为了减少这个可能性,所以我一次多拿几个做备选,这样抢占到的概率就会高一些,至于为什么是3,这个就看实际的实例与定时任务的执行间隔了

3. 写入队列

上面是从队列中拿数据,有拿当然得有写,所以我们简单的封装一下写入队列的case

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Component
public class RedisDelayListWrapper implements ApplicationContextAware {
private static final Long DELETE_SUCCESS = 1L;

private Set<String> topic = new CopyOnWriteArraySet<>();

public void publish(String key, Object val, long delayTime) {
topic.add(key);
String strVal = val instanceof String ? (String) val : JSONObject.toJSONString(val);

redisTemplate.opsForZSet().add(key, strVal, System.currentTimeMillis() + delayTime);
}
}

4. 定时取演示队列消息

接下来就是一个定时任务,不断的调用上面的实现,从zset中获取到期的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Scheduled(fixedRate = 10_000)
public void schedule() {
for (String specialTopic : topic) {
String cell = fetchOne(specialTopic);
if (cell != null) {
applicationContext.publishEvent(new DelayMsg(this, cell, specialTopic));
}
}
}

@ToString
public static class DelayMsg extends ApplicationEvent {
@Getter
private String msg;
@Getter
private String topic;

public DelayMsg(Object source, String msg, String topic) {
super(source);
this.msg = msg;
this.topic = topic;
}
}

上面的定时任务,直接借助Spring的@Schedule来实现,遍历所有的topic,捞出数据之后,通过spring的 event/listener事件机制来实现消息处理的解耦

5. 消息消费

最终就是我们的消息消费逻辑了,主要就是消费前面抛出的DelayMsg,我们这里借助AOP来实现消息过滤

定义一个注解Consumer,用来指定消费哪个topic

1
2
3
4
5
6
7
java复制代码@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface Consumer {
String topic();
}

注意这个注解上面还有 @EventListener,表明它可以监听的spring的事件

aop拦截逻辑,根据topic进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Aspect
@Component
public class ConsumerAspect {

@Around("@annotation(consumer)")
public Object around(ProceedingJoinPoint joinPoint, Consumer consumer) throws Throwable {
Object[] args = joinPoint.getArgs();
boolean check = false;
for (Object obj : args) {
if (obj instanceof RedisDelayListWrapper.DelayMsg) {
check = consumer.topic().equals(((RedisDelayListWrapper.DelayMsg) obj).getTopic());
}
}

if (!check) {
// 不满足条件,直接忽略
return null;
}

// topic匹配成功,执行
return joinPoint.proceed();
}
}

5. 测试demo

最后写一个测试demo,验证下上面的实现

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
java复制代码@EnableScheduling
@RestController
@SpringBootApplication
public class Application {
private static final String TEST_DELAY_QUEUE = "test";
private static final String DEMO_DELAY_QUEUE = "demo";
@Autowired
private RedisDelayListWrapper redisDelayListWrapper;

private Random random = new Random();

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

@GetMapping(path = "publish")
public String publish(String msg, Long delayTime) {
if (delayTime == null) {
delayTime = 10_000L;
}

String queue = random.nextBoolean() ? TEST_DELAY_QUEUE : DEMO_DELAY_QUEUE;
msg = queue + "#" + msg + "#" + (System.currentTimeMillis() + delayTime);
redisDelayListWrapper.publish(queue, msg, delayTime);
System.out.println("延时: " + delayTime + "ms后消费: " + msg + " now:" + LocalDateTime.now());
return "success!";
}


@Consumer(topic = TEST_DELAY_QUEUE)
public void consumer(RedisDelayListWrapper.DelayMsg delayMsg) {
System.out.println("TEST消费延时消息: " + delayMsg + " at:" + System.currentTimeMillis());
}

@Consumer(topic = DEMO_DELAY_QUEUE)
public void consumerDemo(RedisDelayListWrapper.DelayMsg delayMsg) {
System.out.println("DEMO消费延时消息: " + delayMsg + " at:" + System.currentTimeMillis());
}
}

6. 小结

本文属于一个实战小技巧,借助redis的zset来灵活的实现一个简单的延时队列,实现倒是没有太大的难度,其中的一些小细节还是挺有意思的,好的,今天分享到此over,欢迎各位老铁来撩,公众号 一灰灰blog 你值得拥有

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号:一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

某音上线网页版,午休时间撸了个爬虫(抖音网页版爬虫) Pyt

发表于 2021-08-06

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

起因

中午边吃饭边刷手机,突然刷到某音刚上线了网页版 某音网页版,打开一看,好家伙,这不是抄…. 额,不,借鉴的油管么,就想着网站刚上线,肯定有没有那么强的反扒机制,撸个爬虫玩玩吧

目标网站

某音PC网页版

代码仓库

开源地址:某音爬虫

实现方案

主要使用Python3实现

  • requests 用来获取网页html代码,下载视频等
  • re 使用正则表达式从html中匹配链接等
  • selenium 由于视频页有动态数据,采用selenium方案获取加载后的数据 需要配合浏览器驱动使用

实现过程

一、请求一个列表页,获取到html,匹配出页面中全部详情页链接,生成链接池

二、原本想从链接池中取出详情页地址,请求详情页html,从中匹配出视频资源地址,进入视频池,但是测试后发现由于视频资源链接为动态获取,直接requests.get拿不到视频地址,于是采取了第二种方案,使用selenium调起浏览器来加载页面,加载完成后使用xpath定位到视频资源位置,提取资源链接进入视频池

三、从视频池中取出资源地址,下载保存到本地

四、到这里基本功能已经实现,代码执行中发现,链接池只能获取到10个链接,只能下载10个视频,检查后发现是列表页有懒加载功能,每次翻页到底部才能加载出下一页列表

五、到这里已经有了解决思路,只要我们自动翻页,一直翻到最后,加载完全部列表在去抓取链接池就可以了,此处使用selenium插入JS,操作翻页,然后运行代码,发现还是获取不到完整的链接池,只能获取到最后两页的,猜测可以是做了优化,翻过去的列表被移除了

六、这也好解决,我们只要在每次翻页的时候去获取一次链接池就,当翻页完成就可以获取到完整链接了,这样又诞生了一个新的问题,就是会获取到重复的链接,只要在翻页完成时,转换下数据类型,去重就可以了

七、然后就可以快乐的爬视频了

看起来上面说的不少,实际上只需要五十行左右代码就可以实现

具体代码如下

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
python复制代码import requests
import re
from selenium import webdriver
import time
from contextlib import closing


def get_home_ulrs(url):
browser = webdriver.Chrome();
browser.get(url)
num = browser.find_element_by_xpath(
'//*[@id="root"]/div/div[2]/div/div[4]/div[1]/div[1]/div[1]/span').text
num = int(int(num)/15)+1
urls = []
for i in range(num):
newList = re.findall('https\:\/\/www\.douyin\.com\/video\/[\d]*\?previous_page\=', browser.page_source)
urls = [*urls,*newList]
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
time.sleep(2)
print(urls)
print(set(urls))
mainList =set(urls)
browser.close()
return mainList

def get_video(url):
browser = webdriver.Chrome();
browser.get(url)
html = browser.find_element_by_xpath(
'//*[@id="root"]/div/div[2]/div[1]/div[1]/div[1]/div[1]/div/div[1]/div[2]/video/source[1]')
video_url = html.get_attribute('src')
browser.close()
return video_url


def download(url, name):
with closing(requests.get(url=url, verify=False, stream=True)) as res:
with open('video/{}.mp4'.format(name), 'wb') as fd:
for chunk in res.iter_content(chunk_size=1024):
if chunk:
fd.write(chunk)

if __name__ == '__main__':
url = input('请输入个人主页地址')
print('正在获取视频地址')
urls = get_home_ulrs(url)
print(urls)
print('成功获取{}个,开始下载'.format(len(urls)))
index = 1
for url in urls:
video_url = get_video(url)
print('正在下载{}/{}'.format(len(urls),index))
download(video_url, index)
print('下载完成')
index += 1

视频演示

www.bilibili.com/video/BV1Af…

总结

本次爬站没有什么太大的障碍,没有什么复杂的反扒机制,没啥可说的…. 另外问下,这够判几年…

补充

目前本程序已经不适用于当前版本某音,原因是某音增加了新的反扒机制,会弹出滑动验证码,本文仅作学习参考使用

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从 HTTP 切换到 HTTPS,这下我的技术博客安全了吧?

发表于 2021-08-06

掘金 的小伙伴们,大家好,我是刚脱离险境的二哥呀!

很久(大概两年)之前,我就搞了一个独立的个人博客网站,长下面这样。

大家有访问过的,可以在评论区扣 1

可惜一直没搞备案和 HTTPS,导致每次访问都提示不安全,感觉怪别扭的,见上图中红色框框部分。

鉴于整体 PV 只有区区的 18 万+,我也就一直懒得搞。

直到有一天,我看到了小傅哥的个人博客 https://bugstack.cn/,风格和我一样,但却是 HTTPS 的,我就坐不住了。

我这人平常很佛系,但遇到别人比自己好的时候,总是忍不住偷学一把,哈哈哈。于是我就问小傅哥,结果他说 fork 我的,而我是 fork 大哥纯洁的微笑的——的确算是传承了,哈哈哈哈哈。

说实话,这个博客界面挺清爽的,对吧?

偷偷地告诉大家,这个博客是用 GitHub Pages + Jekyll 搞的。

大家都知道,GitHub Pages 是免费的,所以就省了服务器的钱。如果大家也想搭建个人博客的话,推荐用这种方式,省事省心。

不过,天下没有免费的午餐,由于 GitHub 和百度发生过一些误解,GitHub 就把百度搜索给屏蔽了(大哭)。要知道,国内使用百度的还是多呀,这下倒好,一大波流量白白溜走了(害,难顶)。

所以我的博客访问量才只有区区的 18 万+,后面打算优化一波,让百度能搜得到。

不过,GitHub Pages 提交博客还是方便,直接用 GitHub 桌面版,配合 Sublime 编辑器,就可以轻松搞定。

比如说,我通过 Sublime 编辑好了一篇文章(md 格式的)。

直接 GitHub 桌面版提交就 OK 了,非常方便。

再说回域名不安全这事,怎么才能让它安全呢?当然是把 HTTP 访问换成 HTTPS 了。

我是通过腾讯云购买的域名,所以我就到腾讯云官网上找 SSL 证书了。

SSL 证书提供了安全套接层证书的一站式服务,包括证书申请、管理及部署功能,与顶级的数字证书授权(CA)机构和代理商合作,为网站、移动应用提供 HTTPS 解决方案。

顺带再补充下 HTTPS 的优势吧。

  • 防流量劫持(别人没办法在你的网站上强制植入垃圾广告)
  • 提升搜索排名(谷歌就喜欢收录 HTTPS 的)
  • 杜绝钓鱼(有信息加密和身份证书,所以安全)

HTTPS 比 HTTP 多了一层 SSL/TLS 协议:

该协议的基本过程是这样的:

  • 客户端向服务器端索要并验证公钥;
  • 双方协商生成对话秘钥;
  • 双方采用对话秘钥进行加密通信。

想了解更多关于 SSL/TLS 协议的信息,可以参照下面这篇博客:

www.ruanyifeng.com/blog/2014/0…

怎么利用腾讯云(打钱过来吧)生成 SSL 证书呢?

通过以下网址进入腾讯云 SSL 证书选购页面。

cloud.tencent.com/product/ssl

选择「自定义配置」标签页中的「域名型免费版」

然后跳转到证书申请页,填写信息。

选择自动添加 DNS 验证。

然后等待。

稍等片刻,就收到了腾讯云的短信通知,说域名证书已经颁发了。刷新页面,就可以看到证书详情了。

到这一步,域名型免费版的证书已经生成完毕了。只需要等待生效即可,不想哟啊下载证书或者部署了。部署是针对服务器的,比如说 Nginx、Tomcat 等,我们是直接利用 GitHub Pages 生成的网页托管服务。

大概过了三四个小时吧,我无意在谷歌浏览器地址栏敲入 i 的时候, 出现了 itwanger.com 的选项(我之前访问过,所以有记录)。

我选择「切换到这个标签页」。

我惊奇地发现:地址栏前面的小锁变成上锁的状态了。

当时我就下意识地感觉到,HTTPS 起效了!

为了确认,我就把光标聚焦到了地址栏,准备复制,发现确实变成 https://www.itwanger.com 了。

于是我就兴冲冲地去搜“沉默王二”这个关键字:

发现谷歌收录的仍然是 http 格式的网址,点进去一看:

果不其然,仍然提示不安全。

这就证实了一点:HTTP 和 HTTPS 同时起效了,只不过谷歌还没有开始收录 HTTPS。

不过我查了一下,随着时间的推移,谷歌会更倾向于收录 HTTPS,而不是 HTTP,因为使用安全链接的网站能够保证数据传输的安全性。

。。。。。

这不,大概过了两三天,当我再次搜索“沉默王二”关键字的时候,谷歌已经收录的是 HTTPS 网址的了。

OK,不错不错,二哥的小破站终于安全了!

大家的网站有需要升级到 HTTPS 的,赶紧去整一波吧!

叨逼叨

二哥在 掘金 上写了很多 Java 方面的系列文章,有 Java 核心语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等,也算是体系完整了。

为了能帮助到更多的 Java 初学者,二哥把自己连载的《教妹学Java》开源到了 GitHub,尽管只整理了 50 篇,发现字数已经来到了 10 万+,内容更是没得说,通俗易懂、风趣幽默、图文并茂。

GitHub 开源地址(欢迎 star):github.com/itwanger/jm…

如果有帮助的话,还请给二哥点个赞,这将是我继续分享下去的最强动力!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Netty 源码分析系列(二)Netty 架构设计

发表于 2021-08-06

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

前言

上一篇文章,我们对 Netty做了一个基本的概述,知道什么是Netty以及Netty的简单应用。

Netty 源码分析系列(一)Netty 概述

本篇文章我们就来说说Netty的架构设计,解密高并发之道。学习一个框架之前,我们首先要弄懂它的设计原理,然后再进行深层次的分析。

接下来我们从三个方面来分析 Netty 的架构设计。

Selector 模型

Java NIO 是基于 Selector 模型来实现非阻塞的 I/O。Netty 底层是基于 Java NIO 实现的,因此也使用了 Selector 模型。

Selector 模型解决了传统的阻塞 I/O 编程一个客户端一个线程的问题。Selector 提供了一种机制,用于监视一个或多个 NIO 通道,并识别何时可以使用一个或多个 NIO 通道进行数据传输。这样,一个线程就可以管理多个通道,从而管理多个网络连接。

image-20210804231340262

Selector 提供了选择执行已经就绪的任务的能力。从底层来看,Selector 会轮询 Channel 是否已经准备好执行每个 I/O 操作。Selector 允许单线程处理多个 Channel 。Selector 是一种多路复用的技术。

SelectableChannel

并不是所有的 Channel 都是可以被 Selector 复用的,只有抽象类 SelectableChannel的子类才能被 Selector 复用。

例如,FileChannel 就不能被选择器复用,因为 FileChannel 不是SelectableChannel的子类。

为了与 Selector 一起使用,SelectableChannel必须首先通过register方法来注册此类的实例。此方法返回一个新的SelectionKey对象,该对象表示Channel已经在Selector进行了注册。向Selector注册后,Channel将保持注册状态,直到注销为止。

一个 Channel 最多可以使用任何一个特定的 Selector 注册一次,但是相同的 Channel 可以注册到多个 Selector 上。可以通过调用 isRegistered方法来确定是否向一个或多个 Selector 注册了 Channel。

SelectableChannel可以安全的供多个并发线程使用。

Channel 注册到 Selector

使用 SelectableChannel的register方法,可将Channel注册到Selector。方法接口源码如下:

1
2
3
4
5
6
7
java复制代码    public final SelectionKey register(Selector sel, int ops)
throws ClosedChannelException
{
return register(sel, ops, null);
}

public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;

其中各选项说明如下:

  • sel:指定 Channel 要注册的 Selector。
  • ops : 指定 Selector需要查询的通道的操作。

一个Channel在Selector注册其代表的是一个SelectionKey事件,SelectionKey的类型包括:

  • OP_READ:可读事件;值为:1<<0
  • OP_WRITE:可写事件;值为:1<<2
  • OP_CONNECT:客户端连接服务端的事件(tcp连接),一般为创建SocketChannel客户端channel;值为:1<<3
  • OP_ACCEPT:服务端接收客户端连接的事件,一般为创建ServerSocketChannel服务端channel;值为:1<<4

具体的注册代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码 // 1.创建通道管理器(Selector)
Selector selector = Selector.open();

// 2.创建通道ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 3.channel要注册到Selector上就必须是非阻塞的,所以FileChannel是不可以使用Selector的,因为FileChannel是阻塞的
serverSocketChannel.configureBlocking(false);

// 4.第二个参数指定了我们对 Channel 的什么类型的事件感兴趣
SelectionKey key = serverSocketChannel.register(selector , SelectionKey.OP_READ);

// 也可以使用或运算|来组合多个事件,例如
SelectionKey key = serverSocketChannel.register(selector , SelectionKey.OP_READ | SelectionKey.OP_WRITE);

值得注意的是:一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set。

SelectionKey

Channel 和 Selector 关系确定后之后,并且一旦 Channel 处于某种就绪状态,就可以被选择器查询到。这个工作再调用 Selector 的 select 方法完成。select 方法的作用,就是对感兴趣的通道操作进行就绪状态的查询。

1
2
java复制代码// 当注册事件到达时,方法返回,否则该方法会一直阻塞
selector.select();

SelectionKey 包含了 interest 集合,代表了所选择的感兴趣的事件集合。可以通过 SelectionKey 读写 interest 集合,例如:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 返回当前感兴趣的事件列表
int interestSet = key.interestOps();

// 也可通过interestSet判断其中包含的事件
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

// 可以通过interestOps(int ops)方法修改事件列表
key.interestOps(interestSet | SelectionKey.OP_WRITE);

可以看到,用位与操作 interest 集合和给定的 SelectionKey 常量,可以确定某个确定的事件是否在 interest 集合中。

SelectionKey 包含了ready集合。ready 集合是通道已经准备就绪的操作的集合。在一次选择之后,会首先访问这个 ready 集合。可以这样访问 ready 集合:

1
2
3
4
5
6
7
java复制代码int readySet = key.readyOps();

// 也可通过四个方法来分别判断不同事件是否就绪
key.isReadable(); //读事件是否就绪
key.isWritable(); //写事件是否就绪
key.isConnectable(); //客户端连接事件是否就绪
key.isAcceptable(); //服务端连接事件是否就绪

我们可以通过SelectionKey来获取当前的channel和selector

1
2
3
4
5
java复制代码//返回当前事件关联的通道,可转换的选项包括:`ServerSocketChannel`和`SocketChannel`
Channel channel = key.channel();

//返回当前事件所关联的Selector对象
Selector selector = key.selector();

可以将一个对象或者其他信息附着到 SelectionKey 上,这样就能方便地识别某个特定的通道。

1
2
java复制代码key.attach(theObject);
Object attachedObj = key.attachment();

还可以在用 register() 方法向 Selector 注册 Channel 的时候附加对象。

1
java复制代码SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

遍历 SelectionKey

一旦调用了 select 方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用 selector 的 selectedKey()方法,访问 SelectionKey 集合中的就绪通道,如下所示:

1
java复制代码Set<SelectionKey> selectionKeys = selector.selectedKeys();

可以遍历这个已选择的键集合来访问就绪的通道,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 获取监听事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 迭代处理
while (iterator.hasNext()) {
// 获取事件
SelectionKey key = iterator.next();
// 移除事件,避免重复处理
iterator.remove();
// 可连接
if (key.isAcceptable()) {
...
}
// 可读
if (key.isReadable()) {
...
}
//可写
if(key.isWritable()){
...
}
}

事件驱动

Netty是一款异步的事件驱动的网络应用程序框架。在 Netty 中,事件是指对某些操作感兴趣的事。例如,在某个Channel注册了 OP_READ,说明该 Channel 对读感兴趣,当 Channel 中有可读的数据时,它会得到一个事件的通知。

在 Netty 事件驱动模型中包括以下核心组件。

Channel

Channel(管道)是 Java NIO 的一个基本抽象,代表了一个连接到如硬件设备、文件、网络 socket 等实体的开放连接,或者是一个能够完成一种或多种不同的I/O 操作的程序。

回调

回调 就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者,Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个ChannelHandler接口处理。

例如:在上一篇文章中,Netty 开发的服务端的管道处理器代码中,当Channel中有可读的消息时,NettyServerHandler的回调方法channelRead就会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class NettyServerHandler extends ChannelInboundHandlerAdapter {

//读取数据实际(这里我们可以读取客户端发送的消息)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx =" + ctx);
Channel channel = ctx.channel();
//将 msg 转成一个 ByteBuf
//ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:" + channel.remoteAddress());
}


//处理异常, 一般是需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

Future

Future 可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问,Netty 提供了 ChannelFuture 用于在异步操作的时候使用,每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture(完全是异步和事件驱动的)。

以下是一个 ChannelFutureListener使用的示例。

1
2
3
4
5
6
7
8
9
10
java复制代码    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ChannelFuture future = ctx.channel().close();
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
//..
}
});
}

事件及处理器

在 Netty 中事件按照出/入站数据流进行分类:

入站数据或相关状态更改触发的事件包括:

  • 连接已被激活或者失活。
  • 数据读取。
  • 用户事件。
  • 错误事件,

出站事件是未来将会出发的某个动作的操作结果:

  • 打开或者关闭到远程节点的连接。
  • 将数据写或者冲刷到套接字。

每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。如下图展示了一个事件是如何被一个这样的ChannelHandler链所处理的。

image-20210805153230027

ChannelHandler 为处理器提供了基本的抽象,可理解为一种为了响应特定事件而被执行的回调。

责任链模式

责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它为请求创建了一个处理对象的链。其链中每一个节点都看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。

责任链模式的重点在这个 “链”上,由一条链去处理相似的请求,在链中决定谁来处理这个请求,并返回相应的结果。在Netty中,定义了ChannelPipeline接口用于对责任链的抽象。

责任链模式会定义一个抽象处理器(Handler)角色,该角色对请求进行抽象,并定义一个方法来设定和返回对下一个处理器的引用。在Netty中,定义了ChannelHandler接口承担该角色。

责任链模式的优缺点

优点:

  • 发送者不需要知道自己发送的这个请求到底会被哪个对象处理掉,实现了发送者和接受者的解耦。
  • 简化了发送者对象的设计。
  • 可以动态的添加节点和删除节点。

缺点:

  • 所有的请求都从链的头部开始遍历,对性能有损耗。
  • 不方便调试。由于该模式采用了类似递归的方式,调试的时候逻辑比较复杂。

使用场景:

  • 一个请求需要一系列的处理工作。
  • 业务流的处理,例如文件审批。
  • 对系统进行扩展补充。

ChannelPipeline

Netty 的ChannelPipeline设计,就采用了责任链设计模式, 底层采用双向链表的数据结构,,将链上的各个处理器串联起来。

客户端每一个请求的到来,Netty都认为,ChannelPipeline中的所有的处理器都有机会处理它,因此,对于入栈的请求,全部从头节点开始往后传播,一直传播到尾节点(来到尾节点的msg会被释放掉)。

入站事件:通常指 IO 线程生成了入站数据(通俗理解:从 socket 底层自己往上冒上来的事件都是入站)。
比如EventLoop收到selector的OP_READ事件,入站处理器调用socketChannel.read(ByteBuffer)接受到数据后,这将导致通道的ChannelPipeline中包含的下一个中的channelRead方法被调用。

出站事件:通常指 IO 线程执行实际的输出操作(通俗理解:想主动往 socket 底层操作的事件的都是出站)。
比如bind方法用意时请求server socket绑定到给定的SocketAddress,这将导致通道的ChannelPipeline中包含的下一个出站处理器中的bind方法被调用。

将事件传递给下一个处理器

处理器必须调用ChannelHandlerContext中的事件传播方法,将事件传递给下一个处理器。

入站事件和出站事件的传播方法如下图所示:

以下示例说明了事件传播通常是如何完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class MyInboundHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Connected!");
ctx.fireChannelActive();
}
}

public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {

@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
System.out.println("Closing...");
ctx.close(promise);
}
}

总结

正是由于 Netty 的分层架构设计非常合理,基于 Netty 的各种应用服务器和协议栈开发才能够如雨后春笋般得到快速发展。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring里面的Import注解到底是个啥?

发表于 2021-08-06

 Spring提供了几种方式来注册Bean,日常开发中使用最多的是ComponentScan。得益于ComponentScan,注册bean非常的简单,只需要在被注册的类上声明@Component或者@Service等注解即可。

 除了ComponentScan,Spring还支持使用Configuration注解来注册Bean。在大型的项目中,模块化开发能极大地降低系统的复杂性,这时需要每个模块来定义本模块Bean注册情况,Configuration发挥着巨大的作用。

1
2
3
4
5
java复制代码@Configuration
@ConditionalOnProperty(prefix = "module.wx-login", value = "enable", havingValue = "true")
@ComponentScan(basePackages = "com.lin.decorator.wxlogin")
public class WxLoginConfiguration {
}

 每个模块定义了Configuration之后,需要将多个模块的Configuration组合。Spring提供了Import注解来实现多个Configuration组合。

1
2
3
4
5
6
java复制代码@Import(WxLoginConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

 Spring官方文档中关于Import的描述如下:

Provides functionality equivalent to the <import/> element in Spring XML. Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes (as of 4.2; analogous to AnnotationConfigApplicationContext.register(java.lang.Class<?>...)). @Bean definitions declared in imported @Configuration classes should be accessed by using @Autowired injection. Either the bean itself can be autowired, or the configuration class instance declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly navigation between @Configuration class methods.

 除了Configuration,Import还支持引入ImportSelector和ImportBeanDefinitionRegistrar。既然要全面了解Import机制,那另外两个也要一探究竟。

ImportSelector

 Spring官方文档中,对ImportSelector的描述如下:
Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

 从字面上理解,ImportSelector可以根据注解里面的一个或多个属性来决定引入哪些Configuration。举个例子:

 小伙伴都用过Transactional注解,Transactional注解生效的前提是EnableTransactionManagement生效。看过EnableTransactionManagement源代码的小伙伴应该都知道,它通过Import引入了一个ImportSelector。

1
2
3
4
5
6
7
8
9
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}

 而TransactionManagementConfigurationSelector会根据注解里面的AdviceMode不同,来确定引入不同的Configuration。

1
2
3
4
5
6
7
8
9
10
11
java复制代码protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

ImportBeanDefinitionRegistrar

 Spring官方文档中,对ImportBeanDefinitionRegistrar的描述如下:
Interface to be implemented by types that register additional bean definitions when processing @Configuration classes. Useful when operating at the bean definition level (as opposed to @Bean method/instance level) is desired or necessary.

 字面意思是,通过继承这个接口可以额外定义Bean。举个例子:

 在使用Mybatis的时候,会使用到MapperScan这个注解,这个注解通过Import引入了ImportBeanDefinitionRegistrar,这也解释了为什么我们只在Interface上申明了一个Mapper,mybatis就帮我们生成好了Bean。

1
2
3
4
5
6
7
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}

 有小伙伴在编码过程中,并没有使用MapperScan,为什么也能正常使用呢?其实是Mybatis starter的功劳。在MybatisAutoConfiguration里面定义了ImportBeanDefinitionRegistrar,当MapperScan没有激活时,它就会生效。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
@Override
public void afterPropertiesSet() {
logger.debug(
"Not found configuration for registering mapper bean using @MapperScan," +
"MapperFactoryBean and MapperScannerConfigurer.");
}
}

Import执行流程

 了解了Import支持的三种不同类型的资源之后,接下来debug看一下import的执行过程。通过设置断点,发现在ConfigurationClassParser类中,通过深度遍历来处理Import。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

 而上面介绍的ImportSelector,需要调用selectImports方法进行解析。

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
java复制代码private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {

if (importCandidates.isEmpty()) {
return;
}

if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
1
复制代码这样递归调用,就实现了资源的加载。
1
复制代码

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【排序】堆排序

发表于 2021-08-05

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

介绍

堆结构就是用数组实现的完全二叉树结构,也叫做优先级队列结构,堆排序是利用堆这种数据结构而设计的一种排序算法, 堆排序是一种选择排序, 它的最坏, 最好平均时间复杂度均为 O(nlogn), 它也是不稳定排序。

堆是具有以下性质的完全二叉树: 每个结点的值都大于或等于其左右孩子结点的值, 称为大顶堆(注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系,)每个结点的值都小于或等于其左右孩子结点的值, 称为小顶堆。

image.png

思路

1.首先我们来看构建大顶堆的两个主要方法:

heapInsert: 向堆中加入元素,先加在末尾,然后通过比较其与父节点之间的大小关系不断向上交换。直到子节点的值小于父节点。
heapify: 将以index位置为根节点的树调整为大根堆的形式
注意: 以上方法也可以构建小根堆,只是条件稍微改变一下

2.通过heapInsert首先将数组构成一个大根堆

3.整个序列的最大值就是堆顶的根节点。

4.将其与末尾元素进行交换, 此时末尾就为最大值

5.然后将剩余 n-1 个元素通过heapify重新构造成一个堆, 这样会得到 n 个元素的次小值。 如此反复执行, 便能得到一个有序序列了

代码

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
java复制代码   public void heapSort(int[] arr) {
//首先我们假设依次把数组中的数加入堆中,以此方法来构建堆结构
for (int i = 1; i < arr.length; i++) {
heapInsert(arr, i);
}
// for (int i = arr.length - 1; i >= 0; i++) { //这种调整成堆的时间复杂度O(N)
// heapify(arr, i, arr.length);
// }
for (int i = arr.length - 1; i >= 0; i--) { //这个是O(N * logN)级别的
swap(arr, i, 0);
heapify(arr, 0, i);
}
}

public void heapInsert(int[] arr, int index) {
//在数组尾部插入
//和父节点比较,一步一步向上走
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}

//size表示当前堆的大小
public void heapify(int[] arr, int index, int size) { //这个是调整成大根堆结构的过程
//index表示从哪个下标开始调整
int left = index * 2 + 1;
while (left < size) { //表示存在左子节点
int largestIndex = index * 2 + 2 < size && arr[left + 1] > arr[left] ? left + 1 : left;
largestIndex = arr[largestIndex] > arr[index] ? largestIndex : index;
if (largestIndex == index) break;
swap(arr, largestIndex, index);
index = largestIndex; //需要调整的节点移动到被交换值的子节点的位置
left = index * 2 + 1;//这个时候left就是子节点的left了
}
}

public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

堆排序扩展题目

已知一个几乎有序的数组, 几乎有序是指, 如果把数组排好顺序的话, 每个元素移动的距离可以不超过k, 并且k相对于数组来说比较小。 请选择一个合适的排序算法针对这个数据进行排序。

思路

1.可以使用一个容量为k+1个小根堆进行操作,

2.将数组中索引位置为i到i+k上面的k+1个元素加入到小根堆中,将堆顶的最小值放到i位置上

3.此时<=i位置的元素已经排序好了然后把k+2上面的元素加入到小根堆中,继续重复操作

4.添加到arr.length-1位置元素的时候把直接把堆中元素弹出放到对应位置即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public void sortArrayDistanceLessK(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
int i = 0;
for (; i < k; i++) { //先向小根堆中添加k个数
heap.add(arr[i]);
}
for (; i < arr.length; i++) {
heap.add(arr[i]);
arr[i - k] = heap.poll();

}
while (!heap.isEmpty()) {
arr[i - k] = heap.poll();
i++;
}

}

时间复杂度

O(N * logN)
headInsert和heapify的时间复杂度都是logN

1
2
3
4
java复制代码        for (int i = arr.length - 1; i >= 0; i--) { //这个是O(N * logN)级别的
swap(arr, i, 0);
heapify(arr, 0, i);
}

for循环遍历一遍元素然后heapify的时间复杂度为logN所以堆排序的时间复杂度为O(N * logN)

额外空间复杂度

O(1)
只用到有限几个变量

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dubbo 30 DubboService 的扫描

发表于 2021-08-05

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

先看总结更高效!!!🎁🎁🎁

这一篇来看一看 @DubboService 标注的类是如何扫描和使用的 , 扫描逻辑如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 之前我们就了解到 , Dubbo 通过 PostProcessor 在初始化中做二次处理 , Dubbo 中主要有以下 PostProcessor

// Dubbo 自动配置类
C- DubboAutoConfiguration

// 使用AbstractConfig#getId()设置Dubbo Config bean别名的后处理器类
C- DubboConfigAliasPostProcessor

// 一个BeanFactoryPostProcessor,用于处理java配置类中的@Service注释类和注释bean
// 它也是dubbbo:annotation上的XML BeanDefinitionParser的基础结构类
C- ServiceAnnotationPostProcessor

// 注册一些不存在的基础结构bean
C- DubboInfraBeanRegisterPostProcessor

二 . DubboServicer 加载

2.1 基础案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@DubboService
public class DemoServiceImpl implements DemoService {
private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);

@Override
public String sayHello(String name) {
logger.info("Hello " + name + ", request from consumer: " + RpcContext.getServiceContext().getRemoteAddress());
return "Hello " + name + ", response from provider: " + RpcContext.getServiceContext().getLocalAddress();
}

@Override
public CompletableFuture<String> sayHelloAsync(String name) {
return null;
}

}

2.2 扫描的入口

Dubbo 的扫描同样是通过 BeanPostProcess 进行处理的 , 主要处理类为 ServiceAnnotationPostProcessor ,
在 SpringBoot 加载的时候 , 通过 refresh 的 invokeBeanFactoryPostProcessors 逻辑发起处理

1
2
3
4
5
6
7
8
9
10
11
12
13
JAVA复制代码// 发起扫描  C- ServiceAnnotationPostProcessor
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
this.registry = registry;

// Step 1 : 获得扫描的 Package 路径
Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
// Step 2 : 扫描 路径下的 Service
scanServiceBeans(resolvedPackagesToScan, registry);
} else {
}
}

补充 : postProcessBeanDefinitionRegistry 方法

postProcessBeanDefinitionRegistry 方法是 BeanDefinitionRegistryPostProcessor 接口的唯一方法 , 该方法可以通过 BeanDefinitionRegistry 自定义的注册 Bean .

2.3 Bean 的扫描 (ServiceAnnotationPostProcessor)

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
java复制代码// Step 2-1 : 扫描 Bean
private void scanServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

// PRO0001
DubboClassPathBeanDefinitionScanner scanner =
new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

// PRO0002
BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);
scanner.setBeanNameGenerator(beanNameGenerator);

// PRO0003
for (Class<? extends Annotation> annotationType : serviceAnnotationTypes) {
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
}

ScanExcludeFilter scanExcludeFilter = new ScanExcludeFilter();
scanner.addExcludeFilter(scanExcludeFilter);

for (String packageToScan : packagesToScan) {

// 避免重复扫描 -> PRO0004
if (servicePackagesHolder.isPackageScanned(packageToScan)) {
continue;
}

// 扫描所有的 @Service 注解 (Spring) ?
scanner.scan(packageToScan);

// Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or not.
Set<BeanDefinitionHolder> beanDefinitionHolders =
findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {
// 此处的 serviceClasses 主要是为了 log 处理 , 打印详细的 Service
// PS : 意味着此处可以通过配置该属性打印详细的 Service 类
if (logger.isInfoEnabled()) {
List<String> serviceClasses = new ArrayList<>(beanDefinitionHolders.size());
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
serviceClasses.add(beanDefinitionHolder.getBeanDefinition().getBeanClassName());
}
}

for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
// -> 2.4 BeanDefinition 的处理
processScannedBeanDefinition(beanDefinitionHolder, registry, scanner);
// 添加以及加载的 Bean
servicePackagesHolder.addScannedClass(beanDefinitionHolder.getBeanDefinition().getBeanClassName());
}
} else {

}
// 添加扫描的 package
servicePackagesHolder.addScannedPackage(packageToScan);
}
}

PRO0001 补充一 : DubboClassPathBeanDefinitionScanner 的占用

1
2
3
4
5
6
7
java复制代码
核心 : DubboClassPathBeanDefinitionScanner 继承于 ClassPathBeanDefinitionScanner , 在这个基础上添加了属性 :
private final ConcurrentMap<String, Set<BeanDefinition>> beanDefinitionMap = new ConcurrentHashMap<>();

在调用父类 findCandidateComponents 的时候 , 会缓存在该类中

目的 :

PRO0002 补充二 : resolveBeanNameGenerator 的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码String CONFIGURATION_BEAN_NAME_GENERATOR = "org.springframework.context.annotation.internalConfigurationBeanNameGenerator"
private BeanNameGenerator resolveBeanNameGenerator(BeanDefinitionRegistry registry) {

BeanNameGenerator beanNameGenerator = null;

if (registry instanceof SingletonBeanRegistry) {
SingletonBeanRegistry singletonBeanRegistry = SingletonBeanRegistry.class.cast(registry);
beanNameGenerator = (BeanNameGenerator) singletonBeanRegistry.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
}

if (beanNameGenerator == null) {
beanNameGenerator = new AnnotationBeanNameGenerator();
}
return beanNameGenerator;

}

PRO0003 补充三 : AnnotationTypeFilter 处理了哪些 ?

1
2
3
4
5
6
7
8
9
10
java复制代码private final static List<Class<? extends Annotation>> serviceAnnotationTypes = asList(
// @since 2.7.7 Add the @DubboService , the issue : https://github.com/apache/dubbo/issues/6007
DubboService.class,
// @since 2.7.0 the substitute @com.alibaba.dubbo.config.annotation.Service
Service.class,
// @since 2.7.3 Add the compatibility for legacy Dubbo's @Service , the issue : https://github.com/apache/dubbo/issues/4330
com.alibaba.dubbo.config.annotation.Service.class
);

这里主要是 DubboService , 下方的 2 个 Service 官方标注已过时

补充四 : ServicePackagesHolder 的作用

1
2
3
4
5
6
7
8
java复制代码public class ServicePackagesHolder {
// 可以看到 , 其中主要是2个set 集合用于保存已经加载的 class 和 package
public static final String BEAN_NAME = "dubboServicePackagesHolder";
private final Set<String> scannedPackages = new HashSet<>();
private final Set<String> scannedClasses = new HashSet<>();


}

补充五 : findServiceBeanDefinitionHolders 处理 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
private Set<BeanDefinitionHolder> findServiceBeanDefinitionHolders(
ClassPathBeanDefinitionScanner scanner, String packageToScan, BeanDefinitionRegistry registry,
BeanNameGenerator beanNameGenerator) {
// 扫描 Bean , 此处实际上是依赖了 Spring 的体系
Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(packageToScan);

Set<BeanDefinitionHolder> beanDefinitionHolders = new LinkedHashSet<>(beanDefinitions.size());
// 所有的Service 类 , 此处的类型 org.springframework.context.annotation.ScannedGenericBeanDefinition
for (BeanDefinition beanDefinition : beanDefinitions) {

String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
BeanDefinitionHolder beanDefinitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
beanDefinitionHolders.add(beanDefinitionHolder);

}

return beanDefinitionHolders;

}

2.4 BeanDefinition 的处理

前面会对 BeanDefinitionHolder 进行Bean 处理 ,之前已经了解到这个对象中有2个set 集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// BeanDefinition 加载主流程 C- ServiceAnnotationPostProcessor
private void processScannedBeanDefinition(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
DubboClassPathBeanDefinitionScanner scanner) {

Class<?> beanClass = resolveClass(beanDefinitionHolder);

// 获取 Bean 的注解及属性
Annotation service = findServiceAnnotation(beanClass);
Map<String, Object> serviceAnnotationAttributes = AnnotationUtils.getAttributes(service, true);

// 返回接口类
Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);

String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

// ServiceBean Bean name
String beanName = generateServiceBeanName(serviceAnnotationAttributes, interfaceClass);

AbstractBeanDefinition serviceBeanDefinition =
buildServiceBeanDefinition(serviceAnnotationAttributes, interfaceClass, annotatedServiceBeanName);

registerServiceBeanDefinition(beanName, serviceBeanDefinition, interfaceClass);

}

Step 1 : 解析接口名

这里分别会从三个属性中尝试获取 : interfaceName / interfaceClass / Class 上面获取

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
Java复制代码// 从@Service注释属性解析服务接口名称
// 注意:如果是通用服务,服务接口类可能在本地找不到
public static String resolveInterfaceName(Map<String, Object> attributes, Class<?> defaultInterfaceClass) {
Boolean generic = getAttribute(attributes, "generic");
// 1. get from DubboService.interfaceName()
String interfaceClassName = getAttribute(attributes, "interfaceName");
if (StringUtils.hasText(interfaceClassName)) {
if (GenericService.class.getName().equals(interfaceClassName) ||
com.alibaba.dubbo.rpc.service.GenericService.class.getName().equals(interfaceClassName)) {
throw new IllegalStateException...;
}
return interfaceClassName;
}

// 2. get from DubboService.interfaceClass()
Class<?> interfaceClass = getAttribute(attributes, "interfaceClass");
if (interfaceClass == null || void.class.equals(interfaceClass)) { // default or set void.class for purpose.
interfaceClass = null;
} else if (GenericService.class.isAssignableFrom(interfaceClass)) {
throw new IllegalStateException....;
}

// 3. get from annotation element type, ignore GenericService
if (interfaceClass == null && defaultInterfaceClass != null && !GenericService.class.isAssignableFrom(defaultInterfaceClass)) {
// 此处拿到的为 interface org.apache.dubbo.demo.DemoService
Class<?>[] allInterfaces = getAllInterfacesForClass(defaultInterfaceClass);
if (allInterfaces.length > 0) {
interfaceClass = allInterfaces[0];
}
}

return interfaceClass.getName();
}

Step 2 : 构建 buildServiceBeanDefinition

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
JAVA复制代码// C- ServiceAnnotationPostProcessor
private AbstractBeanDefinition buildServiceBeanDefinition(Map<String, Object> serviceAnnotationAttributes,
Class<?> interfaceClass,
String refServiceBeanName) {

BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBean.class);

AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();

String[] ignoreAttributeNames = of("provider", "monitor", "application", "module", "registry", "protocol",
"interface", "interfaceName", "parameters");

propertyValues.addPropertyValues(new AnnotationPropertyValuesAdapter(serviceAnnotationAttributes, environment, ignoreAttributeNames));

//set config id, for ConfigManager cache key
//builder.addPropertyValue("id", beanName);
// References "ref" property to annotated-@Service Bean
addPropertyReference(builder, "ref", refServiceBeanName);
// Set interface
builder.addPropertyValue("interface", interfaceClass.getName());
// Convert parameters into map
builder.addPropertyValue("parameters", convertParameters((String[]) serviceAnnotationAttributes.get("parameters")));
// Add methods parameters
List<MethodConfig> methodConfigs = convertMethodConfigs(serviceAnnotationAttributes.get("methods"));
if (!methodConfigs.isEmpty()) {
builder.addPropertyValue("methods", methodConfigs);
}

// convert provider to providerIds
String providerConfigId = (String) serviceAnnotationAttributes.get("provider");
if (StringUtils.hasText(providerConfigId)) {
addPropertyValue(builder, "providerIds", providerConfigId);
}

// Convert registry[] to registryIds
String[] registryConfigIds = (String[]) serviceAnnotationAttributes.get("registry");
if (registryConfigIds != null && registryConfigIds.length > 0) {
resolveStringArray(registryConfigIds);
builder.addPropertyValue("registryIds", StringUtils.join(registryConfigIds, ','));
}

// Convert protocol[] to protocolIds
String[] protocolConfigIds = (String[]) serviceAnnotationAttributes.get("protocol");
if (protocolConfigIds != null && protocolConfigIds.length > 0) {
resolveStringArray(protocolConfigIds);
builder.addPropertyValue("protocolIds", StringUtils.join(protocolConfigIds, ','));
}

// TODO Could we ignore these attributes: applicatin/monitor/module ? Use global config
// monitor reference
String monitorConfigId = (String) serviceAnnotationAttributes.get("monitor");
if (StringUtils.hasText(monitorConfigId)) {
addPropertyReference(builder, "monitor", monitorConfigId);
}

// application reference
String applicationConfigId = (String) serviceAnnotationAttributes.get("application");
if (StringUtils.hasText(applicationConfigId)) {
addPropertyReference(builder, "application", applicationConfigId);
}

// module reference
String moduleConfigId = (String) serviceAnnotationAttributes.get("module");
if (StringUtils.hasText(moduleConfigId)) {
addPropertyReference(builder, "module", moduleConfigId);
}

return builder.getBeanDefinition();

}

// 这一步主要是属性的处理 , 没什么看的

Step 3 : 注册 Bean

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
JAVA复制代码// C- ServiceAnnotationPostProcessor
private void registerServiceBeanDefinition(String serviceBeanName, AbstractBeanDefinition serviceBeanDefinition, Class<?> interfaceClass) {
// check service bean
if (registry.containsBeanDefinition(serviceBeanName)) {
BeanDefinition existingDefinition = registry.getBeanDefinition(serviceBeanName);
if (existingDefinition.equals(serviceBeanDefinition)) {
// exist equipment bean definition
return;
}

// ..... 省略异常抛出
throw new BeanDefinitionStoreException(....)
}

registry.registerBeanDefinition(serviceBeanName, serviceBeanDefinition);

}


// [PRO] : generateServiceBeanName 的简述
private String generateServiceBeanName(Map<String, Object> serviceAnnotationAttributes, Class<?> interfaceClass) {
ServiceBeanNameBuilder builder = create(interfaceClass, environment)
.group((String) serviceAnnotationAttributes.get("group"))
.version((String) serviceAnnotationAttributes.get("version"));
return builder.build();
}

// PS : 注意 ,此处创建的名称是 ServiceBean:org.apache.dubbo.demo.DemoService
// 这个很重要 , 后面会使用

三 . BeanDefinition 的使用

上文对 BeanDefinition 进行了扫描 , 这一轮来简单看一下这些扫描构建的对象是怎么进行注册的

上文在 generateServiceBeanName 时生成了一个 ServiceBean:org.apache.dubbo.demo.DemoService 的 Bean , 这个 Bean 是非常重要的 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 在 AbstractApplicationContext # refresh 中 , 
public void refresh() throws BeansException, IllegalStateException {
// Bean 的扫描逻辑
invokeBeanFactoryPostProcessors(beanFactory);

//...... 中间处理

// Bean 的注册逻辑
finishBeanFactoryInitialization(beanFactory);

}

// Step 1 : 实例化所有的 Bean
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();

// Step 2 : 获取 Bean
for (String beanName : beanNames) {
//...
getBean(beanName)
}

// Step 3: 使用 -> 下一篇看看

此处可以观察到 , Bean 已经在里面了

image.png

总结

啥也不说了 , 看图

Dubbo-DubboService.jpg
简单来说就是扫描Bean后 ,生成特殊的Bean融入容器体系中 , 后续通过 Spring 来加载

看了这么多框架 , Dubbo 对 Spring 的使用已经达到很深的底层了, 就好像看 Spring 源码一样 .

这才叫用框架呀~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 8 函数式接口javautilfunction

发表于 2021-08-05

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

前言

  Java 8中有很多新实用的特性,其中就有函数式接口.相信很多小伙伴都是第一次听到这个名词,下面进行函数式接口相关知识的学习。

函数式接口概念

  函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
Java 8函数式接口可以对现有的函数友好地支持 lambda。

函数式接口组成

包含三部分:

  • 1、一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
  • 2、一个箭头符号:->
  • 3、方法体,可以是表达式和代码块。
1
rust复制代码(parameters) -> expression 或者 (parameters) -> { statements; }

java.util.function 的函数式接口

接口 描述
BiConsumer<T,U> 代表了一个接受两个输入参数的操作,并且不返回任何结果
BiFunction<T,U,R> 代表了一个接受两个输入参数的方法,并且返回一个结果
BinaryOperator 代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果
BiPredicate<T,U> 代表了一个两个参数的boolean值方法
BooleanSupplier 代表了boolean值结果的提供方
Consumer 代表了接受一个输入参数并且无返回的操作
DoubleBinaryOperator 代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。
DoubleConsumer 代表一个接受double值参数的操作,并且不返回结果。
DoubleFunction 代表接受一个double值参数的方法,并且返回结果
DoublePredicate 代表一个拥有double值参数的boolean值方法
DoubleSupplier 代表一个double值结构的提供方
DoubleToIntFunction 接受一个double类型输入,返回一个int类型结果。
DoubleToLongFunction 接受一个double类型输入,返回一个long类型结果
DoubleUnaryOperator 接受一个参数同为类型double,返回值类型也为double 。
Function<T,R> 接受一个输入参数,返回一个结果。
IntBinaryOperator 接受两个参数同为类型int,返回值类型也为int 。
IntConsumer 接受一个int类型的输入参数,无返回值 。
IntFunction 接受一个int类型输入参数,返回一个结果 。
IntPredicate 接受一个int输入参数,返回一个布尔值的结果。
IntSupplier 无参数,返回一个int类型结果。
IntToDoubleFunction 接受一个int类型输入,返回一个double类型结果 。
IntToLongFunction 接受一个int类型输入,返回一个long类型结果。
IntUnaryOperator 接受一个参数同为类型int,返回值类型也为int 。
LongBinaryOperator 接受两个参数同为类型long,返回值类型也为long。
LongConsumer 接受一个long类型的输入参数,无返回值。
LongFunction 接受一个long类型输入参数,返回一个结果。
LongPredicate R接受一个long输入参数,返回一个布尔值类型结果。
LongSupplier 无参数,返回一个结果long类型的值。
LongToDoubleFunction 接受一个long类型输入,返回一个double类型结果。
LongToIntFunction 接受一个long类型输入,返回一个int类型结果。
LongUnaryOperator 接受一个参数同为类型long,返回值类型也为long。
ObjDoubleConsumer 接受一个object类型和一个double类型的输入参数,无返回值。
ObjIntConsumer 接受一个object类型和一个int类型的输入参数,无返回值。
ObjLongConsumer 接受一个object类型和一个long类型的输入参数,无返回值。
Predicate 接受一个输入参数,返回一个布尔值结果。
Supplier 无参数,返回一个结果。
ToDoubleBiFunction<T,U> 接受两个输入参数,返回一个double类型结果
ToDoubleFunction 接受一个输入参数,返回一个double类型结果
ToIntBiFunction<T,U> 接受两个输入参数,返回一个int类型结果。
ToIntFunction 接受一个输入参数,返回一个int类型结果。
ToLongBiFunction<T,U> 接受两个输入参数,返回一个long类型结果。
ToLongFunction 接受一个输入参数,返回一个long类型结果。
UnaryOperator 接受一个参数为类型T,返回值类型也为T。

结语

  java.util.function很多类是包含函数式接口的,函数式接口是一个比较抽象的概念,可能刚刚接触或者了解感到无从下手,多编程练习,先会使用,慢慢就了解到了函数式接口的内涵与优点。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于JavaSwing ATM取款机系统的设计和实现

发表于 2021-08-05

这是我参与 8 月更文挑战的第 5 天,活动详情查看: [8月更文挑战]
​
)​

​

前言:

本项目是使用Java swing开发,可实现ATM系统/银行系统的基本登陆、转账、查询余额、存取款业务。界面设计比较简介、适合作为Java课设设计以及学习技术使用。

需求分析:

随着生活水平的提高,消费量的增大,开销也越来越大,自然离不开的就是钱。人们有的要取钱,有的要存钱,可是只能去银行,而银行的遍布并不是很广,它可能在人流密集度比较大的地方会设立,或者稍大范围内设立一个,但是对于比较偏远地区的人们,无疑造成了非常大的困难。那么,如何来解决这个问题那?研发ATM柜员机即为广大用户提供了便捷,改善了生活。您无需再到银行排队办理一些简单的业务, ATM柜员机为您提供取款,存款,余额查询,修改密码等功能操作。而且ATM的遍及范围远远大于银行,主要是ATM的自身功能容易实现日容易布局,不需要耗费大量的空间,人力及物力,可以在很多点来设立。也正是在这种情况下, ATM柜员机得到了人们的喜爱并得到了大量的普及,可以说对银行和人们都非常有益的。本系统通过设计与开发Windows系统,主要完成了余额查询功能,取款功能,存款功能,转账功能,退出系统功能,目的在于通过 ATM自动存取款 机实现一些简单的动能。

主要模块:

用户登录、注册、重置、存款、查询余额、取款、转账、更改密码、退卡等具体功能

功能截图:

登录注册:

运行程序启动mian方法进入登录页面

)​

首页

)​

存款

存入输入的金额点击确认完成存款、存款的时候输入的必须是整数

)​

查询余额

查询自己的余额以及操作记录信息

)​

取款

取款金额不能大于账户余额

)​

转账

转账的时候必须正确输入用户id信息、否则转款失败

)​

更改密码

输入原密码进行校验后、输入2次相同的新密码完成修改密码功能

)​

数据库设计:

这个ATM暂时没用数据库、是以文本txt的形式进行存储数据、更方便快捷简单话

部分关键代码:

主启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码  public static void main(String[] args)throws Exception {  
usersList = new ArrayList<Account>();
//System.out.println(usersList);
/**********************用户文本**********************/
File users = new File("users.txt");
if (!users.exists()) {
try {
users.createNewFile();
Writer fw = new FileWriter("users.txt");
fw.write("123456 123456 10000");
fw.flush();
fw.close();
} catch (Exception e1) {
JOptionPane.showMessageDialog(null, "创建用户文档失败");
}

}
usersFile = users;//创建用户文档,存储用户账户,密码,余额信息;
usersListRead();
usersListUpdate();
/*****************************Login****************************/

LoginGui loginGui = new LoginGui();
}

账号相关:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
java复制代码package atm;
//import com.sun.deploy.util.SyncFileAccess;
//import com.sun.org.apache.regexp.internal.RE;

import javax.swing.*;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class Account {
int money;
String id;//账号名

String password;
Date now=new Date();
Date currentTime;
SimpleDateFormat formatter;
Reader fr;
;
public Account(String id, String password, String money) {//构造方法
this.id = id;

this.password = password;
this.money=Integer.parseInt(money);
}

public void outMoney (int money)throws Exception {//抛出异常,由相关的界面类弹窗处理异常,下面几个方法同理
//如在取钱界面取钱,则会调用此函数,进行try/catch处理,获得这个函数的异常,弹窗说明异常
if (money > this.money) {
throw new Exception("余额不足");
}
if(money<0)
{
throw new Exception("不能取出负数");
}
formatter = new SimpleDateFormat("yy-MM-dd HH:mm:ss");//时间格式
currentTime = new Date();//当前时间
String dateString = formatter.format(currentTime);//处理当前时间格式
Writer fw = new FileWriter(Test.file);
fw.write(Test.recordString.append(dateString + "\t" + Test.currentAccount.id + "\t取出" + money + "元\r\n").toString());//将这次的取钱行为添加到记录文件中
fw.flush();//写进文件
fw.close();
this.money -= money;
Test.usersListUpdate();//更新用户文档(信息)
}

public void inMoney(int money)throws Exception
{
try {
Writer fw = new FileWriter(Test.file);
// System.out.println(Test.file);
formatter = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
currentTime=new Date();
String dateString=formatter.format(currentTime);
fw.write(Test.recordString.append(dateString+"\t"+Test.currentAccount.id+"\t存入" + money + "元\r\n").toString());
fw.flush();//写进文件
fw.close();

this.money+=money;

Test.usersListUpdate();//更新当前用户信息

}
catch (Exception e1)
{
throw new Exception("写入记录失败");
}

}

public void transfer(int money,String id)throws Exception//转账
{
if(id.equals(Test.currentAccount.id))
{
throw new Exception("不能转给自己");
}
if(money>this.money)
{
throw new Exception("余额不足");
}
if(money<0) {
throw new Exception("不能转入负数");
}


for(int i=0;i<Test.usersList.size();i++)
{
if(Test.usersList.get(i).id.equals(id))//找到要转帐的用户
{
Test.usersList.get(i).money+=money;//转入
this.money-=money;//扣钱

FileWriter fw=new FileWriter(Test.file);
formatter = new SimpleDateFormat("yy-MM-dd HH:mm:ss");//声明时间格式
currentTime=new Date();//获取当前时间
String dateString=formatter.format(currentTime);//转换时间格式
fw.write(Test.recordString.append(dateString+"\t向"+id+"\t转出"+money+"元\r\n").toString());//Test类中的静态字符串拼接上这个字符串覆盖写入当前用户文档
fw.close();

/********************向转入目标写入转账信息*************************/
try {
fr = new FileReader(id+".txt");//字符流
}
catch (Exception e)
{
System.out.println("字符流创建失败");
}

BufferedReader bfr = new BufferedReader(fr);

String temp="";
String temp1;

while ((temp1 = bfr.readLine()) != null)
{
temp+=temp1;
}
temp=temp.replace("元","元\n\r")+dateString+"\t由"+Test.currentAccount.id+"\t转进"+money+"元\r\n";
System.out.println(temp);
fw=new FileWriter(id+".txt");
fw.write(temp);
fw.close();


JOptionPane.showMessageDialog(null,"转账成功");
Test.usersListUpdate();//更新用户文档

return;
}
}
throw new Exception("目标用户不存在");
}

public void ChangePassword(String newPassword)throws Exception
{
if(newPassword.equals(this.password))
{
throw new Exception("原密码和新密码不能一样");
}

else
{
if(newPassword.equals(""))
{
throw new Exception("密码不能为空");
}

}
password=newPassword;
Test.usersListUpdate();


}



}

修改密码:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
scss复制代码package atm;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ChangePassword implements ActionListener{
public JPasswordField oldPassword,newPassword,checkPassword;
public JFrame iframe;
public JPanel ip0,ip1,ip2,ip3,ip4;
public JButton confirm,cancel,exit;
public ChangePassword() {
iframe=new JFrame("更改密码");
iframe.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

ip2=new JPanel();
ip2.add(new JLabel("原密码:"));
oldPassword=new JPasswordField(20);
ip2.add(new JLabel("<html><br/><html>"));//换行
ip2.add(oldPassword);

ip0=new JPanel();
ip0.add(new JLabel("新密码:"));
newPassword=new JPasswordField(20);
ip0.add(new JLabel("<html><br/><html>"));//换行
ip0.add(newPassword);

ip4=new JPanel();
ip4.add(new JLabel("再次输入新密码:"));
checkPassword=new JPasswordField(20);
ip4.add(new JLabel("<html><br/><html>"));//换行
ip4.add(checkPassword);

ip3=new JPanel();
confirm=new JButton("确定");
ip3.add(confirm);
cancel=new JButton("返回");
ip3.add(cancel);

iframe.add(ip2);
iframe.add(ip0);
iframe.add(ip4);
iframe.add(ip3);
iframe.add(confirm);
iframe.add(cancel);
iframe.setLayout(new FlowLayout());
iframe.setVisible(true);
iframe.setTitle("密码更改");//窗体标签
iframe.setSize(400, 200);//窗体大小
iframe.setLocationRelativeTo(null);//在屏幕中间显示(居中显示)
confirm.addActionListener(this);

cancel.addActionListener(this);

}
public void pw_clean() {
newPassword.setText("");
oldPassword.setText("");
checkPassword.setText("");
}

@Override
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("确定")) {
if (Test.currentAccount.password.equals(oldPassword.getText())) {
try {
if(newPassword.getText().equals(checkPassword.getText())) {
if(newPassword.getText().length()>=6) {
Test.currentAccount.ChangePassword(newPassword.getText());
JOptionPane.showMessageDialog(null, "更改密码成功");
iframe.setVisible(false);
Test.menu.mframe.setVisible(false);//关闭菜单界面
new LoginGui();
}else {
JOptionPane.showMessageDialog(null,"密码不能少于6位!\n请重新输入","提示消息",JOptionPane.ERROR_MESSAGE);
pw_clean();
}
}
else
{
JOptionPane.showMessageDialog(null, "两次输入的密码不一致");
pw_clean();
}
}
catch (Exception e1) {//捕获账户类中更改密码函数的异常并弹窗显示
JOptionPane.showMessageDialog(null, e1.getMessage());
pw_clean();
}
} else {

JOptionPane.showMessageDialog(null, "原密码错误");
pw_clean();
}
}
else//如果点击cancel
{
iframe.setVisible(false);
}
}
}

文档结构图:

)​

总结:

在本次课程设计中我主要负责登陆界面部分和界面优化。通过这次课程设计。我学到了许多令我受益匪浅的知识。感觉java的界面设计和 mfc差不多。只是java没有可视化的界面做起来太累了。其他主要是类和对象的问题。实现起来还是挺简单的。

大家可以点赞、收藏、关注、评论我啦

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring使用WebSocket、SockJS、STOMP

发表于 2021-08-05

WebSocket

概述

WebSocket协议提供了通过一个套接字实现全双工通信的功能。除了其他的功能之外,它能够实现Web浏览器和服务器之间的异步通信。全双工意味着服务器可以发送消息给浏览器,浏览器也可以发送消息给服务器。

使用Spring的低层级WebSocketAPI

按照其最简单的形式,WebSocket只是两个应用之间通信的通道。位于WebSocket一端的应用发送消息,另一端接收消息。因为它是全双工的,所以每一端都可以发送和处理消息。
这里写图片描述
WebSocket通信可以应用于任何类型的应用中,但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。
编写简单的WebSocket样例(基于JavaScript的客户端与服务器的一个无休止的“Marco Polo”游戏)

为了在Spring使用较底层级的API来处理消息,我们必须编写一个实现WebSocketHandler的类。
WebSocketHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public interface WebSocketHandler {


void afterConnectionEstablished(WebSocketSession session) throws Exception;


void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;


void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;


void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;


boolean supportsPartialMessages();

}

不过更为简单的方法是扩展AbstractWebSocketHandler,这是WebSocketHandler的一个抽象实现。
MarcoHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class MarcoHandler extends AbstractWebSocketHandler {

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("Received message: " + message.getPayload());
Thread.sleep(2000);
session.sendMessage(new TextMessage("Polo!"));
}

@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("Connection established!");
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
System.out.println("Connection closed. Status: " + status);
}

尽管AbstractWebSocketHandler是一个抽象类,但是它并不要求我们必须重载任何特定的方法。相反,它让我们来决定该重载哪一个方法。除了重载WebSocketHandler中定义的五个方法以外,我们还可以重载AbstractWebSocketHandler中所定义的三个方法:

  • handleBinaryMessage()
  • handlePongMessage()
  • handleTextMessage()

这三个方法只是handleMessage()方法的具体化,每个方法对应于某一种特定类型的消息。
所以没有重载的方法都由AbstractWebSocketHandler以空操作的方式进行。这意味着MarcoHandler也能处理二进制和pong消息,只是对这些消息不进行任何操作而已。

另外一种方案我们可以扩展TextWebSocketHandler,TextWebSocketHandler是AbstractWebSocketHandler的子类,它会拒绝处理二进制消息。它重载了handleBinaryMessage()方法,如果收到二进制消息,将会关闭WebSocket连接。与之类似,BinaryWebSocketHandler也是AbstractWebSocketHandler的子类,它重载了handleTextMessage()方法,如果收到文本消息的话,将会关闭连接。

1
2
3
4
5
6
7
java复制代码public class MarcoHandler extends TextWebSocketHandler {
...
}

public class MarcoHandler extends BinaryWebSocketHandler{
...
}

WebSocketConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(marcoHandler(), "/marco"); //注册信息管理器,将MarcoHandler映射到"/marco"
}

@Bean
public MarcoHandler marcoHandler() {
return new MarcoHandler();
}

}

WebAppInitializer.java

1
2
3
4
java复制代码@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebSocketConfig.class};
}

JavaScript客户端代码

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
xml复制代码<script>

var url = 'ws://' + window.location.host + '/yds(你的项目名称)/marco';
var sock = new WebSocket(url); //打开WebSocket

sock.onopen = function() { //处理连接开启事件
console.log('Opening');
sock.send('Marco!');
};

sock.onmessage = function(e) { //处理信息
console.log('Received Message: ', e.data);
setTimeout(function() {
sayMarco()
}, 2000);
};

sock.onclose = function() { //处理连接关闭事件
console.log('Closing');
};

function sayMarco() { //发送信息函数
console.log('Sending Marco!');
sock.send('Marco!');
}
</script>

这里写图片描述
这里写图片描述
在本例中,URL使用了ws://前缀,表明这是一个基本的WebSocket连接,如果是安全WebSocket的话,协议的前缀将会是wss://。
注意: jar包一定要导正确,我是用的Spring5.0、jackson2.9.3。一些老版本的jar包老是报各种NoSuchMethodException,又或者Spring与jackson版本不兼容

WebSocket简单示例

个人感觉上面的那种太复杂了,如果只是简单的通信的话,可以像下面这样写:

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
xml复制代码<script>

if('WebSocket' in window)
{
var url = 'ws://' + window.location.host + '/TestsWebSocket(项目名)/websocket(服务端定义的端点)';
var sock = new WebSocket(url); //打开WebSocket
}else
{
alert("你的浏览器不支持WebSocket");
}

sock.onopen = function() { //处理连接开启事件
console.log('Opening');
sock.send('start');
};

sock.onmessage = function(e) { //处理信息
e = e || event; //获取事件,这样写是为了兼容IE浏览器
console.log(e.data);
};

sock.onclose = function() { //处理连接关闭事件
console.log('Closing');
};

</script>
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
java复制代码import java.io.IOException;
import java.util.Date;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value = "/websocket") //声明这是一个Socket服务
public class MyWebSocket {
//session为与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

/**
* 连接建立成功调用的方法
* @param session 可选的参数
* @throws Exception
*/
@OnOpen
public void onOpen(Session session) throws Exception {
this.session = session;
System.out.println("Open");
}

/**
* 连接关闭调用的方法
* @throws Exception
*/
@OnClose
public void onClose() throws Exception {
System.out.println("Close");
}

/**
* 收到消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
* @throws Exception
*/
@OnMessage
public void onMessage(String message, Session session) throws Exception {
if (message != null){
switch (message) {
case "start":
System.out.println("接收到数据"+message);
sendMessage("哈哈哈哈哈哈哈哈");
break;
case "question":
case "close":
System.out.println("关闭连接");
onClose();
default:
break;
}
}
}

/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}

/**
* 发送消息方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message); //向客户端发送数据
}

}

运行,浏览器与服务端的输出如图:

在这里插入图片描述
在这里插入图片描述
SockJS
==========================

概述

WebSocket是一个相对比较新的规范,在Web浏览器和应用服务器上没有得到一致的支持。所以我们需要一种WebSocket的备选方案。
而这恰恰是SockJS所擅长的。SockJS是WebSocket技术的一种模拟,在表面上,它尽可能对应WebSocket API,但是在底层非常智能。如果WebSocket技术不可用的话,就会选择另外的通信方式。

使用SockJS

WebSocketConfig.java

1
2
3
4
java复制代码 @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(marcoHandler(), "/marco").withSockJS();
}

只需加上withSockJS()方法就能声明我们想要使用SockJS功能,如果WebSocket不可用的话,SockJS的备用方案就会发挥作用。
JavaScript客户端代码
要在客户端使用SockJS,需要确保加载了SockJS客户端库。

1
xml复制代码<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>

除了加载SockJS客户端库外,要使用SockJS只需要修改两行代码即可:

1
2
3
4
ruby复制代码        var url = 'marco';
var sock = new SockJS(url); //SockJS所处理的URL是http://或https://,不再是ws://和wss://
//使用相对URL。例如,如果包含JavaScript的页面位于"http://localhost:8080/websocket"的路径下
// 那么给定的"marco"路径将会形成到"http://localhost:8080/websocket/marco"的连接

运行效果一样,但是客户端–服务器之间通信的方式却有了很大的变化。

使用STOMP消息

概述

STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息的语义。STOMP帧由命令、一个或多个头信息以及负载所组成。例如如下就是发送数据的一个STOMP帧:

1
2
3
4
5
javascript复制代码>>> SEND
destination:/app/marco
content-length:20

{"message":"Maeco!"}

在这个简单的样例中,STOMP命令是SEND,表明会发送一些内容。紧接着是两个头信息:一个用来表示消息要发送到哪里的目的地,另外一个则包含了负载的大小。然后,紧接着是一个空行,STOMP帧的最后是负载内容。
STOMP帧中最有意思的是destination头信息了。它表明STOMP是一个消息协议。消息会发布到某个目的地,这个目的地实际上可能真的有消息代理作为支撑。另一方面,消息处理器也可以监听这些目的地,接收所发送过来的消息。

启用STOMP消息功能

WebSocketStompConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer{

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {

registry.addEndpoint("/marcopolo").withSockJS();//为/marcopolo路径启用SockJS功能
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry)
{

//表明在topic、queue、users这三个域上可以向客户端发消息。
registry.enableSimpleBroker("/topic","/queue","/users");
//客户端向服务端发起请求时,需要以/app为前缀。
registry.setApplicationDestinationPrefixes("/app");
//给指定用户发送一对一的消息前缀是/users/。
registry.setUserDestinationPrefix("/users/");
}

}
1
2
3
4
java复制代码 @Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebSocketStompConfig.class,WebConfig.class};
}

WebSocketStompConfig 重载了registerStompEndpoints()方法,将/marcopolo注册为STOMP端点。这个路径与之前接收和发送消息的目的地路径有所不同。这是一个端点,客户端在订阅或发布消息到目的地前,要连接该端点。
WebSocketStompConfig还通过重载configureMessageBroker()方法配置了一个简单的消息代理。这个方法是可选的,如果不重载它的话,将会自动配置一个简单的内存消息代理,用它来处理以“/topic”为前缀的消息。

处理来自客户端的STOMP消息

testConroller.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Controller
public class testConroller {
@MessageMapping("/marco")
public void handleShout(Shout incoming)
{
System.out.println("Received message:"+incoming.getMessage());
}

@SubscribeMapping("/subscribe")
public Shout handleSubscribe()
{
Shout outing = new Shout();
outing.setMessage("subscribes");
return outing;
}
}

@MessageMapping注解,表明handleShout()方法能够处理指定目的地上到达的消息。本例中目的地也就是“/app/marco”。(“/app”前缀是隐含 的,因为我们将其配置为应用的目的地前缀)
@SubscribeMapping注解,与@MessageMapping注解相似,当收到了STOMP订阅消息的时候,带有@SubscribeMapping注解的方法将会被触发。

Shout.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Shout {
private String message;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}

客户端JavaScript代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script>
var url = 'http://'+window.location.host+'/yds/marcopolo';
var sock = new SockJS(url); //创建SockJS连接。
var stomp = Stomp.over(sock);//创建STOMP客户端实例。实际上封装了SockJS,这样就能在WebSocket连接上发送STOMP消息。
var payload = JSON.stringify({'message':'Marco!'});
stomp.connect('guest','guest',function(frame){
stomp.send("/app/marco",{},payload);
stomp.subscribe('/app/subscribe', function(message){

});
});
</script>

Received message:Marco!
这里写图片描述
这里写图片描述
这里写图片描述

发送消息到客户端

如果你想要在接收消息的时候,同时在响应中发送一条消息,那么需要做的仅仅是将内容返回就可以了。

1
2
3
4
5
6
7
java复制代码@MessageMapping("/marco")	
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}

当@MessageMapping注解标示的方法有返回值的时候,返回的对象将会进行转换(通过消息转换器)并放到STOMP帧的负载中,然后发给消息代理。
默认情况下,帧所发往的目的地会与触发处理器方法的目的地相同,只不过会加上“/topic”前缀。

1
2
javascript复制代码stomp.subscribe('/topic/marco', function(message){    订阅后将会接收到消息。
});

这里写图片描述
不过我们可以通过为方法添加@SendTo注解,重载目的地:

1
2
3
4
5
6
7
8
java复制代码@MessageMapping("/marco")
@SendTo("/queue/marco")
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}
1
2
javascript复制代码stomp.subscribe('/queue/marco', function(message){ 
});

这里写图片描述

在应用的任意地方发送消息

Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。
使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessageSendingOperations)自动装配到所需的对象中。

1
2
3
4
5
6
7
8
9
java复制代码 @Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;


@RequestMapping("/test")
public void sendMessage()
{
simpMessageSendingOperations.convertAndSend("/topic/test", "测试SimpMessageSendingOperations ");
}

访问/test后:
这里写图片描述

为目标用户发送消息

使用@SendToUser注解,表明它的返回值要以消息的形式发送给某个认证用户的客户端。

1
2
3
4
5
6
7
8
java复制代码    @MessageMapping("/message")
@SendToUser("/topic/sendtouser")
public Shout message()
{
Shout outing = new Shout();
outing.setMessage("SendToUser");
return outing;
}
1
2
javascript复制代码stomp.subscribe('/users/topic/sendtouser', function(message){//给指定用户发送一对一的消息前缀是/users/。
});

这里写图片描述
这个目的地使用了/users作为前缀,以/users作为前缀的目的地将会以特殊的方式进行处理。以/users为前缀的消息将会通过UserDestinationMessageHandler进行处理。
这里写图片描述
UserDestinationMessageHandler的主要任务是将用户消息重新路由到某个用户独有的目的地上。在处理订阅的时候,它会将目标地址中的/users前缀去掉,并基于用户的会话添加一个后缀。

为指定用户发送消息

SimpMessagingTemplate还提供了convertAndSendToUser()方法。convertAndSendToUser()方法能够让我们给特定用户发送消息。

1
arduino复制代码simpMessageSendingOperations.convertAndSendToUser("1", "/message", "测试convertAndSendToUser");
1
2
3
javascript复制代码stomp.subscribe('/users/1/message', function(message){ 

});

这里写图片描述
客户端接收一对一消息的主题是”/users/“+usersId+”/message”,这里的用户Id可以是一个普通字符串,只要每个客户端都使用自己的Id并且服务器端知道每个用户的Id就行了。

以上只是学习所做的笔记,如有错误请指正。谢谢啦!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…577578579…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%