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

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


  • 首页

  • 归档

  • 搜索

51job招聘网爬取保姆式教程,带你打造自己的职业信息库!

发表于 2021-02-24

爬取前准备

网页查看

在这里插入图片描述

在这里插入图片描述


建立mysql数据库及表

建立数据库
在这里插入图片描述

建立表

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码CREATE TABLE `51job` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT NULL,
`company` varchar(100) DEFAULT NULL,
`price` varchar(100) DEFAULT NULL,
`education` varchar(100) DEFAULT NULL,
`experience` varchar(100) DEFAULT NULL,
`welfare` varchar(100) DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`text` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=124 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

完整代码及代码分析

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
python复制代码from selenium import webdriver
import time
import pymysql
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ChromeOptions
from bs4 import BeautifulSoup
from urllib import parse


num = 2
sql = "insert into 51job(id,title,company,price,education,experience, welfare,address,text) values(null,%s,%s,%s,%s,%s,%s,%s,%s)"
name = ""

#初始化浏览器
def init():
global name
# 实现无可视化界面得操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')

# 设置chrome_options=chrome_options即可实现无界面
driver = webdriver.Chrome(chrome_options=chrome_options)
time.sleep(0.5)
# 把浏览器实现全屏
# driver.maximize_window()
time.sleep(0.5)
#发送请求
driver.get("https://search.51job.com/list/000000,000000,0000,00,9,99,"+str(name)+",2,1.html")
source = driver.page_source
# 返回driver和页面源码
return driver,source


#解析
def download(driver,page_text,conn,cur):
#引入全局变量
global num
global name
global sql

# 使用lxml XML解析器
bs = BeautifulSoup(page_text, "lxml")
#参考图1
div_list = bs.find(class_="j_joblist").find_all(class_="e")
for li in div_list:
bs = BeautifulSoup(str(li), "lxml")
#参考图2
#职位名称
title = bs.find(class_="jname at").text
#工资
price = bs.find(class_="sal").text

#抛异常的是因为有的公司是没有该值的
try:
#公司福利
welfare = bs.find(class_="tags")["title"]
except:
welfare = "无福利"
#公司名称
company = bs.find(class_="cname at")['title']
#详情页URL
url = bs.find(class_="el")['href']
#经验
experience = bs.find(class_="d at").text.split("|")[1]
try:
#学历
education = bs.find(class_="d at").text.split("|")[2]
except:
education = "无介绍"

#51job有的详情页URL对应的网页跟大多数详情页是不一样的没有对应的数据,可参考图3
#请求详情页
if "https://jobs.51job.com/" in url:
time.sleep(0.5)
#请求详情页URL
driver.get(url)
page_source = driver.page_source
bs_page = BeautifulSoup(page_source, "lxml")
#参考图4
text = bs_page.find(class_="bmsg job_msg inbox").text.replace("微信分享","").strip()
try:
#参考图5
address = bs_page.find(class_="bmsg inbox").find(class_="fp").text.replace("上班地址:","")
except:
address = "无说明"
#print(title)

#执行插入语句
cur.execute(sql, (title, company, price, education, experience, welfare,address,text))
#提交事务
conn.commit()

#进行多页爬取
#num控制页数
if num <= 3:
next_url = "https://search.51job.com/list/000000,000000,0000,00,9,99,"+str(name)+",2,"+str(num)+".html"
time.sleep(0.5)
driver.get(next_url)
num += 1
download(driver,driver.page_source,conn,cur)

return conn,cur

def init_mysql():
dbparams = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'password': 'wad07244058664',
'database': '51job',
'charset': 'utf8'
}
conn = pymysql.connect(**dbparams)
cur = conn.cursor()
return conn,cur

def close_mysql(conn,cur):
cur.close()
conn.close()

if __name__ == "__main__":
name = input("请输入爬取职位名称:")
#进行二次转码,具体可参考博主文章
text1 = parse.quote(name)
name = parse.quote(text1)
#浏览器初始化
driver,source = init()
#mysql初始化
conn,cur = init_mysql()
#数据爬取
conn,cur = download(driver,source,conn,cur)
#关闭MySQL链接
close_mysql(conn,cur)

图片辅助分析

图1
在这里插入图片描述
图2
在这里插入图片描述
图3
在这里插入图片描述

图4
在这里插入图片描述
图5
在这里插入图片描述


运行结果

在这里插入图片描述


博主会持续更新,有兴趣的小伙伴可以点赞、关注和收藏下哦,你们的支持就是我创作最大的动力!

更多Python爬虫有关文章

在这里插入图片描述

本文转载自: 掘金

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

阿里云OSS有关图片、文件上传有过期时间的坑

发表于 2021-02-24

前言

有关阿里云OSS有关图片上传设置过期时间的问题,困扰了我几个小时的时间,之前也不是没用过阿里OSS,但此次开发中发现原来的方法上传不了,经百度得到的代码上传之后,图片又有过期时间,这tm确实迷惑了我。
在这里插入图片描述
经过我仔细对比之前使用阿里OSS发现,我的Bucket设置的读写权限为==私有==
在这里插入图片描述
在这里插入图片描述
修改回==公共读==后就不需要设置过期时间了
在这里插入图片描述
希望各位小伙伴别再遇到我这样的问题了!

在这里插入图片描述


1、导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码        <!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>

<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.3</version>
</dependency>

2、想要设置过期时间

Bucket的读写权限要设置为==私有==

在这里插入图片描述

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
java复制代码@Service
public class OssServiceImpl implements OssService {

//inputStream:文件的输入流file.getInputStream();
//module:指定的文件夹
//originalFilename:文件名称即file.getOriginalFilename()
@Override
public String uploadFileAvatar(InputStream inputStream,String module,String originalFilename) {
//工具类获取值
String endpoint = ConstantPropertiesUtils.END_POINT;
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;

OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

String folder = new DateTime().toString("yyyy/MM/dd");
String fileName = UUID.randomUUID().toString();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
// oss中的文件夹名
String objectName = module + "/" + folder + "/" + fileName + fileExtension;

// 创建上传文件的元信息,可以通过文件元信息设置HTTP header(设置了才能通过返回的链接直接访问)。
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType("image/jpg");
// 文件上传
ossClient.putObject(bucketName, objectName, inputStream,objectMetadata);
// 设置URL过期时间为1小时。
Date expiration = new Date(System.currentTimeMillis() + 3600 * 100000);

String url = ossClient.generatePresignedUrl(bucketName, objectName, expiration).toString();

return url;
}
}

3、不想设置过期时间

Bucket的读写权限要设置为==公共读==

在这里插入图片描述

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
java复制代码@Service
public class OssServiceImpl implements OssService {

//inputStream:文件的输入流file.getInputStream();
//module:指定的文件夹
//originalFilename:文件名称即file.getOriginalFilename()
@Override
public String uploadFileAvatar(InputStream inputStream,String module,String originalFilename) {
//工具类获取值
String endpoint = ConstantPropertiesUtils.END_POINT;
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;


//创建OSS实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

String folder = new DateTime().toString("yyyy/MM/dd");
String fileName = UUID.randomUUID().toString();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
// oss中的文件夹名
String objectName = module + "/" + folder + "/" + fileName + fileExtension;

// 创建上传文件的元信息,可以通过文件元信息设置HTTP header(设置了才能通过返回的链接直接访问)。
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType("image/jpg");

ossClient.putObject(bucketName, objectName, inputStream,objectMetadata);

// 关闭OSSClient。
ossClient.shutdown();

String url = "http://"+bucketName+"."+endpoint+"/"+objectName;

return url;
}
}

本篇博文到此结束,觉得不错的小伙伴可以一键三连哦!,感谢支持!!!

Java学习路线目录索引

在这里插入图片描述

本文转载自: 掘金

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

Python爬虫JS解密详解,学会直接破解80%的网站(二)

发表于 2021-02-24

Python爬虫JS解密详解,学会直接破解80%的网站!!!

29个爬虫项目宝藏教程,你值得拥有!


前言

==Glidedsky==这关的==JS解密==不同于我之前见到的,希望大家好好看,好好学!
在这里插入图片描述

==温馨提示==:保护好头发!

在这里插入图片描述


1、网页查看

在这里插入图片描述


2、JS解密过程(细心看哦)

既然是JS加密过的,那么数据肯定不是静态的,如下

直接请求该页面,或取到的html代码粘贴到html文件打开是没有数字的

在这里插入图片描述

打开控制台查看XHR
在这里插入图片描述

在这里插入图片描述
这里有个问题,我之前查看是可以查看到数据的,但不知道为什么又看不到数据了,有知道的小伙伴麻烦在评论区告诉我下,谢谢。

网上查的说什么==网页可以感应用户打开了控制台==,咱也不知道,咱也不敢问,还有这么吊的操作

在这里插入图片描述

如何还有不明白的小伙伴,可以参考我这篇JS解密文章 Python爬虫JS解密详解,写的很详细,这玩意搞多了就有经验了

不说了,咱们接着看

往下翻可以看见,该请求带了3个参数

  • page:当前页数
  • t:类似于时间戳
  • sign:进过某种方法加密后的数据
    在这里插入图片描述

按住Ctrl+Shift+f 进行搜索,输入==sign==,可见有6个匹配的

在这里插入图片描述
有兴趣的小伙伴可以点进去再次搜索==sign==,都是些跟下图一样==牛头不对马嘴==的匹配
在这里插入图片描述
在这里插入图片描述
按我之前的JS解密经验,不应该是直接匹配到,然后搞个什么函数加密的嘛 o(╥﹏╥)o

都看到这份上了,接直接放弃也不是我个性格,耐着性子接着研究研究。。。。

然后发现个新办法,现在教给大家——就是打XHR断点,如下

在这里插入图片描述
复制部分URL就好了,不用全部复制
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
==现在进入最关键步骤——使用python代码得到上面的数据==

==获取t值==
在这里插入图片描述
==获取sign值==

安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准(Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA),SHA1比MD5的安全性更强。对于长度小于2^ 64位的消息,SHA1会产生一个160位的消息摘要。

不要慌,python中提供了hashlib库解决,真是厉害啊!

在这里插入图片描述
==成功了,老铁们可以来波点赞嘛!(*^▽^*)==

==拼接URL请求,注意:返回数据为json格式==
在这里插入图片描述

完美


3、解密答案(完整代码)

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
python复制代码import requests
import hashlib
import time
import math


headers = {
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36",
#注意Cookie自己填哦
"Cookie": ""
}

sum = 0

def get(response):
global sum
for i in response['items']:
sum += int(i)


if __name__ == '__main__':

#1000个页面
for i in range(1000):
#获取t值
t = math.floor(time.time())

#获取sign值
sha1 = hashlib.sha1()
data = 'Xr0Z-javascript-obfuscation-1' + str(t)
sha1.update(data.encode('utf-8'))
sign = sha1.hexdigest()
print("第"+str(i+1)+"页")

#拼接url
url = "http://glidedsky.com/api/level/web/crawler-javascript-obfuscation-1/items?page="+str(i+1)+"&t="+str(t)+"&sign="+str(sign)
response = requests.get(url=url,headers=headers).json()
get(response)

#打印最终数字
print(sum)

在这里插入图片描述
闯关成功,解密成功!!!
在这里插入图片描述

==注意填上Cookie,我提供的代码没有填上Cookie值==
在这里插入图片描述


博主会持续更新,有兴趣的小伙伴可以==点赞==、==关注==和==收藏==下哦,你们的支持就是我创作最大的动力!

Java学习从入门到大神学习目录索引

博主开源Python爬虫教程目录索引(宝藏教程,你值得拥有!)

在这里插入图片描述

本文转载自: 掘金

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

狂补计算机基础知识,让我上了瘾

发表于 2021-02-24

掘金的小伙伴们,大家好,我是沉默王二。最近我在狂补计算机基础知识,感觉有点上瘾!以前觉得很枯燥的知识,现在竟然有点香!不知道为什么。

我算是科班出身,大学的专业就是计算机网络,不过老实说,在实际的开发中,大学的很多课程,要么“用处”不大,要么可以换个方式,更高效的学习。

其中的原因,一方面是实际开发更关注知识的“实用”,另外一方面,大学的教学确实和产业有些脱钩。所以我就从实际应用的角度出发,重新来梳理一下计算机基础方面的知识。

01、数据结构

毫无疑问,数据结构对一名程序员来说非常重要,还是那句话程序=数据结构+算法,这种说法无论放在什么时候都是成立的。

大部分的数据结构课程,关注的重点都在如何从数学上实现一个数据结构,但在实际开发中,大部分主流语言都已经内置了常见的数据结构,比如说 Java。也就是说,对于大部分程序员来说,实际开发中,很难有需求要从零开始实现一个数据结构。因此我们只需要做到下面几点。

1、熟悉常见数据结构的概念,比如说数组、堆栈、链表、哈希表等。

2、了解常用数据结构之间的差异,比如说 ArrayList 和 LinkedList,我在之前的文章中有详细说明过,可以点击链接去查看下。

3、关注常用数据结构的外围算法,比如说如何对 List 和 Map 进行查找。

4、关注数据结构使用中容易出错的地方,比如说线程是否安全等。

…

B 站上浙江大学的一个数据结构的课还不错,很系统很经典,适合小白入门。

视频地址如下。

www.bilibili.com/video/BV1JW…

02、算法

以我接触的领域来说,大部分普通的业务系统都不会涉及到太复杂的算法,因此我没有在算法上投入过多时间。

但如果你在一些特殊的领域,如果算法跟不上,可以说是“寸步难行”,比如说图形处理领域,无论是图像的变化还是增强,无一例外都要用到矩阵变换,因此就必然涉及到线性代数的内容,再往深处学的话,必然会牵扯出更多的知识。

所以学习算法要视情况而定,如果想走算法岗,那么《算法第4版》、《算法导论》、《数学之美》、《编程珠玑》、《剑指offer》这些书都要耐着性子啃一啃。电子书可以在下面这个 GitHub 仓库上找得到。

github.com/itwanger/Ja…

顺带再推荐一份阿里大佬的算法刷题笔记吧,在 GitHub 上已经有 13.3k 的 star 了。里面的每道题都写了解题思路,是通过 GO 语言实现的,每道题都 runtime beats 100% 了。

在线阅读地址如下所示:

books.halfrost.com/leetcode/

03、设计模式

我认为设计模式是初中级程序员迈向高级程序员的必经之路。有不少程序员,前期冲劲十足,但后继乏力,都是吃了设计模式的亏。

在工作的前几年,大部分程序员都处于熟悉编程语言的阶段,也就是处于“技”的阶段,随后就要进入“术”的阶段了。在编程领域,“术”的典型代表就是“设计模式”。

我自己推荐的一本书是《设计模式之禅》,代码是用 Java 实现的,读起来比较轻松,也更符合国内程序员的阅读习惯。

另外就是我上次推荐的 Refactoring Guru 网站,里面附带的图片非常精美,阅读起来的视觉效果会比较丰富。

在线阅读地址如下所示:

refactoring.guru

离线版可以通过下面这个链接下载(无套路,不需要解压密码):

设计模式,牛逼!

04、软件工程

实现一个软件系统的过程,不仅只有编码,还涉及到项目安排,团队协调等一系列非技术因素,如果想从一名程序员走向管理岗,成为 team leader 或者开发经理,软件工程方面的知识就必须得跟得上。

这里我推荐几本不错的经典书,其中有一本邹欣老师的书,他刚出任 CSDN 副总裁,希望 CSDN 能在他的入驻后变得更好一些。他这本书的书名叫做《构建之法》,我看过,还是很不错的。

另外还有两本必读,就是《人月神话》和《人件》,虽然有了岁月的痕迹,但依然值得深读。当然,关于软件工程,最好的学习方法是观察,观察你所在的团队是如何处理工程问题的,然后思考,最终形成自己的方法观。

电子书可以在下面这个 GitHub 仓库上找得到。

github.com/itwanger/Ja…

05、架构 & 设计

要想写出一个好而美的程序,需要经过三个阶段。

第一阶段,有扎实的基本功,简单点说,就是要做到语法熟练、框架熟练,成为一名能够完成开发任务的“码农”。

第二阶段,从“码农”到“工程师”,在局部上,不仅要能够实现功能,还能关注功能之外的维度,比如健壮性、低耦合、可扩展等指标。

第三阶段,从“工程师”到“架构师”,不仅在局部上追求一个模块的好坏,而且还要从整个系统层面去掌控,合理安排资源的优先级,保证整个系统不会出现腐败等等。

要想成为一名优秀的架构师,除了自身的努力,也需要一点点运气,但靠读书可能不够,但这些经典书籍还是要阅读的,能给日常的工作带来巨大的帮助。

  • 《代码大全》
  • 《重构:改善既有代码的设计》
  • 《设计原本》
  • 《大型网站技术架构核心原理与案例分析》

电子书可以在下面这个 GitHub 仓库上找得到。

github.com/itwanger/Ja…

最后

技术这条道路并不好走,不仅要学习很多新技术、新框架,还要及时补充必要的计算机基础知识,底子硬的同时还要学会拥抱变化。

学习的过程,就好像登山一样,大概有 80% 的人在这个过程中会掉队。那么请相信我,只要目标明确,努力加上坚持,再加上一点点好运气,你就能登顶!

好了,我是沉默王二,希望这篇文章能够帮到你!下期见。

本文转载自: 掘金

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

计算机是如何做加法的?(1)——构建多位加法器

发表于 2021-02-24

计算机做加法是对人做加法的模拟。那么人是怎么做加法的呢?让我们来考察一下。

人做加法的过程

从一般的情况出发,比如怎么计算“24+35”呢?

我们把个位与个位相加,4+5=9,再把十位与十位相加,2+3=5,再合起来得到 59。

这就是所谓的 分而治之(divide and conque) 了,用打仗的话来说,也可以说是各个击破。

显然,会做两个多位数加法的基础是会做两个一位数加法。那么,问题又来了,如何做两个一位数的加法呢?

其实,你靠的是记忆!我们会在后面再去深入探讨这一问题。

自顶向下

目前,我们考虑的是如何构造多位加法器,重点是如何去模拟多位加法器的工作过程。

对于两个一位数加法的问题,我们暂且认为:经过反复学习,在我们的大脑中形成了一个关于一位数加法的神经网络,它的功能可由以下的一个抽象接口模型来描述:

abstract adder model

它能接受两个一位数,并快速给出相加的结果。以此为基础,多位数的加法可以分解成多次一位数的加法,并把结果合并起来。

先不考虑一位加法器的实现细节,相反,假定它已经有了我们想要的功能,我们现在先考虑如何用它构建高级的多位加法器。

这种思路我们称之为“自顶向下(top down)”,先把目光聚焦在高层的结构上。

一位加法器是为多位加法器服务的,从服务对象考虑起能让我们更好地确定底层接口的规范。

假定底层已有我们想要的功能,这种思路又称为“wishful thinking(按愿望思维)”。它避免了我们过早地纠结在底层实现的细节上去。

关于进制与表示的问题

有人可能会想,人做加法是用十进制,计算机或者说电路做加法是用二进制,这样去类比可行吗?是不是应该先讲讲二进制的知识先呢?

进制差异自然会带来影响,特别越到底层,影响越显著。

不过,我们将看到,进制的选择暂时不会影响高层的结构。对于我们而言,十进制更加自然,到后面,会逐步转到二进制上去讨论。

至于数字的表示问题,10 进制的一大麻烦就是要表示的状态太多了,既然要用电路来实现,首先很容易想到用电压来表示数字。

比如数字 1 就用 1V 的电压来表示,同理,数字 9 就用 9V 来表示。

自然,这种表示不是唯一的,你也可以用 0.1V 来表示 1,那么相应地用 0.9V 来表示 9,关键或许在于保持这种比率关系。

两种策略

考虑前面人做加法的例子:24+35

串行

一种策略就是只用一个一位数加法器,分成多次,每一次把同一位上的两个数扔给这个一位加法器去运算:

比如先把个位的 4 和 5 扔进去,得出一个结果 9,存起来;

再把十位上的 2 和 3 扔进去…

当然,这种方式的挑战在于如何存储中间结果,如何依次地把数据送进去,另外还有速度的问题等等。

打造出一个如此的加法器几乎相当于一个初级计算机的雏形了。

显然,人做多位加法的过程也是串行式的。

并行

那么,一种更加简单粗暴的策略就是把几个一位加法器拼接起来,同时进行计算,一方面无需关注存储,依次送入数据等问题,另外速度上也更快了。

adder example no carry

24+35,按照我们的习惯,左边是高位(十位),右边是低位(个位)。

如果需要更多的位数,那么简单放置更多的一位加法器就完了。这种方式需要更多的硬件,但我们也因此得到了好处。

当然了,无限的并行下去也是不行的,最终我们要的是一种中庸之道,平衡之术,盲目推崇或排斥某种方式往往都是不可取的。

比如一个 32 位机如何做 64 位的加法呢?最终还是靠串行做 2 次 32 位加法来实现。

进位的处理

当然,有一个很重要的问题被有意无意地忽略了,那就是进位。

考虑以下多位数的加法:“256+128”, 那么,通过组合三个一位数加法器,分别处理个位,十位与百位数字的相加,结果如下:

adder example with carry problem

显然,3714 不是我们想要的结果,因为没有考虑到进位的影响。正确的结果应该是 384。

到目前为止,我们关于一位加法器的构想还是很粗糙的。首先,从最终结果的表示层面而言,按前面约定的表示,输出应该限制在 0V-9V 的区间。

如果是 0V,按电路上一般的约定,可用一个接地符号来表示:

ground symbol

对于上述个位数加法而言,那么就是输入一个 6V 和一个 8V 的电压,结果输出的 14V 电压,这不是我们想要的结果。

显然,上述例子中,个位中只需要输出 4V 即可,而不是 14V,14 需要拆分成 1 和 4。

现在需要修正一位加法器的原型。首先至少要两个输出,一个是 加位(sum,S) 的输出,一个 进位(carry,C) 的输出:

improved abstract adder model

其次,进位 的输出还要参与到下一级的输入中去,也即要有三个输入,输出则只要显示 加位 的结果即可:

adder example with carry

对于右边最低位,不存在进位,使用一个接地符号表示输入为 0V。

其余的进位输入则来自低位的进位输出。

图中红色的线表示产生了进位。对于中间的十位数加法,实际是 5+2+1=8。

经过这样的调整,多位加法器就能正常工作了,但这样一来,模型也复杂多了。

全加器

综合进位及显示的需要,那么,一个具有普遍性的一位加法器至少要三个输入,其中有一个进位输入(Carry In);以及两个输出(其中,CO 指 Carry out,进位输出):

full adder

这样的一个东西,我们称之为全加器(Full Adder,FA)。如果你是计算机或相关专业科班出身,你就知道为何这么叫了,我也不想引入新的叫法。

前面说到,如果从高层考虑起,能让我们更好地确定底层接口的规范,我们现在的确得到了一个更明确的接口。

如果一开始就一头猛扎进去实现最初设想的一位加法器原型,恐怕到构建多位加法器时又不得不返工了。

现在尽管我们不知道怎么去实现它,但我们却更清楚它应该是怎样的。

你心中有个 Big Picture(大图景),你更加不容易走偏。

多位加法器

然后,以此为基础,把三个一位加法器封装在一起,把同一个数的三个位放在一起,得到以下一个三位加法器的原型:

3bit adder

走线就有点随意了,我也不是什么专业绘电路图的,大家能看明白原理就行了。

执行上述加法的过程如下:

3bit adder example

通过梳理线路的走向,就无须像前面那般两个数字交叉在一起很不直观。

线未必非得要走到一块,有种说法是“接口要对用户友好”,无论你是设计硬件还是软件的接口,都应该记住这一点。

明白了它的内部实现,之后,再提到它时,只需要一个简化的接口模型:

encapsulated 3bit adder

那么,这就是搞软件的人口中所谓的抽象呀,封装呀,提供接口呀,隐藏实现呀,blah, blah…

模块化

以它为基础,我们又可以构建比如一个 6 位加法器,在它里面封装了两个三位加法器:

6bit adder by two 3bit adder

如果想呈现全部的细节,那么就像下面这样了,线很多,但道理还是一样的:

6bit adder

当然也可以跳过三位加法器,直接构建在 6 个全加器基础上。本质上还是一样,不过少了层封装。

自然,按照同样的思路,你还可以打造 12 位加法器。当然你也可以先造个 2 位的加法器,然后再一层一层弄出 4 位,8 位的等等。

你甚至可用一个 2 位加一个 3 位来造一个 5 位的加法器,有何不可呢?随便你怎么去组合。

搞软件的同学是不是又想到了什么分层次呀,模块化呀,然后又是一大堆理论,大道理,blabla…

这些原则其实是软硬通吃的。

层次性

当然,以上这些最终都建立在全加器功能的基础上。

6bit and 3bit adder build on top of full adder

而后面我们将看到,全加器它又是建立在半加器等的基础之上。

full adder build on top of half adder and transistor

构造一层又一层的抽象,这就是我们应付复杂性的重要手段。

当不知道全加器是如何工作时,我们可能会觉得不够踏实,似乎一切建立在无源之水,无本之木之上一样。

我们将在下一篇再探讨如何去构建全加器。见计算机是如何做加法的?(2)——构建一位加法器

本文转载自: 掘金

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

摊牌了!策略模式在项目设计中用的最多

发表于 2021-02-24

前言

日常 Coding 过程中,设计模式三板斧:模版、构建者、策略,今天来说下第三板斧 策略设计模式

策略模式还是比较简单并且使用较多的,平常我们多运用策略模式用来消除 if-else、switch 等多重判断的代码,消除 if-else、switch 多重判断 可以有效应对代码的复杂性

如果分支判断会不断变化(增、删、改),那么可以使用别的技巧让其满足开闭原则,提高代码的扩展性 (策略模式场景主要负责解耦,开闭原则需要额外支持)

下文中会详细列举如何使用设计模式做个 Demo 、模式的真实场景以及策略模式的好处

策略设计模式大纲如下:

  1. 什么是策略模式
  2. Spring 项目中真实的应用场景
  3. 框架源码底层如何玩耍策略模式
  4. 策略模式总结

什么是策略模式

策略模式在 GoF 的《设计模式》一书中,是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

定义一组算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化,客户端代指使用算法的代码

看到上面的介绍可能不太明白策略模式具体为何物,这里会从最基本的代码说起,一步一步彻底掌握此模式。下述代码可能大家都能联想出对应的业务,根据对应的优惠类型,对价格作出相应的优惠

这段代码是能够满足项目中业务需求的,而且很多已上线生产环境的代码也有这类代码。但是,这一段代码存在存在两个弊端

  1. 代码的复杂性,正常业务代码逻辑肯定会比这个代码块复杂很多,这也就 导致了 if-else 的分支以及代码数量过多。这种方式可以通过将代码拆分成独立函数或者拆分成类来解决
  2. 开闭原则,价格优惠肯定会 随着不同的时期作出不同的改变,或许新增、删除或修改。如果在一个函数中修改无疑是件恐怖的事情,想想可能多个开发者分别进行开发,杂乱无章的注释,混乱的代码逻辑等情况十有八九会发生

如何运用策略模式优化上述代码,使程序设计看着简约、可扩展等特性

  1. 简化代码的复杂性,将不同的优惠类型定义为不同的策略算法实现类
  2. 保证开闭原则,增加程序的健壮性以及可扩展性

策略模式示例

将上述代码块改造为策略设计模式,大致需要三个步骤

  1. 定义抽象策略接口,因为业务使用接口而不是具体的实现类的话,便可以灵活的替换不同的策略
  2. 定义具体策略实现类,实现自抽象策略接口,其内部封装具体的业务实现
  3. 定义策略工厂,封装创建策略实现(算法),对客户端屏蔽具体的创建细节

目前把抽象策略接口、具体的策略实现类以及策略工厂都已经创建了,现在可以看一下客户端需要如何调用,又是如何对客户端屏蔽具体实现细节的

根据代码块图片得知,具体策略类是从策略工厂中获取,确实是取消了 if-else 设计,在工厂中使用 Map 存储策略实现。获取到策略类后执行具体的优惠策略方法就可以获取优惠后的金额

通过分析大家得知,目前这种设计确实将应用代码的复杂性降低了。如果新增一个优惠策略,只需要新增一个策略算法实现类即可。但是,添加一个策略算法实现,意味着需要改动策略工厂中的代码,还是不符合开闭原则

如何完整实现符合开闭原则的策略模式,需要借助 Spring 的帮助,详细案例请继续往下看

项目中真实的应用场景

最近项目中设计的一个功能用到了策略模式,分为两类角色,笔者负责定义抽象策略接口以及策略工厂,不同的策略算法需要各个业务方去实现,可以联想到上文中的优惠券功能。因为是 Spring 项目,所以都是按照 Spring 的方式进行处理,话不多说,上代码

可以看到,比对上面的示例代码,有两处明显的变化

  1. 抽象策略接口中,新定义了 mark() 接口,此接口用来标示算法的唯一性
  2. 具体策略实现类,使用 @Component 修饰,将对象本身交由 Spring 进行管理

小贴士:为了阅读方便,mark() 返回直接使用字符串替代,读者朋友在返回标示时最好使用枚举定义

接下来继续查看抽象策略工厂如何改造,才能满足开闭原则

和之前责任链模式相同 (TODO 添加链接),都是通过 InitializingBean 接口实现中调用 IOC 容器查找对应策略实现,随后将策略实现 mark() 方法返回值作为 key, 策略实现本身作为 value 添加到 Map 容器中等待客户端的调用

这里使用的 SpringBoot 测试类,注入策略工厂 Bean,通过策略工厂选择出具体的策略算法类,继而通过算法获取到优惠后的价格。小插曲:如果不想把策略工厂注入 Spring 也可以,实现方法有很多

总结下本小节,我们通过和 Spring 结合的方式,通过策略设计模式对文初的代码块进行了两块优化:应对代码的复杂性,让其满足开闭原则。更具体一些呢就是 通过抽象策略算法类减少代码的复杂性,继而通过 Spring 的一些特性同时满足了开闭原则,现在来了新需求只要添加新的策略类即可,健壮易扩展

源码底层如何耍策略模式

自己用肯定觉得不够,必要时候还得看看设计开源框架源码的大佬们如何在代码中运用策略模式的

在作者了解中,JDK、Spring、SpringMvc、Mybatis、Dubbo 等等都运用了策略设计模式,这里就以 Mybatis 举例说明

Mybatis 中 Executor 代表执行器,负责增删改查的具体操作。其中用到了两种设计模式,模版方法以及策略模式

Executor 代表了抽象策略接口,刚才说到的模版方法模式源自 BaseExecutor

Configuration 代表策略工厂,负责创建具体的策略算法实现类

SimpleExecuto、ReuseExecutor… 表示封装了具体的策略算法实现类

上述代码块发生在 Configuration 类中创建执行器 Executor,通过 executorType 判断创建不同的策略算法。

上述代码块并没有彻底消除 if-else,因为 Mybatis 中执行器策略基本是固定的,也就是说它只会有这些 if-else 判断,基本不会新增或修改。如果非要消除 if-else,可以这么搞,这里写一下伪代码

这种方式叫做 “查表法”,通过策略工厂实现消除 if-else 分支。最后,Mybatis 太过详细的设计这里不再赘述,有兴趣的小伙伴可以去把源码下载啃一啃

到了这里可能有读者看出了问题,策略模式就算消除了 if-else 但是如果添加新的策略类,不还是会违反开闭原则么?

没错,因为 Mybatis 本身没有引入 Spring 依赖,所以没有办法借助 IOC 容器实现开闭原则。Spring 是一种开闭原则解决方式,那还有没有别的解决方式?

解决方式有很多,开闭原则核心就是 对原有代码修改关闭,对新增代码开放。可以通过扫描指定包下的自定义注解亦或者通过 instanceof 判断是否继承自某接口都可以。不过, 项目如果用了 Spring 还是消停的吧

结言

文章中图文并茂的方式介绍策略模式,通过价格优惠的场景,进而引用本文的重点:策略设计模式,相信小伙伴看完后都会有一定的收获

策略模式的本质依然是对代码设计解耦合,通过三类角色贯穿整个策略模式:抽象策略接口、策略工厂以及具体的策略实现类。通过细粒度的策略实现类避免了主体代码量过多,减少了设计中的复杂性

作者听到过很多小伙伴觉得自己做的都是 CRUD 工作,没有挑战性没意思。其实,我想说的是:业务代码一样牛逼,一样能体现程序员的水平。不一定非要高并发、大数据等场景。颇有一屋不扫何以扫天下的意思

最后抛出一个问题:出现 if-else 的代码,一定要使用策略模式优化么

如果 if-else 判断分支不多并且是固定的,后续不会出现新的分支,那我们完全 可以通过抽函数的方式降低程序复杂性;不要想法设法去除 if-else 语句,存在即合理。而且,使用策略模式会导致类增多,没有必要为了少量的判断分支引入策略模式

关于策略设计模式本文就讲到这里,后面会陆续输出工厂、原型、享元等模式;如果文章对你有帮助那就点个关注支持下吧,祝好。

参考文章

  • 《设计模式之美:策略模式》

微信搜索【源码兴趣圈】,关注公众号后回复 123 领取内容涵盖 GO、Netty、Seata、SpringCloud Alibaba、开发规范、面试宝典、数据结构等学习资料!

本文转载自: 掘金

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

物联网网关开发:基于MQTT消息总线的设计过程(下)

发表于 2021-02-24

道哥的第 022 篇原创

一、前言

在上一篇文章中物联网网关开发:基于MQTT消息总线的设计过程(上),我们聊了在一个物联网系统的网关中,如何利用 MQTT 消息总线,在嵌入式系统内部实现多个进程之间的相互通信问题。

这个通信模型的最大几个优点是:

  1. 模块之间解耦合;
  2. 各模块之间可以并行开发;
  3. 把 TCP 链接和粘包问题交给消息总线处理,我们只需要处理业务层的东西;
  4. 调试方便;

以上只是描述了在一个嵌入式系统内部,进程之间的通信方式,那么网关如何与云平台进行交互呢?

在上一篇文章中已经提到过:网关与云平台之间的通信方式一般都是客户指定的,就那么几种(阿里云、华为云、腾讯云、亚马逊AWS平台)。一般都要求网关与云平台之间处于长连接的状态,这样云端的各种指令就可以随时发送到网关。

这一篇文章,我们就来聊一聊这部分内容。

在公众号回复:mqtt,获取示例代码的网盘地址。

二、与云平台之间的 MQTT 连接

目前的几大物联网云平台,都提供了不同的接入方式。对于网关来说,应用最多的就是 MQTT 接入。

我们知道,MQTT 只是一个协议而已,不同的编程语言中都有实现,在 C 语言中也有好几个实现。

在网关内部,运行着一个后台 deamon: MQTT Broker,其实就是 mosquitto 这个可执行程序,它充当着消息总线的功能。这里请大家注意:因为这个消息总线是运行在嵌入式系统的内部,接入总线的客户端就是需要相互通信的那些进程。这些进程的数量是有限的,即使是一个比较复杂的系统,最多十几个进程也就差不多了。因此,mosquitto 这个实现是完全可以支撑系统负载的。

那么,如果在云端部署一个 MQTT Broker,理论上是可以直接使用 mosquitto 这个实现来作为消息总线的,但是你要评估接入的客户端(也就是网关)在一个什么样的数量级,考虑到并发的问题,一定要做压力测试。

对于后台开发,我的经验不多,不敢(也不能)多言,误导大家就罪过了。不过,对于一般的学习和测试来说,在云端直接部署 mosquitto 作为消息总线,是没有问题的。

三、Proc_Bridge 进程:外部和内部消息总线之间的桥接器

下面这张图,说明了 Proc_Bridge 进程在这个模型中的作用:

  1. 从云平台消息总线接收到的消息,需要转发到内部的消息总线;
  2. 从内部消息总线接收到的消息,需要转发到云平台的消息总线;

如果用 mosquitto 来实现,应该如何来实现呢?

1. mosquitto 的 API 接口

mosquitto 这个实现是基于回调函数的机制来运行的,例如:

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
scss复制代码// 连接成功时的回调函数
void my_connect_callback(struct mosquitto *mosq, void *obj, int rc)
{
// ...
}

// 连接失败时的回调函数
void my_disconnect_callback(struct mosquitto *mosq, void *obj, int result)
{
// ...
}

// 接收到消息时的回调函数
void my_message_callback(struct mosquitto *mosq, void *obj, const struct mosquitto_message *message)
{
// ..
}

int main()
{
// 其他代码
// ...

// 创建一个 mosquitto 对象
struct mosquitto g_mosq = mosquitto_new("client_name", true, NULL);

// 注册回调函数
mosquitto_connect_callback_set(g_mosq, my_connect_callback);
mosquitto_disconnect_callback_set(g_mosq, my_disconnect_callback);
mosquitto_message_callback_set(g_mosq, my_message_callback);
// 这里还有其他的回调函数设置

// 开始连接到消息总线
mosquitto_connect(g_mosq, "127.0.0.1", 1883, 60);

while(1)
{
int rc = mosquitto_loop(g_mosq, -1, 1);
if (rc) {
printf("mqtt_portal: mosquitto_loop rc = %d \n", rc);
sleep(1);
mosquitto_reconnect(g_mosq);
}
}

mosquitto_destroy(g_mosq);
mosquitto_lib_cleanup();
return 0;
}

以上代码就是一个 mosquitto 客户端的最简代码了,使用回调函数的机制,让程序的开发非常简单。

mosquitto 把底层的细节问题都帮助我们处理了,只要我们注册的函数被调用了,就说明发生了我们感兴趣的事件。

这样的回调机制在各种开源软件中使用的比较多,比如:glib 里的定时器、libevent通讯处理、libmodbus 里的数据处理、linux 内核中的驱动开发和定时器,都是这个套路,一通百通!

在网关中的每个进程,只需要添加上面这部分代码,就可以挂载到消息总线上,从而可以与其它进程进行收发数据了。

2. 利用 UserData 指针,实现多个 MQTT 连接

上面的实例仅仅是连接到一个消息总线上,对于一个普通的进程来说,达到了通信的目的。

但是对于 Proc_Bridge 进程来说,还没有达到目的,因为这个进程处于桥接的位置,需要同时连接到远程和本地这两个消息总线上。那么应该如何实现呢?

看一下 mosquitto_new 这个函数的签名:

1
2
3
4
5
c复制代码/*
* obj - A user pointer that will be passed as an argument to any
* callbacks that are specified.
*/
libmosq_EXPORT struct mosquitto *mosquitto_new(const char *id, bool clean_session, void *obj);

最后一个参数的作用是:可以设置一个用户自己的数据(作为指针传入),那么 mosquitto 在回调我们的注册的任何一个函数时,都会把这个指针传入。因此,我们可以利用这个参数来区分这个连接是远程连接?还是本地连接。

所以,我们可以定义一个结构体变量,把一个 MQTT 连接的所有信息都记录在这里,然后注册给 mosquitto。当 mosquitto 回调函数时,把这个结构体变量的指针回传给我们,这样就拿到了这个连接的所有数据,在某种程度上来说,这也是一种面向对象的思想。

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码// 从来表示一个 MQTT 连接的结构体
typedef struct{
char *id;
char *name;
char *pw;
char *host;
int port;
pthread_t tHandle;
struct mosquitto *mosq;
int mqtt_num;
}MQData;

完整的代码已经放到网盘里了,为了让你先从原理上看明白,我把关键几个地方的代码贴在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码// 分配结构体变量
MQData userData = (MQData *)malloc(sizeof(MQData));

// 设置属于这里连接的参数: id, name 等等

// 创建 mosquitto 对象时,传入 userData。
struct mosquitto *mosq = mosquitto_new(userData->id, true, userData);

// 在回调函数中,把 obj 指针前转成 MQData 指针
static void messageCB(struct mosquitto *mosq, void *obj, const struct mosquitto_message *message)
{
MQData *userData = (MQData *)obj;

// 此时就可以根据 userData 指针中的内容分辨出这是哪一个链接了
}

另外一个问题:不知道你是否注意到示例中的 mosquitto_loop() 这个函数?这个函数需要放在 while 死循环中不停的调用,才能出发 mosuiqtto 内部的事件。(其实在 mosuiqtto 中,还提供了另一个简化的函数 mosquitto_loop_forever)。

也就是说:在每个连接中,需要持续的触发 mosquitto 底层的事件,才能让消息系统顺利的收发。因此,在示例代码中,使用两个线程分别连接到云平台的总线和内部的总线。

四、总结

经过这两篇文章,基本上把一个物联网系统的网关中,最基本的通信模型聊完了,相当于是一个程序的骨架吧,剩下的事情就是处理业务层的细节问题了。

万里长征,这才是第一步!

对于一个网关来说,还有其他更多的问题需要处理,比如:MQTT 连接的鉴权(用户名+密码,证书)、通信数据的序列化和反序列化、加密和解密等等,以后慢慢聊吧,希望我们一路前行!

在公众号回复:mqtt,获取示例代码的网盘地址。


【原创声明】

转载:欢迎转载,但未经作者同意,必须保留此段声明,必须在文章中给出原文连接。


不吹嘘,不炒作,不浮夸,认真写好每一篇文章!

欢迎转发、分享给身边的技术朋友,道哥在此表示衷心的感谢!


推荐阅读
我最喜欢的进程之间通信方式-消息总线

C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻

一步步分析-如何用C实现面向对象编程

提高代码逼格的利器:宏定义-从入门到放弃

原来gdb的底层调试原理这么简单

利用C语言中的setjmp和longjmp,来实现异常捕获和协程

关于加密、证书的那些事

深入LUA脚本语言,让你彻底明白调试原理

本文转载自: 掘金

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

教你如何优雅的改写“if-else”

发表于 2021-02-23

摘要:这些场景,你是怎么写的代码?

if-else,这是个再正常不过的coding习惯,当我们代码量小的时候用来做条件判断是再简单不过的了。但对于优秀程序员来说,这却不是好代码。

不信你往下看…

  1. 卫语句提前return

假设有如下代码

通过对判断条件取反,代码在逻辑表达上会更加清晰

  1. 使用Optional简化if判空

2.1 简化1级判空

假设有如下代码

使用Optional后

2.2 简化多级判空

假设有如下代码

使用Optional后

对于没有else的场景,使用ifPresent即可

  1. 策略模式

假设有如下代码:

这就是不要根据不同的参数类型走不同的代码逻辑,这种场景很常见,他还会以switch-case的方式出现:

不同的代码逻辑就代表了不同的策略,我们可以通过如下几个方式改写。

3.1 多态

具体的策略对象可以放在一个Map中,优化后的实现类似如下

关于如何存放到Map中也两个可以参考的方式。

3.1.1 静态表

3.1.2 Spring托管下的动态注册

定义一个注册中心用于接受注册信息

将每个Strategy交由Spring管理,并在构造后注册

使用方式就变成了

3.2 枚举

采用多态会额外产生很多策略类,如果我们已经预先将petType定义成了枚举,就会发现可以把Strategy中的invoke()方法放到枚举中,从而完成了一种映射关系。

这样在调用时的代码就类似如下:

3.3 函数式简化策略

同样面对多态会额外产生很多策略类的问题,除了枚举我们还可以使用函数式的方式来改写,这里有个前提最好是策略的内容不会过于复杂,不然在代码的可读性上会比较差

同样我们会有一个map静态表,不过map里面存放的是lambda

使用方式则变成了

本文分享自华为云社区《改写if-else的几个思路》,原文作者:技术火炬手。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

Spring Security 实战干货:OAuth2登录获

发表于 2021-02-23

  1. 前言

在上一篇Spring Security 实战干货:OAuth2授权回调的核心认证流程中,我们讲了当第三方同意授权后会调用redirectUri发送回执给我们的服务器。我们的服务器拿到一个中间授信凭据会再次进行认证,目的是为了获取Token。而这个逻辑由OAuth2LoginAuthenticationProvider负责,经过上一文的分析后我们发现获取Token的具体逻辑由OAuth2AuthorizationCodeAuthenticationProvider来完成,今天就把它的流程搞清楚,来看看Spring Security OAuth2 认证授权获取Token的具体步骤。

注意:本Spring Security干货系列教程的OAuth2相关部分是在Spring Security 5.x版本的。

  1. OAuth2AuthorizationCodeAuthenticationProvider

该类是AuthenticationProvider针对OAuth 2.0中Authorization Code Grant模式的实现。关于AuthenticationProvider有必要简单强调一下,它已经多次在Spring Security干货系列中出现,十分重要!一定要去看看相关的分析和使用,它是你根据业务扩展认证方式渠道的重要入口。

2.1 OAuth2AccessTokenResponseClient

在该实现中包含了一个OAuth2AccessTokenResponseClient成员变量,它抽象了通过tokenUri端点从认证服务器获取Token的细节。你可以根据OAuth 2.0常用的四种模式来进行实现它, 以达到根据不同的策略来获取Token的能力。

OAuth 2.0 四种模式的对应实现

在Spring Security 5中OAuth 2.0登录的配置中默认使用DefaultAuthorizationCodeTokenResponseClient。如果你想使用自定义实现的话可以通过HttpSecurity来配置:

1
2
3
4
5
6
7
8
9
java复制代码        @Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.tokenEndpoint()
// 注入自定义的 OAuth2AccessTokenResponseClient
.accessTokenResponseClient(authorizationCodeTokenResponseClient);
// 其它省略

}

接下来我们看看DefaultAuthorizationCodeTokenResponseClient实现的获取Token的逻辑:

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复制代码@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
// 1. 封装调用tokenUri所需要的请求参数RequestEntity
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);

ResponseEntity<OAuth2AccessTokenResponse> response;
try {
// 2. 通过RestTemplate 发起请求获取 OAuth2AccessTokenResponse
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}

// 3. 解析 ResponseEntity 组织返回值 OAuth2AccessTokenResponse
OAuth2AccessTokenResponse tokenResponse = response.getBody();

if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {

// originally requested by the client in the Token Request
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
.scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes())
.build();
}

return tokenResponse;
}

这里的方式跟我另一个开源项目Payment Spring Boot的请求方式异曲同工,都是三个步骤:

  1. 组织参数RequestEntity。
  2. RestOperations发起请求。
  3. 解析ResponseEntity组织返回值。

如果有些的OAuth 2.0认证服务器获取Token的方式比较特殊你可以自行实现OAuth2AccessTokenResponseClient。

  1. 总结

OAuth2AccessTokenResponseClient是OAuth2AuthorizationCodeAuthenticationProvider的核心要点。搞清楚它的作用和机制就可以了。这里我们总结一下OAuth2AuthorizationCodeAuthenticationProvider的认证过程:

  1. 检测未授信OAuth2AuthorizationCodeAuthenticationToken的状态是否合法。
  2. 通过OAuth2AccessTokenResponseClient请求OAuth 2.0认证服务器获取Token等信息。
  3. 组装认证过的授信OAuth2AuthorizationCodeAuthenticationToken返回。

到此OAuth 2.0的登录流程就搞清楚了,读者可通过系列文章进行学习批判。我是:码农小胖哥,多多关注,获取实用的编程干货。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

本文转载自: 掘金

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

Spring Boot WebFlux 使用 R2DBC 连

发表于 2021-02-23

本文使用两种方法介绍 Spring Boot WebFlux 使用 R2DBC 连接 MySQL:

  • 使用配置 Java 类
  • 使用配置文件:application.yml

相关技术点:WebFlux、R2DBC 等技术请自行了解。

Spring Boot WebFlux 使用 R2DBC 技术连接 MySQL,首先需要在 WebFlux 项目的基础上导入以下两个依赖:

1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>

方法一:使用 Java 配置文件

在 Spring Boot WebFlux 项目中创建目录 config,添加以下两个配置 Java 类即可。

ConnectionFactoryConfiguration.java 配置类,用来配置 MySQL 的连接信息。如下:

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
java复制代码package com.codergeshu.webfluxr2dbc.config;

import dev.miku.r2dbc.mysql.MySqlConnectionConfiguration;
import dev.miku.r2dbc.mysql.MySqlConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @Date: 2021/2/23 10:28
* @author: Eric
*/

@Configuration
public class ConnectionFactoryConfiguration {
@Bean
public ConnectionFactory connectionFactory() {
return MySqlConnectionFactory.from(MySqlConnectionConfiguration.builder()
.host("127.0.0.1") // 主机地址
.port(3306) // 端口
.username("root") // 用户名
.password("123456") // 密码
.database("webflux_demo") // 连接的数据库名称
.build());
}
}

R2dbcConfiguration.java 配置类,用来启用 R2DBC 技术连接 MySQL。代码如下:

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
java复制代码package com.codergeshu.webfluxr2dbc.config;

import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;

/**
* @Date: 2021/1/26 11:29
* @author: Eric
*/

@Configuration
@EnableR2dbcRepositories
public class R2dbcConfiguration extends AbstractR2dbcConfiguration {

private final ConnectionFactory connectionFactory;

public R2dbcConfiguration(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}

@Override
public ConnectionFactory connectionFactory() {
return this.connectionFactory;
}
}

至此使用配置类方式就完成了使用 R2DBC 连接 MySQL,之后就可以使用 ReactiveCrudRepository 进行相关数据库操作了。

方法二:使用 application.yml配置文件

除了使用上述两个配置 Java 类外,我们还可以使用 application.yml 文件进行 R2DBC 连接 MySQL 的配置,内容如下:

1
2
3
4
5
6
7
8
9
java复制代码spring:
r2dbc:
url: r2dbc:mysql://127.0.0.1:3306/webflux_demo?serverTimezone=GMT&characterEncoding=UTF-8
username: root
password: 123456
name: r2dbc
pool:
validation-query: SELECT 1
enabled: true

其配置信息类似于 ConnectionFactoryConfiguration.java 和 R2dbcConfiguration.java 的结合体。

总结

以上两种方法都能够使 WebFlux 项目使用 R2DBC 连接到 MySQL,选择其中一种方法即可,推荐选择 application.yml 配置文件,毕竟比较清晰且容易管理。

作者信息

大家好,我是 CoderGeshu,一个热爱生活的程序员,如果这篇文章对您有所帮助,还请大家给点个赞哦 👍👍👍

另外,欢迎大家关注本人同名公众号:CoderGeshu,一个致力于分享编程技术知识的公众号!!

一个人可以走的很快,而一群人可以走的很远……

本文转载自: 掘金

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

1…716717718…956

开发者博客

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