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

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


  • 首页

  • 归档

  • 搜索

Python面向对象编程04:重写object通用函数 Py

发表于 2021-11-18

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

正式的Python专栏第39篇,同学站住,别错过这个从0开始的文章!

前篇学委展示分享了类的继承和重写,面向对象还有一些概念,我们看看一些object的通用函数,继续跟上吧!

Python Override

重写就重新定义,在程序中就是子类覆盖父类的函数的这种行为。

Override还能重写类的一些通用函数,它们是:

  • __init__
  • __str__
  • __eq__

这里手动写几个,因为我们双击object这个base class可以看到一系例的类的函数:

屏幕快照 2021-11-18 下午11.45.02.png

学委准备了下面的代码:

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello
"""
下面是一个程序员类定义
"""


class Programmer(object):
def __init__(self, name):
self.name = name

def code(self):
print(f"{self.name}: life is short, why not python?")


p = Programmer("学委粉丝")
# p.code() #TypeError: code() missing 1 required positional argument: 'lang'
p.code()
print("p:", p)
print("namespace:", Programmer.__dict__)


class JavaProgrammer(object):
def __init__(self, name):
self.name = name

def code(self):
print(f"{self.name}: like if like a box of chocolate?")

def __str__(self):
return f"JavaProgrammer(name:{self.name})"


p = JavaProgrammer("学委粉丝2号")
# p.code() #TypeError: code() missing 1 required positional argument: 'lang'
p.code()
print("p:", p)

这是运行结果:

屏幕快照 2021-11-18 下午11.41.33.png

我们下面在上面的代码基础上重写__eq__ 函数。

重写 == 操作:

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复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello
"""
下面是一个程序员类定义
"""


class JavaProgrammer(object):
def __init__(self, name):
self.name = name

def code(self):
print(f"{self.name}: like if like a box of chocolate?")

def __str__(self):
return f"JavaProgrammer(name:{self.name})"

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name
return False


p1 = JavaProgrammer("学委粉丝2号")
# p.code() #TypeError: code() missing 1 required positional argument: 'lang'
p1.code()
print("p1:", p1)

p2 = JavaProgrammer("学委粉丝2号")
# p.code() #TypeError: code() missing 1 required positional argument: 'lang'
p2.code()
print("p2:", p2)
print("same ? ", p1 == p2)

重写之后两个对象果然相等了!(可能初学者会觉得有点奇怪,name不是一样吗)

在Python中两个对象属性都相同,但是它们不一定相等的。

屏幕快照 2021-11-19 上午12.45.58.png

这是注释了eq函数后到运行结果:

屏幕快照 2021-11-19 上午12.43.38.png

Python 默认不支持方法重载!

什么是重载?

重载这种行为就是一个类出现多个同名函数,必然的函数接收的参数不一样(一样不就重复定义了,像Java直接就报错了!)

这在Python中默认不支持的。

我们看看下面的代码,学委写了两个同名函数code但是参数数量稍微区别开了:

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello
"""
下面是一个程序员类定义
"""


class Programmer(object):
def __init__(self, name):
self.name = name

def code(self):
print(f"{self.name}: life is short, why not python?")

def code(self, lang):
print(f"{self.name}: life is short, why not {lang} ?")




p = Programmer("学委粉丝")
#下面的代码取消注释会报错
#p.code() #TypeError: code() missing 1 required positional argument: 'lang'
p.code("java")
print("namespace:", Programmer.__dict__)

运行结果如下:

屏幕快照 2021-11-19 上午8.49.08.png

这里我把运行结果的namespace复制出来了:

namespace: {‘__module__‘: ‘__main__‘, ‘__init__‘: <function Programmer.__init__ at 0x1042ad280>, ‘code’: <function Programmer.code at 0x104494d30>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Programmer’ objects>, ‘__weakref__‘: <attribute ‘_weakref_‘ of ‘Programmer’ objects>, ‘__doc__‘: None}

我们看到namespace里面只有一个code,这告诉我们在内存中,python这个类值映射到一个code函数,明显是后者(第二个code函数)。

但是有库可以做到重载,后面继续说。

总结

读者还可以选取一些object的函数进行重写试试。

类继承带来便利的同时,也带来了复杂度。

因为有时候子类调用父类完成一部分工作,父类调用其他,这样反反复复,整个函数处理逻辑就非常难以一目了然看明白,只能通过看局部代码跳来跳去的拼凑成一个接近全貌的认识。

好处就是重复做的代码少了,组件更加简洁。

学委写了十几年的Java了,但是写的这套Python教程非常务实,对基础编程有任何问题请查看相关文章。

喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

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

golang 微服务中的断路器 hystrix 小案例

发表于 2021-11-18

「这是我参与11月更文挑战的第 17 天,活动详情查看:2021最后一次更文挑战」

上次我们分享了 Hystrix 具体流程,作为断路器实现,我们如何将 hystrix 用在我们的项目代码中呢?

我们可以简单的将 hystrix-go 下载下来

go get github.com/afex/hystrix-go/hystrix

代码会放到我们的 GOPATH 中,的 pkg 下面,例如我的 window 路径是这样的

go\pkg\mod\github.com\afex\hystrix-go@v0.0.0-20180502004556-fa1af6a1f4f5\hystrix

代码目录是酱紫的:

我们来看一下基本代码逻辑,屡一下:

初始化配置

1
2
3
4
5
6
7
go复制代码hystrix.ConfigureCommand(CircuitBreakerName, hystrix.CommandConfig{
Timeout: 1000,
MaxConcurrentRequests: 5,
RequestVolumeThreshold: 3,
SleepWindow: 2000,
ErrorPercentThreshold: 20,
})

可以看到 \go\pkg\mod\github.com\afex\hystrix-go@v0.0.0-20180502004556-fa1af6a1f4f5\hystrix\settings.go

中会有这个结构体定义的默认值

解释一下上述默认值代表的意思

  • Timeout

指的是,命令执行的超时时间

远程调用逻辑执行超过该时间将被强制执行超时,就进行失败回滚中 , 默认是 1000 毫秒

  • MaxConcurrentRequests

最大并发请求数

表示每个 hystrix 命令最大执行的并发协程,用于进行流量控制和资源隔离

当同种的 hystrix 执行的并发数量超过了该值,请求将会直接进入到失败回滚逻辑中,并被标记为拒绝请求上报

  • RequestVolumeThreshold

最小请求阈值

只有滑动窗口时间内的请求数量超过该值,断路器才会执行对应的判断逻辑

在低请求量的时候,断路器是不会发生效应的,即时这些请求全部失败,因为他只要没有超过这个值,就不会触发

  • SleepWindow

超时窗口时间,指的是断路器打开 SleepWindow 时长后,进入半开状态

重新允许远程调用的发生,试探下游服务是否恢复正常

如果接下来的请求都是成功的,那么断路器会关闭,否则就会重新打开

  • ErrorPercentThreshold

指的是,错误比例阈值

当滑动窗口时间内的错误请求频率超过这个值的时候,断路器也会打开

小案例

我们写一个小案例,来使用这个hystrix

  • 配置 hystrix

  • new 一个 hystrix , NewStreamHandler
1
2
go复制代码hystrixStreamHandler := hystrix.NewStreamHandler()
hystrixStreamHandler.Start()
  • 开一个 http 服务器,来专门来访问 掘金主页
  • 使用 wrk 性能测试工具来 打一下 我们的 服务器
    • wrk -c200 -t8 -d40 --latency http://127.0.0.1:9999/juejin

这个性能测试指令的意思是:

  • 开启 200个连接
  • 8个线程
  • 测试 40 s

性能测试小工具的使用可以查看文章 :性能测试小工具 wrk 可以怎么用

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
go复制代码package main

import (
"errors"
"fmt"
"github.com/afex/hystrix-go/hystrix"
"github.com/gin-gonic/gin"
"net/http"
)

// http /juejin 具体执行的逻辑

func CircuitBreakerTest(ctx *gin.Context) {
hystrix.Do("xiaomotong", func() error {
ctx.Next()
code := ctx.Writer.Status()
if code != http.StatusOK {
return errors.New(fmt.Sprintf(" 状态码是 : %d", code))
}
return nil

}, func(err error) error {
if err != nil {

fmt.Printf("断路器检测到错误: %s\n", err.Error())
// 返回熔断错误
ctx.JSON(http.StatusServiceUnavailable, gin.H{
"msg": err.Error(),
})
}
return nil
})
}

// 整个包的初始化, 初始化 hystrix
func init() {
hystrix.ConfigureCommand("xiaomotong", hystrix.CommandConfig{
Timeout: 1000,
MaxConcurrentRequests: 5,
RequestVolumeThreshold: 5,
SleepWindow: 5000,
ErrorPercentThreshold: 20,
})
}

func main() {
hystrixStreamHandler := hystrix.NewStreamHandler()

hystrixStreamHandler.Start()

go http.ListenAndServe(":9999", hystrixStreamHandler)
r := gin.Default()
gin.SetMode(gin.ReleaseMode)

r.GET("/juejin", func(c *gin.Context) {

_, err := http.Get("https://juejin.cn/")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"msg": "success"})
}, CircuitBreakerTest)

r.Run(":9999")
}

使用 wrk 工具,打了之后,我们可以看到服务器的打印效果如下:

出现上述打印,说明,已经达到了最大并发请求数

继续往下看

看到如上错误,说明断路器已经打开了,这个时候来的请求一律拒绝,就不会再去访问 掘金的网站了

等待 SleepWindow 超时窗口时间后,会进入到半开状态,这个时候,若再有请求,并且全都成功,那么断路器就会关闭掉

具体的源码分析,我们可以下篇娓娓道来

今天就到这里,学习所得,若有偏差,还请斧正

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

FFmpeg【5】 - 将视频文件转码成MP4格式(FFmp

发表于 2021-11-18

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

目录

前言

正文

总结


前言

MP4 格式的视频文件在日常工作和生活中很常见,今天我们来看看 FFmpeg 如何将一个视频文件转换成 MP4 格式。

正文

在点播领域,mp4格式应该是最常见的多媒体文件封装格式。我们需要知道,mp4文件是由许多Box和FullBox组成的,其中,每个Box由header和data两部分组成。FullBox是Box的扩展结构,在header中增加了8位version标志和24位的flags标志。

Box和FullBox中的header部分包含了整个Box的长度大小和类型,data部分是存储的实际数据,有可能是数据,也有可能是嵌套的Box(那么此时这个Box会被称为容器)。

好了,大概了解mp4的结构之后,我们来使用ffmpeg将一个ts(或者flv)源文件转换成mp4文件,运行如下命令:

ffmpeg -i benben.ts -c copy -f mp4 benben2.mp4

运行结果如下:

Input #0, mpegts, from ‘benben.ts’:

Duration: 00:00:10.19, start: 1.462011, bitrate: 635 kb/s

Program 1

Metadata:

service_name : Service01

service_provider: FFmpeg

Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p, 368x640 [SAR 45:46 DAR 9:16], 24 fps, 24 tbr, 90k tbn, 48 tbc

Stream #0:10x101: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 67 kb/s

[mp4 @ 02eac480] Codec for stream 0 does not use global headers but container format requires global headers

[mp4 @ 02eac480] Codec for stream 1 does not use global headers but container format requires global headers

Output #0, mp4, to ‘benben2.mp4’:

Metadata:

encoder : Lavf56.40.100

Stream #0:0: Video: h264 ([33][0][0][0] / 0x0021), yuv420p, 368x640 [SAR 45:46 DAR 9:16], q=2-31, 24 fps, 24 tbr, 90k tbn, 90k tbc

Stream #0:1(eng): Audio: aac ([64][0][0][0] / 0x0040), 48000 Hz, stereo, 67 kb/s

Stream mapping:

Stream #0:0 -> #0:0 (copy)

Stream #0:1 -> #0:1 (copy)

Press [q] to stop, [?] for help

[mp4 @ 02eac480] Malformed AAC bitstream detected: use the audio bitstream filter ‘aac_adtstoasc’ to fix it (‘-bsf:a aac_adtstoasc’ option with ffmpeg)

av_interleaved_write_frame(): Operation not permitted

frame= 6 fps=0.0 q=-1.0 Lsize= 84kB time=00:00:00.14 bitrate=4700.5kbits/s

video:83kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.068532%

Conversion failed!

运行结果报错了:

)​

报错信息提示我们应该设置音频转化模式,修改后运行如下命令:

ffmpeg -i benben.ts -c copy -bsf:a aac_adtstoasc -f mp4 benben2.mp4

输出结果:

[mp4 @ 0547f420] Codec for stream 0 does not use global headers but container format requires global headers

[mp4 @ 0547f420] Codec for stream 1 does not use global headers but container format requires global headers

Output #0, mp4, to ‘benben2.mp4’:

Metadata:

encoder : Lavf56.40.100

Stream #0:0: Video: h264 ([33][0][0][0] / 0x0021), yuv420p, 368x640 [SAR 45:46 DAR 9:16], q=2-31, 24 fps, 24 tbr, 90k tbn, 90k tbc

Stream #0:1(eng): Audio: aac ([64][0][0][0] / 0x0040), 48000 Hz, stereo, 67 kb/s

Stream mapping:

Stream #0:0 -> #0:0 (copy)

Stream #0:1 -> #0:1 (copy)

Press [q] to stop, [?] for help

frame= 244 fps=0.0 q=-1.0 Lsize= 707kB time=00:00:10.06 bitrate= 575.9kbits/s

video:620kB audio:79kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.147096%

文件转换成功了:

)​

最后,用mp4info查看一下目标视频文件的组成结构:

)​

总结

在使用ffmpeg转换flv文件时,需要根据具体的使用场景修正参数,出现报错不要慌。通过分析具体的报错原因,有的放矢,我们一定可以搞定,Good luck!

如果有问题,欢迎评论留言或者私信沟通!

作者简介:大家好,我是 liuzhen007,是一位音视频技术爱好者,同时也是CSDN博客专家、华为云社区云享专家、签约作者,欢迎关注我分享更多音视频相关内容!

本文转载自: 掘金

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

从JDK中学习设计模式——代理模式

发表于 2021-11-18

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

概述

代理模式(Proxy Pattern)通过代理对象来访问目标对象,它是一种对象结构型模式。

代理模式通过引入代理对象来连接客户端和目标对象,起到中介的作用,它的作用是隐藏客户不能看到的内容和增添客户需要的额外服务。例如,张三开了一家水壶制造厂,客户只需要委托张三来获取需要的产品就可以了,然后由张三来正真下达生产的指令。客户告知的需求就是抽象主题,张三起到代理主题的角色,水壶是真实的主题。

结构

代理模式UML.png

  • Subject(抽象主题):一般是抽象类或接口,声明了真实主题和代理主题的共同接口,客户端通常针对抽象主题角色进行编程。
  • Proxy(代理主题):包含了对真实主题的引用,它可以在任何时候替代真实主题和控制对真实主题的使用。
  • RealSubject(真实主题):实现了真实的业务操作,客户端可以通过代理主题间接调用真实主题中定义的操作。

分类

根据代理对象不同分为静态代理和动态代理:

  1. 静态代理:代理类在程序编译运行前已经创建完毕,方法增强规则已被写好,同时规则也已经适配到具体的类的方法中了。
  2. 动态代理:也叫做 JDK 代理、接口代理。代理类不是提前创建好的,而是在程序运行过程中通过反射机制来创建的。方法规则已被写好,但要适配这些规则的类方法不确定。

代理模式根据其目的和实现方式不同可分为很多种类:

  1. 远程代理(Remote Proxy):为位于不同地址空间的对象提供代理,这个地址空间可以在同一台主机中,也可以在另一台主机中。
  2. 虚拟代理(Virtual Proxy):通过创建一个资源消耗较小的对象来表示资源消耗较大的对象,真实对象只在需要时才会被真正创建。
  3. 保护代理(Protect Proxy):又称防火墙代理,可以给不同的用户提供不同级别的使用权限。
  4. 缓存代理(Cache Proxy):提供某个对象的缓存,使多个客户端可以高效的共享这些对象。
  5. 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作。

优点

  1. 客户端可以针对抽象主题编程,增加和更换代理类无须修改源代码,符合开闭原则。
  2. 通过代理对象来调用真实业务,降低了系统的耦合度。

此外,不同类型的代理模式也具有独特的优点:

  1. 远程代理可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。
  2. 虚拟代理能够节省系统的开销。
  3. 缓存代理通过缓存提升了系统性能。
  4. 保护代理增强了系统的安全性。

缺点

  1. 由于在客户端和真实主题之间增加了代理对象,因此可能会造成请求的速度变慢,并且增加了系统的复杂度。
  2. 实现代理模式需要额外的工作,并且代理的实现过程可能十分复杂。

应用场景

  1. 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
  2. 当需要用一个资源消耗较少的对象来代表一个资源消耗较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理。
  3. 当某个对象被频繁调用,并且每次调用无需重新计算时,可以使用缓存代理。
  4. 当需要为不同用户提供不同级别的访问权限时,可以使用保护代理。
  5. 当需要为一个对象的访问提供一些额外的操作时,可以使用智能引用代理。

JDK 中的应用

在 JDK 中 java.lang.reflect.Proxy 就使用了代理模式,它是其中的动态代理。

本文转载自: 掘金

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

SpringBoot基础之花式使用MybatisPlus

发表于 2021-11-18

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

前言

Mybatis像汽车,但是加持了MybatisPlus绝对是绝对是加装了氮气的超跑.

在没有使用MybatisPlus之前,在查询单表的时候要不默默的写sql代码,要不用代码生成器生成基础的代码,一旦参数有多种匹配方式,那么要不copy一份重写,要不加一个if条件,还是比较难受的.

但是一旦用了MybatisPlus 那仿佛是另一个世界.

MybatisPlus集成项目请看SpringBoot基础之集成MybatisPlus

前置基础信息

SQL

1
2
3
4
5
6
7
8
9
10
less复制代码CREATE TABLE `student` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`info` varchar(255) COMMENT 'zouzdc',
`del_flag` tinyint(1) DEFAULT '0',
`create_time` datetime,
`create_by` varchar(255),
`update_time` datetime ,
`update_by` varchar(255),
PRIMARY KEY (`id`)
) ;

Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ruby复制代码@Data
@NoArgsConstructor
public class Student {

/**
* id
*/
@TableId
private Long id;

/**
* 其他信息
*/
private String info;

/**
* 是否伪删除 0否 1是
*/
@TableLogic
private String delFlag;

/**
* 创建日期
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;

/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;

/**
* 更新日期
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}

MyBatisPlus增强配置-字段自动填充

正常来说,createTime和createBy和del_flag和租户等信息在输入插入的时候就需要赋值,而updateTime和updateBy在数据变动的时候就需要重新赋值,对于这种模式固定的字段,可以使用MyBatisPlus的字段自动填充功能,让程序自动帮助我们做.

需要变动的位置,添加一个配置类,需要在Bean上需要的字段上添加注解

需要填充的字段上加注解@TableField(

例子如上图的Student

@TableField(fill = FieldFill.INSERT) 在新增会自动填充该字段,如果字段有值,则不会覆盖

@TableField(fill = FieldFill.INSERT_UPDATE) 在新增和更新都会自动填充该字段,如果字段有值则会覆盖

@TableField(fill = FieldFill.UPDATE) 在更新会自动填充该字段,如果字段有值则会覆盖

字段自动填充配置类

在配置类中,自定义配置每一个需要自动填充数据的字段的填充规则

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

@Override
public void insertFill(MetaObject metaObject) {

this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "createBy", String.class, getUserBy());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateBy", String.class, getUserBy());
// 另一种方式 通用填充功能
//this.setFieldValByName("createTime",new Date(),metaObject);

}

@Override
public void updateFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateBy", String.class, getUserBy());
}

private String getUserBy(){
//获取当前操作人的方法,根据自己业务操作
return "admin";
}
}

数据逻辑删除

业务系统中大部分表不会真正的删除,所以需要使用某一个字段,定义是否伪删除. 只有调用MybatisPlus自带的方法且定义了删除字段的才会伪删除,否则会真删除.对于自己写的sql需要自行处理.

此处使用del_flag来定义是否伪删除,0是没有删除1是删除

application.yml

1
2
3
4
5
6
yaml复制代码mybatis-plus:
global-config:
db-config:
logic-delete-field: delFlag # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Bean类上

1
2
3
4
5
arduino复制代码/**
* 是否伪删除 0否 1是
*/
@TableLogic
private String delFlag;

官网介绍自从3.3.0开始,在application.yml中配置了全局后, 可以不用再Bean上的字段配置@TableLogic

单表增删改查使用

controller层使用增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码@RestController
@RequestMapping("/student")
public class StudentController {

@Autowired(required = false)
public StudentService studentService;

@GetMapping("getById")
public R getStudentById( Student student){
//增
studentService.save(student);
//改
studentService.updateById(student);
//查
Student student = studentService.getById(student.getId());
//删
studentService.removeById(student.getId());
//列表 带参数查询
List<Student> zouzdc = studentService.lambdaQuery().eq(Student::getInfo, "zouzdc").list();
return R.success();
}
}

service层使用增删改查

增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码@Service
@Slf4j
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {

@Autowired(required = false)
private StudentMapper studentMapper;


@Override
public void savePlus(Student student) {
//增
studentMapper.insert(student);
//改
studentMapper.updateById(student);
//查
baseMapper.selectById(student.getId());
//删
baseMapper.deleteById(student.getId());
//列表 带参数查询
List<Student> list = this.lambdaQuery().eq(Student::getId, student.getId()).list();
}
}

Service中自带默认的Mapper对象即baseMapper,等同于这里的studentMapper

单表条件查询示例

lambda语法

查询info等于zouzdc的list

1
perl复制代码 List<Student> zouzdc = studentService.lambdaQuery().eq(Student::getInfo, "zouzdc").list();

查询info等于zouzdc的一个对象

1
2
ini复制代码Student zouzdc = studentService.lambdaQuery().eq(Student::getInfo, "zouzdc").one();
Student zouzdc = studentService.lambdaQuery().eq(Student::getInfo, "zouzdc").last(" limit 1").one();

对于非唯一数据,直接使用one(),可能会出现非一条数据报错,可以使用last()拼接参数获取第一条数据

更新id=1的info字段

1
perl复制代码 studentService.lambdaUpdate().set(Student::getInfo,"zdc").eq(Student::getId,1).update();

删除info是’zouzdc’的数据

1
csharp复制代码studentService.lambdaUpdate().set(Student::getInfo,"zouzdc").remove()

更多的条件构造请参看官方文档 中条件构造器一节

变种语法 不建议使用
1
2
perl复制代码 List<Student> zouzdc1 = studentService.list(new QueryWrapper<Student>().lambda().eq(Student::getInfo, "zouzdc"));
Student zouzdc2 = studentService.getOne(new LambdaQueryWrapper<Student>().eq(Student::getInfo, "zouzdc"));

分页查询

分页配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Configuration
@MapperScan("zdc.enterprise.mapper.*")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置最大单页限制数量,,-1 不受限制
paginationInterceptor.setLimit(1000);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}

lambda模式分页

1
2
3
4
5
6
7
8
perl复制代码
public IPage<StudentDto> listPage(StudentDto vo){

return this.lambdaQuery()
.eq(vo.getInfo() != null, Student::getInfo, vo.getInfo())
.eq(Student::getDelFlag, "0")
.page(new Page(vo.getCurrent(), vo.getSize()));
}

混合模式分页

Service层

1
2
3
4
5
6
7
8
9
10
11
scss复制代码 public  Page<StudentDto> listPage(StudentDto vo){

Page<Student> page = new Page(vo.getCurrent(), vo.getSize());
page.addOrder(OrderItem.desc("a.create_time"));

QueryWrapper<Student> wrapper = new QueryWrapper();
wrapper.apply(" a.del_flag =0 ");
wrapper.eq("a.info","zouzdc")
.le("a.id",10);
return baseMapper.listPage(page, wrapper);
}

Mapper层

1
less复制代码Page<StudentDto> listPage(@Param("page") Page<Student> page, @Param("ew") QueryWrapper<Student> wrapper);

@Param("ew")是固定的默认的

XML层

1
2
3
4
5
csharp复制代码<select id="listPage" resultType="zdc.enterprise.dto.StudentDto">
select * from student a
${ew.customSqlSegment}

</select>

这种写法是在service层写sql形式的where条件,然后通过${ew.customSqlSegment}拼接到xml中sql的后面.会自带where

还有更多写法…自己研究吧

1
2
3
4
arduino复制代码作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

LeetCode破解之文件夹系统

发表于 2021-11-18

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

题目描述

你是一位系统管理员,手里有一份文件夹列表 folder,你的任务是要删除该列表中的所有 子文件夹,并以 任意顺序 返回剩下的文件夹。

我们这样定义「子文件夹」:

  • 如果文件夹 folder[i] 位于另一个文件夹 folder[j] 下,那么 folder[i] 就是 folder[j] 的子文件夹。
    文件夹的「路径」是由一个或多个按以下格式串联形成的字符串:
  • / 后跟一个或者多个小写英文字母。
    例如,/leetcode 和 /leetcode/problems 都是有效的路径,而空字符串和 / 不是。

示例 1:

输入:folder = [“/a”,”/a/b”,”/c/d”,”/c/d/e”,”/c/f”]
输出:[“/a”,”/c/d”,”/c/f”]
解释:”/a/b/“ 是 “/a” 的子文件夹,而 “/c/d/e” 是 “/c/d” 的子文件夹。

示例 2:

输入:folder = [“/a”,”/a/b/c”,”/a/b/d”]
输出:[“/a”]
解释:文件夹 “/a/b/c” 和 “/a/b/d/“ 都会被删除,因为它们都是 “/a” 的子文件夹。

提示:

  • 1 <= folder.length <= 4 * 10^4
  • 2 <= folder[i].length <= 100
  • folder[i] 只包含小写字母和 /
  • folder[i] 总是以字符 / 起始
  • 每个文件夹名都是唯一的

思路

利用排序和Set数据结构,先对数据排序,同时定义一个Set和一个数组,存放排序后的第一个值。再用循环遍历数据,每次和数组最后一个比较,因为排序好了的,所以数组最后一项长度是要大于要比较那项的长度。

特殊情况:

  • /a/b和/a/c
  • /a/b和/a/bc

通过使用Set去重,最后return的结果再转为数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public List<String> removeSubfolders(String[] folder) {
Arrays.sort(folder);
List<String> list = new ArrayList<>();
String ans = "";
boolean findFirst = false;
for (String s : folder) {
if ("/".equals(s)) {
continue;
}
if (!findFirst) {
ans = s;
findFirst = true;
list.add(ans);
continue;
}
if (s.startsWith(ans) && s.charAt(ans.length()) == '/') {
continue;
}
ans = s;
list.add(ans);
}
return list;
}

分析:其实感觉题目想要我们搞个字典树出来,但我感觉没必要。这里在提供一种滑动窗口+排序的方法。c++版本的,还是c++写算法方便

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
cpp复制代码class Solution {
public:
bool ispre(string s1,string s2){
if(s1.size()==s2.size()) return false;
int i;
for(i=0;i<s1.size();i++){
if(s2[i]!=s1[i]) return false;
}
return s2[i]=='/';
}
public:
vector<string> removeSubfolders(vector<string>& folder) {
sort(folder.begin(),folder.end());
vector<string> ans;
int s=folder.size(),l=0,r=1;
if(s<=1) return ans;
ans.push_back(folder[0]);
while(l<s&&r<s){
if(ispre(folder[l],folder[r])) r++;
else{
l=r;
r=l+1;
ans.push_back(folder[l]);

}
}
return ans;
}
};

本文转载自: 掘金

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

【Spark】RDD 广播变量和累加器

发表于 2021-11-18

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

一、概述

有时候需要在多个任务之间共享变量, 或者在任务(Task) 和 Driver Program 之间共享变量。

在 Spark 作业中,用户编写的高阶函数会在集群中的 Executor 里执行,这些 Executor 可能会用到相同的变量,这些变量被复制到每个 Executor 中,而 Executor 对变量的更新不会传回 Driver。

为了满足这种需求, Spark 还是提供了两类共享变量:

  • 广播变量(broadcast variable)
  • 累加器(accumulator)。

当然,对于分布式变量,如果不加限制会出现一致性的问题,所以共享变量是两种非常特殊的变量。

这两种变量可以认为是在用算子定义的数据管道外的两个全局变量,供所有计算任务使用。

广播变量、累加器主要作用是为了优化 Spark 程序。

二、广播变量

广播变量类似于 MapReduce 中的 DistributeFile,通常来说是一份不大的数据集,一旦广播变量在 Driver 中被创建,整个数据集就会在集群中进行广播,能让所有正在运行的计算任务以只读方式访问。

广播变量支持一些简单的数据类型,如整型、集合类型等,也支持很多复杂数据类型,如一些自定义的数据类型。

广播变量为了保证数据被广播到所有节点,使用了很多办法。

这其实是一个很重要的问题,我们不能期望 100 个或者 1000 个 Executor 去连接 Driver,并拉取数据,这会让 Driver 不堪重负。
Executor 采用的是通过 HTTP 连接去拉取数据,类似于 BitTorrent 点对点传输。
这样的方式更具扩展性,避免了所有 Executor 都去向 Driver 请求数据而造成 Driver 故障。

Spark 广播机制运作方式是这样的:

  1. Driver 将已序列化的数据切分成小块,然后将其存储在自己的块管理器 BlockManager 中,当 Executor 开始运行时,每个 Executor 首先从自己的内部块管理器中试图获取广播变量,如果以前广播过,那么直接使用;
  2. 如果没有,Executor 就会从 Driver 或者其他可用的 Executor 去拉取数据块。一旦拿到数据块,就会放到自己的块管理器中。供自己和其他需要拉取的 Executor 使用。这就很好地防止了 Driver 单点的性能瓶颈。

广播变量将变量在节点的 Executor 之间进行共享(由 Driver 广播出去);
广播变量用来高效分发较大的对象。向所有工作节点(Executor)发送一个较大的只读值, 以供一个或多个操作使用。

使用广播变量的过程如下:

  1. 对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。任何可序列化的类型都可以这么实现(在 Driver 端)
  2. 通过 value 属性访问该对象的值(在 Executor 中)
  3. 变量只会被发到各个 Executor 一次, 作为只读值处理

如图所示:

广播变量的相关参数:

  • spark.broadcast.blockSize (缺省值: 4m)
  • spark.broadcast.checksum (缺省值: true)
  • spark.broadcast.compress (缺省值: true)

三、累加器

累加器的作用: 可以实现一个变量在不同的 Executor 端能保持状态的累加;

累计器在 Driver 端定义, 读取; 在 Executor 中完成累加;

累加器也是 lazy 的, 需要 Action 触发; Action 触发一次, 执行一次, 触发多次, 执行多次;

累加器一个比较经典的应用场景是用来在 Spark Streaming 应用中记录某些事件的数量;

实操:

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码val data = sc.makeRDD(Seq("hadoop map reduce", "spark mllib"))
// 方式1
val count1 = data.flatMap(line => line.split("\\s+")).map(word => 1).reduce(_ + _)
println(count1)


// 方式2。错误的方式
var acc = 0
data.flatMap(line => line.split("\\s+")).foreach(word => acc += 1)
println(acc)

// 在Driver中定义变量,每个运行的Task会得到这些变量的一份新的副本,但在Task中更新这些副本的值不会影响 Driver中对应变量的值

Spark 内置了三种类型的累加器, 分别是:

  • LongAccumulator:用来累加整数型
  • DoubleAccumulator:用来累加浮点型
  • CollectionAccumulator:用来累加集合元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码val data = sc.makeRDD("hadoop spark hive hbase java scala hello world spark scala java hive".split("\\s+"))

val acc1 = sc.longAccumulator("totalNum1")
val acc2 = sc.doubleAccumulator("totalNum2")
val acc3 = sc.collectionAccumulator[String]("allWords")
val rdd = data.map { word =>
acc1.add(word.length)
acc2.add(word.length)
acc3.add(word)
word
}

rdd.count
rdd.collect
println(acc1.value)
println(acc2.value)
println(acc3.value)

本文转载自: 掘金

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

OAuth 20(四):手把手带你写代码接入 OAuth

发表于 2021-11-18

大家好,我是橘长,昨天分享了 「 令牌机制 」,大家可以自行回顾一下 OAuth 2.0 的核心是什么,令牌机制是什么、如何使用、开源组件等。

今天我们开始落地写代码,基于橘长之前接入过农业银行的授权,今天首先作为第三方服务来和大家分享「 手把手接入 OAuth 2.0 授权服务 」。

一、业务背景

近期团队帮银行做了一个互动营销活动,活动入口在行方的 App 上,当用户在行方 App 点击活动 banner 页跳转活动的时候参与。

活动入口.jpg

在进活动之前作为业务方自然需要知道参与活动的人是谁,如何给它构建登录态。

这就是为什么橘长这边需要接入 行方 OAuth 2.0 组件的原因,本质就是获取 客户信息,回到活动业务形成登录态,进而可以参与活动。

使用的是 OAuth 2.0 中最完备的 授权许可机制 的这种接入方式,服务端发起型授权,为了方便展示,大部分采用了硬编码。

二、作为第三方软件需要做什么

1、静态注册

也可以称之为备案(注册信息) ,需要在 行方 的开放平台的管理态申请 OAuth 2.0 接入客户端,对于行方来说,确保第三方软件是可信的。

准备信息:第三方应用服务端 ip 地址、第三方应用回调通知地址、申请权限等。

1
2
ini复制代码String ip = "xxx.xx.xx.xx";
String callBackUrl = "https://xxx.xxx.xx/oauth/login/success";

等授权服务的后台人员处理之后会颁发相关配置,用来表示唯一标识 第三方软件 的相关配置。

如下是一个简单示例模板:

1
2
3
ini复制代码String appid = "appid_001";
String appSecret = "appSecret_001";
String scope = "user_info";

2、引导用户授权

1)第一步:用户访问第三方软件,判断凭据

如果没有携带 JWT 或 已过期,服务端响应 Http 状态码 401 给前端,前端会请求服务端的发起授权接口。

1
2
3
4
5
6
7
8
9
ruby复制代码// 比方说访问活动首页接口
curl https://xxx.xx.xx/api/activity/act-01/index;


// 服务端响应 401
{
"message": "用户会话已过期,请重新登录!",
"statusCode": 401
}

2)第二步:客户端收到 401,请求授权接口

第三方软件后端会和授权服务交互,获取授权页地址,然后 302 跳转引导用户到授权页。

授权组合页.jpg

  • controller 层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@Slf4j
@RequestMapping("/oauth")
@RestController
public class OAuthController {


@GetMapping("/login")
public RedirectView login(final String redirect) {
String sceneId = IdUtil.simpleUUID();
JSONObject sceneInfo = JSONUtil.createObj().putOnce(OAuthConstant.REDIRECT_FOR_FRONT, redirect);
// 缓存前端通知地址
cache.set(OAuthConstant.KEY_PRE_OAUTH_SCENE + sceneId, sceneInfo, 300);

String oauthUrl = oAuthService.buildOauthUrl(sceneId, callbackUrl);
log.info("[发起 xxx 行方 OAuth 授权] 构建OAuth url为:[{}]", oauthUrl);

// 302 跳转
return new RedirectView(oauthUrl);
}
}

日志打印:

1
perl复制代码xxxx [发起授权] 构建的授权地址为:[http://xxx/oauth/authorize?client_id=xxx&redirect_uri=http%3A%2F%2Fxxx%2F%2Foauth%2Flogin%2Fsuccess&state=d8cb3943cd3a45818711fa4f6a8820e9&scope=custid%2Cphone&response_type=code]
  • service 实现
1
2
3
4
5
6
7
8
9
typescript复制代码@Override
public String buildOauthUrl(final String sceneId) {
// 回调地址
String callbackUrl = applicationConfig.getAppUrl() + "/oauth/login/success";
// 带参
String notifyUrl = UrlBuilder.of(callbackUrl, CharsetUtil.CHARSET_UTF_8)
.addQuery("sceneId", sceneId).build();
return xxxOAuthService.buildOauthUrl(notifyUrl);
}

service 这层根据标准 OAuth 2.0 的要求更合理做法是 请求授权服务获取,这里授权服务设计有点不合理,后续调整。

3)第三步:授权服务回调通知,分发临时授权码

1
2
3
4
5
6
7
arduino复制代码@RequestMapping("/login/success")
public RedirectView loginSuccess(final String sceneId,
final String code) {

log.info("[xxx 行方授权回调通知] sceneId:[{}], code:[{}]", sceneId, code);
// 后续业务操作
}

日志打印:

1
css复制代码xxxx [oauth 服务]-回调,接收到数据为:code:[xxx], state:[xxx]

4)第四步:第三方服务通过 code 换取 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
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
typescript复制代码private String getToken(final String clientId, 
final String clientSecret,
final String code) {

JSONObject tokenJson = this.getTokenFromOAuthServer(clientId, clientSecret, code, redirectUri);
String accessToken = tokenJson.getStr("access_token");
if (!JSONUtil.isNull(tokenJson) && StrUtil.isNotEmpty(accessToken)) {
return accessToken;
}
throw new RuntimeException("token获取异常!");
}


/**
* 从 授权服务 获取 token
*
* @author huangyin
*/
private JSONObject getTokenFromOAuthServer(final String clientId,
final String clientSecret,
final String code,
final String redirectUri) {

// 请求资源地址
String requertUrl = oauthServerConfig.getBaseUrl() + "/oauth/token";

// 构建请求参数
Map<String, Object> formMap = new HashMap<>(5);
// 授权码许可模式
formMap.put("grant_type", "authorization_code");
formMap.put("client_id", clientId);
formMap.put("client_secret", clientSecret);
formMap.put("code", code);

// http 请求
JSONObject response = this.doPostFormData(requertUrl, formMap);
log.info("[从授权服务获取 token] 结果为:[{}]", response);
return response;
}


/**
* 抽离 post 请求方法,form-data 传参
*
* @author huangyin
*/
private JSONObject doPostFormData(final String sourceUrl,
final Map<String, Object> formArgs) {
try {
// 采用 开源工具 hutool
String response = HttpRequest.post(sourceUrl).form(formArgs).timeout(3000).execute().body();
log.info("[从授权服务post form请求] 请求地址:[{}],请求参数:[{}],原始响应:[{}]", sourceUrl, formArgs, response);
if (JSONUtil.isJson(response)) {
// 依据授权服务 api response 定义做结果处理
JSONObject responseJson = JSONUtil.parseObj(response);
String code = responseJson.getStr("code");
JSONObject data = responseJson.getJSONObject("data");
if ("0000".equals(code) && !JSONUtil.isNull(data)) {
return data;
}
}
} catch (Exception e) {
log.error("[从授权服务 post form 请求] 异常,请求地址:[{}],参数:[{}],异常信息:[{}]", sourceUrl, formArgs, e.getMessage());
}
throw new RuntimeException("授权服务异常!");
}

打印日志:

1
css复制代码xxxx [授权服务 code 获取 token] 结果为:[{"access_token":"xxx","expires_in":7200}]

5)第五步:拿到 凭据,访问业务接口

当用授权码换取到 凭据 之后,通过凭据去获取用户在受保护资源服务的数据,比方说获取用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public String getUserInfoFromOAuthServer(String token) {


String sourceUrl = oauthServerConfig.getBaseUrl() + "/oauth/userInfo";
try {
// header 头部方式提交 凭据
String response = HttpRequest.post(sourceUrl).header("Authorization", "Bearer " + accessToken)
.timeout(3000).execute().body();
log.info("[从授权服务 post 请求获取用户信息] 请求地址:[{}],原始响应:[{}]", sourceUrl, response);
if (JSONUtil.isJson(response)) {
// 依据授权服务 api response 定义做结果处理
JSONObject responseJson = JSONUtil.parseObj(response);
String rtCode = responseJson.getStr("code");
String data = responseJson.getStr("data");
if ("0000".equals(rtCode) && StrUtil.isNotEmpty(data)) {
return data;
}
}
} catch (Exception e) {
log.error("[从授权服务 post 请求获取用户信息] 异常,请求地址:[{}],异常信息:[{}]", sourceUrl, e.getMessage());
}
throw new RuntimeException("授权服务异常!");
}

打印日志:

1
css复制代码xxxx [从授权服务换取用户信息] 解密出来的用户信息为:[{"openid":"xxx","headImg":"xxx"}]

6)第六步:用户信息入库,分发业务 code 给前端

拿到用户信息,写入活动服务的业务表中,然后通知前端说授权完成啦,颁发活动业务的 临时码(code)给客户端,便于客户端来换取活动业务的 JWT。

  • 用户信息入库
1
2
3
4
5
6
7
8
arduino复制代码public RedirectView loginSuccess(String ...) {
// 拷贝
OauthUser oauthUser = BeanUtil.copyProperties(userInfoDto, OauthUser.class);
oauthUserService.createOrUpdate(oauthUser);

// 通知前端
return this.redirectFrontEndUrl(state, userInfoDto);
}
  • 服务端通知前端
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
typescript复制代码private RedirectView redirectFrontEndUrl(final String sceneId, 
final UserInfoDto userInfoDto) {
// 生成 业务 code
String businessCode = IdUtil.simpleUUID();

try {
// 反查拿到前端通知地址
cache.set(SmallBeanOauthConstant.SMALL_KEY_PRE_USER_INFO + businessCode, userInfoDto, 300);
JSONObject sceneInfo = JSONUtil.parseObj(cache.get(SmallBeanOauthConstant.SMALL_KEY_PRE_OAUTH_SCENE + sceneId));
String redirectFrontEndUrl = sceneInfo.getStr("redirectFrontEndUrl");
if (StrUtil.isEmpty(redirectFrontEndUrl)) {
log.warn("授权:分发业务code给前端地址为空!");
// TODO 这块需要设置默认值,或者是响应前端401,重跳授权
}

String notifyUrl = UrlBuilder.of(redirectFrontEndUrl, StandardCharsets.UTF_8)
.addQuery("code", businessCode)
.build();

// 通知前端
return new RedirectView(notifyUrl);
} catch (Exception e) {
log.error("[OAuth 颁发 code 给客户端异常] 授权id:[{}],异常信息:[{}], [{}]", sceneId, e.getMessage(), e.getCause());
}
throw new RuntimeException("授权失败,请稍后重试!");
}

7)第七步:客户端通过 code 换取 业务 jwt

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
typescript复制代码/**
* 业务 code to jwt:构建登录态
*
* @param codeToJwtDto 分发的业务 code
* @return ResponseEntity
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody CodeToJwtDto codeToJwtDto) {
ValidatorUtil.validateEntity(codeToJwtDto);
String code = codeToJwtDto.getCode();
Object cacheUserInfo = cache.get(OAuthConstant.USER_INFO_PREFIX + code);
if (null == cacheUserInfo) {
throw new ParamException("code非法!", HttpStatus.UNPROCESSABLE_ENTITY.value());
}
// 删除掉 code
cache.delete(OAuthConstant.USER_INFO_PREFIX + code);

// 一系列其他业务处理等,然后生成 JWT
Map<String, Object> tokenMap = this.buildJwt(activityUser);
return ResponseEntity.status(HttpStatus.CREATED).body(tokenMap);
}


private Map<String, Object> buildJwt(ActivityUser activityUser) {
// 构建 jwt 需要相关参数
Map<String, Object> claims = new HashMap<>(2);
claims.put("authId", activityUser.getId());
claims.put("authRole", "user");
String token = JwtUtil.generateToken(claims, applicationConfig.getExpiration(), applicationConfig.getTokenSigningKey());

// 构建响应前端 token 信息
Map<String, Object> tokenMap = new HashMap<>(3);
tokenMap.put("accessToken", token);
tokenMap.put("tokenType", "Bearer");
tokenMap.put("expiresIn", applicationConfig.getExpiration());

return tokenMap;
}

获取到的 JWT:

1
2
3
4
5
ini复制代码{
accessToken=header.payload.signature,
tokenType=Bearer,
expiresIn=86400
}

三、总结

今天橘长一步一步带着大家写代码手把手接入 OAuth 2.0 授权服务,大家需要记住几点:

1、关注 授权服务 的官方文档,开放平台接入文档是一个很重要的凭据。

2、第三方软件接入授权尽量采用 服务端发起型 授权,使用 授权码许可机制,因为这更安全、更完备。

3、强烈建议你手把手撸一遍,OAuth 2.0 接入的代码很考验基本功和代码风格,其中用到了 Redis 缓存、Hutool 工具去发起 Http 请求等。

下一篇橘长将给大家带来「 手把手搭建 OAuth 2.0 授权服务 」的解读,感谢你的关注,如果你觉得有所收益,欢迎点赞、转发、评论,感谢认可!

有任何疑问也可以添加我的好友(wx:mbandtr),欢迎沟通。

本文转载自: 掘金

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

从Servlet和SpringBoot整合理解父子容器 Sp

发表于 2021-11-18

SpringBoot和Spring-web的容器分析

在这篇开始之前,建议先看从Servlet和Spring-web整合理解父子容器

编码方式实现Web.xml配置

通过上一篇的文章知道了,要实现Servlet和Spring-web融合,就需要在web.xml中主要配置两个东西

  1. ContextLoaderListener
  2. DispatchServlet

配置之后,就可以在Servlet容器启动的时候,创建WebApplicationContext。从而启动Spring容器。下面介绍,如果通过硬编码的方法来启动做融合。

在这之前需要了解一下ServletContainerInitializer和javax.servlet.annotation.HandlesTypes

Servlet提供的方式

1. ServletContainerInitializer和javax.servlet.annotation.HandlesTypes

  1. ServletContainerInitializer是在javax.servlet包下面的,它是一个接口,允许在WebApplication启动的阶段通知。此外这个接口一般都是要利用HandlesTypes注解的。
1
2
3
4
5
6
java复制代码package javax.servlet;
import java.util.Set;
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
  1. HandlesTypes注解

这注解是和ServletContainerInitializer一块使用的,它表示,value里面声明的类要传递给ServletContainerInitializer来做处理,value表示要探测的类。

1
2
3
4
5
java复制代码@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
Class<?>[] value();
}

秘诀就是这个,对于Spring来说,就是继承这个类,在之前的时候做事情就好了。把之前在ContextLoaderListener干的事情,一部分是可以放在这里来干的。

Spring利用ServletContainerInitializer来做处理

SpringServletContainerInitializer

可以看到,它实现了ServletContainerInitializer,并且在@HandlesTypes里写WebApplicationInitializer,这就会导致servlet容器会将所有的WebApplicationInitializer组装成一个set,作为参数传递进来。

可以看到,在这里面会实例化传递进来的WebApplicationInitializer(前提是,这个WebApplicationInitializer的实例不是接口,不是抽象类)

还会排序(只要实现了Order接口),然后就是循环调用WebApplicationInitializer了。

问题

  1. SpringServletContainerInitializer是怎么被加载的?

ServletContainerInitializer理论上来说,是一个SPI。既然是SPI,肯定是有一个写好的全路径的配置文件的。Spring-web是在META-INF/services/javax.servlet.ServletContainerInitializer里面。这才是一个典型的SPI加载的过程。突然想到了Dubbo。
2. SpringServletContainerInitializer和WebApplicationInitializer有啥区别?

SpringServletContainerInitializer负责实例化WebApplicationInitializer。并且将ServletContext传递给WebApplicationInitializer。

WebApplicationInitializer是自定义ServletContext的。

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
java复制代码@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {

List<WebApplicationInitializer> initializers = new LinkedList<>();

if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}

if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}

WebApplicationInitializer

这个接口是通过编程的方式来配置ServletContext,和之前的web.xml的功能基本差不多,但这里是通过编程方式来做的。这个接口的实现类会自动被SpringServletContainerInitializer被识别并且实例化。

话说回来,Spring能通过编程的方式来配置,本质上还是人家ServletContext支持这种方式,要是没有对应的api,肯定是不行的。

这个方法本身也很简单,将servletContext传递过来,子类就可以自己做操作了

1
2
3
4
5
java复制代码public interface WebApplicationInitializer {

void onStartup(ServletContext servletContext) throws ServletException;

}

先看看类图

图片.png

作为Spring来说,怎么可能就提供一个简简单单的接口呢?肯定是一堆实现类。下面具体看看他们的操作

AbstractContextLoaderInitializer

在这里面主要干了下面的几件事情

  1. 给ServletContext创建rootAppContext。
  2. 将创建好的rootAppContext传递过去,创建ContextLoaderListener。
  3. 给ContextLoaderListener设置ApplicationContextInitializer。
  4. 给servletContext添加listener。

ContextLoaderListener熟悉把。之前配置在web.xml中的listener。现在编码方式搞进去。并且它的这个构造方法也有有考究的。还记得这个吗?

在这里插入图片描述

在看看他的才有参数的构造函数,这都有是考究的。这里直接设置进去就不需要在ContextLoaderListener创建rootWebApplicationContext。

并且这是抽象类,使用模板设计方法。将创建rootApplicationContext和获取ApplicationContextInitializer的操作留给子类。

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
java复制代码public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
protected final Log logger = LogFactory.getLog(getClass());

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
registerContextLoaderListener(servletContext);
}
// 创建rootApplication,并且创建ContextLoaderListener,通过ApplicationContextInitializer来自定义,并且给servletContext添加listener
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}

@Nullable
protected abstract WebApplicationContext createRootApplicationContext();

@Nullable
protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
return null;
}

}
AbstractDispatcherServletInitializer

看这个名字就知道了,给ServletContext配置了一个Servlet,(话说,Spring的起名字确实有一手)。

主要看了下面的几个事情

  1. 调用父类的onStartup方法。
  2. 开始给ServletContext注册DispatchServlet
1. 确定Servlet的名字(重写getServletName方法,可以改变servlet的名字)。
2. 创建webApplicationContext。
3. 创建DispatchServlet(createDispatcherServlet)。
4. 获取ApplicationContextInitializer(getServletApplicationContextInitializers)
5. 给DispatchServlet配置ApplicationContextInitializer。
6. 将servlet添加给ServletContext。
7. 设置刚刚添加进去的Servlet(通过ServletRegistration.Dynamic,默认支持servlet异步)
8. 获取Filter(getServletFilters)
9. 给Servlet添加Filter,并且解决重名问题。(利用循环解决重名问题,最多100),配置Filter
10. 留给子类拓展方法,主要是修改DispatchServlet在ServletContext中的属性,对比web.xml中`servlet`标签里面的配置(customizeRegistration)。

问题

1. 上面说的解决重名的问题,100是啥意思?


在调用servletContext添加filter的时候,如果重名,方法的返回值就是空,如果是空,就走到循环里面,下次的名字就是



1
2
3
> 	java复制代码registration = servletContext.addFilter(filterName + "#" + counter, filter);
>
>
counter会一直到100。如果到了100,就直接报错,同一个filter添加了100次。。也就是说,他允许添加一个filter的时候重名100个对象。
2. 重名的问题,在添加Servlet的时候会有吗?


会。如果重名就直接报错,他可没有处理的操作。
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
java复制代码
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {


public static final String DEFAULT_SERVLET_NAME = "dispatcher";


@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
registerDispatcherServlet(servletContext);
}


protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");

WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}

registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());

Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}

customizeRegistration(registration);
}


protected String getServletName() {
return DEFAULT_SERVLET_NAME;
}

// 给servlet创建ApplicationContext
protected abstract WebApplicationContext createServletApplicationContext();

// 创建dispatchServlet。
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
return new DispatcherServlet(servletAppContext);
}

// 获取ApplicationContextInitializer,这是专门给ServletApplicationContext中的
@Nullable
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return null;
}

// 获取ServletMapping。这类比的就是<servlet-mapping>
protected abstract String[] getServletMappings();


@Nullable
protected Filter[] getServletFilters() {
return null;
}
// 注册filter
protected FilterRegistration.Dynamic registerServletFilter(ServletContext servletContext, Filter filter) {
String filterName = Conventions.getVariableName(filter);
Dynamic registration = servletContext.addFilter(filterName, filter);

if (registration == null) {
int counter = 0;

// 处理重名
while (registration == null) {
if (counter == 100) {
throw new IllegalStateException("Failed to register filter with name '" + filterName + "'. " +
"Check if there is another filter registered under the same name.");
}
registration = servletContext.addFilter(filterName + "#" + counter, filter);
counter++;
}
}

registration.setAsyncSupported(isAsyncSupported());
registration.addMappingForServletNames(getDispatcherTypes(), false, getServletName());
return registration;
}

private EnumSet<DispatcherType> getDispatcherTypes() {
return (isAsyncSupported() ?
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC) :
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
}


protected boolean isAsyncSupported() {
return true;
}

protected void customizeRegistration(ServletRegistration.Dynamic registration) {
}
}
AbstractAnnotationConfigDispatcherServletInitializer

这个类,更加的具体,创建具体的WebApplicationContext。重写了父类的createRootApplicationContext和createServletApplicationContext。提供了两个方法,指定配置类。(getRootConfigClasses,getServletConfigClasses)这样就可以利用配置类来启动了。

但是得注意,这里只是调用的context.register()方法,要知道,这个方法之后是必要要调用refresh方法Spring容器才是可以继续启动的。但是这里没有哦。

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
java复制代码public abstract class AbstractAnnotationConfigDispatcherServletInitializer
extends AbstractDispatcherServletInitializer {


@Override
@Nullable
protected WebApplicationContext createRootApplicationContext() {
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
return context;
}
else {
return null;
}
}


@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}

@Nullable
protected abstract Class<?>[] getRootConfigClasses();


@Nullable
protected abstract Class<?>[] getServletConfigClasses();
}

okk。到这里编码方式实现Web.xml配置就结束了,可能会觉得突然断片了,其实后面的内容和从Servlet和Spring-web整合理解父子容器 ,是一样的逻辑。无非就是少了一步创建的过程。别的配置逻辑都是一样的。

上面的流程,结合从Servlet和Spring-web整合理解父子容器 ,画了一张图便于理解。

【Tomacat和Spring启动的过程图示】

Springboot融合方式

有了上面的了解,Springboot就比较好理解了,因为Springboot有嵌入式的Server。所以Springboot的融合方式和上面的不一样。

按照上面的写法,干线分为两个部分

  1. 创建tomcat的时候创建ServletContainerInitializer做初始化rootwebApplication。
  2. DispatchServlet的webApplication初始化。

1. TomcatStarter

TomcatStarter实现了ServletContainerInitializer借口,这个接口的作用上面已经说了,但是需要注意这个类没有使用HandlesTypes注解,所以,onStartup方法里面第一个参数就是一个空参数。在这个里面主要是 触发ServletContextInitializer.

下面看TomcatStarter是在哪里创建的,并且在Tomcat启动的时候ServletContextInitializer有用的类有哪些,都有什么作用?

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复制代码class TomcatStarter implements ServletContainerInitializer {

private static final Log logger = LogFactory.getLog(TomcatStarter.class);

private final ServletContextInitializer[] initializers;

private volatile Exception startUpException;

TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
// 添加进来,tomcat启动得时候,就会调用,在Spring的编程配置web.xml的时候,会使用@TypeHandle的注解,但是这里不需要
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
catch (Exception ex) {
this.startUpException = ex;
/
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
+ ex.getMessage());
}
}
}
Exception getStartUpException() {
return this.startUpException;
}
}

2. ServletContextInitializer在Springboot启动时候的作用

既然ServletContextInitializer是给Tomcat使用的,并且Springboot的Tomcat是嵌入式的,所以就从创建Tomcat的时候看,肯定有添加的操作。

ServletContextInitializer起作用的地方

ServletWebServerApplicationContext#onRefresh方法

这个方法是在Refresh方法里面定义的模板方法,留给子类拓展。ConfigurableApplicationContext接口中定义Refresh方法,并且在AbstractApplicationContext里面定义了refresh模板方法。

可以看到,在onRefresh调用了createWebServer方法,创建了一个webServer。

1
2
3
4
5
6
7
8
9
10
java复制代码	@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
创建webServer

简单的分析了一下,创建webServer不是这一篇文章的主题,之后专门出一篇,这里就简单的分析分析createWebServer代码逻辑,下面主要干了几点事情:

  1. 如果有ServletContext,直接调用ServletContextInitializer,这说明什么事情,Tomcat已经创建好了。
  2. 如果没有,先获取ServletWebServerFactory,调用factory.getWebServer方法,传递ServletContextInitializer集合。获取webServer
  3. 将webServer包装成WebServerGracefulShutdownLifecycle,WebServerStartStopLifecycle,并且将它注册到beanFactory中。
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复制代码	private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// 替换值,这都是小问题。
initPropertySources();
}

从上面有一个关键点(getSelfInitializer方法),这方法返回值就是一个ServletContextInitializer,那这就是我们的关注点。

有一个题外话,如果不清楚Spring的Refresh方法,有一个简单的方法,看日志看日志,然后搜,比如看下面这种日志:

图片.png

从这个日志就可以知道,这里已经初始化好了webApplicationContext,所以,从这里看准没有错,然后就断点一点点的看。就ok了(一点点的小小的经验)


我们还是顺着上面的方法看下去,getSelfInitializer已经传递给factory.getWebServer方法了,下来的就简单了,看哪些方法传递了给他。

图片.png

看看springboot支持哪些的嵌入式的服务器。

这里主要看TomcatServletWebServerFactory

TomcatServletWebServerFactory#getWebServer(ServletContextInitializer的地方)

在这里面会真正的创建webServer,这篇博客不是分析webServer的创建过程。这里主要看ServletContextInitializer引用的地方。一直看,就会看到下面的代码

可以看到,这里添加了两个

  1. 1
    java复制代码(servletContext) -> this.initParameters.forEach(servletContext::setInitParameter) // 设置init参数
  2. 1
    java复制代码new SessionConfiguringInitializer(this.session) // 配置Session
1
2
3
4
5
6
7
8
9
java复制代码	protected final ServletContextInitializer[] mergeInitializers(ServletContextInitializer... initializers) {
// 这里添加了两个
List<ServletContextInitializer> mergedInitializers = new ArrayList<>(); ServletContextInitializer的实现类,
mergedInitializers.add((servletContext) -> this.initParameters.forEach(servletContext::setInitParameter));
mergedInitializers.add(new SessionConfiguringInitializer(this.session));
mergedInitializers.addAll(Arrays.asList(initializers));
mergedInitializers.addAll(this.initializers);
return mergedInitializers.toArray(new ServletContextInitializer[0]);
}

上面的方法返回的 ServletContextInitializer 数字,将会在TomcatServletWebServerFactory#configureContext里面,创建TomcatStarter,将ServletContextInitializer数字作为参数传递过去。

总结:

到此,知道了在整个Springboot启动的时候,先是会有三个ServletContextInitializer发挥作用。他们是。在创建WebApplicationContext之后,还会获取别的ServletWebContextInitializer。来进行具体的配置化。

  1. ServletWebServerApplicationContext#selfInitialize方法,这就只有一个(这才是重点)
  2. AbstractServletWebServerFactory#mergeInitializers方法,这里面配置了两个(一个是配置session的,一个是配置init-param参数的)

给ServletContext创建WebApplication

上面已经说了,直接看selfInitialize方法,主要干了下面的几个事情

  1. 给ServletContext设置rootWebApplication,为当前的ApplicationContext。给当前的ApplicationContext设置ServletContext(prepareWebApplicationContext)
  2. 将ServletContext包装成ServletContextScope,注册到BeanFactory中,并且添加到servletContext的属性中。(ServletContextScope就是为了包装了ServletContext,为了方便的访问和获取值)
  3. 给bean工厂中注册servletContext(bean名字是ServletContext),ServletConfig(bean名字是servletConfig),parameterMap,这是ServletContext的参数,(这就是一个mapString:String,bean的名字是contextParameters),attributeMap,这是Servlet的属性,(这也是一个Map String:Object,bean的名字是contextAttributes)
  4. 遍历所有的ServletContextInitializer。调用onStartup方法将servletContext传递过去,来做定制化的配置。
1
2
3
4
5
6
7
8
9
java复制代码private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext); //给当前的servletCOntext设置rootWebApplication,将当前的applicationContext设置,并且也设置ServletContext
registerApplicationScope(servletContext); // 这个简单了,创建一个ServletContextScope,然后讲他添加到servletContext和applicationContext中
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
// 这里会创建dispatchServlet了 lcnote 这里得注意
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}

到第四步的时候,已经给ServletContext创建好了WebApplicationContext了,重点是getServletContextInitializerBeans方法,主要的作用,

getServletContextInitializerBeans分析

ServletContextInitializerBeans继承了AbstractCollection,表示,他是一个集合。集合里面存放的是ServletContextInitializer。所以,上面的方法里面才可以利用forEarch来遍历。

不是说继承了AbstractCollection,什么不干,就可以利用forEarch遍历了,肯定还有操作,下面先看这个。

主要是重写了迭代器方法,迭代器返回的是sortedList的。sortedList是这里面用来存放数据的地方,在构造函数里面已经设置好了。所以就可以利用forearch了。

1
2
3
4
5
6
7
8
9
10
java复制代码// 主要是重写了下面的方法
@Override
public Iterator<ServletContextInitializer> iterator() {
return this.sortedList.iterator();
}

@Override
public int size() {
return this.sortedList.size();
}

下面具体看看这里面干的事情

  1. 初始化initializers。
  2. 确认initializerTypes,默认是ServletContextInitializer。之后会通过这个类型会从beanFactory中获取对应的bean。(要知道从beanFactory中获取bean的操作会走一边获取bean的过程,自动注入也是在这里发生的)
  3. 根据initializerTypes的值去beanFactory中获取值,将这些bean添加到initializers(bean的集合)里面,并且会获取到这些特殊的bean处理的Resource,添加到seen里面。
  4. 利用适配器,对系统中还存在的Filter和Servlet做适配器。并且也会添加到initializers和seen
  5. 排序,赋值给sortedList。(想想之前的迭代器。就是利用这个来做处理的)
  6. 简单的打印日志
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码	public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class);
addServletContextInitializerBeans(beanFactory);
addAdaptableBeans(beanFactory);
List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
logMappings(this.initializers);
}
ServletContextInitializerBeans#addServletContextInitializerBeans

从这个方法里面会从beanFactory中获取值(要知道,从beanFactory中获取bean,要是没有的话,就直接会创建,并且在这个途中会应用到所有的BeanPostProcess,自动注入)

1
2
3
4
5
6
7
8
java复制代码private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
initializerType)) {
addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
}
}
}
ServletContextInitializerBeans#addServletContextInitializerBean

上一步从beanFactory中获取到bean,然后通过不同的类型做处理。注意看addServletContextInitializerBean的source参数。source是ServletRegistrationBean里面的衍生物,下面的这几个,只是参数的类型不同,都是调用了addServletContextInitializerBean方法。将值添加到initializers和seen里面。

问题:

  1. 上面一直提到的seen和initializers是什么?

initializers是一个数组。

定义如下:

private final MultiValueMap<Class<?>, ServletContextInitializer> initializers;

这里面保存的是key是class对象,value是这个class关联的ServletContextInitializer链表。到最后的时候,会把value的值组成一个list,赋值给sortedList

seen是一个Set

定义如下:

private final Set seen = new HashSet<>();

这个Set会在addServletContextInitializerBean里面添加值,这里面添加的值是ServletRegistrationBean或者FilterRegistrationBean或者DelegatingFilterProxyRegistrationBean或者ServletListenerRegistrationBean调用各自的方法获取的值,可以理解为这些的衍生物。所以是一个Set。

  • FilterRegistrationBean和ServletRegistrationBean和DelegatingFilterProxyRegistrationBean和ServletListenerRegistrationBean都是一些什么东西

详情在下面看。

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复制代码	private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
> > ListableBeanFactory beanFactory) {
> > if (initializer instanceof ServletRegistrationBean) {
> > Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
> > addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
> > }
> > else if (initializer instanceof FilterRegistrationBean) {
> > Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
> > addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
> > }
> > else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
> > String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
> > addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
> > }
> > else if (initializer instanceof ServletListenerRegistrationBean) {
> > EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
> > addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
> > }
> > else {
> > addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
> > initializer);
> > }
> > }
> >
> >
RegistrationBean和它的实现类分析

图片.png

从上面的图可以很清楚的看到,他是实现了ServletContextInitializer接口,ServletContextInitializer。在创建WebApplicationContext的时候已经创建了三个了,这三个都交给TomcatStarter来循环调用了,所以,这里的这些实现类都是在容器里面注入的,和之前的是没有关系的,但是他们的功能都是一样,在ServletContext启动的时候配置东西。

既然实现了ServletContextInitializer接口,直接先看onStartup方法

1. RegistrationBean#onStartup方法

一个简单的模板方法,先获取description,在判断this是否启动,没有就直接打日志,有的话就调用register方法。

getDescription,isEnabled,register这都是留给子类来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
> > java复制代码	@Override
> > public final void onStartup(ServletContext servletContext) throws ServletException {
> >
> > String description = getDescription();
> > if (!isEnabled()) {
> > logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
> > return;
> > }
> > register(description, servletContext);
> > }
> >
> >

继续按照上面的图来看,

2. DynamicRegistrationBean

在之前的基础上,增加了配置Registration.Dynamic的能力,Registration.Dynamic在之前介绍过,就是给ServletContext添加Servlet或者Filter之后的返回值,还记得上面说的 100吗?

这里就在之前的基础上,增加了对配置到ServletContext中的Servlet和Filter做配置。

直接看它是怎么写

好家伙,写的还挺骚的,可以看到,处理继承RegistrationBean,还增加了范型支持,这里面处理的值是继承于Registration.Dynamic的。并且增加了一些基础的配置,比如asyncSupported,初始参数。

既然继承了RegistrationBean,直接看它重写的方法,register

在register里面又增加了模板方法。addRegistration和configure,先调用addRegistration将servletContext传递过去,作为子类来说,就可以做一些额外的配置了, 比如对于ServletRegistrationBean来说,就可以在这个里面添加DispatchServlet,在添加完了之后,ServletContext就可以返回一个Registration.Dynamic的子类,传递给configure,就可以做自定义的配置了,也可以重写这个方法,比如对于ServletRegistrationBean来说,配置init参数,添加Servlet能够处理的Mapping等扽信息。

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
> > java复制代码public abstract class DynamicRegistrationBean<D extends Registration.Dynamic> extends RegistrationBean {
> >
> > private static final Log logger = LogFactory.getLog(RegistrationBean.class);
> >
> > private String name;
> >
> > private boolean asyncSupported = true;
> >
> > private Map<String, String> initParameters = new LinkedHashMap<>();
> >
> > public void setName(String name) {
> > Assert.hasLength(name, "Name must not be empty");
> > this.name = name;
> > }
> >
> > public void setAsyncSupported(boolean asyncSupported) {
> > this.asyncSupported = asyncSupported;
> > }
> >
> > public boolean isAsyncSupported() {
> > return this.asyncSupported;
> > }
> >
> >
> > public void setInitParameters(Map<String, String> initParameters) {
> > Assert.notNull(initParameters, "InitParameters must not be null");
> > this.initParameters = new LinkedHashMap<>(initParameters);
> > }
> >
> > public Map<String, String> getInitParameters() {
> > return this.initParameters;
> > }
> >
> >
> > public void addInitParameter(String name, String value) {
> > Assert.notNull(name, "Name must not be null");
> > this.initParameters.put(name, value);
> > }
> >
> > @Override
> > protected final void register(String description, ServletContext servletContext) {
> > D registration = addRegistration(description, servletContext);
> > if (registration == null) {
> > logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
> > return;
> > }
> > configure(registration);
> > }
> >
> > protected abstract D addRegistration(String description, ServletContext servletContext);
> >
> > protected void configure(D registration) {
> > registration.setAsyncSupported(this.asyncSupported);
> > if (!this.initParameters.isEmpty()) {
> > registration.setInitParameters(this.initParameters);
> > }
> > }
> >
> >
> > protected final String getOrDeduceName(Object value) {
> > return (this.name != null) ? this.name : Conventions.getVariableName(value);
> > }
> > }
> >
> >

就先分析到这里把,子类的实现可以自己去看看,已经很清楚了,现在还有一个问题,这些DynamicRegistrationBean是在哪里注入的?

一开始我也不知道,说说我是怎么做的。比如DispatcherServletRegistrationBean吧。

先点开他,然后看它的引用,然后打new, 肯定是要创建的,基本上就搞定了。

图片.png

这一看就是下面的两个,上面的是端点

图片.png

在DispatcherServletAutoConfiguration里面,可以看到这个bean是需要一个dispatchServlet的,那这个DispatchServlet是在哪里注入的呢?

图片.png

就在这个方法的上面,可以看到,直接new出了dispatchServlet,要清楚,这里的DisaptchServlet是添加到Bean容器里面的。所以,它也走bean的生命周期那一套。

addAdaptableBeans分析

显示从beanFactory中获取MultipartConfigElement,如果有多个,只获取一个。

重点就是下面的这几个方法了

1. addAsRegistrationBean方法
2. ServletRegistrationBeanAdapter类和FilterRegistrationBeanAdapter和ServletListenerRegistrationBeanAdapter

问题:

1. addAdaptableBeans的目的是为了什么?
我觉得是为了将applicationContext中的配置的Servlet和Filter添加到ServletContext中,并且容易配置。也就是说在Springboot中也是可以配置多个Servlet的。
1
2
3
4
5
6
7
8
9
10
11
> > java复制代码	protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
> > MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);
> > addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
> > addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
> > for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) {
> > addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType,
> > new ServletListenerRegistrationBeanAdapter());
> > }
> > }
> >
> >
addAsRegistrationBean分析

从bean工厂中获取beanType类型的bean,这些bean没有在seen集合里面。得到一个list,遍历这list,调用传递进来的适配器,适配,给适配器设置原来bean的order,添加到seen和initializers里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> > java复制代码private <T, B extends T> void addAsRegistrationBean(ListableBeanFactory beanFactory, Class<T> type,
> > Class<B> beanType, RegistrationBeanAdapter<T> adapter) {
> > List<Map.Entry<String, B>> entries = getOrderedBeansOfType(beanFactory, beanType, this.seen);
> > for (Entry<String, B> entry : entries) {
> > String beanName = entry.getKey();
> > B bean = entry.getValue();
> > if (this.seen.add(bean)) {
> > // One that we haven't already seen
> > RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size());
> > int order = getOrder(bean);
> > registration.setOrder(order);
> > this.initializers.add(type, registration);
> > if (logger.isTraceEnabled()) {
> > logger.trace("Created " + type.getSimpleName() + " initializer for bean '" + beanName + "'; order="
> > + order + ", resource=" + getResourceDescription(beanName, beanFactory));
> > }
> > }
> > }
> > }
> >
> >

下面的这三个都实现了RegistrationBeanAdapter接口,这个接口表示是可以将给定的bean转为RegistrationBean,这个RegistrationBean意味着在ServletContext中可以添加的类。重点看它的createRegistrationBean方法

ServletRegistrationBeanAdapter

就是将source转化为ServletRegistrationBean。要是再配置类里面注入的,可以手动的设置进去那个范型具体的值,但是这里就只能自己来了。

如果除了seen里面包含的bean之外,只有一个,url就是/,否则就是”/“ + name + “/“。

如果要创建的bean 的名字是DISPATCHER_SERVLET_NAME,就默认/,其余的就是创建ServletRegistrationBean,并且设置multipartConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
> > java复制代码	@Override
> > public RegistrationBean createRegistrationBean(String name, Servlet source, int totalNumberOfSourceBeans) {
> > String url = (totalNumberOfSourceBeans != 1) ? "/" + name + "/" : "/";
> > if (name.equals(DISPATCHER_SERVLET_NAME)) {
> > url = "/"; // always map the main dispatcherServlet to "/"
> > }
> > ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>(source, url);
> > bean.setName(name);
> > bean.setMultipartConfig(this.multipartConfig);
> > return bean;
> > }
> >
> >
FilterRegistrationBeanAdapter

这里其实和上面也是一样,不过返回的是FilterRegistrationBean。

1
2
3
4
5
6
7
8
> > java复制代码	@Override
> > public RegistrationBean createRegistrationBean(String name, Filter source, int totalNumberOfSourceBeans) {
> > FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(source);
> > bean.setName(name);
> > return bean;
> > }
> >
> >
ServletListenerRegistrationBeanAdapter

这里也是一样的,返回的是ServletListenerRegistrationBean。

1
2
3
4
5
6
7
> > java复制代码	@Override
> > public RegistrationBean createRegistrationBean(String name, EventListener source,
> > int totalNumberOfSourceBeans) {
> > return new ServletListenerRegistrationBean<>(source);
> > }
> >
> >

到这里就知道了,ServletContextInitializerBeans里面干了什么事情。剩下的就是循环调用了。再结合上面的代码,总结下来就是:

1. 创建ServletContextInitializerBeans。再这里面会加载所有的实现了ServletContextInitializer的类,
2. 其中有一个特殊的bean(RegistrationBean),再这个实现类里面会配置ServletContext。
3. RegistrationBean的实现类是自动配置类注入的。其中就有DispatchServlet。会创建dispatchServlet,并且会创建DispatcherServletRegistrationBean,将DispatchServlet传回去,再调用ServletContextInitializer的onStartup方法的时候会将DispatchServlet添加到ServletContext中,并且做配置。

到现在已经说清楚了,webApplicationContext和ServletContext的关联,DispatchServlet和ServletContext的关联,但是现在还有一个问题,回想到之前说的,DispatchServlet也有一个webApplicationContext,一直到这里也没有看到。

下面就来分析分析

3. DispatchServlet中的applicationContext

再创建dispatchServlet的时候,webApplicationContext已经创建好。DispatchServlet已经添加容器了。也没有看到DispatchServlet设置applicationContext的操作。那它怎么是怎么做的?

图片.png

dispatchServlet实现了ApplicationContextAware接口,通过这个接口就会将applicationContext传递过来,dispatchServlet创建的时候走了完整的过程,所以,这个applicationContext就是之前的Context。也就是和RootWebApplication一样。

举个例子看看

在之前的spring-web里面,有两个容器,子可以访问父,父不能访问子。也就是controller不能注入到service中,但是在Springboot中是可以的。

图片.png
到此,从Servlet和SpringBoot整合理解父子容器就结束了。

关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

本文转载自: 掘金

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

【Spring Boot 快速入门】十九、Spring Bo

发表于 2021-11-18

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

前言

  相信很多后端的小伙伴,在做权限认证的时候,首先想到的是基于session或者token的认证。当我们在做分布式站点集群的用户单点登录的时候,基于session和token的认证就有局限性了,那么有没有更好的方式去处理认证问题呢,下面与大家介绍一下JWT。

什么是JWT

  JWT是JSON WEB TOKEN的简称,JWT是一个开放标准(RFC 7519)经常用于在多方之间安全的传输信息。JWT由于有较高的信息安全性,被广泛的应用于授权和信息交互方面。在被JWT加密的信息是一种紧密的自包含的数据串。

JWT格式

  JWT格式是由三段信息构成的,它们之间用圆点(.)连接,第一部分是头部信息,第二部分是载荷信息,第三部分是签证信息。如下字符串是一个JWT格式的字符串。

  • eyJhbGciOiJIUzI1NiJ9:是头部信息,头部信息主要是token的类型和算法等信息。
  • eyJqdGkiOiIxIiwic3ViIjoiYWRtaW4xMjM0NTYiLCJpc3MiOiJ1c2VyIiwiaWF0IjoxNjM3MjM2NDI5LCJleHAiOjE2MzcyMzY0Mzl9:载体信息,主要是加密的数据资源信息。
  • t5pAMGV0Rx3C455f5c812yvqnC6UqwiTpALeo5EFvR8:签证信息。签证信息主要用于验证消息在传递过程中有没有被篡改。

  需要注意的是,在头部信息和载体信息z中放置敏感的信息,需要先加密处理。

1
复制代码eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoiYWRtaW4xMjM0NTYiLCJpc3MiOiJ1c2VyIiwiaWF0IjoxNjM3MjM2NDI5LCJleHAiOjE2MzcyMzY0Mzl9.t5pAMGV0Rx3C455f5c812yvqnC6UqwiTpALeo5EFvR8

快速开始

  本次将基于Spring Boot 搭建一个学习Mybatis_Plus的乐观锁的Demo。开发环境如下:

1
2
3
4
sql复制代码JDK1.8
SpringBoot 2.3.0.RELEASE
java-jwt 3.2.0
jjwt 0.7.0

依赖

  本次需要加入JWT相关的依赖包,具体依赖如下,如有不同版本,可能存在于SpringBoot兼容性问题,找到实配版本即可。

1
2
3
4
5
6
7
8
9
10
xml复制代码    <dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

创建JWT

SignatureAlgorithm中定义的标准JWT签名算法支持的算法名称,可以直接引用即可。具体定义的算法名称如下图:

图片.png

本次采用的签名算法是:SHA-256。
首先指定本次JWT使用的签名算法。具体信息如下:

1
js复制代码SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

再次设置秘钥信息SecretKey。采用Base64对秘钥转换成字节数组。创建一个SecretKeySpec密钥规范。采用AES加密。

1
2
3
js复制代码
byte[] encodedKey = Base64.decode(JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

下面根据JwtBuilder创建Jwt对象,JwtBuilder继承了ClaimsMutator。调用Jwts.builder(),设置builder的基本参数创建JwtBuilder。

1
2
3
4
5
6
7
js复制代码  JwtBuilder builder = Jwts.builder().setId(id).setSubject(subject)
.setIssuer("juejin")
.setIssuedAt(now)
.setExpiration(expDate)
.signWith(signatureAlgorithm, secretKey);

builder.compact()

主要包含:ID ,subject加密内容、Issuer签发者信息、IssuedAt签发时间、Expiration过期时间、signatureAlgorithm签名使用的算法和secretKey秘钥信息。
然后调用builder.compact()方法即可创建JWT字符串。例如使用字符串:“掘金——代码不止,掘金不停”生成一串JWT信息如下:

1
js复制代码eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoi5o6Y6YeR4oCU4oCU5Luj56CB5LiN5q2i77yM5o6Y6YeR5LiN5YGcIiwiaXNzIjoidXNlciIsImlhdCI6MTYzNzI0MjY0NywiZXhwIjoxNjM3MjQyNjU3fQ.mMkMRyxVTcOSZCDrR5cGvY6KcRPCVVQwETSyPQMSAQo

验证JWT

  上面已经创建了JWT信息,当我们获取到JWT信息怎么验证和读取JWT中包含的信息呢,下面就开始介绍验证JWT。其中secretKey是上面介绍到的秘钥信息,获取方法与上面类似,参数jwt是创建的JWT信息。然后通过getBody()获得Claims、

1
js复制代码Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();

JWT Claims主要包含:用户身份标识ID,iss签发人信息,sub内容信息,iat签发时间,exp过期时间等。以上是JWT创建过程的建立和验证,下面在Spring Boot项目中进行验证使用。

测试JWT

在Spring Boot项目中进行验证使用。本次验证使用两个接口调用验证,一个是获取JWT接口,一个是验证JWT接口,将获取到的JWT信息传输到验证接口,看是否能获取到JWT中的信息。接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码
@GetMapping("setJwt")
@ResponseBody
public String setJwt(String info){
return JwtUtils.createJWT("juejin",info,10000L);
}

@GetMapping("getJwt")
@ResponseBody
public String getJwt(String info)throws Exception{
return JwtUtils.validateJWT(info).getClaims().getSubject();
}

启动项目之后,先调用获取JWT的接口,可以看到已经获取到了JWT。

图片.png

1
js复制代码eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJqdWVqaW4iLCJzdWIiOiLmjpjph5HigJTigJTku6PnoIHkuI3mraLvvIzmjpjph5HkuI3lgZwiLCJpc3MiOiJ1c2VyIiwiaWF0IjoxNjM3MjQ1MTQwLCJleHAiOjE2MzcyNDUxNTB9.WKso2dPHEtSx_ItCCXmq2QjSDR6AIBzetKft4CJ8wNQ

然后经获取到的JWT调用验证JWT的接口,返回“掘金——代码不止,掘金不停”,验证成功。
图片.png

结语

  好了,以上就是Spring Boot 集成JWT的一个简单示例,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

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

本文转载自: 掘金

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

1…286287288…956

开发者博客

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