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

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


  • 首页

  • 归档

  • 搜索

我给SpringBoot提了个issue,被采纳了…

发表于 2021-09-17

事情是这样的

项目中使用了springboot + spring data redis,但是公司规定,redis密码一律托管,只能远程获取。

开发环境使用的单实例redis,连接池用的是lettuce,同事的是实现是把Spring Data Redis自动装载的代码copy一份搬到项目里,原因从下面的分析中可以看出,Spring相关配置核心类都是包可见的,在外部根本无法继承和引用。

但是,好事者,也就是在下,觉得这“不够Spring”,于是,深挖了一番,并在一番分析之后,给社区提了一个比较中肯的Issue,并且被采纳。

Spring Data Redis 自动装配机制

在org.springframework.boot.autoconfigure.data.redis中有RedisAutoConfiguration, 其通过@Import依赖于LettuceConnectionConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

}

LettuceConnectionConfiguration 继承自RedisConnectionConfiguration,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class) // -->①
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true) // -->②
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {

LettuceConnectionConfiguration(RedisProperties properties,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
super(properties, sentinelConfigurationProvider, clusterConfigurationProvider);
}

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class) // -->③
LettuceConnectionFactory redisConnectionFactory(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
ClientResources clientResources) {
LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
getProperties().getLettuce().getPool());
return createLettuceConnectionFactory(clientConfig);
}
}

从中可以看出,Spring boot 自动装配Lettuce连接工厂的条件如下

① 存在 RedisClient , lettuce.io 中自带的redis 客户端类

② 项目中使用配置spring.redis.client-type 为lettuce

③ 项目代码中只要不定义RedisConnectionFactory , 便会自动按照配置文件创建 LettuceConnectionFactory


其中,包含两处关键,

  • 构造函数LettuceConnectionConfiguration 出现的RedisProperties 和两个ObjectProvider,并且调用了父类构造函数
  • redisConnectionFactory 中包含两个重要方法getLettuceClientConfiguration 和 createLettuceConnectionFactory, 其中getLettuceClientConfiguration 主要处理Pool连接池的相关配置,不做赘述,从下面的分析也可以知道,properties其实就是RedisProperties,重点看createLettuceConnectionFactory

下面,逐个解析这些关键点。

父类构造函数 RedisConnectionConfiguration

1
2
3
4
5
6
7
java复制代码protected RedisConnectionConfiguration(RedisProperties properties,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
this.properties = properties;
this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable();
}

理解这段代码的关键是ObjectProvider, 其实你如果细心留意,你会发现,Springboot的代码,特别是构造函数,大量的用到ObjectProvider

ObjectProvider

关于ObjectProvider , 可以简单聊两句 Spring 4.3的一些改进

当构造方法的参数为单个构造参数时,可以不使用@Autowired进行注解

1
2
3
4
5
6
7
java复制代码@Service
public class FooService {
private final FooRepository repository;
public FooService(FooRepository repository) {
this.repository = repository
}
}

比如,上面这段代码是spring 4.3之后的版本,不需要@Autowired 也可以正常运行。

同样是在Spring 4.3版本中,不仅隐式的注入了单构造参数的属性,还引入了ObjectProvider接口。

1
2
3
4
5
6
java复制代码//A variant of ObjectFactory designed specifically for injection points, allowing for programmatic optionality and lenient not-unique handling.
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
// ...省略了部分代码
@Nullable
T getIfAvailable() throws BeansException;
}

从源码注释中可以得知,ObjectProvider接口是ObjectFactory接口的扩展,专门为注入点设计的,可以让注入变得更加宽松和更具有可选项。

其中,由getIfAvailable()可见,当待注入参数的Bean为空或有多个时,便是ObjectProvider发挥作用的时候。

  • 如果注入实例为空时,使用ObjectProvider则避免了强依赖导致的依赖对象不存在异常
  • 如果有多个实例,ObjectProvider的方法会根据Bean实现的Ordered接口或@Order注解指定的先后顺序获取一个Bean, 从而了提供了一个更加宽松的依赖注入方式

回到,RedisConnectionConfiguration这个父类构造函数本身,其实就是实现这样的功能:如果用户提供了RedisSentinelConfiguration和 RedisSentinelConfiguration , 会在构造函数中加载进来,而RedisProperties则比较简单,就是redis的相关配置。

RedisProperties

从配置中读取redis的相关配置,最简单的单机redis配置的是简单的属性,sentinel是哨兵相关配置,cluster是集群相关配置,Pool是连接池的相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private int port = 6379;

private String username;
private String password;

private Sentinel sentinel;
private Cluster cluster;
public static class Pool {}
public static class Cluster {}
public static class Sentinel {}
// ... 省略非必要代码
}

小结一下,目前,我们可以看到RedisAutoConfiguration依赖于配置类LettuceConnectionConfiguration, 其构造函数读取了用户定义的redis配置,其中包含 单机配置+集群配置+哨兵配置+连接池配置,其中集群配置和哨兵配置是两个允许用户自定义的Bean。

createLettuceConnectionFactory

LettuceConnectionConfiguration中实现连接池的方法中调用了createLettuceConnectionFactory, 其实现如下

1
2
3
4
5
6
7
8
9
java复制代码private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
if (getSentinelConfig() != null) {
return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
}
if (getClusterConfiguration() != null) {
return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
}
return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
}

其实就是依次读取哨兵的配置,集群的配置 以及 单机的配置,如果有就创建连接池返回。

其中getSentinelConfig() 和 getClusterConfiguration() 是父类的方法,其实现如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码protected final RedisSentinelConfiguration getSentinelConfig() {
if (this.sentinelConfiguration != null) {
return this.sentinelConfiguration;
}
RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel();
if (sentinelProperties != null) {
RedisSentinelConfiguration config = new RedisSentinelConfiguration();
// 省略装载代码
config.setDatabase(this.properties.getDatabase());
return config;
}
return null;
}

protected final RedisClusterConfiguration getClusterConfiguration() {
if (this.clusterConfiguration != null) {
return this.clusterConfiguration;
}
if (this.properties.getCluster() == null) {
return null;
}
RedisProperties.Cluster clusterProperties = this.properties.getCluster();
RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
// 省略装载代码
return config;
}

从中,我们可以知道,其优先读取在构造函数中由ObjectProvider引入的可能存在的用户自定义配置Bean,如果没有,再通过读取RedisProperties完成装配。

但是,细心的读者要问了,How about 单机配置?

image-20210917000348170.png

1
2
3
4
5
6
7
8
9
10
11
java复制代码protected final RedisStandaloneConfiguration getStandaloneConfig() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
if (StringUtils.hasText(this.properties.getUrl())) {
// 省略装载代码
}
else {
// 省略装载代码
}
config.setDatabase(this.properties.getDatabase());
return config;
}

是的,你没有看错,单身狗不配……

src=http___dn-p-tystore.qbox.me_p_chapter_attachment_eBjVEBIWES_Egfveg6VeB6segjUetbteluCfn9rG7Ldgn5Gig5aJHeOHuHa90ueJ6S.jpg&refer=http___dn-p-tystore.qbox.jpg

总结起来就是,在构造函数中获取合适的配置bean,然后在创建连接池的方法里面查找,如果没有就用配置文件构造一个,但是不支持单实例的redis。

提一个issue吧

保护单身狗,人人有责,于是,我以“单身狗保护协会”的名义给SpringBoot社区提了一个issue

image-20210917000825318.png
然后,大佬回复,可以保护可以支持,很开心。

image-20210917000939528.png
其中,有提到使用BeanPostProcessor的方法去改写RedisProperties的配置,中途我有想到,所以把issue关了,沉吟一阵,觉得不优雅,不开心,又把issue给打开了,很感谢开源团队的支持和理解,备受鼓舞。

本文转载自: 掘金

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

java做一个学生随机点名器的案例

发表于 2021-09-16

考察点

学生类Student中通过get、set方法访问私有属性

代码

  • Student类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package cn.itcast.demo07;

public class Student {
private String name;
private int age;

public String getName() {
return name;
}

public int getAge() {
return age;
}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}
}
  • CallName主功能类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
java复制代码package cn.itcast.demo07;

import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

public class CallName {
public static void main(String[] args) {
//1.1创建一个可以存储多个同学名字的容器
ArrayList<Student> list = new ArrayList<Student>();

/*
* 1.存储全班同学信息
*/
addStudent(list);

/*
* 2.打印全班同学每一个人的信息(姓名、年龄)
*/
printStudent(list);

/*
* 3.随机对学生点名,打印学生信息
*/
randomStudent(list);

}

/**
* 1.存储全班同学名字
*/
public static void addStudent(ArrayList<Student> list) {
//键盘输入多个同学名字存储到容器中
// Scanner sc = new Scanner(System.in);
// for(int i = 0; i < 3;i++) {
// // 创建学生
// Student s = new Student();
// System.out.println("存储第"+i+"个学生姓名:");
// String name = sc.next();
// s.setName(name);
// System.out.println("存储第"+i+"个学生年龄:");
// int age = sc.nextInt();
// s.setAge(age);
// //添加学生到集合
// list.add(s);
// }
Student s1 = new Student();
s1.setName("lisi1");
s1.setAge(15);

Student s2 = new Student();
s2.setName("lisi2");
s2.setAge(16);

Student s3 = new Student();
s3.setName("lisi3");
s3.setAge(16);

Student s4 = new Student();
s4.setName("lisi4");
s4.setAge(17);

list.add(s1);
list.add(s2);
list.add(s3);
list.add(s4);
}

/**
* 2.打印全班同学每一个人的信息(姓名、年龄)
*/
public static void printStudent(ArrayList<Student> list) {
for(int i = 0;i<list.size();i++) {
Student s = list.get(i);
System.out.println("姓名:" + s.getName() + ",年龄:" + s.getAge());
}
}

/**
* 3.随机对学生点名,打印学生信息
*/
public static void randomStudent(ArrayList<Student> list) {
//在班级总人数范围内,随机产生一个随机数
int index = new Random().nextInt(list.size());
//在容器(ArrayList集合)中,查找该随机数所对应的同学信息(姓名、年龄)
Student s = list.get(index);
System.out.println("被随机点名的同学:"+s.getName() + ",年龄:" + s.getAge());
}
}

THE END.

本文转载自: 掘金

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

Go 专栏|并发编程:goroutine,channel 和

发表于 2021-09-16

原文链接: Go 专栏|并发编程:goroutine,channel 和 sync

优雅的并发编程范式,完善的并发支持,出色的并发性能是 Go 语言区别于其他语言的一大特色。

在当今这个多核时代,并发编程的意义不言而喻。使用 Go 开发并发程序,操作起来非常简单,语言级别提供关键字 go 用于启动协程,并且在同一台机器上可以启动成千上万个协程。

下面就来详细介绍。

goroutine

Go 语言的并发执行体称为 goroutine,使用关键词 go 来启动一个 goroutine。

go 关键词后面必须跟一个函数,可以是有名函数,也可以是无名函数,函数的返回值会被忽略。

go 的执行是非阻塞的。

先来看一个例子:

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

import (
"fmt"
"time"
)

func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) // Fibonacci(45) = 1134903170
}

func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}

func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}

从执行结果来看,成功计算出了斐波那契数列的值,说明程序在 spinner 处并没有阻塞,而且 spinner 函数还一直在屏幕上打印提示字符,说明程序正在执行。

当计算完斐波那契数列的值,main 函数打印结果并退出,spinner 也跟着退出。

再来看一个例子,循环执行 10 次,打印两个数的和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import "fmt"

func Add(x, y int) {
z := x + y
fmt.Println(z)
}

func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}

有问题了,屏幕上什么都没有,为什么呢?

这就要看 Go 程序的执行机制了。当一个程序启动时,只有一个 goroutine 来调用 main 函数,称为主 goroutine。新的 goroutine 通过 go 关键词创建,然后并发执行。当 main 函数返回时,不会等待其他 goroutine 执行完,而是直接暴力结束所有 goroutine。

那有没有办法解决呢?当然是有的,请往下看。

channel

一般写多进程程序时,都会遇到一个问题:进程间通信。常见的通信方式有信号,共享内存等。goroutine 之间的通信机制是通道 channel。

使用 make 创建通道:

1
go复制代码ch := make(chan int) // ch 的类型是 chan int

通道支持三个主要操作:send,receive 和 close。

1
2
3
4
5
go复制代码ch <- x // 发送
x = <-ch // 接收
<-ch // 接收,丢弃结果

close(ch) // 关闭

无缓冲 channel

make 函数接受两个参数,第二个参数是可选参数,表示通道容量。不传或者传 0 表示创建了一个无缓冲通道。

无缓冲通道上的发送操作将会阻塞,直到另一个 goroutine 在对应的通道上执行接收操作。相反,如果接收先执行,那么接收 goroutine 将会阻塞,直到另一个 goroutine 在对应通道上执行发送。

所以,无缓冲通道是一种同步通道。

下面我们使用无缓冲通道把上面例子中出现的问题解决一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码package main

import "fmt"

func Add(x, y int, ch chan int) {
z := x + y
ch <- z
}

func main() {

ch := make(chan int)
for i := 0; i < 10; i++ {
go Add(i, i, ch)
}

for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}

可以正常输出结果。

主 goroutine 会阻塞,直到读取到通道中的值,程序继续执行,最后退出。

缓冲 channel

创建一个容量是 5 的缓冲通道:

1
go复制代码ch := make(chan int, 5)

缓冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个 goroutine 执行接收。相反,如果通道是空的,接收会阻塞,直到另一个 goroutine 执行发送。

有没有感觉,其实缓冲通道和队列一样,把操作都解耦了。

单向 channel

类型 chan<- int 是一个只能发送的通道,类型 <-chan int 是一个只能接收的通道。

任何双向通道都可以用作单向通道,但反过来不行。

还有一点需要注意,close 只能用在发送通道上,如果用在接收通道会报错。

看一个单向通道的例子:

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

import "fmt"

func counter(out chan<- int) {
for x := 0; x < 10; x++ {
out <- x
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}

func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}

func main() {
n := make(chan int)
s := make(chan int)

go counter(n)
go squarer(s, n)
printer(s)

}

sync

sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。

当一个 goroutine 获取了 Mutex 后,其他 goroutine 不管读写,只能等待,直到锁被释放。

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

import (
"fmt"
"sync"
"time"
)

func main() {
var mutex sync.Mutex
wg := sync.WaitGroup{}

// 主 goroutine 先获取锁
fmt.Println("Locking (G0)")
mutex.Lock()
fmt.Println("locked (G0)")

wg.Add(3)
for i := 1; i < 4; i++ {
go func(i int) {
// 由于主 goroutine 先获取锁,程序开始 5 秒会阻塞在这里
fmt.Printf("Locking (G%d)\n", i)
mutex.Lock()
fmt.Printf("locked (G%d)\n", i)

time.Sleep(time.Second * 2)
mutex.Unlock()
fmt.Printf("unlocked (G%d)\n", i)

wg.Done()
}(i)
}

// 主 goroutine 5 秒后释放锁
time.Sleep(time.Second * 5)
fmt.Println("ready unlock (G0)")
mutex.Unlock()
fmt.Println("unlocked (G0)")

wg.Wait()
}

RWMutex 属于经典的单写多读模型,当读锁被占用时,会阻止写,但不阻止读。而写锁会阻止写和读。

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

import (
"fmt"
"sync"
"time"
)

func main() {
var rwMutex sync.RWMutex
wg := sync.WaitGroup{}

Data := 0
wg.Add(20)
for i := 0; i < 10; i++ {
go func(t int) {
// 第一次运行后,写解锁。
// 循环到第二次时,读锁定后,goroutine 没有阻塞,同时读成功。
fmt.Println("Locking")
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
go func(t int) {
// 写锁定下是需要解锁后才能写的
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
}

wg.Wait()
}

总结

并发编程算是 Go 的特色,也是核心功能之一了,涉及的知识点其实是非常多的,本文也只是起到一个抛砖引玉的作用而已。

本文开始介绍了 goroutine 的简单用法,然后引出了通道的概念。

通道有三种:

  1. 无缓冲通道
  2. 缓冲通道
  3. 单向通道

最后介绍了 Go 中的锁机制,分别是 sync 包提供的 sync.Mutex(互斥锁) 和 sync.RWMutex(读写锁)。

goroutine 博大精深,后面的坑还是要慢慢踩的。


文章中的脑图和源码都上传到了 GitHub,有需要的同学可自行下载。

地址: github.com/yongxinz/go…

Go 专栏文章列表:

  1. Go 专栏|开发环境搭建以及开发工具 VS Code 配置
  2. Go 专栏|变量和常量的声明与赋值
  3. Go 专栏|基础数据类型:整数、浮点数、复数、布尔值和字符串
  4. Go 专栏|复合数据类型:数组和切片 slice
  5. Go 专栏|复合数据类型:字典 map 和 结构体 struct
  6. Go 专栏|流程控制,一网打尽
  7. Go 专栏|函数那些事
  8. Go 专栏|错误处理:defer,panic 和 recover
  9. Go 专栏|说说方法
  10. Go 专栏|接口 interface

本文转载自: 掘金

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

Java 中的 Lambda 表达式

发表于 2021-09-16

在 Java 8之前,一个实现了只有一个抽象方法的接口的匿名类看起来更像Lambda 表达式。下面的代码中,anonymousClass方法调用waitFor方法,参数是一个实现接口的Condition类,实现的功能为,当满足某些条件,Server 就会关闭。
下面的代码是典型的匿名类的使用。

1
2
3
4
5
6
7
8
java复制代码void anonymousClass() {
final Server server = new HttpServer();
waitFor(new Condition() {
@Override
public Boolean isSatisfied() {
return !server.isRunning();
}
}

下面的代码用 Lambda 表达式实现相同的功能:

1
2
3
4
java复制代码void closure() { 
Server server = new HttpServer();
waitFor(() -> !server.isRunning());
}

其实,上面的waitFor方法,更接近于下面的代码的描述:

1
2
3
4
5
6
7
java复制代码class WaitFor {
static void waitFor(Condition condition) throws
InterruptedException {
while (!condition.isSatisfied())
Thread.sleep(250);
}
}

一些理论上的区别
实际上,上面的两种方法的实现都是闭包,后者的实现就是Lambda 表示式。这就意味着两者都需要持有运行时的环境。在 Java 8 之前,这就需要把匿名类所需要的一切复制给它。在上面的例子中,就需要把 server 属性复制给匿名类。

因为是复制,变量必须声明为 final 类型,以保证在获取和使用时不会被改变。Java 使用了优雅的方式保证了变量不会被更新,所以我们不用显式地把变量加上 final 修饰。

Lambda 表达式则不需要拷贝变量到它的运行环境中,从而 Lambda 表达式被当做是一个真正的方法来对待,而不是一个类的实例。

Lambda 表达式不需要每次都要被实例化,对于 Java 来说,带来巨大的好处。不像实例化匿名类,对内存的影响可以降到最小。

总体来说,匿名方法和匿名类存在以下区别:

类必须实例化,而方法不必;
当一个类被新建时,需要给对象分配内存;
方法只需要分配一次内存,它被存储在堆的永久区内;
对象作用于它自己的数据,而方法不会;
静态类里的方法类似于匿名方法的功能。

一些具体的区别
匿名方法和匿名类有一些具体的区别,主要包括获取语义和覆盖变量。

获取语义
this 关键字是其中的一个语义上的区别。在匿名类中,this 指的是匿名类的实例,例如有了内部类为 Foo$InnerClass,当你引用内部类闭包的作用域时,像Foo.this.x的代码看起来就有些奇怪。
在 Lambda 表达式中,this 指的就是闭包作用域,事实上,Lambda 表达式就是一个作用域,这就意味着你不需要从超类那里继承任何名字,或是引入作用域的层级。你可以在作用域里直接访问属性,方法和局部变量。
例如,下面的代码中,Lambda 表达式可以直接访问firstName变量。

1
2
3
4
5
6
7
8
9
10
java复制代码public class Example {
private String firstName = "Tom";

public void example() {
Function<String, String> addSurname = surname -> {
// equivalent to this.firstName
return firstName + " " + surname; // or even,
};
}
}

这里的firstName就是this.firstName的简写。
但是在匿名类中,你必须显式地调用firstName,

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Example {
private String firstName = "Jerry";

public void anotherExample() {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return Example.this.firstName + " " + surname;
}
};
}
}

覆盖变量
在 Lambda 表达式中,

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

private String firstName = " Tim";

public void shadowingExample(String firstName) {
Function<String, String> addSurname = surname -> {
return this.firstName + " " + surname;
};
}
}

因为 this 在Lambda 表达式中,它指向的是一个封闭的作用域,所以this.firstName对应的值是“Tim”,而不是跟它同名的参数的值。如果去掉this,那么引用的则是方法的参数。

在上面的例子中,如果用匿名类来实现的话,firstName指的就是方法的参数;如果想访问最外面的firstName,则使用Example.this.firstName。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ShadowingExample {

private String firstName = "King";

public void anotherShadowingExample(String firstName) {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return firstName + " " + surname;
}
};
}
}

Lambda 表达式基本语法
Lambda 表达式基本上就是匿名函数块。它更像是内部类的实例。例如,我们想对一个数组进行排序,我们可以使用Arrays.sort方法,它的参数是Comparator接口,类似于下面的代码。

1
2
3
4
5
6
java复制代码Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});

参数里的Comparator实例就是一个抽象片段,本身没有别的。在这里只有在 sort 方法中被使用。
如果我们用新的语法来替换,用 Lambda 表达式的方式来实现:

1
java复制代码Arrays.sort(numbers, (first, second) -> first.compareTo(second));

这种方式更加简洁,实际上,Java 把它当做Comparator类的实例来对待。如果我们把 sort 的第二个参数从 Lambda 表达式中抽取出来,它的类型为Comparator。

1
2
java复制代码Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);

语法分解

你可以把单一的抽象方法转换成 Lambda 表达式。
举例,如果我们有一个接口名为Example,里面只有一个抽象方法apply,该抽象方法返回某一类型。

1
2
3
java复制代码interface Example {
R apply(A args);
}

我们可以匿名实现此接口里的方法:

1
2
3
4
5
6
java复制代码new Example() {
@Override
public R apply(A args) {
body
}
};

转换成 Lambda 表达式的话,我们去掉实例和声明,去掉方法的细节,只保留方法的参数列表和方法体。

1
2
3
java复制代码(args) {
body
}

我们引入新的符号(->)来表示 Lambda 表达式。

1
2
3
java复制代码(args) -> {
body
}

拿之前排序的方法为例,首先我们用匿名类来实现:

1
2
3
4
5
6
java复制代码Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});

下一步,去掉实例和方法签名:

1
2
3
java复制代码Arrays.sort(numbers, (Integer first, Integer second) {
return first.compareTo(second);
});

引用 Lambda 表达式:

1
2
3
java复制代码Arrays.sort(numbers, (Integer first, Integer second) -> {
return first.compareTo(second);
});

完成!但有些地方可以进一步优化。你可以去掉参数的类型,编译器已经足够聪明知道参数的类型。

1
2
3
java复制代码Arrays.sort(numbers, (first, second) -> {
return first.compareTo(second);
});

如果是一个简单的表达式的话,例如只有一行代码,你可以去掉方法体的大括号,如果有返回值的话,return 关键字也可以去掉。

1
java复制代码Arrays.sort(numbers, (first, second) -> first.compareTo(second));

如果Lambda 只有一个参数的话,参数外面的小括号也可以去掉。

1
java复制代码(x) -> x + 1

去掉小括号后,

1
java复制代码x -> x + 1

下一步我们做下总结,

1
2
3
4
5
java复制代码(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hello");
System.out::println;

第一个方式是完整的 Lambda 的声明和使用的方式,不过有些冗余,其实,参数的类型可以省略;
第二个方式是去掉参数类型的 Lambda 表达式;
第三个方式是,如果你的方法体只有一行语句,你可以直接省略掉大括号和 return 关键字;
第四个方式是没有参数的 Lambda 表达式;
第五个方式是Lambda 表达式的变种:是Lambda 表达式的一种简写,称为方法引用。例如:

1
java复制代码 System.out::println;

实际上它是下面Lambda 表达式的一种简写:

1
java复制代码(value -> System.out.prinltn(value)

深入 Lambda表达式

函数式接口

Java 把 Lambda表达式当作是一个接口类型的实例。它把这种形式被称之为函数式接口。一个函数式接口就是一个只有单一方法的接口,Java把这种方法称之为“函数式方法”,但更常用的名字为单一抽象方法(single abstract method” 或 SAM)。例如JDK中存在的接口例如Runnable和Callable。

@FunctionalInterface

Oracle 引入了一个新的注解为@FunctionalInterface, 用来标识一个接口为函数式接口。它基本上是用来传达这一用途,除此而外,编辑器还会做一些额外的检查。
比如,下面的接口:

1
2
3
java复制代码public interface FunctionalInterfaceExample {
// compiles ok
}

如果加上@FunctionalInterface注解,则会编译错误:

1
2
3
4
java复制代码@FunctionalInterface // <- error here
public interface FunctionalInterfaceExample {
// doesn't compile
}

编译器就会报错,错误的详细信息为“Invalid ‘@FunctionalInterface’ annotation; FunctionalInterfaceExample is not a functional interface”。意思是没有定义一个单一的抽象方法。
而如果我们定义了两个抽象方法会如何?

1
2
3
4
5
java复制代码@FunctionalInterface
public interface FunctionalInterfaceExample {
void apply();
void illegal(); // <- error here
}

编译器再次报错,提示为”multiple, non-overriding abstract methods were found”。所以,一旦使用了此注解,则在接口里只能定义一个抽象方法。

而现在有这样一种情况,如歌一个接口继承了另一个接口,会怎么办?我们创建一个新的函数式接口为A,定义了另一个接口B,B继承A,则B仍然是一个函数式接口,它继承了A的apply方法。

1
2
3
4
5
6
java复制代码@FunctionalInterface
interface A {
abstract void apply();
}

interface B extends A {

如果你想看起来更加清晰,可以复写父类的方法:

1
2
3
4
5
6
7
8
9
java复制代码@FunctionalInterface
interface A {
abstract void apply();
}

interface B extends A {
@Override
abstract void apply();
}

我们可以用下面的代码来测试一下上面的两个接口是否为函数式接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@FunctionalInterface
public interface A {
void apply();
}

public interface B extends A {
@Override
void apply();
}

public static void main(String... args) {
A a = () -> System.out.println("A");
B b = () -> System.out.println("B");
a.apply(); // 打印:A
b.apply(); // 打印:B
}

如果B接口继承了A接口,那么在B接口中就不能定义新的方法了,否则编译器会报错。

除了这些,在Java 8 中接口有了一些新的改进:

可以添加默认方法;
可以包含静态接口方法;
在java.util.function包中增加了一些新的接口,例如,Function 和 Predicate。

方法引用

简单来说,方法引用就是 Lambda 表达式的一种简写。当你创建一个 Lambda 表达式时,你创建了一个匿名方法并提供方法体,但你使用方法引用时,你只需要提供已经存在的方法的名字,它本身已经包含方法体。
它的基本语法如下;

1
java复制代码Class::method

或一个更加简洁明了的例子:

1
java复制代码String::valueOf

“::”符号前面表示的是目标引用,后面表示方法的名字。所以,在上面的例子,String 类作为目标类,用来寻找它的方法valueOf,我们指的就是 String 类上的静态方法。

1
java复制代码public static String valueOf(Object obj) { ... }

“::”称之为定界符,当我们使用它的时候,只是用来引用要使用的方法,而不是调用方法,所以不能在方法后面加()。

1
java复制代码String::valueOf(); // error

你不能直接调用方法引用,只是用来替代 Lambda 表达式,所以,哪里使用 Lambda 表达式了,哪里就可以使用方法引用了。
所以,下面的代码并不能运行:

1
2
3
java复制代码public static void main(String... args) {
String::valueOf;
}

这是因为该方法引用不能转化为Lambda 表达式,因为编译器没有上下文来推断要创建哪种类型的Lambda。
我们知道这个引用其实是等同于下面的代码:

1
java复制代码(x) -> String.valueOf(x)

但编译器还不知道。虽然它可以知道一些事情。它知道,作为一个Lambda,返回值应该是字符串类型,因为valueOf方法的返回值为字符串类型。但它不知道作为论据需要提供什么信息。我们需要给它一点帮助,给它更多的上下文信息。
下面我们创建一个函数式接口Conversion,

1
2
3
4
java复制代码@FunctionalInterface
interface Conversion {
String convert(Integer number);
}

接下来我们需要创建一个场景去使用这个接口作为一个 Lambda,我们定义了下面的方法:

1
2
3
java复制代码public static String convert(Integer number, Conversion function) {
return function.convert(number);
}

其实,我们已经给编译器提供了足够多的信息,可以把一个方法引用转换成一个等同的 Lambda。当我们调用convert方法时,我们可以把如下代码传递给 Lambda。

1
java复制代码convert(100, (number) -> String.valueOf(number));

我们可以用把上面的 Lambda 替换为方法引用,

1
java复制代码convert(100, String::valueOf);

另一种方式是我们告诉编译器,把引用分配给一个类型:

1
java复制代码Conversion b = (number) -> String.valueOf(number);

用方法引用来表示:

1
java复制代码Conversion b = String::valueOf

方法引用的种类

在 Java 中,有四种方法引用的类型:

构造方法引用;
静态方法引用:
两种实例方法引用。
最后两个有点混乱。第一种是特定对象的方法引用,第二个是任意对象的方法引用,而是特定类型的方法引用。区别在于你想如何使用该方法,如果你事先并不知道有没有实例。

构造方法引用

构造方法的基本引用如下:

1
java复制代码String::new

它会创建一个 Lambda 表达式,然后调用String 无参的构造方法。
它实际上等同于:

1
java复制代码() -> new String()

需要注意的是构造方法引用没有括号,它只是引用,并不是调用,上面的例子只是引用了 String类的构造方法,并没有真正去实例化一个字符串对象。
接下来我们看一个实际应用构造方法引用的例子。
看先的例子,循环十遍为 list 增加对象。

1
2
3
4
5
6
java复制代码public void usage() {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Object());
}
}

如果我们想复用实例化的功能,我们可以抽取出一个新的方法initialise用factory创建对象。

1
2
3
4
5
6
7
8
9
10
java复制代码public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, ...);
}

private void initialise(List<Object> list, Factory<Object> factory){
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}

Factory是一个函数式接口,包含一个create方法,此方法返回 Object 对象,我们可以用 Lambda 的方式向 list 中添加对象。

1
2
3
4
java复制代码public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, () -> new Object());
}

或者我们用构造方法引用的方式来替换:

1
2
3
4
java复制代码public void usage() {
List<Object> list = new ArrayList<>();
initialise(list, Object::new);
}

上面的方法其实还有待改进,上面只是创建 Object 类型的对象,我们可以增加泛型,实现可以创建更多类型的方法。

1
2
3
4
5
6
7
8
9
10
java复制代码public void usage() {
List<String> list = new ArrayList<>();
initialise(list, String::new);
}

private <T> void initialise(List<T> list, Factory<T> factory) {
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}

到现在为知,我们演示的都是无参的构造方法的引用,如果是带有参数的构造方法的引用该如何处理呢?
当有多个构造函数时,使用相同的语法,但编译器计算出哪个构造函数是最佳匹配。它基于目标类型和推断功能接口,它可以用来创建该类型。
例如,我们有个 Person 类,它有一个多个参数的构造方法。

1
2
3
4
5
java复制代码class Person {
public Person(String forename, String surname, LocalDate
birthday, Sex gender, String emailAddress, int age) {
// ...
}

回到上面的例子,我们可以如下使用:

1
2
java复制代码initialise(people, () -> new Person(forename, surname, birthday,
gender, email, age));

但是如果想使用这个构造方法引用,则需要 Lambda 表达式提供如下参数:

1
2
java复制代码initialise(people, () -> new Person(forename, surname, birthday,
gender, email, age));

特定对象的方法引用
下面是特定对象的方法引用的例子:

1
java复制代码x::toString

x就是我们想要得到的对象。它等同于下面的Lambda 表达式。

1
java复制代码() -> x.toString()

这种方法引用可以为我们提供便利的方式在不同的函数式接口类型中进行切换。看例子:

1
java复制代码Callable<String> c = () -> "Hello";

Callable的方法为call,当被调用时返回“Hello”。
如果我们有另外一个函数式接口Factory,我们可以使用方法引用的方式来转变Callable这个函数式接口。

1
java复制代码Factory<String> f = c::call;

我们可以重新创建一个 Lambda表达式,但是这个技巧是重用预定义的Lambda的一个有用的方式。 将它们分配给变量并重用它们以避免重复。
我们有下面一个例子:

1
2
3
4
java复制代码public void example() {
String x = "hello";
function(x::toString);
}

这个例子中方法引用使用了闭包。他创建了一个 Lambda用来调用x对象上的toString方法。
上面function方法的签名和实现如下所示:

1
2
3
java复制代码public static String function(Supplier<String> supplier) {
return supplier.get();
}

函数式接口Supplier的定义如下:

1
2
3
4
java复制代码@FunctionalInterface
public interface Supplier<T> {
T get();
}

当使用此方法时,它通过get方法返回一个字符串,而且这是唯一的在我们的结构中获取字符串的方式。它等同于:

1
2
3
4
java复制代码public void example() {
String x = "";
function(() -> x.toString());
}

需要注意的是,这里的 Lambda 表达式没有参数。这表明x变量在Lambda的局部作用域里是不可用的,如果可用必须要放在它的作用域之外。我们必须要掩盖变量x。
如果用匿名类来实现的话,应该是下面的样子,这些需要主意,x变量是如何传递的。

1
2
3
4
5
6
7
8
9
java复制代码public void example() {
String x = "";
function(new Supplier<String>() {
@Override
public String get() {
return x.toString(); // <- closes over 'x'
}
});
}

任意对象的实例方法引用(实例随后提供)
最后一种类型的实例方法引用的格式是这样的:

1
java复制代码Object::toString

尽管在“::”左边指向的是一个类(有点类似于静态方法引用),实际上它是指向一个对象,toString方法是Object类上的实例方法,不是静态方法。您可能不使用常规实例方法语法的原因是,还没有引用的实例。
在以前,当我们调用x::toString时,我们是知道x的类型,但是有些情况我们是不知道的,但你仍然可以传递一个方法引用,但是在后面使用此语法时需要提供对应的类型。
例如,下面的表达式等同于x没有限制的类型。

1
java复制代码(x) -> x.toString()

有两种不同的实例方法的引用基本是学术上的。有时候,你需要传递一些东西,其他时候,Lambda 的用法会为你提供。
这个例子类似于一个常规的方法引用;它这次调用String 对象的toString方法,该字符串提供给使用 Lambda 的函数,而不是从外部作用域传递的函数。

1
2
3
java复制代码public void lambdaExample() {
function("value", String::toString);
}

这个String看起来像是引用一个类,其实是一个实例。是不是有些迷惑,为了能清晰一些,我们需要看一个使用 Lambda 表达式的方法,如下:

1
2
3
java复制代码public static String function(String value, Function<String, String> function) {
return function.apply(value);
}

所以,这个 String 实例直接传递给了方法,它看起来像一个完全合格的Lambda。

1
2
3
java复制代码public void lambdaExample() {
function("value", x -> x.toString());
}

上面的代码可以简写成String::toString, 它是在说在运行时给我提供对象实例。
如果你想用匿名类展开加以理解,它是这个样子的。参数x是可用的并没有被遮蔽,所以它更像是Lambda 表达式而不是闭包。

1
2
3
4
5
6
7
8
9
10
java复制代码public void lambdaExample() {
function("value", new Function<String, String>() {
@Override
// takes the argument as a parameter, doesn't need to close
over it
public String apply(String x) {
return x.toString();
}
});
}

方法引用的总结

Oracle描述了四种类型的方法引用,如下所示。

种类 举例
静态方法引用 ContainingClass::staticMethodName
特定对象的实例方法引用 ContainingObject::instanceMethodName
特定类型的任意对象的实例方法引用 ContainingType::methodName
构造方法引用 ClassName::new

下面是方法引用的语法和具体的例子。

种类 语法 举例
静态方法引用 Class::staticMethodName String::valueOf
特定对象的实例方法引用 object::instanceMethodName x::toString
特定类型的任意对象的实例方法引用 Class::instanceMethodName String::toString
构造方法引用 ClassName::new String::new

最后,上面的方法引用等同于下面对应的 Lambda 表达式。

种类 语法 Lambda
静态方法引用 Class::staticMethodName (s) -> String.valueOf(s)
特定对象的实例方法引用 object::instanceMethodName () -> “hello”.toString()
特定类型的任意对象的实例方法引用 Class::instanceMethodName (s) -> s.toString()
构造方法引用 ClassName::new () -> new String()
本文由樊兔教育图二UR整理发布,樊兔教育是一个泛互联网职业教育平台,官网地址:ftuedu.com/

本文转载自: 掘金

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

Nodejs ORM框架Sequelize快速入门

发表于 2021-09-16

引言

nodejs 开发框架有很多种,比如Express,Koa, Egg,它们之间又有什么关系,关联呢?

可以这么说,Express是爷爷,那Koa就是儿子,而Egg就是孙子,当然这里并不是说它们的能力,而是说它们的来历。

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法。

egg.js是阿里旗下的一个基于nodejs和koa2的企业级应用框架,基于es6,es7 和nodejs,是『约定优先于配置』的一个 Node.js web 框架
Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,团队内部采用这种方式可以减少开发人员的学习成本,开发人员不再是『钉子』,可以流动起来。没有约定的团队,沟通成本是非常高的,比如有人会按目录分栈而其他人按目录分功能,开发者认知不一致很容易犯错。但约定不等于扩展性差,相反 Egg 有很高的扩展性,可以按照团队的约定定制框架。使用 Loader 可以让框架根据不同环境定义默认配置,还可以覆盖 Egg 的默认约定。

到这里大家应该都明白它们之间的关系了,但是它们之间又有什么区别呢?

一句话总结: koa,express,都是偏底层的无态度的Web框架;egg.js是在koa的基础上做了一层很好的面向大型企业级应用的框架封装, 中小型项目推荐egg.js,上手快,概念易懂。

而这里,我们不需要关心那么多,直接用最原生的Express框架+Sequelize链接Mysql数据库。

安装环境

本地数据库
小白推荐大家安装,phpstudy,里面集成了Apache,Nginx,MySql,Ftp等

本地数据库管理工具,大家可以自行下载,有的可以忽略
链接:pan.baidu.com/s/15VSP0lgU…
提取码:4hhz

支持链接数据库种类非常之多,免费、免费、免费。

(1)安装完成后,开始连接数据库

(2)双击打开连接后,右击连接名称,新建数据库

(3)库建好后,可以点击上面的新建表

(4)新建表时,点击添加字段,每个表都需要添加一个主键,可以默认勾选下面的自增,这样没插入一条数据,就会自动增长,不需要人工干预

(4)时间自动,create_time是每次插入都会记录插入的时间,update_time是每次更新数据时都会记录更新时间。记住设置的区别。

ok,有了第一个表,开始撸码时间

Express搭建一个web服务

1、本地工程下面新建一个package.json

1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"name": "node-orm",
"version": "1.0.0",
"description": "",
"author": "javan(www.javanx.cn)",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon index.js --watch ./",
"start": "cross-env NODE_ENV=production node index.js"
}
}

2、安装插件

1
bash复制代码npm install express sequelize mysql2 cross-env --save
1
bash复制代码npm install nodemon --save-dev

3、根目录新建index.js

1
2
3
4
5
6
7
8
javascript复制代码
const express = require('express')
const app = express()

// 定义端口8111
app.listen('8111', function () {
console.log("The server is running at *: 8111");
});

4、启动服务

1
bash复制代码npm run dev

打印"The server is running at *: 8111服务已经启动,打开浏览器,输入http://localhost:8111/

服务正常,接下来连接数据

Sequelize连接数据库

1、更目录新建config目录,存放项目的一些配置js,然后新建config/db.js

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
javascript复制代码/**
* mysql 数据库配置
*/

// 生成环境
let sqlConfig = {
host: 'localhost',
user: 'root',
password: 'root',
database: 'test',
port: 3306
}

// 本地环境
// process.env.NODE_ENV 取决于package.json里面的配置
if(process.env.NODE_ENV !== 'production'){
sqlConfig = {
host: 'localhost',
user: 'root',
password: 'root',
database: 'test',
port: 3306
};
}

module.exports = sqlConfig

2、更目录新建db目录,存放数据库model以及sequelize配置

此时项目目录

3、db/sequelize.js配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
javascript复制代码var Sequelize = require('sequelize')
// 导入数据库配置
var db = require('../config/db')

var sequelize = new Sequelize(db.database, db.user, db.password, {
logging: function(sql) {
// logger为log4js的Logger实例
if(process.env.NODE_ENV !== 'production'){
console.log(sql)
}
},
port:db.port,
host: db.host,
dialect: 'mysql',
dialectOptions: {
dateStrings: true,
typeCast: true
},
pool: {
max: 20,
min: 1,
acquire: 60000,
idle: 10000
},
timezone: '+08:00' //东八时区
});

exports.sequelize = sequelize;

exports.defineModel = function (name, attributes) {
var attrs = {};
for (let key in attributes) {
let value = attributes[key];
if (typeof value === 'object' && value['type']) {
value.allowNull = value.allowNull || false;
attrs[key] = value;
} else {
attrs[key] = {
type: value,
// allowNull: false
};
}
}
attrs.version = {
type: Sequelize.BIGINT,
// allowNull: false
};
attrs.createUser = {
type: Sequelize.STRING,
allowNull: false
};
attrs.updateUser = {
type: Sequelize.STRING,
allowNull: false
};
return sequelize.define(name, attrs, {
tableName: name,
timestamps: true,
paranoid: true,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
hooks: {
beforeBulkCreate: function(obj){
obj.version = 0 ;
},
beforeValidate: function(obj){
if(obj.isNewRecord){
console.log('first');
obj.version = 0 ;
}else{
console.log('not first');
obj.version = obj.version + 1 ;
}
}
}
});
};

4、db/model/user.js配置,这里的字段和数据库对应起来

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
javascript复制代码/**
* 用户
*/

var Sequelize = require('sequelize');
var {sequelize} = require('../sequelize.js');

var user = sequelize.define('user',{
id: {
type: Sequelize.BIGINT(20),
primaryKey: true,
allowNull: false,
unique: true,
autoIncrement: true
},
name: Sequelize.STRING(255), // 名字
age: Sequelize.BIGINT(11), // 年龄
sex: Sequelize.BIGINT(1), // 性别
create_time: Sequelize.DATE,
update_time: Sequelize.DATE
},{
timestamps: false, // 不要默认时间戳 数据库没有时间戳字段时,设置为false,否则报错 SequelizeDatabaseError: Unknown column 'createdAt' in 'field list'
freezeTableName: true
});

module.exports = user;

Sequelize数据类型与数据库类型对应关系

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
javascript复制代码Sequelize.STRING                      // VARCHAR(255)
Sequelize.STRING(1234) // VARCHAR(1234)
Sequelize.STRING.BINARY // VARCHAR BINARY
Sequelize.TEXT // TEXT
Sequelize.TEXT('tiny') // TINYTEXT

Sequelize.INTEGER // INTEGER
Sequelize.BIGINT // BIGINT
Sequelize.BIGINT(11) // BIGINT(11)

Sequelize.FLOAT // FLOAT
Sequelize.FLOAT(11) // FLOAT(11)
Sequelize.FLOAT(11, 12) // FLOAT(11,12)

Sequelize.REAL // REAL 仅限于PostgreSQL.
Sequelize.REAL(11) // REAL(11) 仅限于PostgreSQL.
Sequelize.REAL(11, 12) // REAL(11,12) 仅限于PostgreSQL.

Sequelize.DOUBLE // DOUBLE
Sequelize.DOUBLE(11) // DOUBLE(11)
Sequelize.DOUBLE(11, 12) // DOUBLE(11,12)

Sequelize.DECIMAL // DECIMAL
Sequelize.DECIMAL(10, 2) // DECIMAL(10,2)

Sequelize.DATE // DATETIME 针对 mysql / sqlite, TIMESTAMP WITH TIME ZONE 针对 postgres
Sequelize.DATE(6) // DATETIME(6) 针对 mysql 5.6.4+. 小数秒支持多达6位精度
Sequelize.DATEONLY // DATE 不带时间.
Sequelize.BOOLEAN // TINYINT(1)

Sequelize.ENUM('value 1', 'value 2') // 一个允许具有 “value 1” 和 “value 2” 的 ENUM
Sequelize.ARRAY(Sequelize.TEXT) // 定义一个数组。 仅限于 PostgreSQL。
Sequelize.ARRAY(Sequelize.ENUM) // 定义一个 ENUM 数组. 仅限于 PostgreSQL。

Sequelize.JSON // JSON 列. 仅限于 PostgreSQL, SQLite and MySQL.
Sequelize.UUID // PostgreSQL 和 SQLite 的 UUID 数据类型, CHAR(36) BINARY 针对于 MySQL (使用默认值: Sequelize.UUIDV1 或 Sequelize.UUIDV4 来让 sequelize 自动生成 ID)

Sequelize.GEOMETRY // 空间列. 仅限于 PostgreSQL (具有 PostGIS) 或 MySQL.
Sequelize.GEOMETRY('POINT') // 具有几何类型的空间列. 仅限于 PostgreSQL (具有 PostGIS) 或 MySQL.
Sequelize.GEOMETRY('POINT', 4326) // 具有几何类型和SRID的空间列. 仅限于 PostgreSQL (具有 PostGIS) 或 MySQL.

5、测试连接
index.js导入db/model/user.js测试一波

1
2
3
4
5
6
7
8
javascript复制代码...
const user = require('./db/model/user')

user.findAll().then(data=>{
console.log(data)
})

...

打印执行的sql,和结果,由于我们新建数据库没有插入数据,此时打印的是一个空数组[]

我们去数据库新建一条数据,大家看好我们只填入姓名、性别和年龄,id和时间都会自动插入

在查询一次,直接修改index.js,保存程序自动热更新

数据查询回来了,本节就到此结束了,希望大家都有所收获。

《前端玩转后台》

本文转载自: 掘金

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

分布式文件系统—FastDFS FastDFS的上传与下载

发表于 2021-09-16

FastDFS的上传与下载

安装

安装gcc(编译时需要)

1
r复制代码yum install -y gcc gcc-c++

安装libevent(运行时需求)

1
复制代码yum -y install libevent

安装 libfastcommon

libfastcommon是FastDFS官方提供的,libfastcommon包含了FastDFS运行所需要的一些基础库。

    1. 上传 libfastcommon-master.zip 到 /opt
1
2
3
makefile复制代码安装解压zip包的命令: yum install -y unzip
解压包: unzip libfastcommon.zip
进入目录: cd libfastcommon-master
    1. 编译
1
bash复制代码./make.sh
  • 如果:make.sh的权限不够,则需要授权(可执行的权利)
1
bash复制代码chmod 777 make.sh
    1. 安装
1
bash复制代码./make.sh install
  • libfastcommon安装好后会在/usr/lib64 目录下生成 libfastcommon.so 库文件
    1. 拷贝库文件
1
2
bash复制代码cd /usr/lib64
cp libfastcommon.so /usr/lib

安装Tracker

下载 FastDFS_v5.05.tar.gz,并上传到 /opt

1
2
3
4
bash复制代码tar -zxvf FastDFS_v5.05.tar.gz
cd FastDFS
./make.sh
./make.sh install

安装成功将安装目录下的conf下的文件拷贝到/etc/fdfs/下

1
bash复制代码cp /opt/FastDFS/conf/* /etc/fdfs/

配置

  • Tracker配置
1
bash复制代码vim /etc/fdfs/tracker.conf
1
2
3
4
5
6
ini复制代码#端口号
port=22122

#基础目录(Tracker运行时会向此目录存储storage的管理数据)(基础目录不存在的话,需要自行创建
mkdir /home/fastdfs)
base_path=/home/fastdfs
  • Storage配置
1
bash复制代码vim /etc/fdfs/storage.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码#配置组名
group_name=group1
#端口
port=23000
#向tracker心跳间隔(秒)
heart_beat_interval=30
#storage基础目录
#目录不存在,需要自行创建
base_path=/home/fastdfs
#store存放文件的位置(store_path)
#可以理解一个磁盘一个path,多个磁盘,多个store_path
#fdfs_storage目录不存在,需要自行创建
#mkdir /home/fastdfs/fdfs_storage
store_path0=/home/fastdfs/fdfs_storage
#如果有多个挂载磁盘则定义多个store_path,如下
#store_path1=..... (M01)
#store_path2=..... (M02)

#配置tracker服务器:IP
tracker_server=10.1.220.247:22122
#如果有多个则配置多个tracker
#tracker_server=10.1.220.x:22122

启动服务

  • 启动tracker
1
bash复制代码/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
  • 启动storage
1
bash复制代码/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
  • 查看所有运行的端口:
1
复制代码netstat -ntlp

搭建 Java工程

使用IDEA创建maven工程

pom.xml

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!--fastdfs的java客户端-->
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>

创建配置文件

在resources下创建config目录,在config目录下创建 fastdfs-client.properties,内容如下:

1
2
3
4
5
6
7
8
ini复制代码##fastdfs-client.properties
fastdfs.connect_timeout_in_seconds = 5
fastdfs.network_timeout_in_seconds = 30
fastdfs.charset = UTF-8
fastdfs.http_anti_steal_token = false
fastdfs.http_secret_key = FastDFS1234567890
fastdfs.http_tracker_http_port = 80
fastdfs.tracker_servers = 10.1.220.247:22122

文件上传

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
java复制代码package test;

import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;

/**
* @BelongsProject: lagou-fastdfs
* @Author: GuoAn.Sun
* @CreateTime: 2020-07-28 17:33
* @Description: 文件上传
*/
public class TestUpload {
public static void main(String[] args) {
try {
// 加载配置文件
ClientGlobal.initByProperties("config/fastdfs-client.properties");

// 创建tracker客户端
TrackerClient trackerClient = new TrackerClient();
// 通过tracker客户端获取tracker的连接服务并返回
TrackerServer trackerServer = trackerClient.getConnection();
// 声明storage服务
StorageServer storageServer = null;
// 定义storage客户端
StorageClient1 client = new StorageClient1(trackerServer,storageServer);
// 定义文件元信息
NameValuePair[] list = new NameValuePair[1];
list[0] = new NameValuePair("fileName","1.jpg");

String fileID = client.upload_file1("F:\\img\\1.jpg", "jpg", list);
System.out.println("fileID = " + fileID);
// group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg
/*
group1:一台服务器,就是一个组
M00: store_path0 ----> /home/fastdfs/fdfs_storage/data
00/00:两级数据目录
*/
trackerServer.close();
}catch(Exception e){
e.printStackTrace();
}
}
}

文件查询

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
java复制代码package test;

import org.csource.fastdfs.*;

/**
* @BelongsProject: lagou-fastdfs
* @Author: GuoAn.Sun
* @CreateTime: 2020-07-28 17:46
* @Description: 文件查询
*/
public class TestQuery {
public static void main(String[] args) throws Exception {

// 加载配置文件
ClientGlobal.initByProperties("config/fastdfs-client.properties");

// 创建tracker客户端
TrackerClient trackerClient = new TrackerClient();
// 通过tracker客户端获取tracker的连接服务并返回
TrackerServer trackerServer = trackerClient.getConnection();
// 声明storage服务
StorageServer storageServer = null;
// 定义storage客户端
StorageClient1 client = new StorageClient1(trackerServer,storageServer);

FileInfo fileInfo = client.query_file_info1("group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg");
if(fileInfo!=null)
System.out.println("fileInfo = " + fileInfo);
else
System.out.println("查无此文件!");
trackerServer.close();

}
}

文件下载

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复制代码package test;

import org.csource.fastdfs.*;

import java.io.File;
import java.io.FileOutputStream;

/**
* @BelongsProject: lagou-fastdfs
* @Author: GuoAn.Sun
* @CreateTime: 2020-07-28 17:49
* @Description: 文件下载
*/
public class TestDownload {
public static void main(String[] args) throws Exception{
// 加载配置文件
ClientGlobal.initByProperties("config/fastdfs-client.properties");
// 创建tracker客户端
TrackerClient trackerClient = new TrackerClient();
// 通过tracker客户端获取tracker的连接服务并返回
TrackerServer trackerServer = trackerClient.getConnection();
// 声明storage服务
StorageServer storageServer = null;
// 定义storage客户端
StorageClient1 client = new StorageClient1(trackerServer,storageServer);

byte[] bytes = client.download_file1("group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg");

// 通过io将字节数组,转换成一个文件
FileOutputStream fileOutputStream = new FileOutputStream(new
File("F:/xxxxxx.jpg"));
fileOutputStream.write(bytes);
fileOutputStream.close();
trackerServer.close();
System.out.println("下载完毕!");
}
}

项目实战

搭建图片服务器

Nginx模块安装 (Storage)

  1. 上传 fastdfs-nginx-module_v1.16.tar.gz 到 /opt
  2. 解压nginx模块
1
复制代码tar -zxvf fastdfs-nginx-module_v1.16.tar.gz
  1. 修改 config 文件,将文件中的 /usr/local/ 路径改为 /usr/
1
2
arduino复制代码cd /opt/fastdfs-nginx-module/src
vim config
  1. 将 fastdfs-nginx-module/src下的 mod_fastdfs.conf 拷贝至 /etc/fdfs 下
1
bash复制代码cp mod_fastdfs.conf /etc/fdfs/
  1. 修改 /etc/fdfs/mod_fastdfs.conf
1
bash复制代码vim /etc/fdfs/mod_fastdfs.conf
1
2
3
4
5
6
7
8
ini复制代码base_path=/home/fastdfs
tracker_server=10.1.220.247:22122
#(n个tracker配置n行)
#tracker_server=10.1.220.x:22122
#url中包含group名称
url_have_group_name=true
#指定文件存储路径(上面配置的store路径)
store_path0=/home/fastdfs/fdfs_storage
  1. 将 libfdfsclient.so 拷贝至 /usr/lib 下
1
bash复制代码cp /usr/lib64/libfdfsclient.so /usr/lib/
  1. 创建nginx/client目录
1
bash复制代码mkdir -p /var/temp/nginx/client

Nginx安装 (Tracker)

  1. 将 nginx-1.14.0.tar.gz上传到/opt(安装过nginx,此步省略)
  2. 解压:tar -zxvf nginx-1.14.0.tar.gz(安装过nginx,此步省略)
  3. 安装依赖库(安装过nginx,此步省略)
1
2
3
4
5
6
复制代码yum install pcre
yum install pcre-devel
yum install zlib
yum install zlib-devel
yum install openssl
yum install openssl-devel
  1. 进入nginx解压的目录下 cd /opt/nginx-1.14.0
  2. 安装
1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--add-module=/opt/fastdfs-nginx-module/src

注意:上边将临时文件目录指定为 /var/temp/nginx,需要在 /var 下创建 temp 及 nginx 目录:mkdir /var/temp/nginx

  1. 编译:make
  2. 安装:make install
  3. 拷贝配置文件
1
2
3
bash复制代码cd /opt/FastDFS/conf
cp http.conf mime.types /etc/fdfs/
是否覆盖:yes
  1. 修改nginx配置文件
1
2
bash复制代码cd /usr/local/nginx/conf/
vim nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码server {
listen 80;
server_name 10.1.220.247;

#charset koi8-r;

#access_log logs/host.access.log main;

location /group1/M00 {
root /home/fastdfs/fdfs_storage/data;
ngx_fastdfs_module;
}
  1. 关闭nginx,并启动nginx
1
2
bash复制代码pkill -9 nginx
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
  1. 访问nginx并查看图片
    http://10.1.220.247
    http://10.1.220.247/group1/M00/00/00/CgHc918f8l6AFYp0AAWICfQnHuk889.jpg

创建前端页面

1
2
3
4
5
6
7
8
9
jsp复制代码<%--上传文件,文件与文字相比较起来,属于内容较大,必须使用post方式提交--%>
<%--上传文件,和普通文件有区别,action接收参数也会区别对待,所以声明带文件提交的表单为“多部件表单”--%>
<form action="upload" method="post" enctype="multipart/form-data">

<input type="file" name="fname">
<br>
<button>提交</button>

</form>

搭建web服务

pom.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
xml复制代码<packaging>war</packaging>

<dependencies>
<!-- 因为有jsp页面,所以引用servlet依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
<version>2.5</version>
</dependency>
<!-- 页面提交过来的请求,使用springmvc来处理-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!-- java连接fastDFS的客户端工具-->
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<!-- 图片上传到FastDFS需要用的到IO工具-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 图片保存到web服务器需要用到的IO工具-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!--用来转换java对象和json字符串,注意,2.7以上版本必须搭配spring5.0以上-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

spring-mvc.xml

1
2
3
4
5
6
7
8
9
xml复制代码<!--扫描注解的包-->
<context:component-scan base-package="controller"/>
<!--扫描控制器中的注解:@Response-->
<mvc:annotation-driven/>
<!--上传文件的解析器(规定上传文件的大小限制)-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 上传文件最大限制:2GB-->
<property name="maxUploadSize" value="2048000000"/>
</bean>

文件实体类

1
2
3
4
5
java复制代码public class FileSystem implements Serializable {
private String fileId;
private String filePath;
private String fileName;
}

控制层

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复制代码package controller;

import entity.FileSystem;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import java.io.File;
import java.util.UUID;

/**
* @BelongsProject: lagou-imageServer
* @Author: GuoAn.Sun
* @CreateTime: 2020-07-29 14:25
* @Description: 处理上传文件的控制器
*/
@Controller
public class FileAction {

/**
* @param request 多部件表单的请求对象
* @return 上传文件对象的json对象
* @throws Exception
*
* 上传文件的流程:
* 1、先把文件保存到web服务器上
* 2、再从web服务器上将文件 上传 到 FastDFS上
*/

@RequestMapping("upload")
//MultipartHttpServletRequest:是httpservletRequest的强化版本,不仅可以装文本信息,还可以装图片文件信息
public @ResponseBody FileSystem upload(MultipartHttpServletRequest request) throws Exception {
FileSystem fileSystem = new FileSystem();

/* 1、把文件保存到web服务器*/

// 从页面请求中,获取上传的文件对象
MultipartFile file = request.getFile("fname");
// 从文件对象中获取 文件的原始名称
String oldFileName = file.getOriginalFilename();
// 通过字符串截取的方式,从文件原始名中获取文件的后缀 1.jpg
String hou = oldFileName.substring(oldFileName.lastIndexOf(".") + 1);
// 为了避免文件因为同名而覆盖,生成全新的文件名
String newFileName = UUID.randomUUID().toString() + "." + hou;
// 创建web服务器保存文件的目录(预先创建好D:/upload目录,否则系统找不到路径,会抛异常)
File toSaveFile = new File("D:/upload/" + newFileName);
// 将路径转换成文件
file.transferTo(toSaveFile);
// 获取服务器的绝对路径
String newFilePath = toSaveFile.getAbsolutePath();

/* 2、把文件从web服务器上传到FastDFS*/

ClientGlobal.initByProperties("config/fastdfs-client.properties");
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
StorageServer storageServer = null;
StorageClient1 client = new StorageClient1(trackerServer,storageServer);

NameValuePair[] list = new NameValuePair[1];
list[0] = new NameValuePair("fileName",oldFileName);
String fileId = client.upload_file1(newFilePath, hou, list);
trackerServer.close();

// 封装fileSystem数据对象
fileSystem.setFileId(fileId);
fileSystem.setFileName(oldFileName);
fileSystem.setFilePath(fileId); //已经上传到FastDFS上,通过fileId来访问图片,所以fileId即为文件路径

return fileSystem;
}
}

添加fastDFS的配置文件

在resources下创建config目录,在config目录下创建 fastdfs-client.properties

参考:创建配置文件

启动fastDFS服务,测试开始

1
2
3
4
5
6
7
csharp复制代码[root@localhost /]# /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
[root@localhost /]# /usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
[root@localhost /]# /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
[root@localhost /]# netstat -ntlp
[root@localhost /]# systemctl stop firewalld.service
[root@localhost /]# cd /home/fastdfs/fdfs_storage/data/
[root@localhost /]# ls

典型错误

  • 重启linux服务器,可能会到nginx启动失败:
1
2
sql复制代码[root@localhost logs]# /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
[root@localhost /]# nginx: [emerg] open() "/var/run/nginx/nginx.pid" failed (2:No such file or directory)
  • 导致本次错误的原因,是没有修改pid文件的路径,编辑nginx的配置文件:
1
bash复制代码vim /usr/local/nginx/conf/nginx.conf
1
bash复制代码pid /usr/local/nginx/logs/nginx.pid;
  • 再次启动nginx,搞定!

本文转载自: 掘金

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

密码学系列之 bcrypt加密算法详解 简介 bcrypt的

发表于 2021-09-16

简介

今天要给大家介绍的一种加密算法叫做bcrypt, bcrypt是由Niels Provos和David Mazières设计的密码哈希函数,他是基于Blowfish密码而来的,并于1999年在USENIX上提出。

除了加盐来抵御rainbow table 攻击之外,bcrypt的一个非常重要的特征就是自适应性,可以保证加密的速度在一个特定的范围内,即使计算机的运算能力非常高,可以通过增加迭代次数的方式,使得加密速度变慢,从而可以抵御暴力搜索攻击。

bcrypt函数是OpenBSD和其他系统包括一些Linux发行版(如SUSE Linux)的默认密码哈希算法。

bcrypt的工作原理

我们先回顾一下Blowfish的加密原理。 blowfish首先需要生成用于加密使用的K数组和S-box, blowfish在生成最终的K数组和S-box需要耗费一定的时间,每个新的密钥都需要进行大概4 KB文本的预处理,和其他分组密码算法相比,这个会很慢。但是一旦生成完毕,或者说密钥不变的情况下,blowfish还是很快速的一种分组加密方法。

那么慢有没有好处呢?

当然有,因为对于一个正常应用来说,是不会经常更换密钥的。所以预处理只会生成一次。在后面使用的时候就会很快了。

而对于恶意攻击者来说,每次尝试新的密钥都需要进行漫长的预处理,所以对攻击者来说要破解blowfish算法是非常不划算的。所以blowfish是可以抵御字典攻击的。

Provos和Mazières利用了这一点,并将其进一步发展。他们为Blowfish开发了一种新的密钥设置算法,将由此产生的密码称为 “Eksblowfish”(”expensive key schedule Blowfish”)。这是对Blowfish的改进算法,在bcrypt的初始密钥设置中,salt 和 password 都被用来设置子密钥。然后经过一轮轮的标准Blowfish算法,通过交替使用salt 和 password作为key,每一轮都依赖上一轮子密钥的状态。虽然从理论上来说,bcrypt算法的强度并不比blowfish更好,但是因为在bcrpyt中重置key的轮数是可以配置的,所以可以通过增加轮数来更好的抵御暴力攻击。

bcrypt算法实现

简单点说bcrypt算法就是对字符串OrpheanBeholderScryDoubt 进行64次blowfish加密得到的结果。有朋友会问了,bcrypt不是用来对密码进行加密的吗?怎么加密的是一个字符串?

别急,bcrpyt是将密码作为对该字符串加密的因子,同样也得到了加密的效果。我们看下bcrypt的基本算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码Function bcrypt
Input:
cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
salt: array of Bytes (16 bytes) random salt
password: array of Bytes (1..72 bytes) UTF-8 encoded password
Output:
hash: array of Bytes (24 bytes)

//Initialize Blowfish state with expensive key setup algorithm
//P: array of 18 subkeys (UInt32[18])
//S: Four substitution boxes (S-boxes), S0...S3. Each S-box is 1,024 bytes (UInt32[256])
P, S <- EksBlowfishSetup(cost, salt, password)

//Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times
ctext <- "OrpheanBeholderScryDoubt" //24 bytes ==> three 64-bit blocks
repeat (64)
ctext <- EncryptECB(P, S, ctext) //encrypt using standard Blowfish in ECB mode

//24-byte ctext is resulting password hash
return Concatenate(cost, salt, ctext)

上述函数bcrypt 有3个输入和1个输出。

在输入部分,cost 表示的是轮循的次数,这个我们可以自己指定,轮循次数多加密就慢。

salt 是加密用盐,用来混淆密码使用。

password 就是我们要加密的密码了。

最后的输出是加密后的结果hash。

有了3个输入,我们会调用EksBlowfishSetup函数去初始化18个subkeys和4个1K大小的S-boxes,从而达到最终的P和S。

然后使用P和S对”OrpheanBeholderScryDoubt” 进行64次blowfish运算,最终得到结果。

接下来看下 EksBlowfishSetup方法的算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码Function EksBlowfishSetup
Input:
password: array of Bytes (1..72 bytes) UTF-8 encoded password
salt: array of Bytes (16 bytes) random salt
cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
Output:
P: array of UInt32 array of 18 per-round subkeys
S1..S4: array of UInt32 array of four SBoxes; each SBox is 256 UInt32 (i.e. 1024 KB)

//Initialize P (Subkeys), and S (Substitution boxes) with the hex digits of pi
P, S <- InitialState()

//Permutate P and S based on the password and salt
P, S <- ExpandKey(P, S, salt, password)

//This is the "Expensive" part of the "Expensive Key Setup".
//Otherwise the key setup is identical to Blowfish.
repeat (2cost)
P, S <- ExpandKey(P, S, 0, password)
P, S <- ExpandKey(P, S, 0, salt)

return P, S

代码很简单,EksBlowfishSetup 接收上面我们的3个参数,返回最终的包含18个子key的P和4个1k大小的Sbox。

首先初始化,得到最初的P和S。

然后调用ExpandKey,传入salt和password,生成第一轮的P和S。

然后循环2的cost方次,轮流使用password和salt作为参数去生成P和S,最后返回。

最后看一下ExpandKey的实现:

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
less复制代码Function ExpandKey
Input:
password: array of Bytes (1..72 bytes) UTF-8 encoded password
salt: Byte[16] random salt
P: array of UInt32 Array of 18 subkeys
S1..S4: UInt32[1024] Four 1 KB SBoxes
Output:
P: array of UInt32 Array of 18 per-round subkeys
S1..S4: UInt32[1024] Four 1 KB SBoxes

//Mix password into the P subkeys array
for n <- 1 to 18 do
Pn <- Pn xor password[32(n-1)..32n-1] //treat the password as cyclic

//Treat the 128-bit salt as two 64-bit halves (the Blowfish block size).
saltHalf[0] <- salt[0..63] //Lower 64-bits of salt
saltHalf[1] <- salt[64..127] //Upper 64-bits of salt

//Initialize an 8-byte (64-bit) buffer with all zeros.
block <- 0

//Mix internal state into P-boxes
for n <- 1 to 9 do
//xor 64-bit block with a 64-bit salt half
block <- block xor saltHalf[(n-1) mod 2] //each iteration alternating between saltHalf[0], and saltHalf[1]

//encrypt block using current key schedule
block <- Encrypt(P, S, block)
P2n <- block[0..31] //lower 32-bits of block
P2n+1 <- block[32..63] //upper 32-bits block

//Mix encrypted state into the internal S-boxes of state
for i <- 1 to 4 do
for n <- 0 to 127 do
block <- Encrypt(state, block xor salt[64(n-1)..64n-1]) //as above
Si[2n] <- block[0..31] //lower 32-bits
Si[2n+1] <- block[32..63] //upper 32-bits
return state

ExpandKey主要用来生成P和S,算法的生成比较复杂,大家感兴趣的可以详细研究一下。

bcrypt hash的结构

我们可以使用bcrypt来加密密码,最终以bcrypt hash的形式保存到系统中,一个bcrypt hash的格式如下:

1
css复制代码$2b$[cost]$[22 character salt][31 character hash]

比如:

1
2
3
markdown复制代码$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash

上面例子中,$2a$ 表示的hash算法的唯一标志。这里表示的是bcrypt算法。

10 表示的是代价因子,这里是2的10次方,也就是1024轮。

N9qo8uLOickgx2ZMRZoMye 是16个字节(128bits)的salt经过base64编码得到的22长度的字符。

最后的IjZAgcfl7p92ldGxad68LJZdL17lhWy是24个字节(192bits)的hash,经过bash64的编码得到的31长度的字符。

hash的历史

这种hash格式是遵循的是OpenBSD密码文件中存储密码时使用的Modular Crypt Format格式。最开始的时候格式定义是下面的:

  • $1$: MD5-based crypt (‘md5crypt’)
  • $2$: Blowfish-based crypt (‘bcrypt’)
  • $sha1$: SHA-1-based crypt (‘sha1crypt’)
  • $5$: SHA-256-based crypt (‘sha256crypt’)
  • $6$: SHA-512-based crypt (‘sha512crypt’)

但是最初的规范没有定义如何处理非ASCII字符,也没有定义如何处理null终止符。修订后的规范规定,在hash字符串时:

  • String 必须是UTF-8编码
  • 必须包含null终止符

因为包含了这些改动,所以bcrypt的版本号被修改成了 $2a$。

但是在2011年6月,因为PHP对bcypt的实现 crypt_blowfish 中的一个bug,他们建议系统管理员更新他们现有的密码数据库,用$2x$代替$2a$,以表明这些哈希值是坏的(需要使用旧的算法)。他们还建议让crypt_blowfish对新算法生成的哈希值使用头$2y$。 当然这个改动只限于PHP的crypt_blowfish。

然后在2014年2月,在OpenBSD的bcrypt实现中也发现了一个bug,他们将字符串的长度存储在无符号char中(即8位Byte)。如果密码的长度超过255个字符,就会溢出来。

因为bcrypt是为OpenBSD创建的。所以当他们的库中出现了一个bug时, 他们决定将版本号升级到$2b$。

本文已收录于 www.flydean.com/37-bcrypt/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

idea 2021 很卡?一招教你起飞

发表于 2021-09-16

一、Java 17 and IDEA 2021.2.1

Java 17 已经发布,IntelliJ IDEA 2021.2.1 (最新版 2021.2.2 已经发布)支持编写、编译和运行 Java17 代码。升级到新版 IDEA 能更好的体验 Java 17 带来的快感。

1.1 模式匹配

在 IntelliJ IDEA 中,您可以通过使用 Alt+Enter 或通过单击灯泡图标并选择 Replace ‘s’ with pattern variable 以对 instanceof 使用模式匹配:

1.2 模式匹配无限可能

instanceof 模式匹配过于简单可能具有欺骗性。如果你认为这无关紧要,因为它只删除了一行代码,请再想一想。 仅删除一行代码就可以开启多种可能性,你可以在其中修改代码。 例如,除了对 instanceof 使用模式匹配之外,以下代码合并了 if 语句,引入了一个模式变量,并用 Collection.removeIf() 替换了 for 循环:

1.3 switch 表达式

下面的 gif 演示了 switch 表达式的一些用途,例如简洁的代码、改进的代码语义、没有多余的 break 语句、详尽的迭代等等:

1.4 模式匹配 + switch

老的 if-else 语句链使代码很难阅读和理解。 如上一节中的 if 语句链,它可以大致表示如下:

现在使用新的 switch 结构变得一目了然:

二、IDEA 2021.2.1 卡死了?

笔者在升级到 Idea 最新版第一感受就是卡、卡、卡,卡死了。当然笔者这电脑也是有年头了,陪伴着我度过了5~6个春夏秋冬。

mac 2014.jpg

在各种优化配置都没明显改善,最后想出了一招禁用和卸载一些平时用不到的插件。果然效果显著,直接起飞,甚至感觉比 2019 还要巴适。下面是笔者禁用的一些插件:
idea 插件.png
笔者觉得主要可能是 Code With Me 和 Space 引起的,在之前没有这 2 插件的时候也没这么明显的卡顿感,大家可以多试试。

三、最后

大家好,我是 如梦技术春哥(mica 微服务系列开源作者)欢迎一起讨论和分享 Idea 使用中的一些小技巧。

本文转载自: 掘金

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

SpringBoot整合Redis,封装RedisUtils

发表于 2021-09-15

Redis

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis 是目前业界使用最广泛的内存数据存储。相比 Memcached,Redis 支持更丰富的数据结构,例如 hashes, lists, sets 等,同时支持数据持久化。

除此之外,Redis 还提供一些类数据库的特性,比如事务,HA,主从库。可以说 Redis 兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。本文介绍 Redis 在 Spring Boot 中两个典型的应用场景。

Redis API 介绍

Spring Boot 提供的 Redis API 分为 高阶 API 和 低阶 API,高阶 API 是经过一定封装后的 API,而低阶 API 的使用和直接使用 Redis 的命令差不多。

高阶 API 提供了两个类可以供我们使用,分别是 RedisTemplate 和 StringRedisTemplate。StringRedisTemplate 继承自 RedisTemplate,StringRedisTemplate 的序列化方式与 RedisTemplate 的序列化的方式不同。具体在使用上的差别不是特别明显,但是数据在存储到 Redis 中的时候,因为序列化方式的不同,会有一定的差别。

低阶 API 其实也是通过 RedisTemplate 或 StringRedisTemplate 来进行获取。低阶 API 的方法和 Redis 的命令差不多。

Redis序列化

1、什么是序列化和反序列化

序列化:将对象写到IO流中

反序列化:从IO流中恢复对象

序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

2、只需要配置一下,就可以解决刚才出现的问题,但是这么多序列化的手段如何挑选呢,我比较好奇,所以我又稍微深挖了一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码/**
* Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* 序列化的几种方式:
* OxmSerializer
* ByteArrayRedisSerializer
* GenericJackson2JsonRedisSerializer
* GenericToStringSerializer
* StringRedisSerializer
* JdkSerializationRedisSerializer
* Jackson2JsonRedisSerializer
* @param redisConnectionFactory redis连接工厂
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
// 设置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return template;
}
}
名称 说明
ByteArrayRedisSerializer 数组序列化
GenericJackson2JsonRedisSerializer 使用Jackson进行序列化
GenericToStringSerializer 将对象泛化成字符串并序列化,和StringRedisSerializer差不多
Jackson2JsonRedisSerializer 使用Jackson序列化对象为json
JdkSerializationRedisSerializer jdk自带的序列化方式,需要实现Serializable接口
OxmSerializer 用xml格式存储
StringRedisSerializer 简单的字符串序列化
3、比较几种常见序列化手段的差异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
java复制代码@SpringBootTest
class CacheApplicationTests {

/**
* 测试几种序列化手段的效率
*/
@Test
void test() {
User user = new User();
user.setId(1);
user.setUsername("张三");
user.setPassword("123");
List<Object> list = new ArrayList<>();

for (int i = 0; i < 2000; i++) {
list.add(user);
}


// 使用GenericJackson2JsonRedisSerializer做序列化(效率太低,不推荐使用)
GenericJackson2JsonRedisSerializer g2 = new GenericJackson2JsonRedisSerializer();
long g2s = System.currentTimeMillis();
byte[] byteG2 = g2.serialize(list);
long g2l = System.currentTimeMillis();
System.out.println("GenericJackson2JsonRedisSerializer序列化消耗的时间:" + (g2l - g2s) + "ms,序列化之后的长度:" + byteG2.length);
g2.deserialize(byteG2);
System.out.println("GenericJackson2JsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - g2l) + "ms");

// 使用GenericToStringSerializer做序列化(和StringRedisSerializer差不多,效率没有StringRedisSerializer高,不推荐使用)
GenericToStringSerializer g = new GenericToStringSerializer(Object.class);

long gs = System.currentTimeMillis();
byte[] byteG = g.serialize(list.toString());
long gl = System.currentTimeMillis();
System.out.println("GenericToStringSerializer序列化消耗的时间:" + (gl - gs) + "ms,序列化之后的长度:" + byteG.length);
g.deserialize(byteG);
System.out.println("GenericToStringSerializer反序列化的时间:" + (System.currentTimeMillis() - gl) + "ms");


// 使用Jackson2JsonRedisSerializer做序列化(效率高,适合value值的序列化)
Jackson2JsonRedisSerializer j2 = new Jackson2JsonRedisSerializer(Object.class);
long j2s = System.currentTimeMillis();
byte[] byteJ2 = j2.serialize(list);
long j2l = System.currentTimeMillis();
System.out.println("Jackson2JsonRedisSerializer序列化消耗的时间:" + (j2l - j2s) + "ms,序列化之后的长度:" + byteJ2.length);
j2.deserialize(byteJ2);
System.out.println("Jackson2JsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - j2l) + "ms");


// 使用JdkSerializationRedisSerializer,实体类必须实现序列化接口(不推荐使用)
JdkSerializationRedisSerializer j = new JdkSerializationRedisSerializer();
long js = System.currentTimeMillis();
byte[] byteJ = j.serialize(list);
long jl = System.currentTimeMillis();
System.out.println("JdkSerializationRedisSerializer序列化消耗的时间:" + (jl - js) + "ms,序列化之后的长度:" + byteJ.length);
j.deserialize(byteJ);
System.out.println("JdkSerializationRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - jl) + "ms");


// 使用StringRedisSerializer做序列化(效率非常的高,但是比较占空间,只能对字符串序列化,适合key值的序列化)
StringRedisSerializer s = new StringRedisSerializer();

long ss = System.currentTimeMillis();
byte[] byteS = s.serialize(list.toString());
long sl = System.currentTimeMillis();
System.out.println("StringRedisSerializer序列化消耗的时间:" + (sl - ss) + "ms,序列化之后的长度:" + byteS.length);
s.deserialize(byteS);
System.out.println("StringRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - sl) + "ms");


// 使用FastJson做序列化,这个表现为什么这么差我也不是很明白
FastJsonRedisSerializer<Object> f = new FastJsonRedisSerializer<>(Object.class);

long fs = System.currentTimeMillis();
byte[] byteF = f.serialize(list);
long fl = System.currentTimeMillis();
System.out.println("FastJsonRedisSerializer序列化消耗的时间:" + (fl - fs) + "ms,序列化之后的长度:" + byteF.length);
f.deserialize(byteF);
System.out.println("FastJsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - fl) + "ms");


// 使用FastJson(效率高,序列化后占空间也很小,推荐使用)
GenericFastJsonRedisSerializer gf = new GenericFastJsonRedisSerializer();

long gfs = System.currentTimeMillis();
byte[] byteGf = gf.serialize(list);
long gfl = System.currentTimeMillis();
System.out.println("GenericFastJsonRedisSerializer序列化消耗的时间:" + (gfl - gfs) + "ms,序列化之后的长度:" + byteGf.length);
gf.deserialize(byteGf);
System.out.println("GenericFastJsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - gfl) + "ms");
}
}
4、总结
名称 序列化效率 反序列化效率 占用空间 是否推荐使用
StringRedisSerializer 很高 很高 高 推荐给key进行序列化
Jackson2JsonRedisSerializer 高 较高 偏高 推荐给value进行序列化
GenericFastJsonRedisSerializer 高 较低 较低 推荐给value进行序列化
5、附上Redis序列化配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* @param redisConnectionFactory redis连接工厂
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
// 配置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 使用Jackson2JsonRedisSerializer配置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// 使用FastJson配置value的序列化方式
// template.setValueSerializer(new GenericFastJsonRedisSerializer());
return template;
}
}

封装RedisUtils工具类

引入Redis依赖
1
2
3
4
5
xml复制代码<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
设置Redis的Template ——> RedisConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* @Description: Redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
redisTemplate.setConnectionFactory(factory);

// key序列化
redisTemplate.setKeySerializer(redisSerializer);
// value序列化
redisTemplate.setValueSerializer(redisSerializer);
// key hashmap序列化
redisTemplate.setHashKeySerializer(redisSerializer);
// value hashmap序列化
redisTemplate.setHashValueSerializer(redisSerializer);
return redisTemplate;
}
}
设置连接信息
1
2
3
4
5
6
7
8
9
properties复制代码#Redis
# 连接的那个数据库
spring.redis.database=0
# redis服务的ip地址
spring.redis.host=127.0.0.1
# redis端口号
spring.redis.port=6379
# redis的密码,没设置过密码,可为空
spring.redis.password=ycfxhsw
Redis工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
java复制代码/**
* Redis 工具类
*/
@Service
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;

private static double size = Math.pow(2, 32);

/**
* 写入缓存
* @param key
* @param offset
* @param isShow
* @return result
*/
public boolean setBit(String key, long offset, boolean isShow) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.setBit(key, offset, isShow);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存
* @param key
* @param offset
* @return result
*/
public boolean getBit(String key, long offset) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.getBit(key, offset);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 批量删除对应的value
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}

/**
* 删除对应的value
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}

/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}

/**
* 读取缓存
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}

/**
* 哈希 添加
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}

/**
* 哈希获取数据
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}

/**
* 列表添加
* @param k
* @param v
*/
public void lPush(String k, Object v) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(k, v);
}

/**
* 列表获取
* @param k
* @param l
* @param l1
* @return
*/
public List<Object> lRange(String k, long l, long l1) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(k, l, l1);
}

/**
* 集合添加
* @param key
* @param value
*/
public void add(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}

/**
* 集合获取
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}

/**
* 有序集合添加
* @param key
* @param value
* @param scoure
*/
public void zAdd(String key, Object value, double scoure) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key, value, scoure);
}

/**
* 有序集合获取
* @param key
* @param scoure
* @param scoure1
* @return
*/
public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
redisTemplate.opsForValue();
return zset.rangeByScore(key, scoure, scoure1);
}


/**
* 第一次加载的时候将数据加载到 redis 中
* @param name
*/
public void saveDataToRedis(String name) {
double index = Math.abs(name.hashCode() % size);
long indexLong = new Double(index).longValue();
boolean availableUsers = setBit("availableUsers", indexLong, true);
}

/**
* 第一次加载的时候将数据加载到redis中
* @param name
* @return
*/
public boolean getDataToRedis(String name) {
double index = Math.abs(name.hashCode() % size);
long indexLong = new Double(index).longValue();
return getBit("availableUsers", indexLong);
}

/**
* 有序集合获取排名
* @param key 集合名称
* @param value 值
*/
public Long zRank(String key, Object value) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.rank(key, value);
}


/**
* 有序集合获取排名
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start, long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key, start, end);
return ret;
}

/**
* 有序集合添加
* @param key
* @param value
*/
public Double zSetScore(String key, Object value) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.score(key, value);
}


/**
* 有序集合添加分数
* @param key
* @param value
* @param scoure
*/
public void incrementScore(String key, Object value, double scoure) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.incrementScore(key, value, scoure);
}


/**
* 有序集合获取排名
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start, long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key, start, end);
return ret;
}

/**
* 有序集合获取排名
* @param key
*/
public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end);
return ret;
}
}
测试
1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class RedisController {
@Autowired
private RedisUtils redisUtils;

@RequestMapping("setAndGet")
public String test(String k, String v) {
redisUtils.set(k, v);
return (String) redisUtils.get(k);
}
}

本文转载自: 掘金

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

springboot学习笔记----使用 MongoTemp

发表于 2021-09-15

简介:当前这个笔记 记录啦 springboot项目中 如何连接 mongo 并对其集合进行相关操作 主要使用 MongoTemplate 和 MongoRepository 操作的集合

1
2
js复制代码 MongoTemplate使用起来比较灵活 可以根据mongo相关操作数据库语言进行操作
MongoRepository 封装性良好 不但封装啦基础的增删改查 相关构造条件查询 也可以根据规则在接口中自定义相关方法 对应相应的mongo语句 使用起来比较简单

maven引入依赖

1
2
3
4
5
xml复制代码<!--版本号 我这里在父工程指定了 有需要百度  [maven spring-boot-starter-data-mongodb] 进入网页自己选择版本号-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

application.properties 配置连接信息

1
2
xml复制代码#配置信息的参数                    主机地址      端口号  要连数据库名称
spring.data.mongodb.uri=mongodb://192.168.6.100:27017/test

springboot启动原理 会加载124个配置类 【自动配置类检查相应的类是否存在或加载 属性配置类加载配置文件相应的属性】 自动生成相应的操作对象 所以只要添加相应依赖 和 配置连接属性 会自动创建 MongoTemplate 对象 所以直接注入即可

使用 MongoTemplate 操作集合

跟文档数据属性相对应的模型类 User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(collection = "user") //实体模型对应的集合名称
public class User {
@Id //【主键是自动生成的】
private String id;
private String username;
private Integer age;
private String password;
private String address;
}

controller层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
java复制代码
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

@RestController
@RequestMapping("/mongo1")
public class TestMongo1 {

//这个对象 是自动创建出来的 这个关系到springboot启动该原理在开头有解释
@Autowired
private MongoTemplate mongoTemplate;

@GetMapping("create")
public void createUser(){
User user = new User();
user.setAge(20);
user.setUsername("test");
user.setAddress("4932200@qq.com");
User user1 = mongoTemplate.insert(user);

//返回来 有自己生成的 id
System.out.println(user1);
}


//查询所有
@GetMapping("findAll")
public void findUser() {
List<User> userList = mongoTemplate.findAll(User.class);
System.out.println("userList = " + userList);
}

//根据id查询
@GetMapping("findById")
public User getById(){
User user = mongoTemplate.findById("614196627e7800008b0037a5", User.class);
return user;
}

//条件查询
@GetMapping("findUser")
public List<User> findUserList() {

//筛选条件
Query query = new Query(Criteria
.where("username").is("张三")
.and("age").is(23)
);

//执行查询
List<User> userList = mongoTemplate.find(query, User.class);
return userList;

}

//模糊查询
@GetMapping("findLike")
public List<User> findUsersLikeName() {
//正则表达式 模糊查询【】
String username = "张";
String regex = String.format("%s%s%s", "^.*", username, ".*$");
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
//名字符合正则表达式的条件
Query query = new Query(Criteria.where("username").regex(pattern));
//mongodb 文档相对应的模型类
List<User> userList = mongoTemplate.find(query, User.class);
return userList;
}

//分页查询
@GetMapping("findPage")
public void findUsersPage() {
String name = "est";
int pageNo = 1;
int pageSize = 10;

//构造条件
Query query = new Query();
//正则表达式匹配
String regex = String.format("%s%s%s", "^.*", name, ".*$");
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
//添加条件
query.addCriteria(Criteria.where("name").regex(pattern));
//开始统计记录数
int totalCount = (int) mongoTemplate.count(query, User.class);
//分页查询 返回结果
List<User> userList = mongoTemplate.find(query.skip((pageNo - 1) * pageSize).limit(pageSize), User.class);

Map<String, Object> pageMap = new HashMap<>();
pageMap.put("list", userList);
pageMap.put("totalCount",totalCount);
System.out.println(pageMap);
}
//修改
@GetMapping("update")
public void updateUser() {
//修改一般是查询出来整个文档【document mongodb里面的叫法】
User user = mongoTemplate.findById("5ffbfa2ac290f356edf9b5aa", User.class);
user.setUsername("test_1");
user.setAge(25);
user.setPassword("493220990@qq.com");

//修改好相应的数据 根据id进行修改 id是查询出来的
Query query = new Query(Criteria.where("_id").is(user.getId()));
Update update = new Update();
update.set("username", user.getUsername());
update.set("age", user.getAge());
update.set("password", user.getPassword());
//执行更新
UpdateResult result = mongoTemplate.upsert(query, update, User.class);
long count = result.getModifiedCount();
System.out.println(count);
}

//删除操作
@GetMapping("delete")
public void delete() {

//构造条件 根据id删除相应的文档
Query query =
new Query(Criteria.where("_id").is("5ffbfa2ac290f356edf9b5aa"));
DeleteResult result = mongoTemplate.remove(query, User.class);
long count = result.getDeletedCount();
System.out.println(count);
}



}

使用 MongoRepository 操作集合

Spring Data提供了对mongodb数据访问的支持,我们只需要继承MongoRepository类,按照Spring Data规范就可以了

image.png

image.png
SpringData 方法定义规范

1、不是随便声明的,而需要符合一定的规范

2、 查询方法以find | read | get开头

3、 涉及条件查询时,条件的属性用条件关键字连接

4、 要注意的是:条件属性首字母需要大写

5、 支持属性的级联查询,但若当前类有符合条件的属性则优先使用,而不使用级联属性,若需要使用级联属性,则属性之间使用_强制进行连接

实现 UserRepository 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends MongoRepository<User,String> {

//这两个方法 是跟据匹配规则自定义出来的方法
List<User> findByUsername(String username);

List<User> findByUsernameLike(String username);
}

controller层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/mongo2")
public class TestMongo2 {

@Autowired
private UserRepository userRepository;

//添加
@GetMapping("create")
public User createUser(){
User user = new User();
user.setUsername("王恒润");
user.setPassword("admin");
user.setAge(22);
user.setAddress("河北省邢台市 纳河区 郝桥镇 缴存 225");

User user1 = userRepository.insert(user);
return user1;
}

//查询所有
@GetMapping("findAll")
public List<User> findAll(){
List<User> userList = userRepository.findAll();
return userList;
}

//根据id查询
@GetMapping("findId")
public User getById(){
return userRepository.findById("614196627e7800008b0037a5").get();
}

//条件查询
@GetMapping("findQuery")
public List<User> findUserList(){
User user = new User();
user.setUsername("王恒润");
user.setAge(22);
Example<User> userExample = Example.of(user);
return userRepository.findAll(userExample);
}

//模糊查询
@GetMapping("findLike")
public void findUsersLikeName() {
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
User user = new User();
user.setUsername("三");
Example<User> userExample = Example.of(user, matcher);
List<User> userList = userRepository.findAll(userExample);
System.out.println(userList);
}

//分页查询
@GetMapping("findPage")
public void findUsersPage() {
Sort sort = Sort.by(Sort.Direction.DESC, "age");
//0为第一页
Pageable pageable = PageRequest.of(0, 10, sort);
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
User user = new User();
user.setName("三");
Example<User> userExample = Example.of(user, matcher);
//创建实例
Example<User> example = Example.of(user, matcher);
Page<User> pages = userRepository.findAll(example, pageable);
System.out.println(pages);
}

//修改
@GetMapping("update")
public void updateUser() {
//先根据id查询问文档信息
User user = userRepository.findById("60b8d57ed539ed5b124942de").get();
user.setUsername("张三_1");
user.setAge(25);
user.setPassword("883220990@qq.com");
//如果当前文档存在 自动执行更新
User save = userRepository.save(user);
System.out.println(save);
}

//删除
@GetMapping("delete")
public void delete() {
userRepository.deleteById("60b8d57ed539ed5b124942de");
}

//接口中自定义查询方法
@GetMapping("findByUsername")
public List<User> findByUsername(String username){
return userRepository.findByUsername(username);
}

2021-9-17 更新使用MongoRepository 实现分页查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Override

//参数介绍 第几页 每页多少条 跟数据库映射的实体类
public Page<Department> selectPage(Integer page, Integer limit, DepartmentQueryVo departmentQueryVo) {

//分页 根据 createTime 排序 排序规则:DESC
Sort sort = Sort.by(Sort.Direction.DESC,"createTime");

//第几页 每页多少条 排序规则
Pageable pageable = PageRequest.of(page-1,limit,sort);

//条件
//条件构造器
//当前创建 文档映射 实体类对象
Department department = new Department();

//想要往集合插入数据 需要将跟数据库映射的实体类数据 变成跟文档映射的实体类【两个实体类的源码放后边了】
BeanUtils.copyProperties(departmentQueryVo,department);
department.setIsDeleted(0);

//匹配器:指定当前条件模糊查询
ExampleMatcher matcher = ExampleMatcher.matching()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
.withIgnoreCase(true);

//整合匹配器 和 条件构造器
Example<Department> example = Example.of(department,matcher);

//从这里反推 需要什么参数 就创建什么 一直推到第一行 最后返回 page对象
Page<Department> departments = departmentRepository.findAll(example, pageable);
return departments;
}

两个实体类的源码

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
java复制代码//
@Data
@ApiModel(description = "Department")
public class DepartmentQueryVo {

@ApiModelProperty(value = "医院编号")
private String hoscode;

@ApiModelProperty(value = "科室编号")
private String depcode;

@ApiModelProperty(value = "科室名称")
private String depname;

@ApiModelProperty(value = "大科室编号")
private String bigcode;

@ApiModelProperty(value = "大科室名称")
private String bigname;

}



//跟集合交互的文档实体类 声明当前文档的集合 为Department【mongo中的集合】
@Data
@ApiModel(description = "Department")
@Document("Department")
public class Department extends BaseMongoEntity {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "医院编号")
@Indexed //普通索引
private String hoscode;

@ApiModelProperty(value = "科室编号")
@Indexed(unique = true) //唯一索引
private String depcode;

@ApiModelProperty(value = "科室名称")
private String depname;

@ApiModelProperty(value = "科室描述")
private String intro;

@ApiModelProperty(value = "大科室编号")
private String bigcode;

@ApiModelProperty(value = "大科室名称")
private String bigname;

}

本文转载自: 掘金

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

1…527528529…956

开发者博客

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