账号微服务注册、压测与架构核心技术
# 账号微服务注册、压测与架构核心技术
# 注册功能和流程介绍
功能需求
- 使⽤⼿机号注册,已经注册的⼿机号不能︎重复注册,密码不能使⽤简单的 MD5 加密
- ⽤户上传头像需要⽤⽂件存储
安全需求
- ⾼并发下账号唯⼀性 注册邮箱或者⼿机验证码不能被恶意调⽤,验证码 + 唯⼀索引
- 头像⽂件存储访问⽅便扩容和管理,阿⾥云 OSS
- 针对每个功能,初级开发和⾼级开发的思路是不⼀样
- 产品经理提业务需求 安全需求就是⾃⼰的经验,不然最终背锅的还是⾃⼰
- ⾼并发处理,异步 + 池化思想
短链平台选择
- 使⽤短信验证码注册
- 头像存储使⽤阿⾥云 OSS
# 第三⽅短信验证码平台接⼊
短信验证码平台选择考虑点
- 各个类型短信价格
- 短信到达率、到达时间
- 短信内容变量灵活,⽅便⽀持多场景
- ⽀持多种推⼴内容的短信发放,例如业务推⼴、新产品宣讲、会员关怀等内容的短信
- 多维度数据统计 - 查看请求量、发送成功量、失败量等
各个短信平台
- 阿⾥云:https://www.aliyun.com/product/sms
- 腾讯云:https://cloud.tencent.com/product/sms
- 第三⽅⼚商:https://market.aliyun.com/products/57000002/cmapi00046920.html
我们选择接入阿里云市场中的一些第三方产品
阿⾥云市场:https://market.console.aliyun.com/imageconsole/index.htm
# 短信验证码发送⼯具类
dlcloud-common 模块下 net.xdclass.config.RestTemplate 配置
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new
SimpleClientHttpRequestFactory();
factory.setReadTimeout(10000);
factory.setConnectTimeout(10000);
return factory;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dlcloud-account 模块下添加到 application.yaml
#----------sms短信配置--------------
sms:
app-code: 6999d4df3e7d48028470bbe517169a8d
template-id: M72CB42894
2
3
4
net.xdclass.config.SmsConfig 配置
@ConfigurationProperties(prefix = "sms")
@Configuration
@Data
public class SmsConfig {
private String templateId;
private String appCode;
}
2
3
4
5
6
7
8
9
10
net.xdclass.component.SmsComponent
package net.xdclass.component;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.config.SmsConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* @author iekrw
* Date: 2024/2/3/003 11:20
*/
@Component
@Slf4j
public class SmsComponent {
/**
* 发送地址
*/
private static final String URL_TEMPLATE = "https://jmsms.market.alicloudapi.com/sms/send?mobile=%s&templateId=%s&value=%s";
@Autowired
private RestTemplate restTemplate;
@Autowired
private SmsConfig smsConfig;
/**
* 发送短信验证码
* @param to
* @param templateId
* @param value
*/
public void send(String to,String templateId,String value){
String url = String.format(URL_TEMPLATE,to,templateId,value);
HttpHeaders headers = new HttpHeaders();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.set("Authorization","APPCODE "+smsConfig.getAppCode());
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
log.info("url={},body={}",url,response.getBody());
if(response.getStatusCode().is2xxSuccessful()){
log.info("发送短信验证码成功");
}else {
log.error("发送短信验证码失败:{}",response.getBody());
}
}
}
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
# 单元测试
net.xdclass.db.net.xdclass.biz
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SmsTest {
@Autowired
private SmsComponent smsComponent;
@Autowired
private SmsConfig smsConfig;
@Test
public void testSendSms() {
smsComponent.send("1377777777", smsConfig.getTemplateId(), "123456");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 架构核⼼技术 - 池化思想 - 异步结合 性能优化最佳实践
# 接⼝压测和常⽤压⼒测试⼯具对⽐
LoadRunner:性能稳定,压测结果及细粒度⼤,可以⾃定义脚本进⾏压测,但是太过于重⼤,功能⽐较繁多
Apache AB (单接⼝压测最⽅便):模拟多线程并发请求,ab 命令对发出负载的计算机要求很低,既不会占⽤很多 CPU,也不会占⽤太多的内存,但却会给⽬标服务器造成巨⼤的负载,简单 DDOS 攻击等
Webbench:webbench ⾸先 fork 出多个⼦进程,每个⼦进程都循环做 web 访问测试。⼦进程把访问的结果通过 pipe 告诉⽗进程,⽗进程做最终的统计结果。
Jmeter (GUI):开源免费,功能强⼤,在互联⽹公司普遍使⽤
压测不同的协议和应⽤:
- Web - HTTP, HTTPS (Java, NodeJS, PHP, ASP.NET, …)
- SOAP / REST Webservices
- FTP
- Database via JDBC
- LDAP 轻量⽬录访问协议
- Message-oriented middleware (MOM) via JMS
- Mail - SMTP(S), POP3(S) and IMAP(S)
- TCP 等等
使⽤场景及优点:
功能测试
压⼒测试
分布式压⼒测试
纯 java 开发
上⼿容易,⾼性能
提供测试数据分析
各种报表数据图形展示
# Jmeter
需要安装 JDK8 以上,建议安装 JDK 环境,虽然 JRE 也可以,但是压测 https 需要 JDK ⾥⾯的 keytool ⼯具 快速下载 https://jmeter.apache.org/download_jmeter.cgi ⽂档地址:http://jmeter.apache.org/usermanual/get-started.html
目录结构:
- bin: 核⼼可执⾏⽂件,包含配置
- jmeter.bat: windows 启动⽂件 (window 系统⼀定要配置显示⽂件拓展名)
- jmeter: mac 或者 linux 启动⽂件
- jmeter-server:mac 或者 Liunx 分布式压测使⽤的启动⽂件
- jmeter-server.bat:window 分布式压测使⽤的启动⽂件
- jmeter.properties: 核⼼配置⽂件
- extras:插件拓展的包
- lib: 核⼼的依赖包
Jmeter 语⾔版本中英⽂切换:
- 控制台修改 menu -> options -> choose language
- 配置⽂件修改
- bin ⽬录 ->
jmeter.properties - 默认 #language=en,改为
language=zh_CN
- bin ⽬录 ->
# 菜单栏主要组件
添加 ->threads-> 线程组(控制总体并发)

- 线程数:虚拟⽤户数。⼀个虚拟⽤户占⽤⼀个进程或线程
- 准备时⻓(Ramp-Up Period (in seconds)):全部线程启动的时⻓,⽐如 100 个线程,20 秒,则表示 20 秒内 100 个线程都要启动完成,每秒启动 5 个线程
- 循环次数:每个线程发送的次数,假如值为 5,100 个线程,则会发送 500 次请求,可以勾选永远循环
线程组 -> 添加 -> Sampler (采样器) -> Http (⼀个线程组下⾯可以增加⼏个 Sampler)

- 名称:采样器名称
- 注释:对这个采样器的描述
- web 服务器:
- 默认协议是 http
- 默认端⼝是 80
- 服务器名称或 IP :请求的⽬标服务器名称或 IP 地址
- 路径:服务器 URL
查看测试结果
- 线程组 -> 添加 -> 监听器 -> 察看结果树
- 线程组 -> 添加 -> 监听器 -> 聚合报告
常规压测流程
- 内⽹环境
- ⾮ GUI 下压测
- 停⽌其他⽆关资源进程
- 压测机和被压测机器隔离
# 短信发送压测实践
由于我们短信发送是需要费用,进行压测可能会产生大量的费用,我们使用埋点发送多次请求,来获取该接口大概耗时,此处只是进行模拟。在短信发送前和发送后记录请求耗时,然后通过线程睡眠模拟该接口耗时。
SmsComponent
public void send(String to, String templateId, String value) {
long beginTime = CommonUtil.getCurrentTimestamp();
String url = String.format(URL_TEMPLATE, to, templateId, value);
HttpHeaders headers = new HttpHeaders();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.set("Authorization", "APPCODE " + smsConfig.getAppCode());
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
long endTime = CommonUtil.getCurrentTimestamp();
log.info("耗时={},url={},body={}", endTime - beginTime, url, response.getBody());
if (response.getStatusCode().is2xxSuccessful()) {
log.info("发送短信验证码成功");
} else {
log.error("发送短信验证码失败:{}", response.getBody());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SmsTest
@Test
public void testSendSms() {
for (int i = 0; i < 3; i++) {
smsComponent.send("1377777777", smsConfig.getTemplateId(), "123456");
}
}
2
3
4
5
6
7

通过埋点 http 请求得出请求响应耗时【粗略统计,⾮线上⼤量数据测试得出】
NotifyController
@RestController
@RequestMapping("api/notify/v1")
public class NotifyController {
@Autowired
private NotifyService notifyService;
/**
* 测试发送验证码接口,用于压测
*
* @return
*/
@RequestMapping("send_code")
public JsonData senCode() {
notifyService.testSend();
return new JsonData().buildSuccess();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NotifyService
public interface NotifyService {
/**
* 测试方法
*/
void testSend();
}
2
3
4
5
6
7
NotifyServiceImpl
@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {
@Autowired
private RestTemplate restTemplate;
@Override
public void testSend() {
// 使用睡眠模拟耗时
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 发起另外的链接进行测试
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://old.xdclass.net", String.class);
String body = forEntity.getBody();
log.info("body={}", body);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
压测参数配置
- 200 并发
- 2 秒启动
- 循环 500 次

同步发送 + resttemplate 未池化
- 错误:Connection timed out
- 400 到 500 qps
# @Async 组件
⾼并发下异步请求解决⽅案⼀
由于发送短信涉及到⽹络通信,因此 sendMessage ⽅法可能会有⼀些延迟。为了改善⽤户体验,我们可以使⽤异步发送短信的⽅法
什么是异步任务
- 异步调⽤是相对于同步调⽤⽽⾔的,同步调⽤是指程序按预定顺序⼀步步执⾏,每⼀步必须等到上⼀步执⾏完后才能执⾏,异步调⽤则⽆需等待上⼀步程序执⾏完即可执⾏
- 多线程就是⼀种实现异步调⽤的⽅式
- MQ 也是⼀种宏观上的异步
使⽤场景
- 适⽤于处理 log、发送邮件、短信…… 等
- 涉及到⽹络 IO 调⽤等操作
使⽤⽅式
- 启动类⾥⾯使⽤
@EnableAsync注解开启功能,⾃动扫描 - 定义异步任务类并使⽤
@Component标记组件被容器扫描,异步⽅法加上@Async
@MapperScan("net.xdclass.mapper")
@EnableTransactionManagement
@EnableFeignClients
@EnableDiscoveryClient
@EnableAsync
@SpringBootApplication
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
@Override
@Async
public void testSend() {
// 使用睡眠模拟耗时
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 发起另外的链接进行测试
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://old.xdclass.net", String.class);
String body = forEntity.getBody();
log.info("body={}", body);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# @Async 失效情况
- 注解 @Async 的⽅法不是 public ⽅法
- 注解 @Async 的返回值只能为 void 或者 Future
- 注解 @Async ⽅法使⽤ static 修饰也会失效
- spring ⽆法扫描到异步类,没加注解 @Async 或 @EnableAsync 注解
- 调⽤⽅与被调⽅不能在同⼀个类
- Spring 在扫描 bean 的时候会扫描⽅法上是否包含 @Async 注解,动态地⽣成⼀个⼦类(即 proxy 代理类),当这个有注解的⽅法被调⽤的时候,实际上是由代理类来调⽤的,代理类在调⽤时增加异步作⽤
- 如果这个有注解的⽅法是被同⼀个类中的其他⽅法调⽤的,那么该⽅法的调⽤并没有通过代理类,⽽是直接通过原来的那个 bean,所以就失效了
- 所以调⽤⽅与被调⽅不能在同⼀个类,主要是使⽤了动态代理,同⼀个类的时候直接调⽤,不是通过⽣成的动态代理类调⽤
- ⼀般将要异步执⾏的⽅法单独抽取成⼀个类
- 类中需要使⽤ @Autowired 或 @Resource 等注解⾃动注⼊,不能⾃⼰⼿动 new 对象
- 在 Async ⽅法上标注
@Transactional是没⽤的,但在 Async ⽅法调⽤的⽅法上标注@Transactional是有效的,即需要在调用方上添加事务注解,反之在被调用方上添加事务是不会生效的
# 异步调⽤ - 压测⾼ QPS 后的背后原因和问题拆解
使用 @Async 异步调用压测后很快跑完全部内容,是因为都在线程池内部的阻塞队列⾥⾯
但极容易出现 OOM,或者消息丢失
默认 8 个核⼼线程数占⽤满了之后,新的调⽤就会进⼊队列,最⼤值是 Integer.MAX_VALUE,表现为没有执⾏
- task-XXX ⽇志⾥⾯会出现递增
我们设置下 idea 启动进程的 jvm 参数: -Xms50M -Xmx50M ,设置最小和最大堆内存模拟出现 OOM 情况

Spring 默认线程池代码位置
- TaskExecutionProperties
- TaskExecutionAutoConfiguration
说明:
- 直接使⽤ @Async 注解没指定线程池的话,即未设置 TaskExecutor 时
- 默认使⽤ Spring 创建 ThreadPoolTaskExecutor
- 核⼼线程数:8
- 最⼤线程数:Integer.MAX_VALUE (21 亿多)
- 队列使⽤ LinkedBlockingQueue
- 容量是:Integer.MAX_VALUE
- 空闲线程保留时间:60s
- 线程池拒绝策略:AbortPolicy
# @Async+ThreadPoolTaskExecutor ⾃定义线程池
⾃定义线程池可以解决上述的问题
⼤家的疑惑 使⽤线程池的时候搞混淆 ThreadPoolTaskExecutor 和 ThreadPoolExecutor
- ThreadPoolExecutor:这个类是 JDK 中的线程池类,继承⾃ Executor,⾥⾯有⼀个 execute () ⽅法,⽤来执⾏线程,线程池主要提供⼀个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁的额外开销
- ThreadPoolTaskExecutor,是 spring 包下的,是 Spring 为我们提供的线程池类
- Spring 异步线程池的接⼝类是 TaskExecutor,本质还是 java.util.concurrent.Executor
解决⽅式
- spring 会先搜索 TaskExecutor 类型的 bean 或者名字为 taskExecutor 的 Executor 类型的 bean
- 所以我们最好来⾃定义⼀个线程池,加⼊ Spring IOC 容器⾥⾯,即可覆盖
net.xdclass.config.ThreadPollTaskConfig
@Configuration
@EnableAsync // 如果config类中开启async了 启动类中的开启async注解可以删除 没必要多次开启
public class ThreadPollTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//线程池创建的核⼼线程数,线程池维护线程的最少数量,即使没有任务需要执⾏,也会⼀直存活
executor.setCorePoolSize(16);
//如果设置allowCoreThreadTimeout=true(默认false)时,核⼼线程会超时关闭
// executor.setAllowCoreThreadTimeOut(true);
//缓存队列(阻塞队列)当核⼼线程数达到最⼤时,新任务会放在队列中排队等待执⾏
executor.setQueueCapacity(1024);
//最⼤线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
//当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务⽽抛出异常
executor.setMaxPoolSize(64);
//当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
//允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
//如果allowCoreThreadTimeout=true,则会直到线程数量 = 0
executor.setKeepAliveSeconds(30);
//spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() ⽅法的。
//jdk 提供的ThreadPoolExecutor 线程池是没有setThreadNamePrefix() ⽅法的
executor.setThreadNamePrefix("自定义线程池");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy():交由调⽤⽅线程运⾏,⽐如main 线程;如果添加到线程池失败,那么主线程会⾃⼰去执⾏该任务,不会等待线程池中的线程去执⾏
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最⽼的任务,队列满了,会将最早进⼊队列的任务删掉腾出空间,再尝试加⼊队列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
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
调用方使用自定义线程池
@Override
@Async("threadPoolTaskExecutor")
public void testSend() {
// 使用睡眠模拟耗时
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 发起另外的链接进行测试
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://old.xdclass.net", String.class);
String body = forEntity.getBody();
log.info("body={}", body);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
总结:先是 CorePoolSize 是否满⾜,然后是 Queue 阻塞队列是否满,最后才是 MaxPoolSize 是否满⾜
# ThreadPoolTaskExecutor 线程池的⾯试题
- 请你说下 ThreadPoolTaskExecutor 线程池 有哪⼏个重要参数,什么时候会创建线程
- 查看核⼼线程池是否已满,不满就创建⼀条线程执⾏任务,否则执⾏第⼆步。
- 查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执⾏第三步。
- 查看线程池是否已满,即是否达到最⼤线程池数,不满就创建⼀条线程执⾏任务,否则就按照策略处理⽆法执⾏的任务。
- ⾼并发下核⼼线程怎么设置?
- 分 IO 密集还是 CPU 密集
- CPU 密集设置为跟核⼼数⼀样⼤⼩
- IO 密集型设置为 2 倍 CPU 核⼼数
- ⾮固定,根据实际情况压测进⾏调整,俗称【调参程序员】【调参算法⼯程师】
- 分 IO 密集还是 CPU 密集
# 线程池多参数调整 - 性能压测 + 现象对⽐分析
异步发送 + resttemplate 未池化
线程池参数
threadPoolTaskExecutor.setCorePoolSize(4);
threadPoolTaskExecutor.setMaxPoolSize(16);
threadPoolTaskExecutor.setQueueCapacity(32);
2
3
qps 少,等待队列⼩
异步发送 + resttemplate 未池化
线程池参数
threadPoolTaskExecutor.setCorePoolSize(32);
threadPoolTaskExecutor.setMaxPoolSize(64);
threadPoolTaskExecutor.setQueueCapacity(10000);
//如果等待队列⻓度为10万,则qps瞬间很⾼8k+,可能oom
2
3
4
qps,等待队列⼤ (瞬间⾼)
问题
- 采⽤异步发送⽤户体验变好了,但是存在丢失的可能,阻塞队列存储内存中,如果队列⻓度过多则重启容易出现丢失数据情况
- 采⽤了异步发送了 + 阻塞队列存缓冲,刚开始瞬间 QPS ⾼,但是后续也降低很多
- 问题是在哪⾥?消费⽅⻆度,提⾼消费能⼒
# RestTemplate ⾥⾯的存在的问题
# Broken pipe 错误
还原代码(暂时不⽤异步)异步 - ⾥⾯是⽤线程池 - 是池化思想的⼀种应⽤
同步发送 + resttemplate 未池化
- 压测结果 ⼏百吞吐量
- 错误 Caused by: java.io.IOException: Broken pipe,服务端向前端 socket 连接管道写返回数据时 链接(pipe)却断开了
- 从应⽤⻆度分析,这是因为客户端等待返回超时了,主动断开了与服务端链接
- 连接数设置太⼩,并发量增加后,造成⼤量请求排队等待
- ⽹络延迟,是否有丢包
- 内存是否⾜够多⽀持对应的并发量
resttemplate 底层是怎样的?
基于之前的认知 - 池化思想,联想到是否使⽤了 http 连接池?
# 重新认识 RestTemplate
- RestTemplate 是 Spring 提供的⽤于访问 Rest 服务的客户端
- 底层通过使⽤ java.net 包下的实现创建 HTTP 请求
- 通过使⽤ ClientHttpRequestFactory 指定不同的 HTTP 请求⽅式,主要提供了两种实现⽅式
- SimpleClientHttpRequestFactory(默认)
- 底层使⽤ J2SE 提供的⽅式,既 java.net 包提供的⽅式,创建底层的 Http 请求连接
- 主要 createRequest ⽅法( 断点调试),每次都会创建⼀个新的连接,每次都创建连接会造成极⼤的资源浪费,⽽且若连接不能及时释放,会因为⽆法建⽴新的连接导致后⾯的请求阻塞
- HttpComponentsClientHttpRequestFactory
- 底层使⽤ HttpClient 访问远程的 Http 服务
- SimpleClientHttpRequestFactory(默认)
问题解决
- 客户端每次请求都要和服务端建⽴新的连接,即三次握⼿将会⾮常耗时
- 通过 http 连接池可以减少连接建⽴与释放的时间,提升 http 请求的性能
- Spring 的 restTemplate 是对 httpclient 进⾏了封装,⽽ httpclient 是⽀持池化机制
- 拓展
- 对 httpclient 进⾏封装的有:Apache 的 Fluent、es 的 restHighLevelClient、spring 的 restTemplate 等
# RestTemplate 连接池封装
# 封装配置
RestTemplateConfig
// @Bean
// public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
// SimpleClientHttpRequestFactory factory = new
// SimpleClientHttpRequestFactory();
// factory.setReadTimeout(10000);
// factory.setConnectTimeout(10000);
// return factory;
// }
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
@Bean
public HttpClient httpClient() {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
//设置整个连接池最⼤连接数
connectionManager.setMaxTotal(500);
//MaxPerRoute路由是对maxTotal的细分,每个主机的并发,这⾥route指的是域名
connectionManager.setDefaultMaxPerRoute(300);
RequestConfig requestConfig = RequestConfig.custom()
//返回数据的超时时间
.setSocketTimeout(20000)
//连接上服务器的超时时间
.setConnectTimeout(10000)
//从连接池中获取连接的超时时间
.setConnectionRequestTimeout(1000)
.build();
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager).build();
return httpClient;
}
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
# 压测对比
- 同步发送 + resttemplate 未池化
- 压测结果 ⼏百 吞吐量
- 同步发送 + resttemplate 池化

# 短信验证码组件性能优化实战
- 调整代码
- 采⽤异步调⽤⽅式
- 采⽤ resttemplate 池化⽅式
SmsComponent
/**
* 发送短信验证码
*
* @param to
* @param templateId
* @param value
*/
@Async("threadPoolTaskExecutor")
public void send(String to, String templateId, String value) {
long beginTime = CommonUtil.getCurrentTimestamp();
String url = String.format(URL_TEMPLATE, to, templateId, value);
HttpHeaders headers = new HttpHeaders();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.set("Authorization", "APPCODE " + smsConfig.getAppCode());
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
long endTime = CommonUtil.getCurrentTimestamp();
log.info("耗时={},url={},body={}", endTime - beginTime, url, response.getBody());
if (response.getStatusCode().is2xxSuccessful()) {
log.info("发送短信验证码成功");
} else {
log.error("发送短信验证码失败:{}", response.getBody());
}
}
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
删除用于压测进行测试的方法
# 图形验证码开发
Kaptcha 框架介绍 ⾕歌开源的⼀个可⾼度配置的实⽤验证码⽣成⼯具
- 验证码的字体 / ⼤⼩ / 颜⾊
- 验证码内容的范围 (数字,字⺟,中⽂汉字!)
- 验证码图⽚的⼤⼩,边框,边框粗细,边框颜⾊
- 验证码的⼲扰线
- 验证码的样式 (⻥眼样式、3D、普通模糊)
聚合⼯程依赖添加(使⽤国内 baomidou ⼆次封装的 springboot 整合 starter)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
</dependency>
2
3
4
CaptchaConfig
@Configuration
public class CaptchaConfig {
/**
* 验证码配置
* Kaptcha配置类名
*
* @return /**
* 验证码配置类
* @return
*/
@Bean
@Qualifier("captchaProducer")
public DefaultKaptcha kaptcha() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
// //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
// properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "147");
// properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "34");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
// //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");
//验证码个数
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");
//字体间隔
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "8");
//干扰线颜色
// properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");
//干扰实现类
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
//图片样式
properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
//文字来源
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}
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
NotifyController
@Autowired
private Producer captchaProducer;
/**
* 生成验证码
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}", captchaText);
// 存储redis,配置过期时间 todo
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try {
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("获取流出错:{}", e.getMessage());
}
}
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
# Redis6.X 配置连接池实战
连接池好处
- 使⽤连接池不⽤每次都⾛三次握⼿、每次都关闭 Jedis
- 相对于直连,使⽤相对麻烦,在资源管理上需要很多参数来保证,规划不合理也会出现问题
- 如果 pool 已经分配了 maxActive 个 jedis 实例,则此时 pool 的状态就成 exhausted 了
连接池配置 common 项⽬
<!--redis客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
配置 Redis 连接
redis:
client-type: jedis
host: 192.168.130.24
password: xdclass.net
port: 6379
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 100
# 连接池中的最大空闲连接
max-idle: 100
# 连接池中的最小空闲连接
min-idle: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 60000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
序列化配置,common 模块下
RedisTemplateConfiguration
@Configuration
public class RedisTemplateConfiguration {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//配置序列化规则
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//设置key-value序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置hash-value序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
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
# 加⼊缓存 + Try-with-resource 知识巩固
redis 做隔离,多集群:核⼼集群和⾮核⼼集群,⾼并发集群和⾮⾼并发集群
资源隔离
数据保护
提⾼性能
key 规范:业务划分,冒号隔离
- account-service:captcha:xxxx
- ⻓度不能过⻓
NotifyController
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 验证码过期时间
* 临时使⽤10分钟有效,⽅便测试
*/
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
/**
* 生成验证码
*
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}", captchaText);
// 存储redis,配置过期时间
redisTemplate.opsForValue().set(getCaptchaKeY(request), captchaText, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try {
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("获取流出错:{}", e.getMessage());
}
}
private String getCaptchaKeY(HttpServletRequest request) {
String ipAddr = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User_Agent");
String key = "account-service:" + CommonUtil.MD5(ipAddr + userAgent);
log.info("验证码key:{}", key);
return key;
}
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
什么是 try-with-resources
- 资源的关闭很多⼈停留在旧的流程上,jdk7 新特性就有,但是很多⼈以为是 jdk8 的
- 在
try( ...)⾥声 明的资源,会在 try-catch 代码块结束后⾃动关闭掉 - 注意点
- 实现了
AutoCloseable接⼝的类,在 try () ⾥声明该类实例的时候,try 结束后⾃动调⽤的 close ⽅法,这个动作会早于 finally ⾥调⽤的⽅法 - 不管是否出现异常,try () ⾥的实例都会被调⽤ close ⽅法 try ⾥⾯可以声明多个⾃动关闭的对象,越早声明的对象,会越晚被 close 掉
- 实现了
/**
* 生成验证码
*
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}", captchaText);
// 存储redis,配置过期时间
redisTemplate.opsForValue().set(getCaptchaKeY(request), captchaText, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try (ServletOutputStream outputStream = response.getOutputStream();){
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
log.error("获取流出错:{}", e.getMessage());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 验证码接口
net.xdclass.controller.request.SendCodeRequest
@Data
public class SendCodeRequest {
private String captcha;
private String to;
}
2
3
4
5
6
common 模块下 net.xdclass.enums.SendCodeEnum
public enum SendCodeEnum {
/**
* 用户注册
*/
USER_REGISTER;
}
2
3
4
5
6
7
common 模块下 net.xdclass.util.CheckUtil
public class CheckUtil {
/**
* 邮箱正则
*/
private static final Pattern MAIL_PATTERN = Pattern.compile("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");
/**
* 手机号正则
*/
private static final Pattern PHONE_PATTERN = Pattern.compile("^((13[0-9])|(14[0-9])|(15[0-9])|(17[0-9])|(18[0-9]))\\d{8}$");
/**
* @param email
* @return
*/
public static boolean isEmail(String email) {
if (null == email || "".equals(email)) {
return false;
}
Matcher m = MAIL_PATTERN.matcher(email);
return m.matches();
}
/**
* @param phone
* @return
*/
public static boolean isPhone(String phone) {
if (null == phone || "".equals(phone)) {
return false;
}
Matcher m = PHONE_PATTERN.matcher(phone);
boolean result = m.matches();
return result;
}
}
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
NotifyService
public interface NotifyService {
/**
* 发送短信验证码
* @param sendCodeEnum
* @param to
* @return
*/
JsonData sendCode(SendCodeEnum sendCodeEnum, String to);
}
2
3
4
5
6
7
8
9
10
NotifyServiceImpl
@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {
@Autowired
private SmsComponent smsComponent;
@Autowired
private SmsConfig smsConfig;
@Override
public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {
String randomCode = CommonUtil.getRandomCode(6);
if (CheckUtil.isEmail(to)) {
//邮箱验证码 todo
} else if (CheckUtil.isPhone(to)) {
//短信验证码
smsComponent.send(to, smsConfig.getTemplateId(), randomCode);
}
return JsonData.buildSuccess();
}
}
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
NotifyController
@Autowired
private Producer captchaProducer;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private NotifyService notifyService;
/**
* 验证码过期时间
* 临时使⽤10分钟有效,⽅便测试
*/
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
/**
* 生成验证码
*
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}", captchaText);
// 存储redis,配置过期时间
redisTemplate.opsForValue().set(getCaptchaKeY(request), captchaText, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try (ServletOutputStream outputStream = response.getOutputStream();) {
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
log.error("获取流出错:{}", e.getMessage());
}
}
private String getCaptchaKeY(HttpServletRequest request) {
String ipAddr = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User_Agent");
String key = "account-service:" + CommonUtil.MD5(ipAddr + userAgent);
log.info("验证码key:{}", key);
return key;
}
/**
* 测试发送验证码接口,用于压测
*
* @return
*/
@PostMapping("send_code")
public JsonData senCode(@RequestBody SendCodeRequest sendCodeRequest, HttpServletRequest request) {
String key = getCaptchaKeY(request);
String cacheCaptcha = redisTemplate.opsForValue().get(key);
String captcha = sendCodeRequest.getCaptcha();
if (captcha != null && captcha != null && cacheCaptcha.equalsIgnoreCase(captcha)) {
redisTemplate.delete(key);
JsonData jsonData = notifyService.sendCode(SendCodeEnum.USER_REGISTER, sendCodeRequest.getTo());
return jsonData;
} else {
return new JsonData().buildResult(BizCodeEnum.CODE_CAPTCHA_ERROR);
}
}
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
# 短信验证码防刷
注册短信验证码防刷⽅案你能想到⼏个
需求:⼀定时间内禁⽌重复发送短信,⼤家想下有哪⼏种实现⽅式
两个时间要求
- 60 秒后才可以重新发送短信验证码
- 发送的短信验证码 10 分钟内有效
⽅式⼀:前端增加校验倒计时,不到 60 秒按钮不给点击
- 简单
- 不安全,存在绕过的情况
⽅式⼆:增加 Redis 存储,发送的时候设置下额外的 key,并且 60 秒后过期
⾮原⼦操作,存在不⼀致性
增加的额外的 key - value 存储,浪费空间
/** * 前置:判断是否重复发送 * * 1、存储验证码到缓存 * * 2、发送短信验证码 * * 后置:存储发送记录 **/1
2
3
4
5
6
7
8
9
⽅式三:基于原先的 key 拼装时间戳
- 好处:满⾜了当前节点内的原⼦性,也满⾜业务需求
我们选择方式三实现
common 模块下 net.xdclass.constant.RedisKey
public class RedisKey {
/**
* 验证码缓存key,第一个是类型,第二个是手机或邮箱
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
}
2
3
4
5
6
修改 NotifyServiceImpl
@Autowired
private StringRedisTemplate redisTemplate;
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
@Override
public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {
String cacheKey = String.format(RedisKey.CHECK_CODE_KEY, sendCodeEnum.name(), to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
// 如果不为空,再判断是否60秒内重发 value为 123456_1706960556
if (StringUtils.isNoneBlank(cacheValue)) {
long ttl = Long.parseLong(cacheValue.split("_")[1]);
// 判断是否大于60秒
long leftTime = CommonUtil.getCurrentTimestamp() - ttl;
if (leftTime < (1000 * 60)) {
log.info("重复发送短信验证码,时间间隔为{}秒", leftTime);
return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
}
}
String randomCode = CommonUtil.getRandomCode(6);
// 生成拼接好的验证码
String value = randomCode + "_" + CommonUtil.getCurrentTimestamp();
redisTemplate.opsForValue().set(cacheKey, value, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
if (CheckUtil.isEmail(to)) {
//邮箱验证码 todo
} else if (CheckUtil.isPhone(to)) {
//短信验证码
smsComponent.send(to, smsConfig.getTemplateId(), randomCode);
}
return JsonData.buildSuccess();
}
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
# 阿⾥云 OSS 接⼊实战
# 分布式⽂件存储常⻅解决⽅案介绍
⽬前业界⽐较多这个解决⽅案,这边就挑选⼏个介绍下
- MinIO:是在 Apache License v2.0 下发布的对象存储服务器,学习成本低,安装运维简单,主流语⾔的客户端整合都有,号称最强的对象存储⽂件服务器,且可以和容器化技术 docker/k8s 等结合,社区活跃但不够成熟,业界参考资料较少官⽹:https://docs.min.io/cn/
- FastDFS:⼀个开源的轻量级分布式⽂件系统,⽐较少的客户端可以整合,⽬前主要是 C 和 java 客户端,在⼀些互联⽹创业公司中有应⽤⽐较多,没有官⽅⽂档,社区不怎么活跃。架构 + 部署结构复杂,出问题定位⽐较难定位,可以说是 fastdfs 零件的组装过程,需要去理解 fastDFS 的架构设计,才能够正确的安装部署
- 云⼚商
- 阿⾥云 OSS
- 七⽜云
- 腾讯云
- 亚⻢逊云
- CDN 最强:Akamai https://www.akamai.com/cn
选云⼚商理由
- 优点:开发简单,功能强⼤,容易维护(不同⽹络下图⽚质量、⽔印、加密策略、扩容、加速)
- 缺点:要钱,个性化处理,未来转移⽐较复杂,不排除有些⼚商会提供⼀键迁移⼯具
选开源 MinIO 的理由
- 优点:功能强⼤、可以根据业务做⼆次的定制,新⼀代分布式⽂件存储系统,容器化结合强⼤,更重要的是免费(购买磁盘、内存、带宽)
- 缺点:⾃⼰需要有专⻔的团队进⾏维护、扩容等
⽂件上传流程
- web 控制台
- 前端 -> 后端程序 -> 阿⾥云 OSS

# 阿⾥云 OSS 对象存储介绍和开通
对象存储 OSS(Object Storage Service)是阿⾥云提供的海量、安全、低成本、⾼持久的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可⽤性不低于 99.995%。
OSS 具有与平台⽆关的 RESTful API 接⼝,您可以在任何应⽤、任何时间、任何地点存储和访问任意类型的数据。提供标准、低频访问、归档和冷归档四种存储类型,全⾯覆盖从热到冷的各种数据存储场景:
| 标准存储类型 | ⾼持久、⾼可⽤、⾼性能的对象存储服务,⽀持频繁的数据访问。是各种社交、分享类的图⽚、⾳视频应⽤、⼤型⽹站、⼤数据分析的合适选择。 |
|---|---|
| 低频访问存储类型 | 适合⻓期保存不经常访问的数据(平均每⽉访问频率 1 到 2 次)。存储单价低于标准类型,适合各类移动应⽤、智能设备、企业数据的⻓期备份,⽀持实时数据访问。 |
| 归档存储类型 | 适合需要⻓期保存(建议半年以上)的归档数据,在存储周期内极少被访问,数据进⼊到可读取状态需要 1 分钟的解冻时间。适合需要⻓期保存的档案数据、医疗影像、科学资料、影视素材。 |
| 冷归档存储类型 | 适合需要超⻓时间存放的极冷数据。例如因合规要求需要⻓期留存的数据、⼤数据及⼈⼯智能领域⻓期积累的原始数据、影视⾏业⻓期留存的媒体资源、在线教育⾏业的归档视频等。 |
开通阿⾥云 OSS
- 有阿⾥云账号、实名认证
- OSS 介绍:https://www.aliyun.com/product/oss
- OSS 控制台:https://oss.console.aliyun.com/bucket
- 学习路径:https://help.aliyun.com/learn/learningpath/oss.html
开通后的操作
- 创建 Bucket
- 上传⽂件
- 访问⽂件
# RBAC-ACL 模式应⽤之阿⾥云 RAM 访问控制
- ACL: Access Control List 访问控制列表
- 以前盛⾏的⼀种权限设计,它的核⼼在于⽤户直接和权限挂钩
- 优点:简单易⽤,开发便捷
- 缺点:⽤户和权限直接挂钩,导致在授予时的复杂性,⽐较分散,不便于管理
- 例⼦:常⻅的⽂件系统权限设计,直接给⽤户加权限
- RBAC: Role Based Access Control
- 基于⻆⾊的访问控制系统。权限与⻆⾊相关联,⽤户通过成为适当⻆⾊的成员⽽得到这些⻆⾊的权限
- 优点:简化了⽤户与权限的管理,通过对⽤户进⾏分类,使得⻆⾊与权限关联起来
- 缺点:开发对⽐ ACL 相对复杂
- 例⼦:基于 RBAC 模型的权限验证框架与应⽤ Apache Shiro、spring Security
总结:不能过于复杂,规则过多,维护性和性能会下降, 更多分类 ABAC、PBAC 等
RAM 权限介绍
- 阿⾥云⽤于各个产品的权限,基于 RBAC、ACL 模型都有,进⾏简单管理账号、统⼀分配权限、集中管控资源,从⽽建⽴安全、完善的资源控制体系。
- 众多产品,⼀般采⽤⼦账号进⾏分配权限,防⽌越权攻击

- 建⽴⽤户,勾选编程访问(保存 accessKey 和 accessSecret,只出现⼀次)
- ⽤户登录名称 [email protected] AccessKey ID : LTAI5tHVGvYw7twoVFyruB1H AccessKey Secret :r4d0EudzSvPfVMb9Zp0TfmsE32RXmN
- 为新建⽤户授权 OSS 全部权限

# OSS 对象存储客户端集成和测试服务
添加阿⾥云 OSS 的 SDK
地址:https://help.aliyun.com/document_detail/32008.html
账号微服务添加 maven 依赖
<!-- OSS各个项目单独加依赖,根据需要进行添加-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
2
3
4
5
账号微服务配置 OSS
#----------阿里云OSS配置--------------
aliyun:
oss:
endpoint: oss-cn-guangzhou.aliyuncs.com
access-key-id: LTAI5tHVGvYw7twoVFyruB1H
access-key-secret: r4d0EudzSvPfVMb9Zp0TfmsE32RXmN
bucketname: dcloud-link
2
3
4
5
6
7
新建配置类 (配置⾥⾯的横杠会,⾃动转驼峰)
net.xdclass.config.OSSConfig
@ConfigurationProperties(prefix = "aliyun.oss")
@Configuration
@Data
public class OSSConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketname;
}
2
3
4
5
6
7
8
9
net.xdclass.service.FileService
public interface FileService {
/**
* 文件上传
* @param file
* @return
*/
String uploadUserImg(MultipartFile file);
}
2
3
4
5
6
7
8
net.xdclass.service.impl.FileServiceImpl
@Service
@Slf4j
public class FileServiceImpl implements FileService {
@Autowired
private OSSConfig ossConfig;
@Override
public String uploadUserImg(MultipartFile file) {
String bucketname = ossConfig.getBucketname();
String endpoint = ossConfig.getEndpoint();
String accessKeyId = ossConfig.getAccessKeyId();
String accessKeySecret = ossConfig.getAccessKeySecret();
// 创建OSS客户端
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 文件原始名称
String originalFilename = file.getOriginalFilename();
// 存储路径为 user/2024/02/04/xxxx/xxx.jpg
LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String folder = ldt.format(pattern);
String fileName = CommonUtil.generateUUID();
String extendsion = originalFilename.substring(originalFilename.lastIndexOf("."));
// oss上传文件
String newFileName = "user/" + folder + "/" + fileName + extendsion;
try {
PutObjectResult putObjectResult = ossClient.putObject(bucketname, newFileName, file.getInputStream());
// 拼装返回路径
if (putObjectResult != null) {
String url = "https://" + bucketname + "." + endpoint + "/" + newFileName;
return url;
}
} catch (IOException e) {
log.error("文件上传失败{}", e.getMessage());
}
return null;
}
}
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
net.xdclass.controller.AccountController
@Controller
@RequestMapping("api/account/v1")
public class AccountController {
@Autowired
private FileService fileService;
/**
* 文件上传 最大文件大小为默认为1M
* @param file
* @return
*/
@PostMapping("upload")
// @requestPart注解 接收⽂件以及其他更为复杂的数据类型
public JsonData UploadUserImg(@RequestPart("file") MultipartFile file) {
String result = fileService.uploadUserImg(file);
return result != null ? JsonData.buildSuccess(result) : JsonData.buildResult(BizCodeEnum.FILE_UPLOAD_USER_IMG_FAIL);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
⽂件上传流程
- 先上传⽂件,返回 url 地址,再和普通表单⼀并提交(推荐这种,更加灵活,失败率低)
- ⽂件和普通表单⼀并提交(设计流程⽐较多,容易超时和失败)
注意:默认 SpringBoot 最⼤⽂件上传是 1M, ⼤家测试的时候记得关注下
@requestPart 注解 接收⽂件以及其他更为复杂的数据类型
postman 进行测试

# 账号微服务注册 - 登录功能
# 注册功能业务介绍和代码
微服务注册接⼝开发
- 请求实体类编写
- controller
- service
- ⼿机验证码验证
- 密码加密(TODO)
- 账号唯⼀性检查 (TODO)
- 插⼊数据库
- 新注册⽤户福利发放 (TODO)
- mapper
密码存储安全
- 彩虹表暴⼒破解
- ⽹站:https://www.cmd5.com/
- 密码存储常⽤⽅式
- 双重 MD5
- MD5 + 加盐
- 双重 MD5 + 加盐
net.xdclass.controller.request.AccountRegisterRequest
@Data
public class AccountRegisterRequest {
/**
* 头像
*/
private String headImg;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String pwd;
/**
* 邮箱
*/
private String mail;
/**
* 用户名
*/
private String username;
/**
* 短信验证码
*/
private String code;
}
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
common 模块下 net.xdclass.enums.AuthTypeEnum
public enum AuthTypeEnum {
/**
* 默认级别
*/
DEFAULT,
/**
* 实名制
*/
REALNAME,
/**
* 企业
*/
ENTERPRISE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NotifyService
public interface NotifyService {
/**
* 发送短信验证码
* @param sendCodeEnum
* @param to
* @return
*/
JsonData sendCode(SendCodeEnum sendCodeEnum, String to);
/**
*校验验证码
* @return
*/
boolean checkCode(SendCodeEnum sendCodeEnum, String to,String code);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NotifyServiceImpl
/**
* 验证码检验逻辑
* @param sendCodeEnum
* @param to
* @param code
* @return
*/
@Override
public boolean checkCode(SendCodeEnum sendCodeEnum, String to, String code) {
String cacheKey = String.format(RedisKey.CHECK_CODE_KEY, sendCodeEnum.name(), to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNoneBlank(cacheValue)) {
String cacheCode = cacheValue.split("_")[0];
if (cacheCode.equalsIgnoreCase(code)) {
// 删除验证码
redisTemplate.delete(cacheKey);
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AccountService
public interface AccountService {
/**
* 用户注册
* @param accountRegisterRequest
* @return
*/
JsonData register(AccountRegisterRequest registerRequest);
}
2
3
4
5
6
7
8
9
AccountServiceImpl
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private NotifyService notifyService;
@Autowired
private AccountManager accountManager;
/**
* ⼿机验证码验证
* 密码加密(TODO)
* 账号唯⼀性检查(TODO)
* 插⼊数据库
* 新注册⽤户福利发放(TODO)
*
* @param registerRequest
* @return
*/
@Override
public JsonData register(AccountRegisterRequest registerRequest) {
boolean checkCode = false;
// 判断验证码
if (StringUtils.isNoneBlank(registerRequest.getPhone())) {
checkCode = notifyService.checkCode(SendCodeEnum.USER_REGISTER, registerRequest.getPhone(), registerRequest.getCode());
}
// 验证码错误
if (!checkCode) {
return JsonData.buildResult(BizCodeEnum.CODE_ERROR);
}
AccountDO accountDO = new AccountDO();
BeanUtils.copyProperties(registerRequest, accountDO);
// 认证级别
accountDO.setAuth(AuthTypeEnum.DEFAULT.name());
// 密码 密钥 盐
accountDO.setSecret("$1$" + CommonUtil.getStringNumRandom(8));
String cryptPwd = Md5Crypt.md5Crypt(registerRequest.getPwd().getBytes(), accountDO.getSecret());
accountDO.setPwd(cryptPwd);
int rows = accountManager.insert(accountDO);
log.info("rows:{},注册成功:{}", rows, accountDO);
// 用户注册成功,发放福利
userRegisterInitTask(accountDO);
return JsonData.buildSuccess();
}
/**
* 用户初始化 发放福利 流量包 todo
*
* @param accountDO
*/
private void userRegisterInitTask(AccountDO accountDO) {
}
}
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
AccountManager
public interface AccountManager {
int insert(AccountDO accountDO);
}
2
3
4
AccountManagerImpl
@Component
@Slf4j
public class AccountManagerImpl implements AccountManager {
@Autowired
private AccountMapper accountMapper;
@Override
public int insert(AccountDO accountDO) {
return accountMapper.insert(accountDO);
}
}
2
3
4
5
6
7
8
9
10
11
12
AccountController
/**
* 用户注册
* @param registerRequest
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody AccountRegisterRequest registerRequest){
JsonData jsonData = accountService.register(registerRequest);
return jsonData;
}
2
3
4
5
6
7
8
9
10
# 注册⼿机号唯⼀性安全保证⽅案
注册业务,同个时刻注册,需要保证注册⼿机号在数据库⾥唯⼀
⾼并发下问题发现扩⼤
- 万分之⼀的时间,放⼤ 100 万倍
- 不是你的代码安全,⽽是你的并发量过少,⼏个⼏⼗个并发量发现不了问题
- ⼏⼗万⼏百万并发 ,线下难模拟
- 代码暂停思维:假如⾮原⼦性代码运⾏到某⼀⾏暂停,其他线程重新操作是否会出问题
- 时间扩⼤思维:1 纳秒的时间,扩⼤到 1 分钟,代码逻辑是否会有问题
- 类似幂等性处理
解决思路
- Redis:先看 redis 是否有,然后没的话则是新的注册
- key -value 存储,配置 60 秒过期
- ⾮原⼦性操作,存在不⼀致
- 数据库唯⼀索引 (建表的时间已经添加)
- 库分表下 - ⼿机号唯⼀性保证怎么做?
https://www.zhihu.com/people/xdclass/asks

# 登录模块逻辑和解密
核⼼逻辑
- 通过 phone 找数据库记录
- 获取盐,和当前传递的密码就⾏加密后匹配
- ⽣成 token 令牌
# 分布式应⽤的登录检验解决⽅案 JWT
JWT 是⼀个开放标准,它定义了⼀种⽤于简洁,⾃包含的⽤于通信双⽅之间以 JSON 对象的形式安全传递信息的⽅法。 可以使⽤ HMAC 算法或者是 RSA 的公钥密钥对进⾏签名
简单来说:就是通过⼀定规范来⽣成 token,然后可以通过解密算法逆向解密 token,这样就可以获取⽤户信息
{
id:888,
name:'⼩D',
expire:10000
}
funtion 加密(object, appsecret){
xxxx
return base64( token);
}
function 解密(token ,appsecret){
xxxx
//成功返回true,失败返回false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优点
- ⽣产的 token 可以包含基本信息,⽐如 id、⽤户昵称、头像等信息,避免再次查库
- 存储在客户端,不占⽤服务端的内存资源
缺点
- token 是经过 base64 编码,所以可以解码,因此 token 加密前的对象不应该包含敏感信息,如⽤户权限,密码等
- 如果没有服务端存储,则不能做登录失效处理,除⾮服务端改秘钥
JWT 格式组成 头部、负载、签名
header+payload+signature
- 头部:主要是描述签名算法
- 负载:主要描述是加密对象的信息,如⽤户的 id 等,也可以加些规范⾥⾯的东⻄,如 iss 签发者,exp 过期时间,sub ⾯向的⽤户
- 签名:主要是把前⾯两部分进⾏加密,防⽌别⼈拿到 token 进⾏ base 解密后篡改 token
关于 jwt 客户端存储
- 可以存储在 cookie,localstorage 和 sessionStorage ⾥⾯

common 模块下 net.xdclass.util.JWTUtil
@Slf4j
public class JWTUtil {
/**
* 主题
*/
private static final String SUBJECT = "xdclass";
/**
* 加密密钥
*/
private static final String SECRET = "xdclass.net168";
/**
* 令牌前缀
*/
private static final String TOKEN_PREFIX = "dcloud-link";
/**
* token 过期时间 7天
*/
private static final long EXPIRED = 1000 * 60 * 60 * 24 * 7;
/**
* 生成token
*
* @param loginUser
* @return
*/
public static String geneJsonWebToken(LoginUser loginUser) {
if (loginUser != null) {
throw new NullPointerException("loginUser is null");
}
String token = Jwts.builder().setSubject(SUBJECT)
.claim("head_img", loginUser.getHeadImg())
.claim("userame", loginUser.getUserame())
.claim("mail", loginUser.getMail())
.claim("phone", loginUser.getPhone())
.claim("auth", loginUser.getAuth())
.setIssuedAt(new Date())
.setExpiration(new Date(CommonUtil.getCurrentTimestamp() + EXPIRED))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
token = TOKEN_PREFIX + token;
return token;
}
/**
* 解密jwt
*
* @param tokne
* @return
*/
public static Claims checkJWT(String tokne) {
try {
final Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(tokne.replace(TOKEN_PREFIX, "")).getBody();
return claims;
} catch (Exception e) {
log.error("jwt 解析异常:{}", e.getMessage());
return null;
}
}
}
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
common 模块下 net.xdclass.model.LoginUser
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser {
/**
* 账号
*/
private long accountNo;
/**
* 用户名称
*/
private String userame;
/**
* 头像
*/
private String headImg;
/**
* 邮箱
*/
private String mail;
/**
* 手机号
*/
private String phone;
/**
* 认证级别
*/
private String auth;
}
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
net.xdclass.controller.request.AccountLoginRequest
@Data
public class AccountLoginRequest {
private String phone;
private String pwd;
}
2
3
4
5
6
7
AccountController
/**
* 用户登录
*/
@PostMapping("login")
public JsonData login(@RequestBody AccountLoginRequest loginRequest){
return accountService.login(loginRequest);
}
2
3
4
5
6
7
8
AccountService
/**
* 用户登录
* @param loginRequest
* @return
*/
JsonData login(AccountLoginRequest loginRequest);
2
3
4
5
6
AccountServiceImpl
/**
* 用户登录逻辑
* 通过phone找数据库记录
* 获取盐,和当前传递的密码就⾏加密后匹配
* ⽣成token令牌
*
* @param loginRequest
* @return
*/
@Override
public JsonData login(AccountLoginRequest loginRequest) {
List<AccountDO> accountDOList = accountManager.findByPhone(loginRequest.getPhone());
if (accountDOList != null && accountDOList.size() == 1) {
AccountDO accountDO = accountDOList.get(0);
String md5Crypt = Md5Crypt.md5Crypt(loginRequest.getPwd().getBytes(), accountDO.getSecret());
if (md5Crypt.equalsIgnoreCase(accountDO.getPwd())) {
LoginUser loginUser = LoginUser.builder().build();
BeanUtils.copyProperties(accountDO, loginUser);
// 生成token
String token = JWTUtil.geneJsonWebToken(loginUser);
return JsonData.buildSuccess(token);
} else {
return JsonData.buildResult(BizCodeEnum.ACCOUNT_PWD_ERROR);
}
} else {
return JsonData.buildResult(BizCodeEnum.ACCOUNT_UNREGISTER);
}
}
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
AccountManager
List<AccountDO> findByPhone(String phone);
AccountManagerImpl
@Override
public List<AccountDO> findByPhone(String phone) {
List<AccountDO> accountDoList = accountMapper.selectList(new QueryWrapper<AccountDO>().eq("phone", phone));
return accountDoList;
}
2
3
4
5
6
# 登录拦截器开发和⽤户信息传递
开发登录拦截器
- 解密 JWT
- 传递登录⽤户信息
- attribute 传递
- threadLocal 传递
SpringBoot 拦截器代码开发
common 模块下 net.xdclass.interceptor.LoginInterceptor
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// options请求直接放行
if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return true;
}
String accessToken = request.getHeader("token");
if (StringUtils.isBlank(accessToken)) {
accessToken = request.getParameter("token");
}
// 检验token
if (StringUtils.isBlank(accessToken)) {
Claims claims = JWTUtil.checkJWT(accessToken);
if (claims == null) {
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
long accountNo = Long.parseLong(claims.get("account_no").toString());
String headImg = (String) claims.get("head_img");
String username = (String) claims.get("username");
String mail = (String) claims.get("mail");
String phone = (String) claims.get("phone");
String auth = (String) claims.get("auth");
LoginUser loginUser = LoginUser.builder()
.accountNo(accountNo)
.headImg(headImg)
.mail(mail)
.auth(auth)
.phone(phone)
.userame(username)
.build();
threadLocal.set(loginUser);
return true;
}
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除一下 防止内存泄漏
threadLocal.remove();
}
}
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
# ThreadLocal
thread local variable(线程局部变量)功用非常简单,使用场合主要解决多线程中数据因并发产生不一致问题。
ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
总结起来就是:同个线程共享数据
注意:ThreadLocal 不能使用原子类型,只能使用 Object 类型
ThreadLocal 用作每个线程内需要独立保存信息,方便同个线程的其他方法获取该信息的场景。
每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念,比如用户登录令牌解密后的信息传递(还有用户权限信息、从用户系统获取到的用户名、用户 ID)

# ThreadLocal 底层源码 + 原理
ThreadLocal 中的一个内部类 ThreadLocalMap,这个类没有实现 map 接口,就是一个普通的 Java 类,但是实现的类似 map 的功能
每个数据用 Entry 保存,其中的 Entry 继承与 WeakReference,用一个键值对存储,键为 ThreadLocal 的引用。
每个线程持有一个 ThreadLocalMap 对象,每一个新的线程 Thread 都会实例化一个 ThreadLocalMap 并赋值给成员变量 threadLocals,使用时若已经存在 threadLocals,则直接使用已经存在的对象。

# ThreadLocal 常见核心面试题
ThreadLocal 和 Synchronized 的区别
- 都是为了解决多线程中相同变量的访问冲突问题
- Synchronized 是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal 是通过每个线程单独一份存储空间,牺牲空间来解决冲突,
- 对比 Synchronized,ThreadLocal 具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值
为啥什么 ThreadLocal 的键是弱引用,如果是强引用有什么问题?
Java 中除了基础的数据类型以外,其它的都为引用类型。 而 Java 根据其生命周期的长短将引用类型又分为强引用 、 软引用 、 弱引用 、 虚引用 正常情况下我们平时基本上我们只用到强引用类型,而其他的引用类型我们也就在面试中,或者平日阅读类库或其他框架源码的时候才能见到
- 强引用 new 了一个对象就是强引用 Object obj = new Object ();
- 软引用的生命周期比强引用短一些,通过 SoftReference 类实现,当内存空间足够,垃圾回收器就不会回收它;当 JVM 认为内存空间不足时,就会去试图回收软引用指向的对象,也就是说在 JVM 抛出 OutOfMemoryError 之前,会去清理软引用对象 主要用来描述一些【有用但并不是必需】的对象 使用场景:适合用来实现缓存,内存空间充足的时候将数据缓存在内存中,如果空间不足了就将其回收掉
- 弱引用是通过 WeakReference 类实现的,它的生命周期比软引用还要短,在 GC 的时候,不管内存空间足不足都会回收这个对象 使用场景:一个对象只是偶尔使用,希望在使用时能随时获取,但也不想影响对该对象的垃圾收集,则可以考虑使用弱引用来指向该对象。
ThreadLocal 为什么是 WeakReference 呢?
如果是强引用,即使把 ThreadLocal 设置为 null,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏
如果是弱引用引用 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。
# 登录拦截器 InterceptorConfig 拦截和放⾏路径
拦截器配置
- 拦截路径
- 不拦截路径
net.xdclass.config.InterceptorConfig
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
// 添加拦截路径
.addPathPatterns("/api/account/*/**", "/api/traffic/*/**")
// 排除不拦截
.excludePathPatterns(
"/api/account/*/login", "/api/account/*/register","/api/account/*/upload",
"/api/notify/*/captcha", "/api/notify/*/send_code");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15