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

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


  • 首页

  • 归档

  • 搜索

看完这一篇,Python Django 你就学会一半了!

发表于 2021-04-01

Python Django 视图

01 视图返回 JSON 数据

在真实工作中 ,Python Web 工程师会向前端工程师反馈接口数据,接口一般称为 API,常见返回数据的格式是 XML 或者 JSON,接下来,就以最常见的 JSON 格式数据为案例,为你详细说明,Django 中是如何从数据库向前台发送数据的。

修改 views.py 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from django.shortcuts import render
# 导入 JSON 格式数据响应类
from django.http import JsonResponse
from .models import Blog

# Create your views here.
def blog_list(request):
blogs = Blog.objects.all()
# 用列表生成器生成一个对象
context = {
"data": [{"id": blog.id, "title": blog.title} for blog in blogs]
}
return JsonResponse(context)

在该文件头部导入 models 中的 Blog 类,然后在通过新增加一个 blog_list 函数返回 JsonResponse 对象。

02 创建路由

路由相关资料后续会进行补充,本篇博客只需要掌握到通过不同 URL 返回不同数据即可。

在 blog 文件夹中创建一个新的文件 urls.py,代码内容如下:

1
2
3
4
5
6
javascript复制代码from django.urls import path
import blog.views as blog_views

urlpatterns = [
path('list/', blog_views.blog_list)
]

该文件代码编写完毕,还需要修改 my_website 文件夹中的 urls.py 文件,修改部分如下:

1
2
3
4
5
6
7
javascript复制代码from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('blog/', include('blog.urls')),
path('admin/', admin.site.urls),
]

此时你的项目文件结构如下图所示,重点注意 urls.py 文件出现了两次。

通过下述命令运行现在的应用程序:

1
复制代码python manage.py runserver

直接访问 http://127.0.0.1:8000/ 会出如下错误,要求必须访问一个目录。输入 http://127.0.0.1:8000/admin 访问之前博客涉及的后台,输入 http://127.0.0.1:8000/blog/list/ 得到 JSON 格式的数据。

JSON 格式数据如下,中文被进行了 UNICODE 编码

同样可以直接通过开发者工具进行查询,点击下图蓝色矩形区域即可。

应用完成之后,就可以进行复盘学习了。

通过 URL 地址进行访问,如果访问的地址是 /blog,Django 会自动加载 urls.py 中的配置,即下述代码:

1
2
3
4
ini复制代码urlpatterns = [
path('blog/', include('blog.urls')),
path('admin/', admin.site.urls),
]

发现匹配到 url 中的 blog/,然后加载 blog.urls 文件,对应文件代码如下:

1
2
3
4
5
6
javascript复制代码from django.urls import path
import blog.views as blog_views

urlpatterns = [
path('list/', blog_views.blog_list)
]

这里包含了一个 list/ 匹配器,所以通过 blog/list/ 调用了视图中 blog_view.blog_list 函数,该函数用于返回 JSON 格式的数据。注意 blog_view 是导入的 view 模块进行重命名得来,代码在头部 import blog.views as blog_views。

1
2
3
4
5
6
7
python复制代码def blog_list(request):
blogs = Blog.objects.all()
# 用列表生成器生成一个对象
context = {
"data": [{"id": blog.id, "title": blog.title} for blog in blogs]
}
return JsonResponse(context)

先理解逻辑关系,后续在补充专业的语法定义。

03 扩展详情页

有了上文的逻辑关系之后,在实现一个返回单条博客数据的接口,首先编辑 views.py 文件。

1
2
3
4
5
6
7
8
9
10
python复制代码def detail(request, blog_id):
blog = Blog.objects.get(id=blog_id)

blog = {
"id": blog.id,
"title": blog.title,
"content": blog.content,
"create_time": blog.create_time
}
return JsonResponse(blog)

扩展详情页,发现一个单词拼写错误,修改 create_time 之后,注意使用如下命令对 sqlite 进行重新生成。

1
2
3
4
5
vbnet复制代码> python manage.py makemigrations blog
Did you rename blog.creatr_time to blog.create_time (a DateField)? [y/N] y
Migrations for 'blog':
blog\migrations\0002_auto_20210329_0940.py
- Rename field creatr_time on blog to create_time

该命令运行完,再运行下述命令:

1
2
3
4
5
yaml复制代码>python manage.py migrate blog
Operations to perform:
Apply all migrations: blog
Running migrations:
Applying blog.0002_auto_20210329_0940... OK

继续修改 blog 文件夹中的 urls.py 文件,代码如下:

1
2
3
4
5
6
7
javascript复制代码from django.urls import path
import blog.views as blog_views

urlpatterns = [
path('list/', blog_views.blog_list),
path('detail/<int:blog_id>', blog_views.detail)
]

编写完毕以上代码之后,就可以通过 http://127.0.0.1:8000/blog/detail/1 进行单条博客数据获取,地址 URL 的格式为 http://127.0.0.1:8000/blog/detail/{整数序号}。

在地址中输入整数序号,可以被后台的 blog_id 获取到,而 blog_id 将会传递到 detail 方法中。

上述代码还有一个需要特别说明一下 int:blog\_id 前面的 int 被称作路径转换器,常见的路径转换器有如下内容:

str:匹配任何非空字符串,默认值;

int:匹配零或正整数;

uuid:匹配一种特定类型的字符串格式;

path:匹配任何非空字符,可以匹配路径地址,包括 /。

04 分页实现

接下来继续对 blog_list 方法进行扩展,让其实现分页操作。重点对 views.py 中的 blog_list(request) 方法进行改造,核心代码参考下述内容。

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
python复制代码from django.shortcuts import render
# 导入 JSON 格式数据响应类
from django.http import JsonResponse
# 导入分页组件
from django.core.paginator import Paginator
from .models import Blog

# Create your views here.
def blog_list(request):
# blogs = Blog.objects.all()
# # 用列表生成器生成一个对象
# context = {
# "data": [{"id": blog.id, "title": blog.title} for blog in blogs]
# }
# 页码默认获取 page 参数,默认值为 1
page = request.GET.get("page", 1)
# 默认每页 20 条数据
page_size = request.GET.get("page_size", 20)

# 获取全部数据
blog_all = Blog.objects.all()
# 分页对象
paginator = Paginator(blog_all, page_size)

# 当前页码
current_page = paginator.get_page(page)
blogs = current_page.object_list

context = {
"blog_list": [{"id": blog.id, "title": blog.title} for blog in blogs],
"paginator": {
"total_count": paginator.count,
"num_pages": paginator.num_pages,
"page_size": paginator.per_page,
"page_number": current_page.number
}
}

return JsonResponse(context)

在代码顶部导入 from django.core.paginator import Paginator 分页模块,用于后续的分页内容,数据通过 Blog.objects.all() 方法提前获取,该方式存在效率问题,后续学习到进阶内容,将会对本部分进行修改。

编写完毕,通过 URL 相关参数即可实现分页效果。访问 http://127.0.0.1:8000/blog/list/?page=1&page_size=1 得到的效果如下:

05 这篇博客的总结

本篇博客重点学习了 Django 中的视图,本文是基于函数的视图(FBV),在以后的博客中,我们将学习基于类的视图(CBV),在实际的开发中基于类视图应用比较广泛,使用 Django 开发 API 也有成熟的框架借鉴,例如 Django Rest Framework。

本文转载自: 掘金

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

Redission分布式锁

发表于 2021-04-01

Redission是Redis官方推荐的客户端,提供了一个RLock的锁,RLock继承自juc的Lock接口,提供了中断,超时,尝试获取锁等操作,支持可重入,互斥等特性。

基本原理

RLock底层使用Redis的Hash作为存储结构,其中Hash的key用于存储锁的名字,Hash的filed用于存储客户端id,filed对应的value是线程重入次数。

客户端id

客户端id是用于区分每个加锁的线程的,由两部分组成: RedissonLock的成员变量id + 当前线程id Thread.currentThread().getId()。
其中id是一个UUID,在每次实例化Redisson对象实例的时候都会创建一个ConnectionManager,该类会在实例化时生成一个UUID(UUID.randomUUID()), 所以对于每一个Redisson在其生命周期中该id都是相同的,区别在于threadId的不同;而不同Redisson对象之间id也不同,这样可以很好的对不同服务中的线程进行区分。

线程重入次数

可以参考Java的重入锁,用于表示该锁当前被递归的次数,该值>=1,如果等于0时该锁会被删除。

加锁

如何实现可重入加锁

为了实现加锁的原子性,Redisson使用Lua脚本的形式进行加锁。
该脚本位于RedissonLock#tryLockInnerAsync中

1
2
3
4
5
6
7
8
9
10
11
lua复制代码if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);

其中KEYS[1]是Hash的key也就是RLock的名字,ARGV[2]是客户端id,ARGV[1]是Hash的生存时间TTL。
首先判断KEYS[1]对应的Hash是否存在,如果不存在直接创建一个新的Hash,对应的filed值为1也就是说第一次重入次数为1,并且设置超时时间,最终返回null;
如果Hash已经存在且filed为ARGV[2]的项也存在,说明是该线程递归进入该锁,需要对该filed增加一次重入次数,并且更新超时时间,最终返回null;
如果当前锁已经存在,且不是当前线程持有的,就会返回当前锁的TTL。

互斥性实现

每次进行加锁时会返回锁的TTL,如果TTL为null说明加锁成功,直接返回即可,否则说明已有其他线程持有该锁。后续该线程会进行循环去尝试获取锁直到加锁成功。如果使用tryLock则可以在超时时间结束后直接返回。

Lease续约

如果锁设置了持有锁的超时时间,在超时后会进行锁的释放,如果获取锁的时候不指定持有锁的时间,那么默认获取锁30s后超时。为了防止任务没有执行完就释放锁,Redisson使用一个守护线程(看门狗任务)定时刷新(超时时间的 1/3, 默认是10s,也就是每10s续约30s,直到线程自己释放)这个锁超时时间进行续约,也就是只要这个锁被获取了,则力保这个锁一直不超时,除非获取锁的线程主动释放。由于获取到锁和这个续命任务的守护线程是在同一个线程的,当获取锁的线程挂掉了,意味着刷新任务的线程也会停止执行,就不会再刷新锁的超时时间。

解锁

解锁同样使用Lua脚本执行,代码为与#unlockInnerAsync方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lua复制代码if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;

首先判断锁是否是存在的,如果不存在直接返回nil;
如果该线程持有锁,则对当前的重入值-1,如果计算完后大于0,重新设置超时持有实践返回0;
如果算完不大于0,删除这个Hash,并且进行广播,通知watch dog停止进行刷新,并且 返回1.

优缺点

优点

  1. Redisson 通过 Watch Dog机制可以解决锁的续期问题;
  2. 和Zookeeper相比较,Redisson基于Redis性能更高,适合对性能要求高的场景。
  3. Redisson 实现分布式可重入锁,比原生的 SET mylock userId NX PX milliseconds + lua 实现的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节。在等待申请锁资源的进程等待申请锁的实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率;
  4. Redison实现了jucLock接口,完整的实现了超时,中断,可重入等各种特性。

缺点

  1. Redisson没有办法解决节点宕机问题,不能达到ZK的一致性;
  2. Redison的机制比较复杂,如果对其底层实现不是很熟悉会出现很多预期外的

Redisson红锁实现

Redisson锁并没有解决主从节点切换可能导致重复加锁的问题,即某个客户端在Master节点加锁,此时主节点宕机,由于主从之间异步复制,从节点没有来得及复制,此时选举出新的Master后并没有之前的锁,另外一个客户端要对同一个锁进行操作时可以直接加锁,那么有两个客户端同时持有一把锁,这样锁住的共享资源会被重复读取,造成混乱。

Redisson提供了RedissonRedLock锁实现了RedLock,需要同时使用多个独立的Redis实例分别进行加锁,只有超过一半的锁加锁成功,则认为是成功加锁。

本文转载自: 掘金

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

SpringBoot 2x 操作 Excel 导入导出

发表于 2021-04-01

介绍

在本教程中,学习如何使用Apache POI和JExcel APIs来处理Excel电子表格。

这两个库都可用于动态读取,写入和修改Excel电子表格的内容,并提供将Microsoft Excel集成到Java应用程序中的有效方法。

快速创建实例

前往 start.spring.io/ 如下所示

初始化项目
点击GENERATE生产一个zip解压导入IDEA工具即可

Maven 依赖

首先,我们需要切换国内依赖下载源,在pom.xml文件中<build></build>后添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
xml复制代码<repositories>
<!--阿里云主仓库,代理了maven central和jcenter仓库-->
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!--阿里云代理Spring 官方仓库-->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<!--阿里云代理Spring 插件仓库-->
<pluginRepository>
<id>spring-plugin</id>
<name>spring-plugin</name>
<url>https://maven.aliyun.com/repository/spring-plugin</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

然后将以下依赖项添加到我们的pom.xml文件中:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.0.0</version>
</dependency>

<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>

Apache POI

Apache POI 库同时支持.xls和.xlsx文件,并且在处理Excel文件比其他Java库更为优秀。

它提供了用于为Excel文件建模的Workbook界面,以及为Excel文件的元素建模的工作表,行和单元格界面,以及两种文件格式的每个界面的实现。

使用较新的.xlsx文件格式时,应使用XSSFWorkbook,XSSFSheet,XSSFRow和XSSFCell类。

要使用旧的.xls格式,请使用HSSFWorkbook,HSSFSheet,HSSFRow和HSSFCell类。

读取 Excel

让我们创建一个方法来打开一个.xlsx文件,然后从该文件的第一张表中读取内容。

因为单元格中数据的类型不同,读取单元格内容的方法可以自己定义。

可以使用Cell接口的getCellTypeEnum()方法确定单元格内容的类型。

首先,让我们获取文件的后缀来判断用HSSFWorkbook还是XSSFWorkbook进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码String[] postfixArray = path.split("\\.");
int lastIndex = postfixArray.length - 1;
String postfix = postfixArray[lastIndex];
Workbook wb;
switch (postfix) {
case ExcelUtil.MICROSOFT_EXCEL_2003:
wb = new HSSFWorkbook(fileInput);
break;
case ExcelUtil.MICROSOFT_EXCEL_2007:
wb = new XSSFWorkbook(fileInput);
break;
default:
throw new Exception("文件不符合要求");
}

开始读取Excel文件

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
java复制代码    public static List<List<String>> readExcel(String path) throws Exception {
File excelFile = new File(path);
if (!excelFile.exists()) {
throw new FileNotFoundException("文件不存在");
}
FileInputStream fileInput = new FileInputStream(excelFile);
/*
根据文件后缀判断用xls或是xlsx处理
*/
String[] postfixArray = path.split("\\.");
int lastIndex = postfixArray.length - 1;
String postfix = postfixArray[lastIndex];

Workbook wb;
switch (postfix) {
case ExcelUtil.MICROSOFT_EXCEL_2003:
wb = new HSSFWorkbook(fileInput);
break;
case ExcelUtil.MICROSOFT_EXCEL_2007:
wb = new XSSFWorkbook(fileInput);
break;
default:
throw new Exception("文件不符合要求");
}
List<List<String>> dataList = new ArrayList<>();
/*
* wb.getSheetAt(0) 简单的取第一个sheet的表格读取
*/
for (Row row : wb.getSheetAt(0)) {
List<String> rowList = new ArrayList<>();
for (Cell cell : row) {
rowList.add(getCellValue(cell));
}
dataList.add(rowList);
}
return dataList;
}

获取单元格的getCellValue()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static String getCellValue(Cell cell) {
switch (cell.getCellType()) {
case BOOLEAN:
return ......;
case STRING:
return ......;
case NUMERIC:
return ......;
case FORMULA:
return ......;
default:
return "";
}
}

当单元格类型的枚举值为STRING时,将使用Cell接口的getStringCellValue()方法读取内容

1
java复制代码return cell.StringCellValue()

具有NUMERIC内容类型的单元格可以包含日期或数字,可以通过以下方式进行读取:

1
2
3
4
5
6
java复制代码if (DateUtil.isCellDateFormatted(cell)) {
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
return timeFormatter.format(cell.getLocalDateTimeCellValue());
} else {
return cell.getNumericCellValue() + "";
}

对于BOOLEAN值,我们有getBooleanCellValue()方法:

1
java复制代码return String.valueOf(cell.getBooleanCellValue());

当单元格类型为FORMULA时,我们可以使用getCellFormula()方法:

1
java复制代码return cell.getCellFormula() + "";

执行测试结果:

image.png

导出 Excel

Apache POI使用上一节中介绍的相同接口来写入 Excel 文件。

让我们创建一个方法,把之前导入的数据进行导出到新的 Excel 文件:

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复制代码public static void exportExcel(List<List<String>> dataList) throws IOException {
XSSFWorkbook workbook = new XSSFWorkbook();

Sheet sheet = workbook.createSheet("sheetNameTest");

Row header = sheet.createRow(0);

CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFillForegroundColor(IndexedColors.INDIGO.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

XSSFFont font = workbook.createFont();
font.setFontName("Arial");
font.setFontHeightInPoints((short) 16);
font.setColor(IndexedColors.WHITE.getIndex());
headerStyle.setFont(font);
headerStyle.setBorderBottom(BorderStyle.THIN); //下边框
headerStyle.setBorderLeft(BorderStyle.THIN);//左边框
headerStyle.setBorderTop(BorderStyle.THIN);//上边框
headerStyle.setBorderRight(BorderStyle.THIN);//右边框

// 取标题
List<String> headerList = dataList.get(0);
for (int i = 0; i < headerList.size(); i++) {
sheet.setColumnWidth(i, headerList.get(i).length()*512);
Cell headerCell = header.createCell(i);
headerCell.setCellStyle(headerStyle);
headerCell.setCellValue(headerList.get(i));
}

CellStyle style = workbook.createCellStyle();
style.setWrapText(true);
style.setBorderBottom(BorderStyle.THIN); //下边框
style.setBorderLeft(BorderStyle.THIN);//左边框
style.setBorderTop(BorderStyle.THIN);//上边框
style.setBorderRight(BorderStyle.THIN);//右边框

for (int i = 1; i < dataList.size(); i++) {
Row row = sheet.createRow(i);
List<String> rowList = dataList.get(i);
for (int j = 0; j < rowList.size(); j++) {
Cell cell = row.createCell(j);
cell.setCellValue(rowList.get(j));
cell.setCellStyle(style);
}
}
File currDir = new File(".");
String path = currDir.getAbsolutePath();
String fileLocation = path.substring(0, path.length() - 1) + "test_emp.xlsx";

System.out.println(fileLocation);
FileOutputStream outputStream = new FileOutputStream(fileLocation);
workbook.write(outputStream);
workbook.close();

}

测试

1
2
3
4
5
6
7
8
9
10
java复制代码@SpringBootTest
class SpringbootExcelApplicationTests {

@Test
void contextLoads() throws Exception {
List<List<String>> dataList = ExcelUtil.readExcel("employees.xlsx");
ExcelUtil.exportExcel(dataList);
}

}

输出文档截图

image.png

本文转载自: 掘金

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

SpringBoot 整合:Redis延时队列的简单实现(基

发表于 2021-04-01

公众号:Java小咖秀,网站:javaxks.com

作者:大风的博客, 链接: blog.csdn.net/qq330983778…

设计

之前学习 Redis 的时候发现有赞团队之前分享过一篇关于延时队列的设计:有赞延时队列 现在就尝试实现一下

业务流程

首先我们分析下这个流程

  1. 用户提交任务。首先将任务推送至延迟队列中。
  2. 延迟队列接收到任务后,首先将任务推送至 job pool 中,然后计算其执行时间。
  3. 然后生成延迟任务(仅仅包含任务 id)放入某个桶中
  4. 时间组件时刻轮询各个桶,当时间到达的时候从 job pool 中获得任务元信息。
  5. 监测任务的合法性如果已经删除则 pass。继续轮询。如果任务合法则再次计算时间
  6. 如果合法则计算时间,如果时间合法:根据 topic 将任务放入对应的 ready queue,然后从 bucket 中移除。如果时间不合法,则重新计算时间再次放入 bucket,并移除之前的 bucket 中的内容
  7. 消费端轮询对应 topic 的 ready queue。获取 job 后做自己的业务逻辑。与此同时,服务端将已经被消费端获取的 job 按照其设定的 TTR,重新计算执行时间,并将其放入 bucket。
  8. 完成消费后,发送 finish 消息,服务端根据 job id 删除对应信息。

用户任务池延时任务时间循环待完成任务提交任务提交延时任务轮询任务任务已经到达时间用户领取任务设置其完成超时时间, 然后保存进延时任务中任务超时任务完成或者任务删除检测到任务不存在队列中移除用户任务池延时任务时间循环待完成任务

对象

我们现在可以了解到中间存在的几个组件

  1. 延迟队列,为 Redis 延迟队列。实现消息传递
  2. Job pool 任务池保存 job 元信息。根据文章描述使用 K/V 的数据结构,key 为 ID,value 为 job
  3. Delay Bucket 用来保存业务的延迟任务。文章中描述使用轮询方式放入某一个 Bucket 可以知道其并没有使用 topic 来区分,个人这里默认使用顺序插入
  4. Timer 时间组件,负责扫描各个 Bucket。根据文章描述存在多个 Timer,但是同一个 Timer 同一时间只能扫描一个 Bucket
  5. Ready Queue 负责存放需要被完成的任务,但是根据描述根据 Topic 的不同存在多个 Ready Queue。

其中 Timer 负责轮询,Job pool、Delay Bucket、Ready Queue 都是不同职责的集合。

任务状态

  • ready:可执行状态,
  • delay:不可执行状态,等待时钟周期。
  • reserved:已被消费者读取,但没有完成消费。
  • deleted:已被消费完成或者已被删除。

对外提供的接口

接口 描述 数据
add 添加任务 Job 数据
pop 取出待处理任务 topic 就是任务分组
finish 完成任务 任务 ID
delete 删除任务 任务 ID

额外的内容

  1. 首先根据状态状态描述,finish 和 delete 操作都是将任务设置成 deleted 状态。
  2. 根据文章描述的操作,在执行 finish 或者 delete 的操作的时候任务已经从元数据中移除,此时 deleted 状态可能只存在极短时间,所以实际实现中就直接删除了。
  3. 文章中并没有说明响应超时后如何处理,所以个人现在将其重新投入了待处理队列。
  4. 文章中因为使用了集群,所以使用 redis 的 setnx 锁来保证多个时间循环处理多个桶的时候不会出现重复循环。这里因为是简单的实现,所以就很简单的每个桶设置一个时间队列处理。也是为了方便简单处理。关于分布式锁可以看我之前的文章里面有描述。

实现

现在我们根据设计内容完成设计。这一块设计我们分四步完成

任务及相关对象

目前需要两个对象,一个是任务对象(job)一个负责保存任务引用的对象(delay job)

任务对象

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
less复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class Job implements Serializable {

/**
* 延迟任务的唯一标识,用于检索任务
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

/**
* 任务类型(具体业务类型)
*/
private String topic;

/**
* 任务的延迟时间
*/
private long delayTime;

/**
* 任务的执行超时时间
*/
private long ttrTime;

/**
* 任务具体的消息内容,用于处理具体业务逻辑用
*/
private String message;

/**
* 重试次数
*/
private int retryCount;
/**
* 任务状态
*/
private JobStatus status;
}

任务引用对象

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
kotlin复制代码@Data
@AllArgsConstructor
public class DelayJob implements Serializable {


/**
* 延迟任务的唯一标识
*/
private long jodId;

/**
* 任务的执行时间
*/
private long delayDate;

/**
* 任务类型(具体业务类型)
*/
private String topic;


public DelayJob(Job job) {
this.jodId = job.getId();
this.delayDate = System.currentTimeMillis() + job.getDelayTime();
this.topic = job.getTopic();
}

public DelayJob(Object value, Double score) {
this.jodId = Long.parseLong(String.valueOf(value));
this.delayDate = System.currentTimeMillis() + score.longValue();
}
}

容器

目前我们需要完成三个容器的创建,Job 任务池、延迟任务容器、待完成任务容器

job 任务池,为普通的 K/V 结构,提供基础的操作

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
java复制代码@Component
@Slf4j
public class JobPool {

@Autowired
private RedisTemplate redisTemplate;

private String NAME = "job.pool";

private BoundHashOperations getPool () {
BoundHashOperations ops = redisTemplate.boundHashOps(NAME);
return ops;
}

/**
* 添加任务
* @param job
*/
public void addJob (Job job) {
log.info("任务池添加任务:{}", JSON.toJSONString(job));
getPool().put(job.getId(),job);
return ;
}

/**
* 获得任务
* @param jobId
* @return
*/
public Job getJob(Long jobId) {
Object o = getPool().get(jobId);
if (o instanceof Job) {
return (Job) o;
}
return null;
}

/**
* 移除任务
* @param jobId
*/
public void removeDelayJob (Long jobId) {
log.info("任务池移除任务:{}",jobId);
// 移除任务
getPool().delete(jobId);
}
}

延迟任务,使用可排序的 ZSet 保存数据,提供取出最小值等操作

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
typescript复制代码@Slf4j
@Component
public class DelayBucket {

@Autowired
private RedisTemplate redisTemplate;

private static AtomicInteger index = new AtomicInteger(0);

@Value("${thread.size}")
private int bucketsSize;

private List <String> bucketNames = new ArrayList <>();

@Bean
public List <String> createBuckets() {
for (int i = 0; i < bucketsSize; i++) {
bucketNames.add("bucket" + i);
}
return bucketNames;
}

/**
* 获得桶的名称
* @return
*/
private String getThisBucketName() {
int thisIndex = index.addAndGet(1);
int i1 = thisIndex % bucketsSize;
return bucketNames.get(i1);
}

/**
* 获得桶集合
* @param bucketName
* @return
*/
private BoundZSetOperations getBucket(String bucketName) {
return redisTemplate.boundZSetOps(bucketName);
}

/**
* 放入延时任务
* @param job
*/
public void addDelayJob(DelayJob job) {
log.info("添加延迟任务:{}", JSON.toJSONString(job));
String thisBucketName = getThisBucketName();
BoundZSetOperations bucket = getBucket(thisBucketName);
bucket.add(job,job.getDelayDate());
}

/**
* 获得最新的延期任务
* @return
*/
public DelayJob getFirstDelayTime(Integer index) {
String name = bucketNames.get(index);
BoundZSetOperations bucket = getBucket(name);
Set<ZSetOperations.TypedTuple> set = bucket.rangeWithScores(0, 1);
if (CollectionUtils.isEmpty(set)) {
return null;
}
ZSetOperations.TypedTuple typedTuple = (ZSetOperations.TypedTuple) set.toArray()[0];
Object value = typedTuple.getValue();
if (value instanceof DelayJob) {
return (DelayJob) value;
}
return null;
}

/**
* 移除延时任务
* @param index
* @param delayJob
*/
public void removeDelayTime(Integer index,DelayJob delayJob) {
String name = bucketNames.get(index);
BoundZSetOperations bucket = getBucket(name);
bucket.remove(delayJob);
}

}

待完成任务,内部使用 topic 进行细分,每个 topic 对应一个 list 集合

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
typescript复制代码@Component
@Slf4j
public class ReadyQueue {

@Autowired
private RedisTemplate redisTemplate;

private String NAME = "process.queue";

private String getKey(String topic) {
return NAME + topic;
}

/**
* 获得队列
* @param topic
* @return
*/
private BoundListOperations getQueue (String topic) {
BoundListOperations ops = redisTemplate.boundListOps(getKey(topic));
return ops;
}

/**
* 设置任务
* @param delayJob
*/
public void pushJob(DelayJob delayJob) {
log.info("执行队列添加任务:{}",delayJob);
BoundListOperations listOperations = getQueue(delayJob.getTopic());
listOperations.leftPush(delayJob);
}

/**
* 移除并获得任务
* @param topic
* @return
*/
public DelayJob popJob(String topic) {
BoundListOperations listOperations = getQueue(topic);
Object o = listOperations.leftPop();
if (o instanceof DelayJob) {
log.info("执行队列取出任务:{}", JSON.toJSONString((DelayJob) o));
return (DelayJob) o;
}
return null;
}

}

轮询处理

设置了线程池为每个 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
java复制代码@Component
public class DelayTimer implements ApplicationListener <ContextRefreshedEvent> {

@Autowired
private DelayBucket delayBucket;
@Autowired
private JobPool jobPool;
@Autowired
private ReadyQueue readyQueue;

@Value("${thread.size}")
private int length;

@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
ExecutorService executorService = new ThreadPoolExecutor(
length,
length,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue <Runnable>());

for (int i = 0; i < length; i++) {
executorService.execute(
new DelayJobHandler(
delayBucket,
jobPool,
readyQueue,
i));
}

}
}

测试请求

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
typescript复制代码/**
* 测试用请求
* @author daify
* @date 2019-07-29 10:26
**/
@RestController
@RequestMapping("delay")
public class DelayController {

@Autowired
private JobService jobService;
/**
* 添加
* @param request
* @return
*/
@RequestMapping(value = "add",method = RequestMethod.POST)
public String addDefJob(Job request) {
DelayJob delayJob = jobService.addDefJob(request);
return JSON.toJSONString(delayJob);
}

/**
* 获取
* @return
*/
@RequestMapping(value = "pop",method = RequestMethod.GET)
public String getProcessJob(String topic) {
Job process = jobService.getProcessJob(topic);
return JSON.toJSONString(process);
}

/**
* 完成一个执行的任务
* @param jobId
* @return
*/
@RequestMapping(value = "finish",method = RequestMethod.DELETE)
public String finishJob(Long jobId) {
jobService.finishJob(jobId);
return "success";
}

@RequestMapping(value = "delete",method = RequestMethod.DELETE)
public String deleteJob(Long jobId) {
jobService.deleteJob(jobId);
return "success";
}

}

测试

添加延迟任务

通过 postman 请求:localhost:8000/delay/add

img

此时这条延时任务被添加进了线程池中

1
2
arduino复制代码2019-08-12 21:21:36.589  INFO 21444 --- [nio-8000-exec-6] d.samples.redis.delay.container.JobPool  : 任务池添加任务:{"delayTime":10000,"id":3,"message":"tag:testid:3","retryCount":0,"status":"DELAY","topic":"test","ttrTime":10000}
2019-08-12 21:21:36.609 INFO 21444 --- [nio-8000-exec-6] d.s.redis.delay.container.DelayBucket : 添加延迟任务:{"delayDate":1565616106609,"jodId":3,"topic":"test"}

根据设置 10 秒钟之后任务会被添加至 ReadyQueue 中

1
ini复制代码2019-08-12 21:21:46.744  INFO 21444 --- [pool-1-thread-4] d.s.redis.delay.container.ReadyQueue     : 执行队列添加任务:DelayJob(jodId=3, delayDate=1565616106609, topic=test)

获得任务

这时候我们请求 localhost:8000/delay/pop

这个时候任务被响应,修改状态的同时设置其超时时间,然后放置在 DelayBucket 中

1
2
3
arduino复制代码2019-08-09 19:36:02.342  INFO 58456 --- [nio-8000-exec-3] d.s.redis.delay.container.ReadyQueue     : 执行队列取出任务:{"delayDate":1565321728704,"jodId":1,"topic":"测试"}
2019-08-09 19:36:02.364 INFO 58456 --- [nio-8000-exec-3] d.samples.redis.delay.container.JobPool : 任务池添加任务:{"delayTime":10000,"id":1,"message":"延迟10秒,超时30秒","retryCount":0,"status":"RESERVED","topic":"测试","ttrTime":30000}
2019-08-09 19:36:02.384 INFO 58456 --- [nio-8000-exec-3] d.s.redis.delay.container.DelayBucket : 添加延迟任务:{"delayDate":1565321792364,"jodId":1,"topic":"测试"}

按照设计在 30 秒后,任务假如没有被消费将会重新放置在 ReadyQueue 中

1
2
arduino复制代码2019-08-12 21:21:48.239  INFO 21444 --- [nio-8000-exec-7] d.s.redis.delay.container.ReadyQueue     : 执行队列取出任务:{"delayDate":1565616106609,"jodId":3,"topic":"test"}
2019-08-12 21:21:48.261 INFO 21444 --- [nio-8000-exec-7] d.samples.redis.delay.container.JobPool : 任务池添加任务:{"delayTime":10000,"id":3,"message":"tag:testid:3","retryCount":0,"status":"RESERVED","topic":"test","ttrTime":10000}

任务的删除 / 消费

现在我们请求:localhost:8000/delay/delete

img

此时在 Job pool 中此任务将会被移除,此时元数据已经不存在,但任务还在 DelayBucket 中循环,然而在循环中当检测到元数据已经不存的话此延时任务会被移除。

1
2
arduino复制代码2019-08-12 21:21:54.880  INFO 21444 --- [nio-8000-exec-8] d.samples.redis.delay.container.JobPool  : 任务池移除任务:3
2019-08-12 21:21:59.104 INFO 21444 --- [pool-1-thread-5] d.s.redis.delay.handler.DelayJobHandler : 移除不存在任务:{"delayDate":1565616118261,"jodId":3,"topic":"test"}

本篇文章涉及的源码下载地址:gitee.com/daifyutils/…

本文转载自: 掘金

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

Spring5系列(七) spring对象的生命周期(全

发表于 2021-04-01

本篇文章,我们再来研究一下一道高频的面试题,就是spring所管理对象的生命周期。

一. 传统类的生命周期

在传统的java应用中,bean的生命周期很简单。使用Java关键字new进行bean的实例化,然后该bean就可以使用了。一旦bean不再被使用,则有java的垃圾回收器自动进行垃圾回收。

二. spring控制的对象的生命周期

相比之下,spring容器中的bean的生命周期就显得相对复杂多了。我们为什么要学习对象的生命周期呢,因为有了spring之后,现在都是由spring来控制对象的创建,存活和销毁,所以学习对象的生命周期,有利于我们更好的了解spring,使用spring.

我们本次对于对象的生命周期主要讲解三个阶段,分别是创建阶段,初始化阶段和销毁阶段。然后在最后,我们在给出一个spring容器中的对象完整的生命周期。

2.1 对象的创建阶段

在这个阶段,我们有必要回忆一个问题,就是spring工厂在什么时候创建的对象。这个我们在初始spring这个章节的注意细节中有提到,不知道大家有没有注意到。但是我们的案例:

1
xml复制代码<bean id="user" class="com.spring.User" />
1
2
3
4
5
6
7
8
java复制代码public static void main(String[]args){
//指定spring配置文件并创建工厂
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");

// 根据配置文件中的id值获取对象
User user = (User)ctx.getBean("user");

}

然后我们在注意细节中提到,user对象是什么时候创建的呢,其实是在spring工厂创建的时候,也就是当第一行代码,new ClassPathXmlApplicationContext(“/applicationContext.xml”);这行代码执行完毕的时候,User对象就已经创建出来了。但是要注意的是,并不是说所有的对象都是在工厂创建的同时完成创建,这其实是和对象的scope有关的。scope我们前面提到,有单例的和非单例的区别。当对象是单例的scope的时候,对象的创建是在工厂创建的同时完成创建的。但是如果对象的scope=”prototype” 那么他的创建时机就是在获取对象的时候完成创建。这个大家一定要弄明白。也就是如果此时我再 注解上指定scope=”prototype”, 那么user对象就在执行getBean的时候才会创建出来。而对于单例的对象,如果我们也想让他在使用的时候再创建可以么,也是可以的,只需要在bean标签中加入lazy-init=true, 代表懒加载,那么这个单例的对象就会在获取的时候才被创建,要注意 lazy-init=true只对单例对象生效。我们可以通过在构造方法中打印一句话,来追踪对象的创建时机。

2.2 对象的初始化阶段

spring工厂在创建对象后,会调用对象的初始化方法,完成对应的初始化操作。
初始化方法的提供: 由程序员根据需要,提供初始化方法,最终完成初始化操作
初始化方法的调用: 由spring工厂完成调用。

也就相当于,如果我们想在spring创建好的对象,执行一些操作,spring为我们提供了一个钩子,我们通过这个钩子书写一段代码,spring工厂可以在创建好对象后帮我们执行这段代码,其实就是一种接口回调的思想,那么接下来我们来看一下,如何实现初始化操作。

方式一:实现InitializingBean接口,重写 void afterPropertiesSet() 方法。

通过方法名称我们就是,这个方法是在属性赋值完成后执行。也就是如果有依赖注入,先进行注入,再执行这个方法。好了我们在验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Product implements InitializingBean{

public Product(){
System.out.println("Product.product");
}

// 这就是初始化方法,做一些初始化操作,spring会调用
@Override
public void afterPropertiesSet() throws Exception{
System.out.println("Product.afterPropertiesSet");
}
}

然后我们在spring配置文件中,配置Product类,测试了通过工厂获取对象(代码省略)。观察结果

1
2
erlang复制代码Product.product
Product.afterPropertiesSet

在对象创建之后,执行的该方法。

方式二. 如果有一些类无法实现或不想实现InitializingBean接口,但他们也想做一些初始化的操作怎么办呢,spring也为我们提供了一种方式。

那就是自己定义一个方法,在里边执行初始话的操作,然后指定该方法为初始化方法。

1
2
3
4
5
6
java复制代码public class Product{
//自定义初始化方法
public void myInit(){
// 初始化操作
}
}
1
xml复制代码<bean id="product" class="xxx.Product" init-method="myInit" />

通过在bean标签的init-method属性指定自定义的初始化方法,也能实现初始化的效果。

注意事项:

  1. 如果一个对象既实现了InitializingBean,又提供了普通方法。

先执行接口的初始化方法,再执行普通的初始化方法。
2. 如果有属性赋值注入,又有初始化方法,哪个先执行?

先执行注入操作,再执行初始化方法(afterPropertiesSet) 可通过方法名理解
3. 什么叫做初始化操作

主要指对资源的初始化,数据库…..io……网络

2.3 对象的销毁阶段

spring销毁对象前, 会调用对象的销毁方法,完成销毁操作。那么我们就先需要了解几个点。
spring什么时候销毁所创建的对象
工厂关闭的时候, ctx.close(); //注意该方法只在子类中有,所有不能用多态创建工厂
关于销毁方法: 和初始化方法一样,有程序员完成,由spring调用。如果我们在对象销毁前想执行一些操作,可以下载这个里面

方式一. 实现Disposable接口,重写destroy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Product implements InitializingBean,DisposableBean{

public Product(){
System.out.println("Product.product");
}

// 这就是初始化方法,做一些初始化操作,spring会调用
@Override
public void afterPropertiesSet() throws Exception{
System.out.println("Product.afterPropertiesSet");
}

// 销毁操作,就是资源的释放操作
@Override
public void destroy() throws Exception{
System.out.println("Product.destroy");
}
}

测试类:

1
2
3
4
5
6
7
8
9
java复制代码public class Test{
// close方法定义在子类中,使用多态无法调用
public static void main(String[] args){
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("xxx.xml");
Product p = (Product) ctx.getBean("product");
ctx.close();

}
}

注意事项:

  1. 销毁方法只有在工厂调用close() 方法的时候才会调用
  2. close方法只有子类中,ApplicationContext中没有,所以不能使用多态调用。

方式二. bean标签指定销毁方法

自己定义销毁方法,配置文件中指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Product implements InitializingBean,DisposableBean{

public Product(){
System.out.println("Product.product");
}

// 这就是初始化方法,做一些初始化操作,spring会调用
@Override
public void afterPropertiesSet() throws Exception{
System.out.println("Product.afterPropertiesSet");
}

// 销毁操作,就是资源的释放操作
@Override
public void destroy() throws Exception{
System.out.println("Product.destroy");
}

public void myDestroy() throws Exception(){
System.out.println("Product.myDestroy");
}
}
1
xml复制代码<bean id="product" class="xxx.Product" init-method="myInit" destroy-method="myDestroy" />

细节分析:

  1. 销毁方法的操作只作用于 scope=”singleton” 的对象
  2. 销毁操作主要指资源的释放操作
  3. 执行顺序: 接口先于自定义

2.4 后置处理Bean

我们在前面提到了关于类的初始化的方式,除此之外,spring还提供了一个叫做BeanPostProcessor的接口,可以让我们对spring工厂创建的对象进行再加工。他底层的实现原理是通过AOP实现的。我们来看下这个接口。这个接口有两个方法需要实现。

postProcessBeforeInitialization: 该方法执行在初始化代码之前
postProcessAfterInitialization: 该方法执行在初始化代码之后

这里说的初始化方法指的就是我们前面提到的InitializingBean 接口中的afterPropertiesSet方法。

步骤:

  1. 实现BeanPostProcessor
  2. 重写两个方法: postProcessBeforeInitialization postProcessAfterInitialization
  3. postProcessBeforeInitialization: Spring创建完对象并注入后,可以运行该方法进行加工,通过返回值交给spring框架
  4. postProcessAfterInitialization:spring执行完初始化操作后,进行的加工。
  5. 配置文件配置

这里要特别注意下这两个需要实现的方法,跟前面初始化的方式不太一样,首先这两个方法是默认方法,不实现也不会报错,其次这两个方法有参数,有返回值。我们直接上代码。

  1. 开发一个类
1
2
3
4
5
java复制代码@Data
public class Category{
private String name;
private int age;
}
  1. 交给spring管理,并注入属性
1
2
3
4
xml复制代码<bean id="c" class="xxx.Category" >
<property name="name" value="张三" />
<property name="age" value="10" />
</bean>
  1. 创建一个bean的加工工厂,也就是BeanPostProcessor的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MyBeanPostProcessor implements BeanPostProcessor{
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException{
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException{
if(bean instanceof Category) {
Category c = (Category)bean;
c.setName("李四");
}
return bean;
}
}
  1. 配置文件配置
1
xml复制代码<bean id="myBeanPostProcessor" class = "com.xxx.MyBeanPostProcessor" />

测试,获取c对象,打印name, 发现结果变为李四。

注意事项:

  1. BeanPostProcessor 是对spring工厂中的所有类都生效的,所以使用之前,最好使用 instanceof 做判断,对指定类型进行加工,避免出错。
  2. 由于是对所有类做操作,所以第一个参数 Object bean ,就代表被加工的类,spring工厂的的所有类,都会走这个方法,所以这个bean可能是User,也可能是Product,等等等。第二个参数就是类的名称。
  3. 加工之后,要把对象做返回操作,即使你什么都不处理,也要把bean返回。
  4. 一定要注意执行顺序。

2.5 ApplicationContextAware

这个接口也是spring容器中非常重要的一个接口,关于他的使用场景我们后面会有一个更详细的案例来进行解释。这是先给出一个简单的用法。 spring中提供了很多以Aware结尾的接口。比如BeanNameAware, BeanFactoryAware,EnvironmentAware, 以及我们今天要说的ApplicationContextAware.

Aware翻译过来是知道的,已感知的,意识到的意思。其实就是可以帮我们获取到单词前面的对象。那么我们来说下ApplicationContext, 这个是spring的应用上下文,也可以简单理解成spring的工厂,我们可以通过getBean的方式获取spring工厂所管理的bean ,所以相对而言,它是一个比较重量级的资源,我们不能频繁创建,只需要创建一次就好了。那么当我们创建好了之后,如果其他地方也想获得这个工厂应该怎么办呢,就可以实现这个ApplicationContextAware接口,在里面就能得到对应的这个工厂,而不需要重复创建。
我们来看用法:

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
java复制代码public class UserServiceImpl implements UserService implements ApplicationContextAware{

private ApplicationContext ctx;

@Override
public void setApplicationContext(AppliactionContext application){
this.ctx = application;
}

@Override
public void register(User user){
System.out.println("registe----")
// 调用的是原始对象的login方法,---核心功能,切面功能不执行
// 设计目的是: 调用代理对象的login方法
this.login("abc", "123456");
//获取代理对象
UserService userService = (UserService)ctx.getBean("userService");
userService.login("abc", "123456");
}

@Override
public boolean login(String name, String password){
System.out.println("login-----")
}
}

实现这个接口,需要实现setApplicationContext方法,里边的参数就是当前的工厂,我们把它赋值给自己定义的同类型的成员变量就可以使用了。

同理,BeanNameAware接口,就是可以获得我们在配置文件配置的id值,也是通过重写setBeanName实现。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public class User implements BeanNameAware{
private String id;
private String name;
private String address;

public void setBeanName(String beanName) {
//ID保存BeanName的值,就是<bean>标签中的id值
id=beanName;
}
}

三. 总结

好了,本篇文章我们大概完成了,希望大家一定好好理解,真的是干货满满,我也是弄了三天才搞出来。接写来我们就好总结spring bean的生命周期。

  1. 对象的实例化(相当于new了出来)
  2. 填充属性
  3. 调用BeanNameAware的setBeanName方法
  4. 调用BeanFactoryAware的setBeanFacotry方法
  5. 调用ApplicationContextAware的setApplicationContext方法
  6. 调用BeanPostProcessor的postProcessBeforeInitialization方法
  7. 调用InitializingBean的afterPropertySet方法
  8. 调用自定义的初始化方法(init-method = myInit)
  9. 调用BeanPostProcessor的postProcessAfterInitialization方法
  10. bean可以使用了
  11. 容器关闭时调用DisposableBean的destroy方法
  12. 调用自定义的销毁方法

image.png

注意: 上面的步骤主要是为了说明对象生命周期各个阶段的执行顺序,如果实现了对应接口就执行,没实现就不执行。 好了,本篇文章很重要,希望大家有所收获!!!

参考资料:

Spring IN Action(第四版) 中国邮电出版社

孙帅spring详解:www.bilibili.com/video/BV185…

本文转载自: 掘金

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

服务端如何防止重复支付?

发表于 2021-04-01

公众号:Java小咖秀,网站:javaxks.com

作者:废物大师兄, 链接: cnblogs.com/cjsblog/p/1…

如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付中心,支付中心更新自身支

img

如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。

这个过程中经常可能遇到的问题是掉单,无论是超时未收到回调通知也好,还是程序自身报错也好,总之由于各种各样的原因,没有如期收到通知并正确的处理后续逻辑等等,都会造成用户支付成功了,但是服务端这边订单状态没更新,这个时候有可能产生投诉,或者用户重复支付。

由于③⑤造成的掉单称之为外部掉单,由④⑥造成的掉单我们称之为内部掉单

为了防止掉单,这里可以这样处理:

1、支付订单增加一个中间状态 “支付中”,当同一个订单去支付的时候,先检查有没有状态为“支付中” 的支付流水,当然支付(prepay)的时候要加个锁。支付完成以后更新支付流水状态的时候再讲其改成 “支付成功” 状态。

2、支付中心这边要自己定义一个超时时间(比如:30 秒),在此时间范围内如果没有收到支付成功回调,则应调用接口主动查询支付结果,比如 10s、20s、30s 查一次,如果在最大查询次数内没有查到结果,应做异常处理

3、支付中心收到支付结果以后,将结果同步给业务系统,可以发 MQ,也可以直接调用,直接调用的话要加重试(比如:SpringBoot Retry)

4、无论是支付中心,还是业务应用,在接收支付结果通知时都要考虑接口幂等性,消息只处理一次,其余的忽略

5、业务应用也应做超时主动查询支付结果

对于上面说的超时主动查询可以在发起支付的时候将这些支付订单放到一张表中,用定时任务去扫

为了防止订单重复提交,可以这样处理:

1、创建订单的时候,用订单信息计算一个哈希值,判断 redis 中是否有 key,有则不允许重复提交,没有则生成一个新 key,放到 redis 中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作

附上微信支付最佳实践:

img

本文转载自: 掘金

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

reids过期key并不是随随便便就让他过期,监听事件搞起来

发表于 2021-04-01

[TOC]

redis常用语缓存操作,但是redis功能不仅仅于此。今天我们来看看redis的key失效事件

redis安装

  • 为了方便安装。我们直接使用docker安装redis。这里不多赘述docker了。直接贴出代码自己搞成脚本执行就可以了

docker拉取

docker pull redis:3.2

启动

1
2
bash复制代码
docker run -p 6379:6379 -v /opt/soft/docker/redis/redis.conf:/etc/redis/redis.conf -v /opt/soft/docker/redis/data:/data --name=myredis --restart=always -d redis:3.2 redis-server /etc/redis/redis.conf --requirepass "password" --appendonly yes
  • 为了安全我们还是设置下密码,将上述脚本password修改为自己的密码即可
  • 上面的/opt/soft/docker/redis/data这个我们只需要创建空文件夹就行了,这个我们是为了将redis日志映射出来方便定位问题。
  • redis.conf文件去官网上下载就行了。docker安装的redis默认没有配置文件。或者直接复制我这里的。
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
conf复制代码
# Redis配置文件样例

# Note on units: when memory size is needed, it is possible to specifiy
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

# Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
# 启用守护进程后,Redis会把pid写到一个pidfile中,在/var/run/redis.pid
daemonize no

# 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid

# 指定Redis监听端口,默认端口为6379
# 如果指定0端口,表示Redis不监听TCP连接
port 6379

# 绑定的主机地址
# 你可以绑定单一接口,如果没有绑定,所有接口都会监听到来的连接
# bind 127.0.0.1

# Specify the path for the unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /tmp/redis.sock
# unixsocketperm 755

# 当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 0

# 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
# debug (很多信息, 对开发/测试比较有用)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel verbose

# 日志记录方式,默认为标准输出,如果配置为redis为守护进程方式运行,而这里又配置为标准输出,则日志将会发送给/dev/null
logfile stdout

# To enable logging to the system logger, just set 'syslog-enabled' to yes,
# and optionally update the other syslog parameters to suit your needs.
# syslog-enabled no

# Specify the syslog identity.
# syslog-ident redis

# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
# syslog-facility local0

# 设置数据库的数量,默认数据库为0,可以使用select <dbid>命令在连接上指定数据库id
# dbid是从0到‘databases’-1的数目
databases 16

################################ SNAPSHOTTING #################################
# 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# 满足以下条件将会同步数据:
# 900秒(15分钟)内有1个更改
# 300秒(5分钟)内有10个更改
# 60秒内有10000个更改
# Note: 可以把所有“save”行注释掉,这样就取消同步操作了

save 900 1
save 300 10
save 60 10000

# 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes

# 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb

# 工作目录.
# 指定本地数据库存放目录,文件名由上一个dbfilename配置项指定
#
# Also the Append Only File will be created inside this directory.
#
# 注意,这里只能指定一个目录,不能指定文件名
dir ./

notify-keyspace-events Ex

################################# REPLICATION #################################

redis 配置

  • 这里的配置我在上面已经配置了。在官网下载的是默认的配置。上面我加了一个配置notify-keyspace-events Ex 。关于Ex下表中有解释
属性 说明
K 键空间通知,所有通知keyspace@ 为前缀,追对key
E 键事件通知,所有通知已keyspace@为前缀,追对event
g DEL、EXPIRE、RENAME等类型无关的通用命令通知
$ 字符串命令通知
l 列表命令通知
s 集合命令通知
h 哈希命令通知
z zset命令通知
x 过期事件通知,每当key过期就会触发
e 驱逐事件,每当有键因为maxmemory策略被清楚是触发
A g$lshzxe总称

命令监听

  • 完成上述配置后,我们打开redis客户端
1
2
java复制代码
docker exec -it myredis redis-cli
  • myredis是上面安装redis容器的别名。这个读者可以自己设置
  • 因为设置了密码,连接后我们需要进行密码验证
1
2
java复制代码
auth password
  • 然后注册监听器

PSUBSCRIBE __keyevent@*__:expired

  • 其中expired就是我们注册类型 , @ 后面的* 表示DB。这里我们监听所有数据库的key过期事件。

问题

  • 比如我们想监听DB0的key删除事件。我们可以这么注册PSUBSCRIBE __keyevent@0__:del

  • 127.0.0.1:6379后面没有数字说明使用的是默认的db0。

  • 切换到DB1中查看hello没有查到。且6379后面有了数据库索引值。这个时候在DB1新增hello并进行删除。看看另外一个监听DB0的监听器会不会有响应

  • 很明显,我们没有任何的通知。现在我们在DB0 中进行删除hello。看看监听器的效果
  • 这个时候在DB0 中执行删除也没有监控到信息。这里不知道为什么。还望指点

程序监听

  • springboot程序添加依赖
1
2
3
4
5
6
xml复制代码
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
java复制代码

@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
}
  • 这里只是为了演示过期事件的监听。所以这里的redisConfig没有加入太多的配置。
1
2
3
4
5
6
7
8
9
xml复制代码

spring:
redis:
host: 39.102.60.114
port: 6379
database: 0
password: password
timeout: 1000s

具体监听类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码

@Slf4j
@Component
public class RedisKeyExpireListener extends KeyExpirationEventMessageListener {

public RedisKeyExpireListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

@Override
public void onMessage(Message message, byte[] pattern) {
log.info("接受到消息:{},{}",message,new String(pattern));
}
}

效果

总结

  • key过期事件的监听实际使用的不是很多。因为redis大部分都是缓存作用。缓存本来就会可有可无的。所以监听意义不大。但是也可以在不少场景下使用。
  • 订单30分钟未付款自动取消场景
  • 系统定时提醒功能

本文转载自: 掘金

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

面试官:什么是死锁?怎么排查死锁?怎么避免死锁?

发表于 2021-04-01

突然发现我的图解系统缺了「死锁」的内容,这就来补下。

在面试过程中,死锁也是高频的考点,因为如果线上环境真多发生了死锁,那真的出大事了。

这次,我们就来系统地聊聊死锁的问题。

  • 死锁的概念;
  • 模拟死锁问题的产生;
  • 利用工具排查死锁问题;
  • 避免死锁问题的发生;

死锁的概念

在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。

那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。

举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
  • 持有并等待条件;
  • 不可剥夺条件;
  • 环路等待条件;
互斥条件

互斥条件是指多个线程不能同时使用同一个资源。

比如下图,如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。

持有并等待条件

持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。

不可剥夺条件

不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。

环路等待条件

环路等待条件指都是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。

比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。


模拟死锁问题的产生

Talk is cheap. Show me the code.

下面,我们用代码来模拟死锁问题的产生。

首先,我们先创建 2 个线程,分别为线程 A 和 线程 B,然后有两个互斥锁,分别是 mutex_A 和 mutex_B,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;

int main()
{
pthread_t tidA, tidB;

//创建两个线程
pthread_create(&tidA, NULL, threadA_proc, NULL);
pthread_create(&tidB, NULL, threadB_proc, NULL);

pthread_join(tidA, NULL);
pthread_join(tidB, NULL);

printf("exit\n");

return 0;
}

接下来,我们看下线程 A 函数做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码//线程函数 A
void *threadA_proc(void *data)
{
printf("thread A waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread A got ResourceA \n");

sleep(1);

printf("thread A waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread A got ResourceB \n");

pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
return (void *)0;
}

可以看到,线程 A 函数的过程:

  • 先获取互斥锁 A,然后睡眠 1 秒;
  • 再获取互斥锁 B,然后释放互斥锁 B;
  • 最后释放互斥锁 A;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码//线程函数 B
void *threadB_proc(void *data)
{
printf("thread B waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread B got ResourceB \n");

sleep(1);

printf("thread B waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread B got ResourceA \n");

pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
return (void *)0;
}

可以看到,线程 B 函数的过程:

  • 先获取互斥锁 B,然后睡眠 1 秒;
  • 再获取互斥锁 A,然后释放互斥锁 A;
  • 最后释放互斥锁 B;

然后,我们运行这个程序,运行结果如下:

1
2
3
4
5
6
7
arduino复制代码thread B waiting get ResourceB 
thread B got ResourceB
thread A waiting get ResourceA
thread A got ResourceA
thread B waiting get ResourceA
thread A waiting get ResourceB
// 阻塞中。。。

可以看到线程 B 在等待互斥锁 A 的释放,线程 A 在等待互斥锁 B 的释放,双方都在等待对方资源的释放,很明显,产生了死锁问题。


利用工具排查死锁问题

如果你想排查你的 Java 程序是否死锁,则可以使用 jstack 工具,它是 jdk 自带的线程堆栈分析工具。

由于小林的死锁代码例子是 C 写的,在 Linux 下,我们可以使用 pstack + gdb 工具来定位死锁问题。

pstack 命令可以显示每个线程的栈跟踪信息(函数调用过程),它的使用方式也很简单,只需要 pstack <pid> 就可以了。

那么,在定位死锁问题时,我们可以多次执行 pstack 命令查看线程的函数调用过程,多次对比结果,确认哪几个线程一直没有变化,且是因为在等待锁,那么大概率是由于死锁问题导致的。

我用 pstack 输出了我前面模拟死锁问题的进程的所有线程的情况,我多次执行命令后,其结果都一样,如下:

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
shell复制代码$ pstack 87746
Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400725 in threadA_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):
#0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
#1 0x0000000000400806 in main ()

....

$ pstack 87746
Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400725 in threadA_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):
#0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
#1 0x0000000000400806 in main ()

可以看到,Thread 2 和 Thread 3 一直阻塞获取锁(pthread_mutex_lock)的过程,而且 pstack 多次输出信息都没有变化,那么可能大概率发生了死锁。

但是,还不能够确认这两个线程是在互相等待对方的锁的释放,因为我们看不到它们是等在哪个锁对象,于是我们可以使用 gdb 工具进一步确认。

整个 gdb 调试过程,如下:

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
swift复制代码// gdb 命令
$ gdb -p 87746

// 打印所有的线程信息
(gdb) info thread
3 Thread 0x7f60a610a700 (LWP 87747) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
2 Thread 0x7f60a5709700 (LWP 87748) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
* 1 Thread 0x7f60a610c700 (LWP 87746) 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
//最左边的 * 表示 gdb 锁定的线程,切换到第二个线程去查看

// 切换到第2个线程
(gdb) thread 2
[Switching to thread 2 (Thread 0x7f60a5709700 (LWP 87748))]#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0

// bt 可以打印函数堆栈,却无法看到函数参数,跟 pstack 命令一样
(gdb) bt
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6

// 打印第三帧信息,每次函数调用都会有压栈的过程,而 frame 则记录栈中的帧信息
(gdb) frame 3
#3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25
27 printf("thread B waiting get ResourceA \n");
28 pthread_mutex_lock(&mutex_A);

// 打印mutex_A的值 , __owner表示gdb中标示线程的值,即LWP
(gdb) p mutex_A
$1 = {__data = {__lock = 2, __count = 0, __owner = 87747, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},
__size = "\002\000\000\000\000\000\000\000\303V\001\000\001", '\000' <repeats 26 times>, __align = 2}

// 打印mutex_B的值 , __owner表示gdb中标示线程的值,即LWP
(gdb) p mutex_B
$2 = {__data = {__lock = 2, __count = 0, __owner = 87748, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},
__size = "\002\000\000\000\000\000\000\000\304V\001\000\001", '\000' <repeats 26 times>, __align = 2}

我来解释下,上面的调试过程:

  1. 通过 info thread 打印了所有的线程信息,可以看到有 3 个线程,一个是主线程(LWP 87746),另外两个都是我们自己创建的线程(LWP 87747 和 87748);
  2. 通过 thread 2,将切换到第 2 个线程(LWP 87748);
  3. 通过 bt,打印线程的调用栈信息,可以看到有 threadB_proc 函数,说明这个是线程 B 函数,也就说 LWP 87748 是线程 B;
  4. 通过 frame 3,打印调用栈中的第三个帧的信息,可以看到线程 B 函数,在获取互斥锁 A 的时候阻塞了;
  5. 通过 p mutex_A,打印互斥锁 A 对象信息,可以看到它被 LWP 为 87747(线程 A) 的线程持有着;
  6. 通过 p mutex_B,打印互斥锁 A 对象信息,可以看到他被 LWP 为 87748 (线程 B) 的线程持有着;

因为线程 B 在等待线程 A 所持有的 mutex_A, 而同时线程 A 又在等待线程 B 所拥有的mutex_B, 所以可以断定该程序发生了死锁。


避免死锁问题的发生

前面我们提到,产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。

那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。

那什么是资源有序分配法呢?

线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

我们使用资源有序分配法的方式来修改前面发生死锁的代码,我们可以不改动线程 A 的代码。

我们先要清楚线程 A 获取资源的顺序,它是先获取互斥锁 A,然后获取互斥锁 B。

所以我们只需将线程 B 改成以相同顺序的获取资源,就可以打破死锁了。

线程 B 函数改进后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码//线程 B 函数,同线程 A 一样,先获取互斥锁 A,然后获取互斥锁 B
void *threadB_proc(void *data)
{
printf("thread B waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread B got ResourceA \n");

sleep(1);

printf("thread B waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread B got ResourceB \n");

pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
return (void *)0;
}

执行结果如下,可以看,没有发生死锁。

1
2
3
4
5
6
7
8
9
arduino复制代码thread B waiting get ResourceA 
thread B got ResourceA
thread A waiting get ResourceA
thread B waiting get ResourceB
thread B got ResourceB
thread A got ResourceA
thread A waiting get ResourceB
thread A got ResourceB
exit

总结

简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。

所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

本文转载自: 掘金

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

自定义SpringMVC参数解析器

发表于 2021-03-31


问题
–

在维护公司开发的一个项目中,发现很多地方都要获取当前登录用户对象,而且是根据请求头中的token值去redis中获取用户信息,于是就很多地方出现了如下类似的代码

1
2
3
4
5
6
7
8
java复制代码public Object commentAdd(TravelComment comment, HttpServletRequest request){
//根据token获取用户信息
String token = request.getHeader("token");
UserInfo user = userInfoRedisService.getUserByToken(token);

//...其他代码
return JsonResult.success();
}

每次都要写HttpServletRequest request,然后根据请求头中的token去获取当前登录对象,这样就导致了很多地方代码都重复了,要如何解决这种问题呢?

解决方案

在请求映射方法列表声明即可,就可以获取当前登录用户对象,代码如下

1
2
3
4
5
6
java复制代码public Object commentAdd(TravelComment comment, UserInfo userInfo){
UserInfo user = userInfo;

//...其他代码
return JsonResult.success();
}

要如何实现上述操作呢? 那就是自定义SpringMVC的参数解析器来完成这项工作,因为通过现有的SpringMVC自带的参数解析器无法完成我们的需求

创建参数解析器

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
java复制代码/**
* 将请求映射方法列表中UserInfo类型参数进行解析
* 解析成当前登录用户对象
*/
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private IUserInfoRedisService userInfoRedisService;
//判断当前解析器支持解析的参数类型,返回true表示支持意思
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType() == UserInfo.class;
}
//解析器解析规则:
//此处将UserInfo类型参数, 解析成当前登录用户对象。
//当supportsParameter方法返回true时候才执行
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
String token = request.getHeader("token");
return userInfoRedisService.getUserByToken(token);
}
}

将自定义参数解析器添加到Spring容器中

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

//自定义的用户解析器
@Bean
public UserInfoArgumentResolver userInfoArgumentResolver(){
return new UserInfoArgumentResolver();
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userInfoArgumentResolver());
}
}

上述的所有操作,确实可以达到我们想要的结果,但是还是存在一些问题,并不是所有的请求映射方法列表中有UserInfo userInfo都要走自定义的参数解析器,那要如何来区分是用框架自带的解析器还是自定义的呢?

使用自定义的注解来区分

在需要使用自定义解析器的请求映射方法列表中添加自定义注解

1
2
3
4
5
6
7
8
java复制代码/**
* 用户参数注入注解
* 贴有该注解用户参数使用自定义的参数解析器
*/
@Target({ElementType.PARAMETER}) //表示贴在参数上
@Retention(RetentionPolicy.RUNTIME)
public @interface UserParam {
}

接口代码改进

1
2
3
4
5
6
java复制代码public Object commentAdd(TravelComment comment, @UserParam UserInfo userInfo){
UserInfo user = userInfo;

//...其他代码
return JsonResult.success();
}

以上就是最终的解决方法!

本文转载自: 掘金

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

盘点认证框架 SpringSecurity 基础篇

发表于 2021-03-31

总文档 :文章目录

Github : github.com/black-ant

协议差不多说完了 , 剩下的主要是一些针对性很强的协议 , 个人也没有完全跑通过 , 暂时先不录入 , 认证系列开始分析不同的认证框架以及对相关协议的实现.

一 . 前言

SpringSecurity 应该是最常见的认证框架了 , 处于Spring体系中使他能快速的上手 , 这一篇开始作为入门级开篇作 , 来浅浅的讲一下SpringSecurity 的整体结构.

关于Security 的使用 , 官方文档已经太详细了, 建议直接查看 官方文档 , 作为开篇 , 不深入源码 , 只做体系的介绍~~

后续会陆陆续续将笔记中的源码梳理出来 ,笔记很乱, 但愿等整理出体系!

二 . Spring Security 知识体系

Security中需要我们操作的成员大概可以分为以下几种 , 但是涉及的类远远不止他们

  • Filter : 对请求拦截处理
  • Provider : 对用户进行认证
  • Token : 用户认证主体

2.1 Spring Security 主要包结构

  • spring-security-remoting : 提供与 Spring Remoting 的集成
  • spring-security-web : 包含过滤器和相关的网络安全基础设施代码。
  • spring-security-config : 包含安全命名空间解析代码和 Java 配置代码
    ?- 使用 Spring Security xml 命名空间进行配置或 Srping Security 的 Java Configuration 支持
  • spring-security-ldap : 该模块提供 LDAP 身份验证和配置代码
  • spring-security-oauth2-core : 包含支持 OAuth 2.0授权框架和 OpenID Connect Core 1.0的核心类和接口
  • spring-security-oauth2-client : 包含 Spring Security 对 OAuth 2.0授权框架和 OpenID Connect Core 1.0的客户端支持
  • spring-security-oauth2-jose : 包含 Spring Security 对 JOSE (Javascript 对象签名和加密)框架的支持
    • JSON Web Token (JWT)
    • JSON Web Signature (JWS)
    • JSON Web Encryption (JWE)
    • JSON Web Key (JWK)
  • spring-security-oauth2-resource-server : 包含 Spring Security 对 OAuth 2.0资源服务器的支持。它通过 OAuth 2.0承载令牌来保护 api
  • spring-security-acl : 此模块包含专门的域对象 ACL 实现。它用于对应用程序中的特定域对象实例应用安全性
  • spring-security-cas : 该模块包含 Spring Security 的 CAS 客户端集成
  • spring-security-openid : 此模块包含 OpenID web 身份验证支持。它用于根据外部 OpenID 服务器对用户进行身份验证。
  • spring-security-test
  • spring-secuity-taglibs

2.2 Spring Security 核心体系 Filter

SpringSecurity 中存在很多Filter , 抛开一些底层的 , 一般业务中的Filter主要是为了控制以何种方式进行认证 . 一般的体系结构里面 , 都会循环处理 , 例如 CAS 中 , 就是通过 HandlerManager 进行 for each 循环 , 而 SpirngSecurity 中 , 同样通过 SecurityFilterChain 进行循环.

SecurityFilterChain 在后期源码梳理的时候在详细介绍 , 这里先看张图 :

securityfilterchain.png

FilterChainProxy 使用 SecurityFilterChain 来确定应该为此请求调用哪个 Spring 安全过滤器 ,

FilterChainProxy 决定应该使用哪个 SecurityFilterChain。会调用第一个被匹配的SecurityFilterChain ,即匹配是有序的

Filter-多重安全过滤链.jpg

  • 如果请求/api/messages/ 的URL,它将首先匹配 SecurityFilterChain0的 /api/** 模式,因此只会调用 SecurityFilterChain0,即使它也匹配 SecurityFilterChainn。
  • 如果请求/messages/ 的URL,它将不匹配 SecurityFilterChain0的 /api/** 模式,因此 FilterChainProxy 将继续尝试每个 SecurityFilterChain。假设没有其他的 SecurityFilterChai n实例匹配 SecurityFilterChainn 将被调用。 (即无匹配调用最后一个)

已知的 Filter 类

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
java复制代码ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter

2.3 Authentication 体系结构

认证体系是核心的处理体系 , 包含以下主要类 :

SecurityContextHolder : Spring Security 存储被验证者的详细信息的地方。

SecurityContext : 从 SecurityContextHolder 获得,并包含当前经过身份验证的用户的身份验证。。

Authentication : 可以作为 AuthenticationManager 的输入,以提供用户为身份验证或来自 SecurityContext 的当前用户提供的凭据。。

GrantedAuthority : 在身份验证上授予主体的权限(即角色、范围等)。

AuthenticationManager : 定义 Spring Security 的过滤器如何执行身份验证的 API。。

ProviderManager : AuthenticationManager 最常用的实现。。

Providationprovider : 由 ProviderManager 用于执行特定类型的身份验证。。

AuthenticationEntryPoint : 用于从客户机请求凭证(即重定向到登录页面,发送 www 认证响应等)。

AbstractAuthenticationProcessingFilter : 用作验证用户凭据的基本筛选器

AccessDecisionManager : 由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策

后续我们会围绕以上类进行源码梳理:

三 . SpringSeurity 案例

以下是一个很简单的 Security 案例 : >>>>项目源码<<<<

3.1 SpringSecurity 依赖和配置

1
2
3
4
java复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler myAuthenctiationFailureHandler;

@Bean
public UserService CustomerUserService() {
System.out.print("step1============");
return new UserService();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
auth.userDetailsService(CustomerUserService()).passwordEncoder(new BCryptPasswordEncoder());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//此方法中进行了请求授权,用来规定对哪些请求进行拦截
//其中:antMatchers--使用ant风格的路径匹配
//regexMatchers--使用正则表达式匹配
http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/before/**").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/").permitAll()
.anyRequest().authenticated() //其它请求都需要校验才能访问
.and()
.formLogin()
.loginPage("/login") //定义登录的页面"/login",允许访问
.defaultSuccessUrl("/home") //登录成功后默认跳转到"list"
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
.logout() //默认的"/logout", 允许访问
.logoutSuccessUrl("/index")
.permitAll();
http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png");
}
}

其中有几个主要的地方 :

@EnableWebSecurity 干了什么 ?

  • 该配置将创建一个 servlet Filter,作为名为 springSecurityFilterChain 的 bean
  • 创建一个具有用户用户名和随机生成的登录到控制台的密码的 UserDetailsService bean
  • 为每个请求向 Servlet 容器注册名为 springSecurityFilterChain 的 bean 的 Filter

TODO

3.2 准备一个 UserDetailsService

1
2
3
4
5
6
7
8
9
10
java复制代码public class UserService implements UserDetailsService {

//......

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = userRepository.findByUsername(username);
// ....
return user;
}

一个基本的 Demo 就完成了 , 案例能怎么简单 , 其实主要是因为我们复用了以下的类 :

  • UsernamePasswordAuthenticationFilter
  • DaoAuthenticationProvider
  • UsernamePasswordAuthenticationToken

四 . 定制案例

我们把整个结构再定制一下 , 满足我们本身的功能 :

4.1 Step 1 : 定制一个 Filter

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
java复制代码// 我们复用 UsernamePasswordAuthenticationFilter , 将其进行部分定制
public class DatabaseAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

// 修改用户名为 account
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "account";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;

public DatabaseAuthenticationFilter() {
super(new AntPathRequestMatcher("/database/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// username 从下方方法获取
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
// 核心 : 这里替换了 DatabaseUserToken
DatabaseUserToken authRequest = new DatabaseUserToken(username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}

protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}

protected void setDetails(HttpServletRequest request,
DatabaseUserToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getUsernameParameter() {
return usernameParameter;
}

public final String getPasswordParameter() {
return passwordParameter;
}
}

4.2 Step 2 : 准备一个 Token

Token 是在Authentication 中传递的核心 , 它用于后续进行票据的认证

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
java复制代码public class DatabaseUserToken extends AbstractAuthenticationToken {


private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private final Object principal;
private String credentials;
private String type;
private Collection<? extends GrantedAuthority> authorities;

public DatabaseUserToken(Object principal, String credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.type = "common";
setAuthenticated(false);
}

public DatabaseUserToken(Object principal, String credentials, String type) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.type = StringUtils.isEmpty(type) ? "common" : type;
setAuthenticated(false);
}

public DatabaseUserToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}

public DatabaseUserToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
super.setAuthenticated(true); // must use super, as we override
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

/**
* @param isAuthenticated
* @throws IllegalArgumentException
*/
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
super.setAuthenticated(true);
}

@Override
public Object getCredentials() {
return credentials;
}

@Override
public Object getPrincipal() {
return principal;
}
}

4.3 Step 3 : 准备一个 Provider

这里通过 supports 方法判断 token 是否符合 ,从而发起认证过程 (PS : 和 CAS 简直一个思路)

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
java复制代码
public class DatabaseAuthenticationProvider implements AuthenticationProvider {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
private UserInfoService userInfoService;

@Autowired
private AntSSOConfiguration antSSOConfiguration;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

logger.info("------> auth database <-------");
String username = (authentication.getPrincipal() == null)
? "NONE_PROVIDED" : String.valueOf(authentication.getPrincipal());
String password = (String) authentication.getCredentials();

if (StringUtils.isEmpty(password)) {
throw new BadCredentialsException("密码不能为空");
}

UserInfo user = userInfoService.searchUserInfo(new UserInfoSearchTO<String>(username));
logger.info("------> this is [{}] user :{}<-------", username, String.valueOf(user));
if (null == user) {
logger.error("E----> error :{} --user not fount ", username);
throw new BadCredentialsException("用户不存在");
}
String encodePwd = "";
if (password.length() != 32) {
encodePwd = PwdUtils.AESencode(password, AlgorithmConfig.getAlgorithmKey());
logger.info("------> {} encode password is :{} <-------", password, encodePwd);
}

if (!encodePwd.equals(user.getPassword())) {
logger.error("E----> user check error");
throw new BadCredentialsException("用户名或密码不正确");
} else {
logger.info("user check success");
}

DatabaseUserToken result = new DatabaseUserToken(
username,
new BCryptPasswordEncoder().encode(password),
listUserGrantedAuthorities(user.getUserid()));

result.setDetails(authentication.getDetails());
logger.info("------> auth database result :{} <-------", JSONObject.toJSONString(result));
return result;
}

@Override
public boolean supports(Class<?> authentication) {
return (DatabaseUserToken.class.isAssignableFrom(authentication));
}

private Set<GrantedAuthority> listUserGrantedAuthorities(String uid) {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
if (StringUtils.isEmpty(uid)) {
return authorities;
}
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return authorities;
}
}

4.4 隐藏环节

1
2
3
4
5
6
7
java复制代码// 将 Provider 注入体系
auth.authenticationProvider(reflectionUtils.springClassLoad(item.getProvider()));

// 将 Filter 注入体系
AbstractAuthenticationProcessingFilter filter = reflectionUtils.classLoadReflect(item.getFilter());
filter.setAuthenticationManager(authenticationManager);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);

以上的流程可以看到已经不需要 继承 UserService类了 . 重写这些足够我们去实现大部分业务逻辑 , 使用时在Provider 完成对应的认证方式即可

五 . 总结

Security 很好用, 在我的个人实践中 , 正在尝试将常见的协议进行整合 , 做成一个开源脚手架 , 个人感觉SpringSecuity 体系应该可以轻松的完成 .

开篇比较简单 , 正在构思怎样才能从实践的角度将他讲清楚 , 笔记也在陆陆续续整理 , 争取下个月将整套文章发出来 !

附录

HttpSecurity 常用方法

image.png

本文转载自: 掘金

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

1…693694695…956

开发者博客

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