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

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


  • 首页

  • 归档

  • 搜索

线程的调度顺序和线程的并发 Java多线程(二)

发表于 2021-06-26

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


相关文章

Java多线程汇总:Java多线程


一、线程调度顺序

前提: 如果一个进程中同时开三个线程,那么谁先谁后呢?

  • 代码实现案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* 测试线程的执行顺序
*/
public class TestThreadSort implements Runnable{
public static void main(String[] args) {
//使用实现Runnable方法的好处就是可以多实现
TestThreadSort testThreadSort = new TestThreadSort();

//开启线程1
new Thread(testThreadSort,"丁大大").start();
//开启线程2
new Thread(testThreadSort,"甲大大").start();
//开启线程3
new Thread(testThreadSort,"乙大大").start();
}

@Override
public void run() {
//Thread.currentThread().getName() ---获取当前线程的名称
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"看了第"+i+"本书");
}
}
}
  • 执行结果如下:

在这里插入图片描述

  • 注意项:
+ 开启一个线程时,即**new Thread(实例化对象,name)**,为开启的线程取了一个**名字**。
+ 在线程内部可使用**Thread.currentThread().getName()** 获取该线程本身的名称。
  • 小结:
+ 通过以上的代码执行结果来看,我们可以得出结论,线程的执行并**不是按照指定的顺序来**,比如我依次开启线程1、2、3,但实际的执行结果并不受我们的控制,而是由**cpu调度器随机调度执行**的!

二、主线程的执行

  • 代码实现案例

在这里插入图片描述

  • 理论上结果:等待三个人看完,最后才是老师看书。
  • 实际结果如下:

在这里插入图片描述

  • 结论:主线程(main()线程)优先执行。

三、线程的并发

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
—-以上摘自百度百科

3.1、龟兔赛跑案例

  • 在开始并发之前,我们先来看一个好玩的案例。龟兔赛跑。
  • 程序实现
+ 赛道两条
+ 乌龟和兔子
+ 赛道长度
+ 根据历史来看,需要让兔子间隔休息(因为最后兔子输了嘛)
+ sleep(int) 线程休眠,参数单位是毫秒
  • 代码实现案例:
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
java复制代码public class RunGame implements Runnable{

Integer track = 100;//赛道的长度
String turtle = "乌龟";
String rabbit = "兔子";
String winner = "";//胜利者

public static void main(String[] args) {
RunGame runGame = new RunGame();

//开启兔子比赛的线程
new Thread(runGame, runGame.rabbit).start();
//开启乌龟比赛的线程
new Thread(runGame, runGame.turtle).start();
}

@Override
public void run() {
//使用Thread.currentThread().getName()获得当前的参赛者是谁
String ballplayer = Thread.currentThread().getName();
if (ballplayer.equals(rabbit)){
//模拟兔子跑步的速度,兔子跑的快,定义为每秒跑两米
for (int i=0;i<=track;i+=2){
System.out.println(rabbit+"跑了"+i+"米");
//因为兔子比较骄傲,当兔子跑到60米时,睡了一会觉,导致输掉比赛
if (i==50){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i>=100){
winner = rabbit;
System.out.println(winner+"赢了!!!");
}
}
}else if (ballplayer.equals(turtle)){
//模拟乌龟跑步的速度,乌龟跑的慢,定义为每秒跑一米
for (int i=0;i<=track;i++){
System.out.println(turtle+"跑了"+i+"米");
if (i>=100){
winner = turtle;
System.out.println(winner+"赢了!!!");
}
}
}
}
}
  • 执行结果:

在这里插入图片描述

  • 当兔子跑到50米时骄傲自满,睡了觉,导致比赛输掉。

在这里插入图片描述

  • 正常情况下,兔子肯定会赢,去掉休眠即可,我就不放代码和结果上来了,感兴趣的可以自己试试~

3.2、商品抢购案例

  • 并发的场景:
+ 刷猴王雷军发售小米12,首批货源只有100台!!!100台啊!!!
+ 大家知道消息后,纷纷跑来在12点首发时抢购小米12,人数共有500人!
+ 手机,是肯定不够滴~
+ 我们来模拟一下抢购手机的场景
  • 代码实现案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码/**
* 手机抢购案例
*/
public class PhoneSnapUp implements Runnable{

private Integer inventory = 100;//手机库存
private Integer number = 500;//抢购人数

public static void main(String[] args) {
PhoneSnapUp phoneSnapUp = new PhoneSnapUp();
//模拟500人同时抢购,即同时开启500个线程
for (int i=0;i<500;i++){
new Thread(phoneSnapUp,"丁大大"+i+"一号").start();
}
}

@Override
public void run() {
//写个死循环来模拟
while (true){
//当库存为0时,抢购结束
if (inventory <= 0){
break;
}
//模拟延迟,否则结果不容易看出来
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("恭喜!!"+Thread.currentThread().getName()+"--抢到了一台小米12!库存还剩:"+inventory+"台");
//每次抢购完,库存减1
inventory--;
}
}
}
  • 执行结果如下:

在这里插入图片描述

在这里插入图片描述

  • 结论:出现的问题
+ 同一台手机被多人购买
+ 卖出的手机远超库存量
  • 通过上面的案例,可以让我们对并发有个大概的了解!这也是我们在实际开发中需要注意的东西!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

《蹲坑也能进大厂》多线程系列-线程池源码终结篇

发表于 2021-06-25

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

作者:花Gie

微信公众号:Java开发零到壹

前言

多线程系列我们前面已经更新过七个章节了,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录

无论是Java小白还是高级大佬,线程池基本是面试中的必考题,这个知识点虽然涉及的东西较多,但在理解上却不是很难,非常适合在面试上拿分。在上一篇文章《蹲坑也能进大厂》多线程系列-线程池精讲(必看)中,小伙伴们对线程池应该有了一个基本的认识,对线程池不是很了解的同学,建议先看上一篇。

本篇花Gie对如何用好线程池以及它的原理进行探讨,在面试中想要拿到更理想的工资,以及在日常工作中的问题排查,了解它的内部结构是必不可少的。

ps:本文使用JDK8环境讲解

正文

我:狗哥狗哥,学完了上一章,可以帮我总结一下线程池的有哪几个重要部分组成吗?

问题不大,线程池的组成部分主要有四个:

  • 线程池管理器:用于管理线程池,如停止线程池、创建线程池等;
  • 工作线程:用于从队列中读取并执行任务;
  • 任务队列:存放来不及执行的任务;
  • 任务接口:一个一个被用来执行的任务,未执行时存放在任务队列中。

我:线程池的家族史可以介绍一下吗?那么多类我都快乱死了

线程池涉及的类是比较多,但区分下来还是不难理解的,我们先来看这个结构图,这几种是我们经常看到的:

  • Executor:是一个顶级接口,内部只包含一个execute()方法;
  • ExecutorService:也是一个接口,它继承了Executor接口,并新增了shutdown()、submit()等方法;
  • Executors:是一个工具类,它提供了我们常用的创建线程方法,例如:newSingleThreadExecutor、newFixedThreadPool等。
  • ThreadPoolExecutor:是真正意义的线程池。

image-20210625164248917

我:搜得思耐,也不是很难嘛,那如何向线程池中提交任务呢?

花Gie,你居然在我面前装X,看我教你做人。

提交任务方式有两种,其实本质上还是一种,因为submit最终调用的还是execute()方法:

  • execute():用于提交不需要返回值的任务,所以也就意味着无法判断是否执行成功。
1
2
3
4
5
java复制代码ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.execute(new Runnable() {
@Override
public void run() {}
});
  • submit:线程池会返回一个future类型的对象,通过这个future对象可以判读是否执行成功,并且还可以通过get()方法来获取返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
Future<Object> future =(Future<Object>) service.submit(new Runnable() {
@Override
public void run() {
System.out.println(1);
}
});
try {
future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}finally {
//关闭线程池
service.shutdown();
}

我:so easy嘛,狗子,有没有刺激点的

嘚瑟,你再给我嘚瑟,既然基础知识点掌握完了,那我们就来深入了解一下源码吧,用源码的方式了解线程池的一生。

这么突然吗,我甚至还不知道线程池的生命周期有哪几种……

1624620388(1).jpg

线程池的生命周期有五个:

  • RUNNING:此时能够接受新任务,并处理排队任务;
  • SHUTDOWN:不再接受新任务,但是会处理排队任务;
  • STOP:不接受新任务,也不处理排队任务,并且会中断正在执行的任务;
  • TIDYING:所有任务都已终止,workworkerCount为零时,线程就会转换到此状态,并且运行terminated()函数;
  • TERMINATED:terminated()函数运行完成。

我:阿里嘎多欧卡桑,嘤嘤嘤~

花Gie,你这么浪真的好么。接下来正式介绍线程池的一些重要源码吧,首先要看的是上面提到过的execute方法:

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 void execute(Runnable command) {
if (command == null)
throw new NullPointerException();

int c = ctl.get();
//步骤一
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//步骤二
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//步骤三
else if (!addWorker(command, false))
reject(command);
}

我们可以根据条件分为三个大的步骤来分析:

  • 步骤一分析

代码第三局有一个ctl,它是用于记录线程池状态和运行线程数。

1
java复制代码private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

这里会判断正在运行的线程是否达到核心线程数,如果为true,就会调用addWorker新增一个工作线程,并运行当前任务(command),如果新增线程失败,就会重新获取ctl。

1
2
3
4
5
6
7
8
java复制代码//运行线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
//新增线程到线程池,并将当前任务添加到新增的线程中
if (addWorker(command, true))
return;
//创建线程失败,重新获取clt。
c = ctl.get();
}
  • 步骤二

isRunning:判断线程池的是否为运行状态

如果运行线程数不小于核心线程数,就会执行以下6个子步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//1.线程池是运行状态并且运行线程大于核心线程数时,把任务放入队列中。
if (isRunning(c) && workQueue.offer(command)) {
//2.获取线程池状态
int recheck = ctl.get();
//3.如果线程池不是运行状态,把任务移除队列
if (! isRunning(recheck) && remove(command))
//4.执行拒绝策略
reject(command);
//5.判断当前运行线程数是否为0
else if (workerCountOf(recheck) == 0)
//6.创建线程并加入到线程池
addWorker(null, false);
}
1
2
3
4
5
6
java复制代码//移除任务
public boolean remove(Runnable task) {
boolean removed = workQueue.remove(task);
tryTerminate(); // In case SHUTDOWN and now empty
return removed;
}
  • 步骤三

如果前几个条件都不满足,也就是运行线程大于核心线程数时并且队列已满时,就会调用addWorker新建线程执行当前任务,如果新建失败,则表示运行线程已达到最大线程数,不能再次创建新的线程,此时就会执行拒绝策略。

1
2
3
4
java复制代码//创建线程放入线程池中,并且运行当前任务。
else if (!addWorker(command, false))
//运行线程大于最大线程数时,失败则拒绝该任务
reject(command);

上面多次用到addWorker方法,简单看下它的实现逻辑。

这里做一个总结并附上部分源码注释,小伙伴们啃起来,略长:

  • addWorker(command, true):当线程数小于corePoolSize时,创建核心线程并且运行task。
  • addWorker(command, false):当核心线程数已满,阻塞队列已满,并且线程数小于maximumPoolSize时,创建非核心线程并且运行task。
  • addWorker(null, false):如果工作线程为0是,创建一个核心线程但是不运行task。(主要是避免工作队列中还有任务,但是工作线程为0,导致工作队列中的任务一直没有执行)
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
java复制代码private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//获取线程池状态和运行线程数。
int c = ctl.get();
//获取线程池的运行状态
int rs = runStateOf(c);
//线程池处于关闭状态、当前任务为null、队列不为空,斗直接返回失败
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
//获取线程池中的线程数
int wc = workerCountOf(c);
//线程数超过CAPACITY,直接返回false;
//如果core为true,则运行线程数与核心线程数进行比较,为false则与最大线程数进行比较。
//并且运行线程数大于等于core时,返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//尝试增加线程数,如果成功,则跳出第一个for循环
if (compareAndIncrementWorkerCount(c))
break retry;
//如果增加线程数失败,则重新获取ctl
c = ctl.get();
//如果当前的运行状态不等于rs,说明状态已被改变,
//返回第一个for循环继续执行
if (runStateOf(c) != rs)
continue retry;
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//根据当前任务来创建Worker对象
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//获得锁以后,重新检查线程池状态
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
//把刚刚创建的线程加入到线程池中
workers.add(w);
int s = workers.size();
//记录线程池中出现过的最大线程数量
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//启动线程,开始运行任务
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

我:这一波…我可能要啃一个周末了,那线程池最后应该怎样关闭呢?

有两种方式可以关闭正在运行的线程池:

  • shutdown: 将线程池的状态设置成SHUTDOWN状态,然后将没有执行任务的所有线程停止。
  • shutdownNow: 通过遍历线程池中的工作线程,并逐一调用线程的interrupt方法来中断线程,对于无法响应中断的任务可能会永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

无论调用哪一种方式去停止线程,再次调用isShutdown方法都会返回true,当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。

至于我们应该调用哪一种方法来关闭线程池,取决于我们添加到线程池中任务的特性来决定,如果任务不要求执行完整,可以调用shutdownNow,但通常会使用shutdown来关闭线程池。

总结

以上就是线程池的全部内容了,确实有点长,建议小伙伴能够静下心慢慢吭,切不可囫囵吞枣,不然浪费时间还没有学到东西,有疑问的小伙伴可以在下方留言。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

参考链接:

xujiajia.blog.csdn.net/article/det…

blog.csdn.net/heihaozi/ar…

blog.csdn.net/sihai12345/…

ifeve.com/java-thread…

本文转载自: 掘金

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

Spring Security 官方文档学习(1)—— 初步

发表于 2021-06-25

Spring Security 官网文档

简介:
在官方介绍中,我们可以感受到 Spring Security 是一个验证以及访问控制框架,为 Java 项目提供验证、授权、防范常见攻击的功能。它被作为 Spring 项目的安全标准,集成了 servlet API,并可以灵活定制。

前提条件:

  • Spring Security 需要 Java 8 或更高的运行环境。
  • 官方特意提了一下该框架是自包含的(self-contained manner)。理解为我们可以将某个 jar/war 直接从一个系统拷贝到另一个系统,可立即正常工作,即不需要额外的配置文件。

社区:
Spring Security Community 可用于查找相关问题、小例子、直接提问、反馈Bug。

源码: 托管在 GitHub,源码。

5.5.1 版本: 增加了 JWT 验证、并完整支持 JDK 11。

1、验证(Authentication)

刚刚看到验证、授权我并没有很清晰了解两者的区别:正好看一下官网的解释:

  • 验证:Authentication is how we verify the identity of who is trying to access a particular resource. A common way to authenticate users is by requiring the user to enter a username and password. Once authentication is performed we know the identity and can perform authorization.

image.png

上述介绍可以理解到,这里存在一个先后顺序。

  • 验证完成后,我们确定了身份信息。
  • 授权,即指定这个身份可以做什么。

2、密码存储

上面提到通过用户名、密码的方式来验证用户,用户名密码是存在数据库中的,Spring Security 应该可以将从数据库中读取的用户信息先存起来,用于后续的对比。

PasswordEncoder接口:通过一个单向的密码转换,保证安全的密码存储。当需要双向转换时就不用这个接口了。通常,PasswordEncoder 用于存储密码,在身份验证时需要与用户提供的密码进行比较。

密码存储的发展过程: 1、以纯文本形式存储,但是恶意用户通过SQL注入等方式转存数据。2、以密码的单向哈希值存储,即使暴露也是哈希过的密码,但是恶意用户通过建立查找表仍可破解。3、在用户密码的基础上加上盐(随机字节)再进行单向哈希,存储哈希值,这样盐和用户密码的组合很多,无法建立查找表。

  • 上述的密码存储方法对于现代的计算能力来说都不安全了,直接暴力破解。所以我们开始考虑一种自适应的单向函数去存储密码。

所谓自适应,就是为相应的单向函数设计了一个可变的“工作因子”,它将与当今计算能力成正比。所以开发者将控制采用什么程度的工作因子,官方建议采用 1S 的验证时间作为标准,这样比较均衡。

自适应单向函数:
bcrypt、PBKDF2、scrypt、argon2 等。

注意: 因为自适应单向函数有意占用系统计算资源,故会明显降低系统性能。任何框架都无法在这里加速验证,因为验证的安全性就是与计算量成正比的。那么一个比较好的方式就是,将这种长期凭证 (i.e. username and password) 尽量用短期凭证 (i.e. session, OAuth Token, etc) 替换,因为可以快速验证短期凭证,而不造成安全损失。

3、DelegatingPasswordEncoder

在 Spring Security 5.0 之前默认一直使用的 PasswordEncoder 就是NoOpPasswordEncoder 也就是明文密码。这直接导致了最新的 Spring Security 默认还是它。官方说了一系列现实的原因,一个框架毕竟不能做太多破坏性的操作,要考虑到老用户的感受,以及框架的全面性。

所以 Spring Security 团队给出的解决方案是现在的默认编码器为 DelegatingPasswordEncoder。在我看来它就是一个综合性的编码器:

创建默认的 DelegatingPasswordEncoder:

1
java复制代码PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

创建自定义的 DelegatingPasswordEncoder:

1
2
3
4
5
6
7
8
9
java复制代码String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

从这里可以看出,DelegatingPasswordEncode 采用一个 HashMap 来存储编码器,其实就是单向转换函数的名字以及编码器的对应存储,相应的密码存储格式也是如此:

1
text复制代码{id}encodedPassword

id 就是名字,encodedPassword 就是相应编码器编码后的字符串。

如果原始密码为 “password”,那么一个例子如下:

1
2
3
4
5
text复制代码{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

比如第一个意味着,编码器 id 为 bcrypt,编码后的密码为 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,在匹配时这一整体字符串将委托给BCryptPasswordEncoder。


密码编码:
传递给构造函数的 idForEncode 确定将使用哪个 PasswordEncoder 来编码密码。在我们上面构造的 DelegatingPasswordEncoder,这意味着 BCryptPasswordEncoder 进行编码任务,并以 {bcrypt} 为前缀。最终结果如下所示:

1
text复制代码{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

来一个编码小例子:

1
2
3
4
5
6
7
java复制代码User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

这样做在内存中以及编译后代码中会暴露密码,在开发环境中是不安全的!所以我们应该从外部散列密码(我理解为直接在数据库密码列中直接存编译后的密码)。

4、BCryptPasswordEncoder

BCryptPasswordEncoder 实现使用广泛支持的 bcrypt 算法来散列密码。为了使 bcrypt 更能抵抗密码破解,它故意放慢速度。与其他自适应单向函数一样,应该将其调整为在系统上验证密码大约需要1秒的时间。BCryptPasswordEncoder 的默认实现使用强度10,如 BCryptPasswordEncoder。建议在自己的系统上调优和测试强度参数,这样验证密码大约需要 1 秒的时间。

1
2
3
4
java复制代码// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

这里,myPassword 可以是用户从前端输入来的明文密码,result 可以是数据库中的编译后密码。

5、存储配置

Spring Security 默认使用 DelegatingPasswordEncoder。但是,这可以通过将 PasswordEncoder 暴露为 Spring bean 来进行定制。

本文转载自: 掘金

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

前后端数据交互原理

发表于 2021-06-25

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

作为后端开发也可以适当了解一点前端知识,毕竟是平时开发中接触最多,了解一些前后端数据交互的知识,可以帮助后端在处理业务逻辑时考虑到更多。

在开始介绍前端数据交互原理前,先问大家一个问题,在网站上输入地址,打开网页这一过程具体做了什么呢?

输入 www.baidu.com —>域名解析(dns) —> 与服务器建立连接 —> 发起HTTP请求 —> 服务器响应HTTP请求,浏览器得到html代码 —> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片) —> 浏览器对页面进行渲染呈现给用户。

前后端不分离的开发模式:

前端代码(Html、js、css)写在JSP中,甚至JSP中嵌入Java代码。当用户访问网站时,页面数据也就是Html文档,由Servlet容器将jsp编译成Servlet,然后将jsp中的html,css,js代码输出到浏览器。

在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高

劣势和不足:

开发出的软件响应速度慢,质量差,用户体现差。前后端严重耦合,代码混乱,可维护性差。研发人员前后端兼顾,开发效率低下,研发周期变长。

前后端分离模式:

前端需要的就是后端返回正确的数据格式,拿到数据后渲染HTML页面,并展示数据,而后端仅返回前端所需的数据。用户看到什么样的效果,都由前端来控制,并且前端还可以自由的做一些特殊的设计,不需要考虑后端的逻辑。

优势:

增强代码的可维护性,分离后降低服务器负载,系统性能得到提升。通过API衔接,简单明了易维护。

这就引出了前后端的数据交互方式。

前后端数据交互方式

1、通过cookie

前端将登陆信息、账号信息存在cookie中,后端通过req.cookies()获取信息。

2、通过Ajax

利用ajax和JQuery中已经封装好的.ajax、.ajax、.ajax、.post、$.getJSON通过创建一个XMLHttpRequest对象,来进行前后端交互。

常用参数:

1、url 请求地址

2、type 请求方式,默认是’GET’,常用的还有’POST’

3、dataType 设置返回的数据格式,常用的是’json’格式,也可以设置为’html’

4、data 设置发送给服务器的数据

5、success 设置请求成功后的回调函数

6、error 设置请求失败后的回调函数

7、async 设置是否异步,默认值是’true’,表示异步

3、通过jsonp

jsonp是前后端结合跨域方式,因为前端请求到数据需要在回调函数中使用,所以后端得将数据放回到回调函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码$.ajax({

url:"",

dataType:"jsonp",

jsonp:'callback',

success(function(res){

console.log(res)

})

})

4、服务端渲染

后端加工数据,将数据直接渲染,再给浏览器就可以。

在Node中实现服务端渲染:

利用模版引擎,node在渲染模版的时候给模版传入数据,在模版中就可以使用特定的语法来渲染dom了。例如:ejs,jade

5、webSocket 和 Socket.io

通过一个双向的通信连接实现数据的交换,这个连接的一端成为一个scoket,通过建立socket双向连接,就可以让客户端和服务端直接进行双向通信。

  1. 服务器端建立好服务端, var wss=require(“socket.io”)(server)
  2. 创建客户端的连接socket var wsc = io.connect(‘ws://’)
  3. 客户端连接 wsc.on(“connect”,function(e){})
  4. 服务器端接收到客户端连接的消息 wss.on(“connection”,function(socket){})
  5. 客户端发送消息的时候触发 wsc.on(“meaasge”,function(msg){})
  6. 客户端接收到服务器端发送消息 wsc.on(“message”,function(e){})
前端传参数方式

1、cookie

2、URL

get请求的时候把参数值附在url后面传递到其他页面,参数名称和值。

优点:传递参数较少时,操作方便,简单明了

缺点:参数会暴露,不利于保密,不适合多数据传输

3、Form表单

form表单把所有属于表单中的内容提交给后台,例如输入框,单选框,多选框,文本域,文件域等。

在后台可通过对应的name属性获取相应的值。

form表单中的action属性标识提交数据的地址。

method属性指明表单提交的方式。

4、JQuery中的ajax提交

数据多时构建json对象,转换成json格式的string后传递给后台。

5、H5 web storage

localStroage 和 sessionStorage

保存数据:localStorage.setItem(key,value);

读取数据:localStorage.getItem(key);

删除单个数据:localStorage.removeItem(key);

删除所有数据:localStorage.clear();

得到某个索引的key:localStorage.key(index);

本文转载自: 掘金

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

为什么需要分布式ID?大厂的分布式 ID 生成方案是什么样的

发表于 2021-06-25

今日推荐:Github 标星 100k!2021 最新Java 学习线路图是怎样的?

下午好,我是 Guide哥!

今天分享一道朋友去京东面试真实遇到的面试题:“为什么要分布式ID?你项目中是怎么做的?”。

这篇文章我会说说自己的看法,详细介绍一下分布式ID相关的内容包括分布式 ID 的基本要求以及分布式 ID 常见的解决方案。

这篇文章全程都是大白话的形式,希望能够为你带来帮助!

原创不易,若有帮助,点赞/分享就是对我最大的鼓励!

个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!

分布式 ID

何为 ID?

日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。

我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应

简单来说,ID 就是数据的唯一标识。

何为分布式 ID?

分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。

我简单举一个分库分表的例子。

我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。

单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。

在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?

这个时候就需要生成分布式 ID了。

分布式 ID 需要满足哪些要求?

分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。

一个最基本的分布式 ID 需要满足下面这些要求:

  • 全局唯一 :ID 的全局唯一性肯定是首先要满足的!
  • 高性能 : 分布式 ID 的生成速度要快,对本地资源消耗要小。
  • 高可用 :生成分布式 ID 的服务要保证可用性无限接近于 100%。
  • 方便易用 :拿来即用,使用方便,快速接入!

除了这些之外,一个比较好的分布式 ID 还应保证:

  • 安全 :ID 中不包含敏感信息。
  • 有序递增 :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。
  • 有具体的业务含义 :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
  • 独立部署 :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。

分布式 ID 常见解决方案

数据库

数据库主键自增

这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。

以 MySQL 举例,我们通过下面的方式即可。

1.创建一个数据库表。

1
2
3
4
5
6
sql复制代码CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stub 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性。

2.通过 replace into 来插入数据。

1
2
3
4
java复制代码BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;

插入数据这里,我们没有使用 insert into 而是使用 replace into 来插入数据,具体步骤是这样的:

1)第一步: 尝试把数据插入到表中。

2)第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。

这种方式的优缺点也比较明显:

  • 优点 :实现起来比较简单、ID 有序递增、存储消耗空间小
  • 缺点 : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)

数据库号段模式

数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。

如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。

数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的Tinyid 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。

以 MySQL 举例,我们通过下面的方式即可。

1.创建一个数据库表。

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `sequence_id_generator` (
`id` int(10) NOT NULL,
`current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(10) NOT NULL COMMENT '号段的长度',
`version` int(20) NOT NULL COMMENT '版本号',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为: current_max_id ~ current_max_id+step。

version 字段主要用于解决并发问题(乐观锁),biz_type 主要用于表示业余类型。

2.先插入一行数据。

1
2
3
sql复制代码INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
(1, 0, 100, 0, 101);

3.通过 SELECT 获取指定业务下的批量唯一 ID

1
sql复制代码SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101

结果:

1
2
arduino复制代码id	current_max_id	step	version	biz_type
1 0 100 1 101

4.不够用的话,更新之后重新 SELECT 即可。

1
2
sql复制代码UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0  AND `biz_type` = 101
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101

结果:

1
2
arduino复制代码id	current_max_id	step	version	biz_type
1 100 100 1 101

相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。

另外,为了避免单点问题,你可以从使用主从模式来提高可用性。

数据库号段模式的优缺点:

  • 优点 :ID 有序递增、存储消耗空间小
  • 缺点 :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )

NoSQL

一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增。

1
2
3
4
5
6
bash复制代码127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
"2"

为了提高可用性和并发,我们可以使用 Redis Cluser。Redis Cluser 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。

除了 Redis Cluser 之外,你也可以使用开源的 Redis 集群方案Codis (大规模集群比如上百个节点的时候比较推荐)。

除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)。 并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 JavaGuide 对于 Redis 知识点的总结。

Redis 方案的优缺点:

  • 优点 : 性能不错并且生成的 ID 是有序递增的
  • 缺点 : 和数据库主键自增方案的缺点类似

除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。

MongoDB ObjectId 一共需要 12 个字节存储:

  • 0~3:时间戳
  • 3~6: 代表机器 ID
  • 7~8:机器进程 ID
  • 9~11 :自增值

MongoDB 方案的优缺点:

  • 优点 : 性能不错并且生成的 ID 是有序递增的
  • 缺点 : 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) 、有安全性问题(ID 生成有规律性)

算法

UUID

UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。

JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。

1
2
java复制代码//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
UUID.randomUUID()

RFC 4122 中关于 UUID 的示例是这样的:

我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。

5 种不同的 Version(版本)值分别对应的含义(参考维基百科对于 UUID 的介绍):

  • 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
  • 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
  • 版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;
  • 版本 4 : UUID 使用随机性或伪随机性生成。

下面是 Version 1 版本下生成的 UUID 的示例:

JDK 中通过 UUID 的 randomUUID() 方法生成的 UUID 的版本默认为 4。

1
2
java复制代码UUID uuid = UUID.randomUUID();
int version = uuid.version();// 4

另外,Variant(变体)也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。

需要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相关的介绍即可。

从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。

虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。

比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:

  • 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。
  • UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。

最后,我们再简单分析一下 UUID 的优缺点 (面试的时候可能会被问到的哦!) :

  • 优点 :生成速度比较快、简单易用
  • 缺点 : 存储消耗空间大(32 个字符串,128 位) 、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)

Snowflake(雪花算法)

Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:

  • 第 0 位: 符号位(标识正负),始终为 0,没有用,不用管。
  • 第 1~41 位 :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)
  • 第 42~52 位 :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。
  • 第 53~64 位 :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。

如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。

另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。

我们再来看看 Snowflake 算法的优缺点 :

  • 优点 :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)
  • 缺点 : 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。

开源框架

UidGenerator(百度)

UidGenerator 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。

不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。

可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。

UidGenerator 官方文档中的介绍如下:

自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 UidGenerator 的官方介绍。

Leaf(美团)

Leaf 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了!

Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。

Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。

Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:《Leaf——美团点评分布式 ID 生成系统》)。

根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。

Tinyid(滴滴)

Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。

数据库号段模式的原理我们在上面已经介绍过了。Tinyid 有哪些亮点呢?

为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:《Tinyid 原理介绍》)

在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。

这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题:

  • 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。
  • 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。

除此之外,HTTP 调用也存在网络开销。

Tinyid 的原理比较简单,其架构如下图所示:

相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:

  • 双号段缓存 :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
  • 增加多 db 支持 :支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
  • 增加 tinyid-client :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。

Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。

分布式 ID 生成方案总结

这篇文章中,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。

后记

最后再推荐一个非常不错的 Java 教程类开源项目:JavaGuide 。我在大三开始准备秋招面试的时候,创建了 JavaGuide 这个项目。目前这个项目已经有 100k+的 star,相关阅读:《1049 天,100K!简单复盘!》 。

对于你学习 Java 以及准备 Java 方向的面试都很有帮助!正如作者说的那样,这是一份:涵盖大部分 Java 程序员所需要掌握的核心知识的 Java 学习+面试指南!

相关推荐:

  • 图解计算机基础!
  • 阿里ACM大佬开源的学习笔记!TQL!
  • 计算机优质书籍搜罗+学习路线推荐!

我是 Guide哥,拥抱开源,喜欢烹饪。开源项目 JavaGuide 作者,Github:Snailclimb - Overview 。未来几年,希望持续完善 JavaGuide,争取能够帮助更多学习 Java 的小伙伴!共勉!凎!点击查看我的2020年工作汇报!

除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。没有银弹,一定要结合实际项目来选择最适合自己的方案。

本文转载自: 掘金

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

5分钟速读之Rust权威指南(三十)多线程 多线程

发表于 2021-06-25

多线程

前端同学对于WebWorker肯定比较熟悉,对于计算量大的业务,我们可以将计算逻辑分配到多个线程去处理,减少主线程的压力,提高处理速度,在rust中启用多线程很方便,如果用JS的话来说,启动一个线程就像传递一个回调函数一样简单

使用 spawn 创建新线程

使用标准库中的thread模块创建一个spawn子线程:

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
rust复制代码use std::thread; // 引入thread
use std::time::Duration; // 引入time::Duration,用来创建时间类型数据

thread::spawn(|| {
for i in 1..10 {
println!("spawn: {}", i);
// Duration::from_secs方法创建单位为秒的时间类型数据
// 使用sleep方法让spawn线程停止一秒
thread::sleep(Duration::from_secs(1));
}
});

for i in 1..5 {
println!("main: {}", i);
// 让主线程线程停止一秒
thread::sleep(Duration::from_secs(1));
}
// main: 1
// spawn: 1
// main: 2
// spawn: 2
// main: 3
// spawn: 3
// main: 4
// spawn: 4
// spawn: 5

当主线程结束时,spawn线程也会结束,而不管其是否执行完毕,上面spawn线程并没有执行完成。

使用 join 等待所有线程结束

我们可以使用thread::spawn返回值的join方法控制让main线程去等待spawn线程的执行完成:

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
rust复制代码// 获取spawn线程管理工具
let handle = thread::spawn(|| {
for i in 1..10 {
println!("spawn: {}", i);
thread::sleep(Duration::from_secs(1));
}
});

for i in 1..5 {
println!("main: {}", i);
thread::sleep(Duration::from_secs(1));
}
// 阻塞主线程,等待spawn线程执行
handle.join().unwrap();
// main: 1
// spawn: 1
// main: 2
// spawn: 2
// main: 3
// spawn: 3
// spawn: 4
// main: 4
// spawn: 5
// spawn: 6
// spawn: 7
// spawn: 8
// spawn: 9

join会阻塞当前线程直到handle线程结束,阻塞(Blocking)线程意味着阻止该线程执行工作或退出。


如果将handle.join()移动到for循环之前呢?可以猜一下是怎么输出的:

1
2
3
4
5
6
rust复制代码// 略...
handle.join().unwrap();
for i in 1..5 {
println!("main: {}", i);
thread::sleep(Duration::from_secs(1));
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码// spawn: 1
// spawn: 2
// spawn: 3
// spawn: 4
// spawn: 5
// spawn: 6
// spawn: 7
// spawn: 8
// spawn: 9
// main: 1
// main: 2
// main: 3
// main: 4

可以看到spawn线程的循环会先执行完成,再执行主线程的循环。

线程与 move 闭包

上面的spawn线程中的代码对main线程中的数据没有引用,当spawn线程使用main线程中的数据时:

1
2
3
4
5
6
7
rust复制代码let n = vec![1,2,3];

let handle = thread::spawn(|| { // 报错,闭包可能比当前函数存活的时间长,但闭包它借用了'n',而'n'是当前函数拥有的
println!("来自main线程的数据: {:?}", n);
});

handle.join().unwrap();

上边闭包尝试借用 v。然而这有一个问题:rust 不知道这个新建线程会执行多久,所以无法知晓 v 的引用是否一直有效,例如:

1
2
3
4
5
6
7
8
rust复制代码let n = vec![1,2,3];

let handle = thread::spawn(|| {
println!("来自main线程的数据: {:?}", n);
});

drop(n); // 销毁n
handle.join().unwrap();

因为当线程中对n有借用,在线程还没执行的时候,后边的drop已经将n丢弃了。


我们可以通过在闭包之前增加 move 关键字,强制闭包获取其使用的n的所有权:

1
2
3
4
5
6
7
rust复制代码let n = vec![1, 2, 3];

let handle = thread::spawn(move || {
println!("来自main线程的数据: {:?}", n); // 来自main线程的数据: [1, 2, 3]
});

handle.join().unwrap();

上面代码中,将n的所有权移动到了spawn闭包中,所以能够正常执行。


如果仍然使用drop的话:

1
2
3
4
5
6
7
8
rust复制代码let n = vec![1, 2, 3];

let handle = thread::spawn(move || {
println!("来自main线程的数据: {:?}", n);
});

drop(n); // 报错,n已经被移动到上边的闭包中,不能再次在这里使用
handle.join().unwrap();

因为n已经被移动到了spawn闭包中,所以不能在后面以任何方式继续使用,即使println!(“{:?}”,n),也是不允许的。

本文转载自: 掘金

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

Django 如何在单元测试的时候也打印 SQL 语句 前言

发表于 2021-06-25

前言

自己动手丰衣足食

之前在 思否 提了这个问题,但是没人回答啊,那就只能自己解决了!

Django 如何在单元测试的时候也打印 SQL 语句

不仅仅是思否,stackoverflow 不过都没有人回答

如何输出 SQL 语句

在项目的 settings.py 文件中添加如下内容就可以将一切对数据库的操作翻译为 SQL 语句,但是注意这个模式只有在 settings.py 中的 DEBUG 开关为 True 是才有效!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'propagate': True,
'level':'DEBUG',
},
}
}

在单元测试中输出 SQL 语句的需求

上面的语句在做单元测试的时候就变得不好使!

根本就不输出 SQL 语句!!!

我一开始以为是上面代码中的 level 设置的不对,应该将 DEBUG 改为 生产,当然,答案并非如此!

找寻答案

为什么会有这个需求呢?其实还是为了调试代码方便嘛,但既然是调试代码,如果涉及数据库的操作却不输出 SQL 语句,就感觉是雾里看花、黑箱操作让人摸不着头脑!!!作为一名优秀的程序员怎么能允许这种事情发生呢?当然不可以!!!

所以我去通读了 Django 官方文档中相关的内容

  • 编写并运行测试

通过阅读改文档可知:做单元测试的时候会以生产模式运行,这就解释了为什么不输出 SQL 语句,因为我们的日志设置的级别是 DEBUG

无论配置文件中的 DEBUG 设置值是多少,所有的 Django 测试都以 DEBUG=False 运行。这是为了确保你的代码观察到的输出与生产环境下的输出一致。

  • 日志管理快速入门

好的,找到了改进方向了,那就 level 改为生产模式就好了嘛!

但是通过阅读文档发现只有:

+ `DEBUG`:排查故障时使用的低级别系统信息
+ `INFO`:一般的系统信息
+ `WARNING`:描述系统发生了一些小问题的信息
+ `ERROR`:描述系统发生了大问题的信息
+ `CRITICAL`:描述系统发生严重问题的信息这五种模式,最低就是 DEBUG,根本没有生产模式啊!!!
  • 定义测试运行器

读完该篇文档,我懂了,之前想要修改 level 的想法是错误的,我们可以也应该通过自定义测试器 的方式来让单元测试也输出 SQL 语句!!!

说干就干!

说干就干

在根目录下面新建一个 testing 文件夹,然后在其中创建一个 testcases.py 文件,在其中写下如下的代码:

1
2
3
4
5
6
python复制代码from django.test.runner import DiscoverRunner


class DebugDiscoverRunner(DiscoverRunner):
def __init__(self, *args, **kwargs):
super().__init__(debug_sql=True, verbosity=2)

这段代码有几个关键点

  • 继承 DiscoverRunner

这个没什么好说的,默认就是 DiscoverRunner,所以我们继承他

  • debug_sql=True, verbosity=2

继承它不为别的,就像开启上面的两个参数

testing 和 testcases.py 这些名字随意,只是我习惯这么放置和取名字,你随意!!!

然后再 settings.py 文件中的任意位置添加下面的代码:

1
python复制代码TEST_RUNNER = 'testing.testrunner.DebugDiscoverRunner'

这个时候你只需要再终端输入

1
bash复制代码python manage.py test

就会调用我们的 DebugDiscoverRunner 来做单元测试,执行 __init__.py 传递 debug_sql=True, verbosity=2 ,就会再控制台源源不断的打印 SQL 语句!!!

一些细节

你会发现 DebugDiscoverRunner 的 __init__.py 传递给 super().__init__(debug_sql=True, verbosity=2) 的参数没有了 *args, **kwargs

,为什么呢?

因为会报错!!!

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码vagrant@vagrant:/vagrant$ python manage.py test comments
Traceback (most recent call last):
File "manage.py", line 22, in <module>
main()
File "manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.6/dist-packages/django/core/management/__init__.py", line 395, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/commands/test.py", line 23, in run_from_argv
super().run_from_argv(argv)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/base.py", line 330, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/base.py", line 371, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/commands/test.py", line 52, in handle
test_runner = TestRunner(**options)
File "/vagrant/testing/testrunner.py", line 6, in __init__
super(DebugDiscoverRunner, self).__init__(debug_sql=True, verbosity=2, *args, **kwargs)
TypeError: __init__() got multiple values for keyword argument 'verbosity'

参考文章:Django:DiscoverRunner覆盖引发错误

参考归参考,但是这个人的处理方法很不好

所以我们去掉了 *args, **kwargs

本文转载自: 掘金

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

【学习日记1】SpringBoot+MyBatis架构

发表于 2021-06-25

今天开始写学习日记,从学习mall项目(www.macrozheng.com/#/README)开始。

  1. 初始化数据库

新建数据库mall-demo,运行sql文件:mall-demo.sql。文件内容如下:

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
sql复制代码SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for pms_brand
-- ----------------------------
DROP TABLE IF EXISTS `pms_brand`;
CREATE TABLE `pms_brand` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`first_letter` varchar(8) DEFAULT NULL COMMENT '首字母',
`sort` int(11) DEFAULT NULL,
`factory_status` int(1) DEFAULT NULL COMMENT '是否为品牌制造商:0->不是;1->是',
`show_status` int(1) DEFAULT NULL,
`product_count` int(11) DEFAULT NULL COMMENT '产品数量',
`product_comment_count` int(11) DEFAULT NULL COMMENT '产品评论数量',
`logo` varchar(255) DEFAULT NULL COMMENT '品牌logo',
`big_pic` varchar(255) DEFAULT NULL COMMENT '专区大图',
`brand_story` text COMMENT '品牌故事',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=59 DEFAULT CHARSET=utf8 COMMENT='品牌表';

-- ----------------------------
-- Records of pms_brand
-- ----------------------------
INSERT INTO `pms_brand` VALUES ('1', '万和', 'W', '0', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180607/timg(5).jpg', '', 'Victoria\'s Secret的故事');
INSERT INTO `pms_brand` VALUES ('2', '三星', 'S', '100', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180607/timg (1).jpg', null, '三星的故事');
INSERT INTO `pms_brand` VALUES ('3', '华为', 'H', '100', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/17f2dd9756d9d333bee8e60ce8c03e4c_222_222.jpg', null, 'Victoria\'s Secret的故事');
INSERT INTO `pms_brand` VALUES ('4', '格力', 'G', '30', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/dc794e7e74121272bbe3ce9bc41ec8c3_222_222.jpg', null, 'Victoria\'s Secret的故事');
INSERT INTO `pms_brand` VALUES ('5', '方太', 'F', '20', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180607/timg (4).jpg', null, 'Victoria\'s Secret的故事');
INSERT INTO `pms_brand` VALUES ('6', '小米', 'M', '500', '1', '1', '100', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/1e34aef2a409119018a4c6258e39ecfb_222_222.png', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180518/5afd7778Nf7800b75.jpg', '小米手机的故事');
INSERT INTO `pms_brand` VALUES ('21', 'OPPO', 'O', '0', '1', '1', '88', '500', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180607/timg(6).jpg', '', 'string');
INSERT INTO `pms_brand` VALUES ('49', '七匹狼', 'S', '200', '1', '1', '77', '400', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/18d8bc3eb13533fab466d702a0d3fd1f40345bcd.jpg', null, 'BOOB的故事');
INSERT INTO `pms_brand` VALUES ('50', '海澜之家', 'H', '200', '1', '1', '66', '300', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/99d3279f1029d32b929343b09d3c72de_222_222.jpg', '', '海澜之家的故事');
INSERT INTO `pms_brand` VALUES ('51', '苹果', 'A', '200', '1', '1', '55', '200', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180607/timg.jpg', null, '苹果的故事');
INSERT INTO `pms_brand` VALUES ('58', 'NIKE', 'N', '0', '1', '1', '33', '100', 'http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180615/timg (51).jpg', '', 'NIKE的故事');
  1. 初始化SpringBoot项目

用Spring Initializr新建SpringBoot项目:

image.png

加入SpringBoot通用依赖模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

修改MallDemoApplicationTests.java报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
class MallDemoApplicationTests {

@Test
void contextLoads() {
}

}

以上,项目能正常运行。

  1. 集成MyBatis

加入MyBatis依赖模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<!--MyBatis分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- MyBatis 生成器 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.3</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>

配置application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码server:
port: 8080

spring:
datasource:
url: jdbc:mysql://localhost:3306/mall-demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password:

mybatis:
mapper-locations:
- classpath:mapper/*.xml
- classpath*:com/**/mapper/*.xml

配置generator.properties

1
2
3
4
ini复制代码jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://localhost:3306/mall-demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.userId=root
jdbc.password=

配置generatorConfig.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<properties resource="generator.properties"/>
<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<property name="javaFileEncoding" value="UTF-8"/>
<!-- 为模型生成序列化方法-->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<!-- 为生成的Java模型创建一个toString方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!--可以自定义生成model的代码注释-->
<commentGenerator type="com.arielyu.mall.mbg.CommentGenerator">
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true"/>
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!--配置数据库连接-->
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">
<!--解决mysql驱动升级到8.0后不生成指定数据库代码的问题-->
<property name="nullCatalogMeansCurrent" value="true" />
</jdbcConnection>
<!--指定生成model的路径-->
<javaModelGenerator targetPackage="com.arielyu.mall.mbg.model" targetProject="/Users/yuchu/Documents/Gitee/mall-demo/src/main/java"/>
<!--指定生成mapper.xml的路径-->
<sqlMapGenerator targetPackage="com.arielyu.mall.mbg.mapper" targetProject="/Users/yuchu/Documents/Gitee/mall-demo/src/main/resources"/>
<!--指定生成mapper接口的的路径-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.arielyu.mall.mbg.mapper"
targetProject="/Users/yuchu/Documents/Gitee/mall-demo/src/main/java"/>
<!--生成全部表tableName设为%-->
<table tableName="pms_brand">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>

自定义注释生成器:

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
typescript复制代码public class CommentGenerator extends DefaultCommentGenerator {
private boolean addRemarkComments = false;

/**
* 设置用户配置的参数
*/
@Override
public void addConfigurationProperties(Properties properties) {
super.addConfigurationProperties(properties);
this.addRemarkComments = StringUtility.isTrue(properties.getProperty("addRemarkComments"));
}

/**
* 给字段添加注释
*/
@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable,
IntrospectedColumn introspectedColumn) {
String remarks = introspectedColumn.getRemarks();
//根据参数和备注信息判断是否添加备注信息
if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
addFieldJavaDoc(field, remarks);
}
}

/**
* 给model的字段添加注释
*/
private void addFieldJavaDoc(Field field, String remarks) {
//文档注释开始
field.addJavaDocLine("/**");
//获取数据库字段的备注信息
String[] remarkLines = remarks.split(System.getProperty("line.separator"));
for (String remarkLine : remarkLines) {
field.addJavaDocLine(" * " + remarkLine);
}
addJavadocTag(field, false);
field.addJavaDocLine(" */");
}

}

Generator.java

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 class Generator {
public static void main(String[] args) throws Exception {
//MBG 执行过程中的警告信息
List<String> warnings = new ArrayList<String>();
//当生成的代码重复时,覆盖原代码
boolean overwrite = true;
//读取我们的 MBG 配置文件
InputStream is = Generator.class.getResourceAsStream("/generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
is.close();

DefaultShellCallback callback = new DefaultShellCallback(overwrite);
//创建 MBG
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
//执行生成代码
myBatisGenerator.generate(null);
//输出警告信息
for (String warning : warnings) {
System.out.println(warning);
}
}
}

执行Generator.java,生成model和mapper。

本文转载自: 掘金

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

今天又是被“欺负”的一天!!跪求大佬帮帮我~使用Spring

发表于 2021-06-25

在这里插入图片描述

使用Spring JDBCTemplate简化JDBC的操作

接触过JAVA WEB开发的朋友肯定都知道Hibernate框架,虽然不否定它的强大之处,但个人对它一直无感,总感觉不够灵活,太过臃肿了。

今天来说下Spring中关于JDBC的一个辅助类(JDBC Template),它封装了JDBC的操作,使用起来非常方便。

在这里插入图片描述

先说下”傻瓜式”的使用(不依赖于xml配置):

直接写个测试单元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码 1 package com.lcw.spring.jdbc;
2
3 import org.junit.Test;
4 import org.springframework.jdbc.core.JdbcTemplate;
5 import org.springframework.jdbc.datasource.DriverManagerDataSource;
6
7 public class JDBCTemplate {
8
9 @Test
10 public void demo(){
11 DriverManagerDataSource dataSource=new DriverManagerDataSource();
12 dataSource.setDriverClassName("com.mysql.jdbc.Driver");
13 dataSource.setUrl("jdbc:mysql:///spring");
14 dataSource.setUsername("root");
15 dataSource.setPassword("");
16
17 JdbcTemplate jdbcTemplate=new JdbcTemplate(dataSource);
18 jdbcTemplate.execute("create table temp(id int primary key,name varchar(32))");
19
20 }
21
22 }

很简单吧,再来看下使用结合配置文件,完整的实现对一个类的增删改查

首先DEMO目录结构:
在这里插入图片描述

appliactionContext.xml

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
c复制代码 1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:p="http://www.springframework.org/schema/p"
4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xsi:schemaLocation="
6 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
7
8 <!--数据源的配置 -->
9 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
10 <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
11 <property name="url" value="jdbc:mysql:///spring"></property>
12 <property name="username" value="root"></property>
13 <property name="password" value=""></property>
14 </bean>
15
16
17   
18 <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
19 <property name="dataSource" ref="dataSource"></property>
20 </bean>
21
22
23 <bean id="userDao" class="com.curd.spring.impl.UserDAOImpl">
24 <property name="jdbcTemplate" ref="jdbcTemplate"></property>
25 </bean>
26
27
28
29 </beans>

接口:IUserDAO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码 1 package com.curd.spring.dao;
2
3 import java.util.List;
4
5 import com.curd.spring.vo.User;
6
7 public interface IUserDAO {
8
9 public void addUser(User user);
10
11 public void deleteUser(int id);
12
13 public void updateUser(User user);
14
15 public String searchUserName(int id);
16
17 public User searchUser(int id);
18
19 public List<User> findAll();
20
21 }

接口实现类:UserDAOImpl.java

按照以往Spring的依赖注入,我们需要在接口实现类中利用构造器去获取JdbcTemplate

Spring早就帮我们想到了这点,它为我们提供了JdbcDaoSupport支持类,所有DAO继承这个类,就会自动获得JdbcTemplate(前提是注入DataSource)。

1
2
3
4
5
6
7
8
c复制代码1     <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
2 <property name="dataSource" ref="dataSource"></property>
3 </bean>
4
5
6 <bean id="userDao" class="com.curd.spring.impl.UserDAOImpl">
7 <property name="jdbcTemplate" ref="jdbcTemplate"></property>
8 </bean>

在我们的实现类中直接利用getJdbcTemplate就可以获取操作对象了。

JdbcTemplate主要提供下列方法:

  1、execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;

  2、update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;

  3、query方法及queryForXXX方法:用于执行查询相关语句;

  4、call方法:用于执行存储过程、函数相关语句。

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
c复制代码 1 package com.curd.spring.impl;
2
3 import java.sql.ResultSet;
4 import java.sql.SQLException;
5 import java.util.List;
6
7 import org.springframework.jdbc.core.RowMapper;
8 import org.springframework.jdbc.core.support.JdbcDaoSupport;
9 import com.curd.spring.dao.IUserDAO;
10 import com.curd.spring.vo.User;
11
12 public class UserDAOImpl extends JdbcDaoSupport implements IUserDAO {
13
14 public void addUser(User user) {
15 String sql = "insert into user values(?,?,?)";
16 this.getJdbcTemplate().update(sql, user.getId(), user.getUsername(),
17 user.getPassword());
18 }
19
20 public void deleteUser(int id) {
21 String sql = "delete from user where id=?";
22 this.getJdbcTemplate().update(sql, id);
23
24 }
25
26 public void updateUser(User user) {
27 String sql = "update user set username=?,password=? where id=?";
28 this.getJdbcTemplate().update(sql, user.getUsername(),
29 user.getPassword(), user.getId());
30 }
31
32 public String searchUserName(int id) {// 简单查询,按照ID查询,返回字符串
33 String sql = "select username from user where id=?";
34 // 返回类型为String(String.class)
35 return this.getJdbcTemplate().queryForObject(sql, String.class, id);
36
37 }
38
39 public List<User> findAll() {// 复杂查询返回List集合
40 String sql = "select * from user";
41 return this.getJdbcTemplate().query(sql, new UserRowMapper());
42
43 }
44
45 public User searchUser(int id) {
46 String sql="select * from user where id=?";
47 return this.getJdbcTemplate().queryForObject(sql, new UserRowMapper(), id);
48 }
49
50 class UserRowMapper implements RowMapper<User> {
51      //rs为返回结果集,以每行为单位封装着
52 public User mapRow(ResultSet rs, int rowNum) throws SQLException {
53
54 User user = new User();
55 user.setId(rs.getInt("id"));
56 user.setUsername(rs.getString("username"));
57 user.setPassword(rs.getString("password"));
58 return user;
59 }
60
61 }
62
63 }

测试类:UserTest.java

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
c复制代码 1 package com.curd.spring.test;
2
3 import java.util.List;
4
5 import org.junit.Test;
6 import org.springframework.context.ApplicationContext;
7 import org.springframework.context.support.ClassPathXmlApplicationContext;
8
9 import com.curd.spring.dao.IUserDAO;
10 import com.curd.spring.vo.User;
11
12 public class UserTest {
13
14 @Test//增
15 public void demo1(){
16 User user=new User();
17 user.setId(3);
18 user.setUsername("admin");
19 user.setPassword("123456");
20
21 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
22 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
23 dao.addUser(user);
24
25 }
26
27 @Test//改
28 public void demo2(){
29 User user=new User();
30 user.setId(1);
31 user.setUsername("admin");
32 user.setPassword("admin");
33
34 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
35 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
36 dao.updateUser(user);
37 }
38
39 @Test//删
40 public void demo3(){
41 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
42 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
43 dao.deleteUser(3);
44 }
45
46 @Test//查(简单查询,返回字符串)
47 public void demo4(){
48 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
49 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
50 String name=dao.searchUserName(1);
51 System.out.println(name);
52 }
53
54 @Test//查(简单查询,返回对象)
55 public void demo5(){
56 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
57 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
58 User user=dao.searchUser(1);
59 System.out.println(user.getUsername());
60 }
61
62 @Test//查(复杂查询,返回对象集合)
63 public void demo6(){
64 ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
65 IUserDAO dao=(IUserDAO) applicationContext.getBean("userDao");
66 List<User> users=dao.findAll();
67 System.out.println(users.size());
68 }
69
70
71
72 }

怎么样,很简单吧,在不缺JDBC里SQL的灵活操作又去除了繁杂操作~

【参考文献】

本文转载自: 掘金

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

compileflow阿里开源流程引擎

发表于 2021-06-25

1、compileflow是什么

compileflow是一个非常轻量、高性能、可集成、可扩展的流程引擎。

compileflow Process引擎是淘宝工作流TBBPM引擎之一,是专注于纯内存执行,无状态的流程引擎,通过将流程文件转换生成java代码编译执行,简洁高效。当前是阿里业务中台交易等多个核心系统的流程引擎。

compileflow能让开发人员通过流程编辑器设计自己的业务流程,将复杂的业务逻辑可视化,为业务设计人员与开发工程师架起了一座桥梁。

2、功能列表

  1. 高性能:通过将流程文件转换生成java代码编译执行,简洁高效。
  2. 丰富的应用场景:在阿里巴巴中台解决方案中广泛使用,支撑了导购、交易、履约、资金等多个业务场景。
  3. 可集成:轻量、简洁的设计使得可以极其方便地集成到各个解决方案和业务场景中。
  4. 完善的插件支持:流程设计目前有IntelliJ IDEA、Eclipse插件支持,可以在流程设计中实时动态生成java代码并预览,所见即所得。
  5. 支持流程设计图导出svg文件和单元测试代码。

3、Quick Start

Step1: 下载安装IntelliJ IDEA插件(可选)

插件下载地址:github.com/alibaba/com…

安装说明:请使用IntelliJ IDEA本地安装方法进行安装,重新启动IntelliJ IDEA就会生效。

Step2: POM插件

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.compileflow</groupId>
<artifactId>compileflow</artifactId>
<version>1.0.0</version>
</dependency>

可以在 search.maven.org 查看可用的版本。

注意: compileflow仅支持JDK 1.8及以上版本。

Step3: 流程设计

下面以ktv demo为例,通过demo的演示和实践了解节点及属性的配置和API的使用。

demo描述:N个人去ktv唱歌,每人唱首歌,ktv消费原价为30元/人,如果总价超过300打九折,小于300按原价付款。

S3.1

创建bpm文件,如下图:

ktv_demo_s1.png

注:bpm文件路径要和code保持一致,在文件加载模式下流程引擎执行时会根据code找到文件。

S3.2

通过插件进行流程设计或者直接编写流程xml文件。

S3.3 调用流程

编写如下单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public void testProcessEngine() {
final String code = "bpm.ktv.ktvExample";

final Map<String, Object> context = new HashMap<>();
final List<String> pList = new ArrayList<>();
pList.add("wuxiang");
pList.add("xuan");
pList.add("yusu");
context.put("pList", pList);

final ProcessEngine<TbbpmModel> processEngine = ProcessEngineFactory.getProcessEngine();

final TbbpmModel tbbpmModel = processEngine.load(code);
final OutputStream outputStream = TbbpmModelConverter.getInstance().convertToStream(tbbpmModel);
System.out.println(outputStream);
System.out.println(processEngine.getTestCode(code));

processEngine.preCompile(code);

System.out.println(processEngine.start(code, context));
}

compileflow原生只支持淘宝BPM规范,为兼容BPMN 2.0规范,做了一定适配,但仅支持部分BPMN 2.0元素,如需其他元素支持,可在原来基础上扩展。

4、 更多资料

  • DEMO快速开始
  • 原始淘宝BPM规范详细说明

本文转载自: 掘金

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

1…632633634…956

开发者博客

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