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

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


  • 首页

  • 归档

  • 搜索

Redis存储结构体信息是用hash还是string

发表于 2021-04-28

先简单回顾下,Redis的hash和string结构。

string

我们可以将用户信息结构体使用JSON序列化成字符串,然后将序列化后的字符串存入Redis进行缓存。

string数据结构

由于Redis的字符串是动态字符串,可以修改,内部结构类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。如上图所示,内部为当前字符串实际分配的空间capacity,一般高于实际字符串长度len。

假设我们要存储的结构是:

1
2
3
4
json复制代码{
"name": "xiaowang",
"age": "35"
}

如果此时将此用户信息的name改为“xiaoli”,再存到redis中,redis是不需要重新分配空间的。

而且我们在读取和存储数据的时候只需要对做Json序列化与反序列化,比较方便。

hash

Redis的hash相当于Java的HashMap,内部结构实现与HashMap一致,即数组+链表结构。只是rehash方式不一样,

hash数据结构

前面说到string适合存储用户信息,而hash结构也可以存储用户信息,不过是对每个字段单独存储,因此可以在查询时获取部分字段的信息,节省网络流量。不过Redis的hash的值只能是字符串,存储上面的那个例子还好,如果存储的用户信息变为:

1
2
3
4
5
6
7
8
json复制代码{
"name": "xiaowang",
"age": 25,
"clothes": {
"shirt": "gray",
"pants": "read"
}
}

那么该如何存储”clothes”属性又变成了该用string还是hash的问题。

既然两种数据结构都可以存储结构体信息。到底哪种更加合适呢?StackOverflow上也有人问了相同的问题,我们先来看看网友的讨论。

用hash还是string

以下信息出自StackOverflow Redis strings vs Redis hashes to represent JSON: efficiency?

该用户也是同样的疑问, 因为值的长度是不确定的,所以不知道采用string还是hash存储更有效率。

这里主要给大家总结下最高赞的两个答案。

适合用string存储的情况

  • 每次需要访问大量的字段
  • 某些键的值存储差异,不能存储为字符串的时候

适合用hash存储的情况

  • 在大多数情况中只需要访问少量字段
  • 自己始终知道哪些字段可用,防止使用mget时获取不到想要的数据

另外强烈建议参考官方的内存优化的文章:redis.io/topics/memo…,我这里也给出中文网站的文章地址:www.redis.cn/topics/memo…。

在官方文档中,作者强烈建议使用hash存储数据:

尽可能使用散列表(hashes)

小散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.

参考链接

  1. StackOverflow:stackoverflow.com/questions/1…
  2. 官方文档:redis.io/topics/memo…

本文转载自: 掘金

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

使用gin和go-micro实现微服务注册与发现

发表于 2021-04-28

使用Gin + go-micro + consul 实现一个简单的微服务项目,实现服务注册与发现功能

一、引入依赖:

  1. gin框架: go get -u github.com/gin-gonic/gin
  2. go-micro: go get -u -v github.com/micro/micro

go get -u -v github.com/micro/go-micro
3. consul:windows版本:www.consul.io/downloads.h… 下载后解压并添加系统变量

二、编写user微服务:

1. 项目目录:

捕获.PNG

2. router.go:

1
2
3
4
5
6
7
8
9
10
11
package复制代码
import "github.com/gin-gonic/gin"

func InitRouters() *gin.Engine {
ginRouter := gin.Default()
ginRouter.POST("/users/", func(context *gin.Context) {
context.String(200, "get userInfo")
})

return ginRouter
}

3. main.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
package复制代码
import (
"gin_micro/userserver/routers"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/web"
"github.com/micro/go-plugins/registry/consul"
)

var consulReg registry.Registry

func init() {
consulReg = consul.NewRegistry(
registry.Addrs("127.0.0.1:8500"),
)
}

func main() {
ginRouter := routers.InitRouters()

microService := web.NewService(
web.Name("userServer"),
//web.RegisterTTL(30 * time.Second),//设置注册服务的过期时间
//web.RegisterInterval(20 * time.Second),//设置间隔多久再次注册服务
web.Address(":18001"),
web.Handler(ginRouter),
web.Registry(consulReg))

microService.Run()
}

三、编写order微服务:

1. 项目目录:

image.png

2. router.go:

1
2
3
4
5
6
7
8
9
10
11
package复制代码
import "github.com/gin-gonic/gin"

func InitRouters() *gin.Engine {
ginRouter := gin.Default()
ginRouter.POST("/orders/", func(ctx *gin.Context) {
ctx.String(200, "get orderInfo")
})

return ginRouter
}

3. main.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
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
package复制代码
import (
"bytes"
"fmt"
"gin_micro/orderserver/routers"
"github.com/micro/go-micro/client/selector"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/web"
"github.com/micro/go-plugins/registry/consul"
"net/http"
"time"
)

var consulReg registry.Registry

func init() {
consulReg = consul.NewRegistry(
registry.Addrs("127.0.0.1:8500"))
}

func main() {

//初始化路由
ginRouter := routers.InitRouters()

//注册服务
microService := web.NewService(
web.Name("orderServer"),
//web.RegisterTTL(time.Second*30),//设置注册服务的过期时间
//web.RegisterInterval(time.Second*20),//设置间隔多久再次注册服务
web.Address(":18002"),
web.Handler(ginRouter),
web.Registry(consulReg),
)

//服务发现:获取服务地址
hostAddress := GetServiceAddr("userServer")
if len(hostAddress) <= 0 {
fmt.Println("hostAddress is null")
} else {
url := "http://" + hostAddress + "/users"
response, _ := http.Post(url, "application/json;charset=utf-8", bytes.NewBuffer([]byte("")))

fmt.Printf("发现服务,response = %v\n", response)
}

microService.Run()
}

// 服务发现
func GetServiceAddr(serverName string) (address string) {
var retryTimes int
for {
servers, err := consulReg.GetService(serverName)
fmt.Println(servers)
if err != nil {
fmt.Println(err.Error())
}
var services []*registry.Service
for _, value := range servers {
fmt.Println(value.Name, ":", value.Version)
services = append(services, value)
}
// 获取其中一个服务的信息
next := selector.RoundRobin(services)
if node, err := next(); err == nil {
address = node.Address
}
if len(address) > 0 {
return
}

// 重试次数++
retryTimes++
time.Sleep(1 * time.Second)
// 重试5次 返回空
if retryTimes >= 5 {
return
}
}
}

四、启动项目

1. 启动consul:
consul agent -dev -node hhh

2. 服务注册

分别启动 userserver/main.go 和 orderserver/main.go

将 userServer 和 orderServer 发布到consul上

浏览器输入 localhost:8500\ui 进入consul界面,查看已发布的微服务:

image.png

3. 服务发现

orderserver/main.go 中调用 userServer 微服务,并打印返回信息到控制台

image.png

本文转载自: 掘金

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

一文读懂 mmap 原理

发表于 2021-04-28

在《一文看懂零拷贝技术》中我们介绍了 零拷贝技术 的原理,而且我们知道 mmap 也是零拷贝技术的一种实现。在本文中,我们主要介绍 mmap 的原理。

一、传统的读写文件

一般来说,修改一个文件的内容需要如下3个步骤:

  • 把文件内容读入到内存中。
  • 修改内存中的内容。
  • 把内存的数据写入到文件中。

过程如图 1 所示:

read-write.png

如果使用代码来实现上面的过程,代码如下:

1
2
3
c复制代码read(fd, buf, 1024);  // 读取文件的内容到buf
... // 修改buf的内容
write(fd, buf, 1024); // 把buf的内容写入到文件

从图 1 中可以看出,页缓存(page cache) 是读写文件时的中间层,内核使用 页缓存 与文件的数据块关联起来。所以应用程序读写文件时,实际操作的是 页缓存。

二、使用 mmap 读写文件

从传统读写文件的过程中,我们可以发现有个地方可以优化:如果可以直接在用户空间读写 页缓存,那么就可以免去将 页缓存 的数据复制到用户空间缓冲区的过程。

那么,有没有这样的技术能实现上面所说的方式呢?答案是肯定的,就是 mmap。

使用 mmap 系统调用可以将用户空间的虚拟内存地址与文件进行映射(绑定),对映射后的虚拟内存地址进行读写操作就如同对文件进行读写操作一样。原理如图 2 所示:

mmap.png

前面我们介绍过,读写文件都需要经过 页缓存,所以 mmap 映射的正是文件的 页缓存,而非磁盘中的文件本身。由于 mmap 映射的是文件的 页缓存,所以就涉及到同步的问题,即 页缓存 会在什么时候把数据同步到磁盘。

Linux 内核并不会主动把 mmap 映射的 页缓存 同步到磁盘,而是需要用户主动触发。同步 mmap 映射的内存到磁盘有 4 个时机:

  • 调用 msync 函数主动进行数据同步(主动)。
  • 调用 munmap 函数对文件进行解除映射关系时(主动)。
  • 进程退出时(被动)。
  • 系统关机时(被动)。

三、mmap的使用方式

下面我们介绍一下怎么使用 mmap,mmap 函数的原型如下:

1
c复制代码void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

下面介绍一下 mmap 函数的各个参数作用:

  • addr:指定映射的虚拟内存地址,可以设置为 NULL,让 Linux 内核自动选择合适的虚拟内存地址。
  • length:映射的长度。
  • prot:映射内存的保护模式,可选值如下:
+ `PROT_EXEC`:可以被执行。
2. `PROT_READ`:可以被读取。
3. `PROT_WRITE`:可以被写入。
4. `PROT_NONE`:不可访问。
  • flags:指定映射的类型,常用的可选值如下:
+ `MAP_FIXED`:使用指定的起始虚拟内存地址进行映射。
2. `MAP_SHARED`:与其它所有映射到这个文件的进程共享映射空间(可实现共享内存)。
3. `MAP_PRIVATE`:建立一个写时复制(Copy on Write)的私有映射空间。
4. `MAP_LOCKED`:锁定映射区的页面,从而防止页面被交换出内存。
5. ...
  • fd:进行映射的文件句柄。
  • offset:文件偏移量(从文件的何处开始映射)。

介绍完 mmap 函数的原型后,我们现在通过一个简单的例子介绍怎么使用 mmap:

1
2
c复制代码int fd = open(filepath, O_RDWR, 0644);                           // 打开文件
void *addr = mmap(NULL, 8192, PROT_WRITE, MAP_SHARED, fd, 4096); // 对文件进行映射

在上面例子中,我们先通过 open 函数以可读写的方式打开文件,然后通过 mmap 函数对文件进行映射,映射的方式如下:

  • addr 参数设置为 NULL,表示让操作系统自动选择合适的虚拟内存地址进行映射。
  • length 参数设置为 8192 表示映射的区域为 2 个内存页的大小(一个内存页的大小为 4 KB)。
  • prot 参数设置为 PROT_WRITE 表示映射的内存区为可读写。
  • flags 参数设置为 MAP_SHARED 表示共享映射区。
  • fd 参数设置打开的文件句柄。
  • offset 参数设置为 4096 表示从文件的 4096 处开始映射。

mmap 函数会返回映射后的内存地址,我们可以通过此内存地址对文件进行读写操作。我们通过图 3 展示上面例子在内核中的结构:

mmap-address.png

四、总结

本文主要介绍了 mmap 的原理和使用方式,通过本文我们可以知道,使用 mmap 对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。

由于内核不会主动同步 mmap 所映射的内存区中的数据,所以在某些特殊的场景下可能会出现数据丢失的情况(如断电)。为了避免数据丢失,在使用 mmap 的时候可以在适当时主动调用 msync 函数来同步映射内存区的数据。

本文转载自: 掘金

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

动图图解!GMP模型里为什么要有P?背后的原因让人暖心

发表于 2021-04-28

文章持续更新,可以微信搜一搜「golang小白成长记」第一时间阅读,回复【教程】获golang免费视频教程。本文已经收录在GitHub github.com/xiaobaiTech… , 有大厂面试完整考点和成长路线,欢迎Star。

GM模型是什么

GM图

在 Go 1.1版本之前,其实用的就是GM模型。

  • G,协程。通常在代码里用 go 关键字执行一个方法,那么就等于起了一个G。
  • M,内核线程,操作系统内核其实看不见G和P,只知道自己在执行一个线程。G和P都是在用户层上的实现。

除了G和M以外,还有一个全局协程队列,这个全局队列里放的是多个处于可运行状态的G。M如果想要获取G,就需要访问一个全局队列。同时,内核线程M是可以同时存在多个的,因此访问时还需要考虑并发安全问题。因此这个全局队列有一把全局的大锁,每次访问都需要去获取这把大锁。

并发量小的时候还好,当并发量大了,这把大锁,就成为了性能瓶颈。

GM模型

GMP模型是什么

GMP图

基于没有什么是加一个中间层不能解决的思路,golang在原有的GM模型的基础上加入了一个调度器P,可以简单理解为是在G和M中间加了个中间层。

于是就有了现在的GMP模型里。

  • P 的加入,还带来了一个本地协程队列,跟前面提到的全局队列类似,也是用于存放G,想要获取等待运行的G,会优先从本地队列里拿,访问本地队列无需加锁。而全局协程队列依然是存在的,但是功能被弱化,不到万不得已是不会去全局队列里拿G的。
  • GM模型里M想要运行G,直接去全局队列里拿就行了;GMP模型里,M想要运行G,就得先获取P,然后从 P 的本地队列获取 G。

GMP模型

  • 新建 G 时,新G会优先加入到 P 的本地队列;如果本地队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • P 的本地队列为空时,就从全局队列里去取。

GMP模型-获取全局协程队列

  • 如果全局队列为空时,M 会从其他 P 的本地队列偷(stealing)一半G放到自己 P 的本地队列。

GMP模型-stealing2

  • M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

GMP模型-循环执行

为什么P的逻辑不直接加在M上

主要还是因为M其实是内核线程,内核只知道自己在跑线程,而golang的运行时(包括调度,垃圾回收等)其实都是用户空间里的逻辑。操作系统内核哪里还知道,也不需要知道用户空间的golang应用原来还有那么多花花肠子。这一切逻辑交给应用层自己去做就好,毕竟改内核线程的逻辑也不合适啊。

如果文章对你有帮助,看下文章底部右下角,做点正能量的事情(点两下)支持一下。(疯狂暗示,拜托拜托,这对我真的很重要!)

我是小白,我们下期见。

参考资料

[1]《Golang 调度器 GMP 原理与调度全分析》 ——Aceld

[2]《GMP模型为什么要有P》 ——煎鱼

[3]《深度解密Go语言之Scheduler》 ——qcrao

文章推荐:

  • 给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题
  • 硬核!漫画图解HTTP知识点+面试题
  • 程序员防猝死指南
  • TCP粘包 数据包:我只是犯了每个数据包都会犯的错 |硬核图解
  • 硬核图解!30张图带你搞懂!路由器,集线器,交换机,网桥,光猫有啥区别?

别说了,关注公众号:【golang小白成长记】, 一起在知识的海洋里呛水吧

本文转载自: 掘金

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

如何在 Spring/Spring Boot 中优雅地做参数

发表于 2021-04-27

本文已经收录进 SpringBootGuide (SpringBoot2.0+从入门到实战!)

  • Github地址:github.com/CodingDocs/…
  • 码云地址:gitee.com/SnailClimb/…(Github无法访问或者访问速度比较慢的小伙伴可以看码云上的对应内容)

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

最普通的做法就像下面这样。我们通过 if/else 语句对请求的每一个参数一一校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RestController
@RequestMapping("/api/person")
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody PersonRequest personRequest) {
if (personRequest.getClassId() == null
|| personRequest.getName() == null
|| !Pattern.matches("(^Man$|^Woman$|^UGM$)", personRequest.getSex())) {

}
return ResponseEntity.ok().body(personRequest);
}
}

这样的代码,小伙伴们在日常开发中一定不少见,很多开源项目都是这样对请求入参做校验的。

但是,不太建议这样来写,这样的代码明显违背了 单一职责原则。大量的非业务代码混杂在业务代码中,非常难以维护,还会导致业务层代码冗杂!

实际上,我们是可以通过一些简单的手段对上面的代码进行改进的!这也是本文主要要介绍的内容!

废话不多说!下面我会结合自己在项目中的实际使用经验,通过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证(普通的 Java 程序同样适用)。

不了解的朋友一定要好好看一下,学完马上就可以实践到项目上去。

并且,本文示例项目使用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21)

示例项目源代码地址:github.com/CodingDocs/… 。

添加相关依赖

如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>

不过,相信大家都是使用的 Spring Boot 框架来做开发。

基于 Spring Boot 的话,就比较简单了,只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。另外,我们的示例项目中还使用到了 Lombok。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

但是!!! Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上!

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输入

验证请求体

验证请求体即使验证被 @RequestBody 注解标记的方法参数。

PersonController

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。

1
2
3
4
5
6
7
8
9
10
java复制代码@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}

PersonRequest

我们使用校验注解对请求的参数进行校验!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

}

正则表达式说明:

  • ^string : 匹配以 string 开头的字符串
  • string$ :匹配以 string 结尾的字符串
  • ^string$ :精确匹配 string 字符串
  • (^Man$|^Woman$|^UGM$) : 值只能在 Man,Woman,UGM 这三个值中选择

GlobalExceptionHandler

自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。如果对于下面的处理异常的代码不太理解的话,可以查看这篇文章 《SpringBoot 处理异常的几种常见姿势》。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}

通过测试验证

下面我通过 MockMvc 模拟请求 Controller 的方式来验证是否生效。当然了,你也可以通过 Postman 这种工具来验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;
/**
* 验证出现参数不合法的情况抛出异常并且可以正确被捕获
*/
@Test
public void should_check_person_value() throws Exception {
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
mockMvc.perform(post("/api/personRequest")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"));
}
}

使用 Postman 验证

验证请求参数

验证请求参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的方法参数。

PersonController

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}

@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}

ExceptionHandler

1
2
3
4
java复制代码  @ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}

通过测试验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Test
public void should_check_path_variable() throws Exception {
mockMvc.perform(get("/api/person/6")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByID.id: 超过 id 的范围了"));
}

@Test
public void should_check_request_param_value2() throws Exception {
mockMvc.perform(put("/api/person")
.param("name", "snailclimbsnailclimb")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByName.name: 超过 name 的范围了"));
}

使用 Postman 验证

验证 Service 中的方法

我们还可以验证任何 Spring Bean 的输入,而不仅仅是 Controller 级别的输入。通过使用@Validated和@Valid注释的组合即可实现这一需求!

一般情况下,我们在项目中也更倾向于使用这种方案。

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
java复制代码@Service
@Validated
public class PersonService {

public void validatePersonRequest(@Valid PersonRequest personRequest) {
// do something
}

}

通过测试验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {
@Autowired
private PersonService service;

@Test
public void should_throw_exception_when_person_request_is_not_valid() {
try {
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
service.validatePersonRequest(personRequest);
} catch (ConstraintViolationException e) {
// 输出异常信息
e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
}

输出结果如下:

1
2
复制代码name 不能为空
sex 值不在可选范围

Validator 编程方式手动进行参数验证

某些场景下可能会需要我们手动校验并获得校验结果。

我们通过 Validator 工厂类获得的 Validator 示例。另外,如果是在 Spring Bean 中的话,还可以通过 @Autowired 直接注入的方式。

1
2
java复制代码@Autowired
Validator validate

具体使用情况如下:

1
2
3
4
5
6
7
8
java复制代码ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator()
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
Set<ConstraintViolation<PersonRequest>> violations = validator.validate(personRequest);
// 输出异常信息
violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}

输出结果如下:

1
2
复制代码sex 值不在可选范围
name 不能为空

自定以 Validator(实用)

如果自带的校验注解无法满足你的需求的话,你还可以自定义实现注解。

案例一:校验特定字段的值是否在可选范围

比如我们现在多了这样一个需求:PersonRequest 类多了一个 Region 字段,Region 字段只能是China、China-Taiwan、China-HongKong这三个中的一个。

第一步,你需要创建一个注解 Region。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {

String message() default "Region 值不在可选范围内";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

第二步,你需要实现 ConstraintValidator接口,并重写isValid 方法。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class RegionValidator implements ConstraintValidator<Region, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
HashSet<Object> regions = new HashSet<>();
regions.add("China");
regions.add("China-Taiwan");
regions.add("China-HongKong");
return regions.contains(value);
}
}

现在你就可以使用这个注解:

1
2
java复制代码@Region
private String region;

通过测试验证

1
2
3
4
5
6
java复制代码PersonRequest personRequest = PersonRequest.builder()
.region("Shanghai").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范围内"));

使用 Postman 验证

案例二:校验电话号码

校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式。

PhoneNumber.java

1
2
3
4
5
6
7
8
9
java复制代码@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}

PhoneNumberValidator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) {
// can be null
return true;
}
// 大陆手机号码11位数,匹配格式:前三位固定格式+后8位任意数
// ^ 匹配输入字符串开始的位置
// \d 匹配一个或多个数字,其中 \ 要转义,所以是 \\d
// $ 匹配输入字符串结尾的位置
String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$";
return phoneField.matches(regExp);
}
}

搞定,我们现在就可以使用这个注解了。

1
2
3
java复制代码@PhoneNumber(message = "phoneNumber 格式不正确")
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;

通过测试验证

1
2
3
4
5
6
java复制代码PersonRequest personRequest = PersonRequest.builder()
.phoneNumber("1816313815").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("phoneNumber").value("phoneNumber 格式不正确"));

使用验证组

验证组我们基本是不会用到的,也不太建议在项目中使用,理解起来比较麻烦,写起来也比较麻烦。简单了解即可!

当我们对对象操作的不同方法有不同的验证规则的时候才会用到验证组。

我写一个简单的例子,你们就能看明白了!

1.先创建两个接口,代表不同的验证组

1
2
3
4
java复制代码public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}

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
java复制代码@Data
public class Person {
// 当验证组为 DeletePersonGroup 的时候 group 字段不能为空
@NotNull(groups = DeletePersonGroup.class)
// 当验证组为 AddPersonGroup 的时候 group 字段需要为空
@Null(groups = AddPersonGroup.class)
private String group;
}

@Service
@Validated
public class PersonService {

@Validated(AddPersonGroup.class)
public void validatePersonGroupForAdd(@Valid Person person) {
// do something
}

@Validated(DeletePersonGroup.class)
public void validatePersonGroupForDelete(@Valid Person person) {
// do something
}

}

通过测试验证:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码  @Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups() {
Person person = new Person();
person.setGroup("group1");
service.validatePersonGroupForAdd(person);
}

@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups2() {
Person person = new Person();
service.validatePersonGroupForDelete(person);
}

验证组使用下来的体验就是有点反模式的感觉,让代码的可维护性变差了!尽量不要使用!

常用校验注解总结

JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email、@Length、@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation。

JSR 提供的校验注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 提供的校验注解:

  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

拓展

经常有小伙伴问到:“@NotNull 和 @Column(nullable = false) 两者有什么区别?”

我这里简单回答一下:

  • @NotNull是 JSR 303 Bean 验证批注,它与数据库约束本身无关。
  • @Column(nullable = false) : 是 JPA 声明列为非空的方法。

总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。

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

原创不易,欢迎点赞分享。咱们下期再会!

👍推荐2021最新实战项目源码下载

本文转载自: 掘金

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

Java Web实现登录注册(超详细附代码)

发表于 2021-04-27

Java Web实现登录注册(超详细附代码)

1.前言

相信刚学Javaweb的小伙伴第一个接触的个人小项目都是从项目的登录注册开始的。
下面一个小项目中的登录注册将会带大家从零开始学习怎么设计登录注册流程.

2.登录注册设计流程

image.png

3.注册的数据流程

那么我们的前端数据是怎么传向后端的?
1.首先这里我们是用 表单传递 通过form提交

image.png

2.在这里,我们输入自己的姓名和密码,点击注册按钮。此时输入的这三个数据,我们可以看做成是一个表单的数据,这些数据会提交到服务器上:

3.此时,一个叫Tomcat的东西会处理这个请求,

4.得到请求之后,Tomcat会将这个请求交由Servlet来进行处理

5.Servlet调用Dao层写的各种实现方法,与数据库进行交互(curd调用仔)

那么下面就是注册操作的主要调用流程图

register.jspregistServletUserDB
第一步
在这里插入图片描述

第二步
在这里插入图片描述

第三步
在这里插入图片描述

4.登录的数据流程

登录操作的主要调用流程图

Login.jspLoginServletUserDB
在这里插入图片描述
第一步
在这里插入图片描述

第二步
在这里插入图片描述

第三步
在这里插入图片描述

第四步
在这里插入图片描述

5.部分代码的展示

5.1注册

register.jsp注册页面
通过表单实现跳转到servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width",initial->
<link rel="stylesheet" href="style.css">
<title>world message board of the future</title>
</head>

<body background="pictures/02.jpg">
<div class="form-wrapper">
<div class="header">
Register
</div>
<form action="RegistServlet" method = "post">
<!-- 浏览者单击发送按钮发送表单的时候,隐藏域的信息也被一起发送到服务器。 -->
<input type="hidden" name="action" value="regist">
<div class="input-wrapper">
<div class="border-wrapper">
<input type="text" name="username" placeholder="username" class="border-item">
</div>
<div class="border-wrapper">
<input type="text" name="password" placeholder="password" class="border-item">
</div>
<div class="border-wrapper">
<input type="password" name="password2" placeholder="Confirm password" class="border-item">
</div>
<div class="action">
<input type="submit" name="regist" class="btn" value="Regist" ><br>
</div>
</div>
</form>
<center>
<!-- 获取注册是否成功信息 -->
<p class="col">${message}</p>
</center>
</div>
</body>
</html>

RegisterServlet.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
java复制代码package Sevlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import javaBean.User;
import useBean.UserDB;

@WebServlet("/RegistServlet")
public class RegistServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
String message ="";

public RegistServlet() {
super();
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
//获取隐藏域的信息
String action = request.getParameter("action");
String url ="register.jsp";

if(action.equals("regist"))
url = registerUser(request,response);
getServletContext().getRequestDispatcher(url).forward(request, response);
}
//注册函数
private String registerUser(HttpServletRequest request,HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
String password2 = request.getParameter("password2");
String message = "";
//得到HttpSession类型的对象
HttpSession session = request.getSession();
//将数据存储于User对象
User user = new User();
user.setusername (username);
user.setpassword(password);
String url = "/login.jsp";
if(password.equals(password2))
{
if( !UserDB.UserExists(username) ) {
message = "Registration successful! and login in";
session.setAttribute("message", message);
//将注册用户信息写入数据库
UserDB.insert(user);
return url;
}
else
{
message = "The user name already exists";
url = "/register.jsp";
session.setAttribute("message", message);
return url;
}
}
else
{
message = "The password is inconsistent";
session.setAttribute("message", message);
url = "/register.jsp";
return url;
}
}
}

Dao层实现判断用户名是否存在和插入数据库的方法
UserDB.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
java复制代码    //注册时判断用户名是否存在
public static boolean UserExists(String username) {
//可以建立一个连接池保存一定数量的连接,当有对象需要数据库连接时,直接将这个连接返回给该对象,
ConnectionPool pool = ConnectionPool.getInstance();
Connection connection = pool.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;

String qr = "SELECT username FROM User "+ "WHERE username = ?";
try {
ps = connection.prepareStatement(qr);
ps.setString(1, username);
rs = ps.executeQuery();
return rs.next();
} catch (SQLException e) {
System.out.println(e);
return false;
} finally {
DBUtil.closeResultSet(rs);
DBUtil.closePreparedStatement(ps);
pool.freeConnection(connection);
}

}
//将注册用户信息保存至数据库
public static int insert(User user) {
ConnectionPool pool = ConnectionPool.getInstance();
Connection connection = pool.getConnection();
PreparedStatement ps = null;

String qr = "INSERT INTO User (username, password)"+"VALUES (?, ?)";
try {

ps = connection.prepareStatement(qr);
ps.setString(1, user.getusername());
ps.setString(2, user.getpassword());

return ps.executeUpdate();
} catch (SQLException e) {
System.out.println(e);
return 0;
} finally {
DBUtil.closePreparedStatement(ps);
pool.freeConnection(connection);
}

}

5.2登录

login.jsp登录界面
通过表单实现跳转到servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width",initial->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="style.css">
<title>world message board of the future</title>
</head>

<body background="pictures/01.jpg">
<div class="form-wrapper">
<div class="header">
login
</div>
<form action="LoginServlet" method = "post"><!--form表单,你提交后,会跳转到LoginServlet -->
<!-- 浏览者单击发送按钮发送表单的时候,隐藏域的信息也被一起发送到服务器。 -->
<input type="hidden" name="action" value="login">
<div class="input-wrapper">
<div class="border-wrapper">
<input type="text" name="username" placeholder="input username" class="border-item">
</div>
<div class="border-wrapper">
<input type="password" name="password" placeholder="password" class="border-item">
</div>

</div>
<div class="action">
<input type="submit" name="login" class="btn" value="Sign in" ><br>
<button type="submit" formaction="register.jsp" class="btn">register</button>
</div>
</form>
<center>
<!-- 获取登录是否成功信息 -->
<p class="col">${message}</p>
</center>
</div>
</body>
</html>

LoginServlet.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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码package Sevlet;

import java.io.IOException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import javaBean.Message;
import javaBean.User;
import useBean.UserDB;
/**
* Servlet implementation class Login
*/
@WebServlet("/LoginServlet")//读取/LoginServlet完整路径
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
String message="";//定义一个变量
public LoginServlet() {
super();
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String url ="/login.jsp";
String action = request.getParameter("action");
System.out.println("action: " + action);//打印测试
//设置编码
request.setCharacterEncoding("utf-8");
//检测页面是否完成提交
if(action == null)
url="/login.jsp";
else if(action.equals("login"))
//调用login()函数返回一个路径
url = login(request,response);
//设置登入是否成功的提示信息
request.setAttribute("message", message);
//调用forward()方法,转发请求
getServletContext().getRequestDispatcher(url).forward(request, response);
}

//login()函数
private String login(HttpServletRequest request, HttpServletResponse response) {
//获取登入的信息(姓名和密码)
String username = request.getParameter("username");
String password = request.getParameter("password");
//得到HttpSession类型的对象
HttpSession session = request.getSession();
String userid="";
//通过名字得出user的信息
User user = UserDB.selectUser(username);

String url = "/register.jsp";
if(user != null) {
//用户存在,比较密码
if(user.getpassword().equals(password)) {
//用于记录该用户状态
//cookie加入用户名
Cookie u = new Cookie("userid",username);
//设置有效期
u.setMaxAge(60*60*24*365*2);
//设置路径
u.setPath("/");
//将cookie加入浏览器
response.addCookie(u);

userid = user.getid();
session.setAttribute("userid", userid);
//调用selectU从message表中读出所有数据
List<Message> mL = UserDB.selectU();
//返回ml信息
request.setAttribute("MessageList", mL);
url="/MessageList.jsp";

}
else {
message = "Wrong password";
url="/login.jsp";
}
}
else {
message = "The user does not exist";
url = "/login.jsp";
}
return url;
}
}

UserDB.java
Dao层实现查找用户的方法DB.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
java复制代码	
//登录时根据username读出user
public static User selectUser(String username) {
//可以建立一个连接池保存一定数量的连接,当有对象需要数据库连接时,直接将这个连接返回给该对象,
ConnectionPool pool = ConnectionPool.getInstance();
Connection connection = pool.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
//sql语句
String qr = "SELECT * FROM User "+ "WHERE username = ?";
try {
ps = connection.prepareStatement(qr);
//sql语句中问号的解释
ps.setString(1, username);
rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User ();
//设置userd的id,username,passward
user.setid(rs.getString("id"));
user.setusername(rs.getString("username"));
user.setpassword(rs.getString("password"));
}
return user;
} catch (SQLException e) {
System.out.println(e);
return null;
} finally {
//关闭PreparedStatement和ResultSet并释放连接池中此次连接
DBUtil.closeResultSet(rs);
DBUtil.closePreparedStatement(ps);
pool.freeConnection(connection);
}
}

6.总结

以上所有登录注册源代码来自本人的一个项目,感兴趣的小伙伴可以在评论区留言。十分愿意与大家分享共同学习。欢迎批评指正!

在这里插入图片描述

本文转载自: 掘金

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

准时摸鱼,正点收网,Python实现下班倒计时

发表于 2021-04-27

你有过摸鱼时间吗

在互联网圈子里,常常说996上班制,但是也不乏965的,更甚有007的,而007则就有点ICU的感觉了,所以,大家都会忙里偷闲,偶尔摸摸鱼,摸鱼的方式多种多样的,你有过上班摸鱼吗?你的摸鱼时间都干了些什么呢?如果你早早的完成了当天的任务,坐等下班的感觉是不是很爽呢?我想说这时间还是很难熬的,还不如找点事情做来得快呢,那做点什么呢?写个下班倒计时吧,就这么愉快的决定了……

实现思路

倒计时的时间刷新,肯定得需要图形界面,也就是需要GUI编程,这里我用的是tkinter实现本地窗口的界面,使用tkinter可以实现页面布局以及时间的定时刷新显示,而涉及到时间的操作,肯定少不了要用到time模块,这里我还加入了倒计时结束自动关机的功能(注释了的,有需要可以打开),所以还用到了os模块的system实现定时关机功能。

运行环境

Python运行环境:Windows + python3.8

用到的模块:tkinter、time、os

如未安装的模块,请使用pip instatll xxxxxx进行安装,例如:pip install tkinter

界面布局

先来看一下实现后的界面

从截图中可以看到,主要有三个信息:

  • 当前时间:这个是实时显示当前时间,格式为格式化的年月日时分秒
  • 下班时间:这个可以修改的,默认是18:00:00,可以根据自己的下班时间来修改
  • 剩余时间:这里是倒计时的剩余时间,点START后每秒刷新
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
python复制代码# 设置页面数据
tk_obj = Tk()
tk_obj.geometry('400x280')
tk_obj.resizable(0, 0)
tk_obj.config(bg='white')
tk_obj.title('倒计时应用')
Label(tk_obj, text='下班倒计时', font='宋体 20 bold', bg='white').pack()
# 设置当前时间
Label(tk_obj, font='宋体 15 bold', text='当前时间:', bg='white').place(x=50, y=60)
curr_time = Label(tk_obj, font='宋体 15', text='', fg='gray25', bg='white')
curr_time.place(x=160, y=60)
refresh_current_time()
# 设置下班时间
Label(tk_obj, font='宋体 15 bold', text='下班时间:', bg='white').place(x=50, y=110)
# 下班时间-小时
work_hour = StringVar()
Entry(tk_obj, textvariable=work_hour, width=2, font='宋体 12').place(x=160, y=115)
work_hour.set('18')
# 下班时间-分钟
work_minute = StringVar()
Entry(tk_obj, textvariable=work_minute, width=2, font='宋体 12').place(x=185, y=115)
work_minute.set('00')
# 下班时间-秒数
work_second = StringVar()
Entry(tk_obj, textvariable=work_second, width=2, font='宋体 12').place(x=210, y=115)
work_second.set('00')
# 设置剩余时间
Label(tk_obj, font='宋体 15 bold', text='剩余时间:', bg='white').place(x=50, y=160)
down_label = Label(tk_obj, font='宋体 23', text='', fg='gray25', bg='white')
down_label.place(x=160, y=155)
down_label.config(text='00时00分00秒')
# 开始计时按钮
Button(tk_obj, text='START', bd='5', command=refresh_down_time, bg='green', font='宋体 10 bold').place(x=150, y=220)
tk_obj.mainloop()

定时刷新剩余时间

通过获取设置的下班时间,对比当前时间的时间差,从而得到剩余时间,再用while每秒循环处理剩余时间,并实时刷新到界面上,直至剩余时间为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
python复制代码def refresh_down_time():
"""刷新倒计时时间"""
# 当前时间戳
now_time = int(time.time())
# 下班时间时分秒数据过滤
work_hour_val = int(work_hour.get())
if work_hour_val > 23:
down_label.config(text='小时的区间为(00-23)')
return
work_minute_val = int(work_minute.get())
if work_minute_val > 59:
down_label.config(text='分钟的区间为(00-59)')
return
work_second_val = int(work_second.get())
if work_second_val > 59:
down_label.config(text='秒数的区间为(00-59)')
return
# 下班时间转为时间戳
work_date = str(work_hour_val) + ':' + str(work_minute_val) + ':' + str(work_second_val)
work_str_time = time.strftime('%Y-%m-%d ') + work_date
time_array = time.strptime(work_str_time, "%Y-%m-%d %H:%M:%S")
work_time = time.mktime(time_array)
if now_time > work_time:
down_label.config(text='已过下班时间')
return
# 距离下班时间剩余秒数
diff_time = int(work_time - now_time)
while diff_time > -1:
# 获取倒计时-时分秒
down_minute = diff_time // 60
down_second = diff_time % 60
down_hour = 0
if down_minute > 60:
down_hour = down_minute // 60
down_minute = down_minute % 60
# 刷新倒计时时间
down_time = str(down_hour).zfill(2) + '时' + str(down_minute).zfill(2) + '分' + str(down_second).zfill(2) + '秒'
down_label.config(text=down_time)
tk_obj.update()
time.sleep(1)
if diff_time == 0:
# 倒计时结束
down_label.config(text='已到下班时间')
# 自动关机,定时一分钟关机,可以取消
# down_label.config(text='下一分钟将自动关机')
# os.system('shutdown -s -f -t 60')
break
diff_time -= 1

完整代码

为了方便大家测试和顺利摸鱼,我把完整的倒计时程序也贴出来,大家有什么问题也可以及时反馈,想要了解更多的可以去交友网站github.com/gxcuizy上面找我哦

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
距离下班时间倒计时
author: gxcuizy
date: 2021-04-27
"""

from tkinter import *
import time
import os


def refresh_current_time():
"""刷新当前时间"""
clock_time = time.strftime('%Y-%m-%d %H:%M:%S')
curr_time.config(text=clock_time)
curr_time.after(1000, refresh_current_time)


def refresh_down_time():
"""刷新倒计时时间"""
# 当前时间戳
now_time = int(time.time())
# 下班时间时分秒数据过滤
work_hour_val = int(work_hour.get())
if work_hour_val > 23:
down_label.config(text='小时的区间为(00-23)')
return
work_minute_val = int(work_minute.get())
if work_minute_val > 59:
down_label.config(text='分钟的区间为(00-59)')
return
work_second_val = int(work_second.get())
if work_second_val > 59:
down_label.config(text='秒数的区间为(00-59)')
return
# 下班时间转为时间戳
work_date = str(work_hour_val) + ':' + str(work_minute_val) + ':' + str(work_second_val)
work_str_time = time.strftime('%Y-%m-%d ') + work_date
time_array = time.strptime(work_str_time, "%Y-%m-%d %H:%M:%S")
work_time = time.mktime(time_array)
if now_time > work_time:
down_label.config(text='已过下班时间')
return
# 距离下班时间剩余秒数
diff_time = int(work_time - now_time)
while diff_time > -1:
# 获取倒计时-时分秒
down_minute = diff_time // 60
down_second = diff_time % 60
down_hour = 0
if down_minute > 60:
down_hour = down_minute // 60
down_minute = down_minute % 60
# 刷新倒计时时间
down_time = str(down_hour).zfill(2) + '时' + str(down_minute).zfill(2) + '分' + str(down_second).zfill(2) + '秒'
down_label.config(text=down_time)
tk_obj.update()
time.sleep(1)
if diff_time == 0:
# 倒计时结束
down_label.config(text='已到下班时间')
# 自动关机,定时一分钟关机,可以取消
# down_label.config(text='下一分钟将自动关机')
# os.system('shutdown -s -f -t 60')
break
diff_time -= 1


# 程序主入口
if __name__ == "__main__":
# 设置页面数据
tk_obj = Tk()
tk_obj.geometry('400x280')
tk_obj.resizable(0, 0)
tk_obj.config(bg='white')
tk_obj.title('倒计时应用')
Label(tk_obj, text='下班倒计时', font='宋体 20 bold', bg='white').pack()
# 设置当前时间
Label(tk_obj, font='宋体 15 bold', text='当前时间:', bg='white').place(x=50, y=60)
curr_time = Label(tk_obj, font='宋体 15', text='', fg='gray25', bg='white')
curr_time.place(x=160, y=60)
refresh_current_time()
# 设置下班时间
Label(tk_obj, font='宋体 15 bold', text='下班时间:', bg='white').place(x=50, y=110)
# 下班时间-小时
work_hour = StringVar()
Entry(tk_obj, textvariable=work_hour, width=2, font='宋体 12').place(x=160, y=115)
work_hour.set('18')
# 下班时间-分钟
work_minute = StringVar()
Entry(tk_obj, textvariable=work_minute, width=2, font='宋体 12').place(x=185, y=115)
work_minute.set('00')
# 下班时间-秒数
work_second = StringVar()
Entry(tk_obj, textvariable=work_second, width=2, font='宋体 12').place(x=210, y=115)
work_second.set('00')
# 设置剩余时间
Label(tk_obj, font='宋体 15 bold', text='剩余时间:', bg='white').place(x=50, y=160)
down_label = Label(tk_obj, font='宋体 23', text='', fg='gray25', bg='white')
down_label.place(x=160, y=155)
down_label.config(text='00时00分00秒')
# 开始计时按钮
Button(tk_obj, text='START', bd='5', command=refresh_down_time, bg='green', font='宋体 10 bold').place(x=150, y=220)
tk_obj.mainloop()

最后

大家有任何问题,都可以给我留言给我,我会及时回复,如有说的不对的地方,还请大家帮忙纠正。如果大家有什么好玩的摸鱼办法,也可以底部留言给我哈,大家一起愉快的摸鱼!

本文转载自: 掘金

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

ES小白安装入门 Es

发表于 2021-04-27

Es

ElasticSearch总所周知是目前各大公司使用的搜素中间件。下面对于ElasticSearch统一使用Es进行替代。

安装

homebrew安装

如果你已经安装过Homebrew了,那么你可以跳过这一步,直接进行Elasticsearch安装步骤;

Homebrew是一款MacOS平台下的软件包管理工具,拥有安装、卸载、更新、查看、搜索等很多实用的功能,强烈推荐安装。

请复制如下指令到命令行粘贴执行:

1
txt复制代码/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”

对于homebrew常用的命令可以参考homeBrew常用命令

Es安装

1
复制代码执行命令:brew install elasticsearch

安装成功后显示

image.png

安装成功后使用命令 启动Es服务

1
sql复制代码brew servicEs start elasticsearch

通过浏览器 输入localhost:9200 查看Es是否启动成功,以下为成功的截图

image.png

若没有显示 使用brew servicEs list查看服务是否启动 或者 使用jps 命令查看服务

image.png

使用brew info elasticsearch 查看当前Es 安装目录 进入目录中查看启动日志

image.png

Es插件安装

使用科学上网进入谷歌应用商店 下载ElasticSearch Head 插件

image.png

登陆成功后点击插件进入初始页面

image.png

可以获取到信息:head插件连接Es 9200端口 集群的名字为elasticsearch_coapeng 目前的集群状态为green

或者可以使用postman进行集群状态查询

Es名词介绍

Es中有一些达成共识的术语 ,需要进行介绍。 Es是分布式搜索分析引擎,其相对于传统的RDS,突出优势在于对于存储内容采用分析器进行拆解建立倒排索引 实现快速搜索。

Es虽然是搜索分析引擎但是底层仍然是一个存储系统,通过类比RDS的方式可以快速建立起一个概念。

索引:(名词)类似于数据库

文档:类似于数据库中一张表

分片:类似于水平分库分表,将一张表的数据进行拆分保存在多个存储节点(node)中

主分片:数据保存的节点

副分片:主分片的副本,当主分片节点宕机时 成为主分片

分词器:每一列的数据按照一定规则拆分为一个个词组

正排索引:RDS保存的数据形式

![image-20210426220121555](/Users/coapeng/Library/Application Support/typora-user-images/image-20210426220121555.png)

倒排索引:将特定列数据使用分词器分割,分割出来的词组可以作为关键字来查找到对应id(即上一张图中的id 为1)

![image-20210427102637647](/Users/coapeng/Library/Application Support/typora-user-images/image-20210427102637647.png)

分词器:将每一列数据 按照一定的规则进行拆分 es常用的分词器有

  • standard 标准分词 以单词边界切分字符串为terms,根据Unicode文本分割算法。它会移除大部分的标点符号,小写分词后的term,支持停用词

image

  • keyword :无操作分词器,会输出与输入相同的内容作为一个single term

image

  • ik 分词 针对中文开发的分词插件 有ik_max_word 和ik_smart两种模式 分别对应细粒度的拆分组合 和粗粒度的拆分组合

安装ik 分词 可以去github按照readme.md进行安装推荐使用第一种进行安装,安装的时候注意ES版本号 和ik分词插件版本号对其

image

image

  • whitespace 遇到空格字符时切分字符串

image

  • stop:类似简单分词器,同时支持移除停用词 (简单理解为不重要的单词)

image

  • Simple:该分词器会在遇到非字母时切分字符串,小写所有的term

image

本文转载自: 掘金

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

Java后端知识体系

发表于 2021-04-27

基础重点(必须扎实)

Java语言

  • 语言基础 《Java核心技术》
+ 基础语法
+ 面向对象
+ 常用API
+ 异常处理
+ 集合
+ IO
+ 多线程
+ 网络编程
+ 泛型
+ 反射
+ 注解
  • JVM 《深入理解Java虚拟机》
+ 类加载机制


    - 双亲委托
+ 字节码执行机制
+ JVM内存模型


    - 堆区
    - 虚拟机栈
    - 方法区
+ GC垃圾回收
+ JVM性能监控与故障定位
+ JVM调优
  • 多线程、锁、并发 1:《Java并发编程的艺术》、2:《Java并发编程实战》
+ 并发编程基础
+ 线程池
+ 锁


    - 乐观锁、悲观锁
    - 互斥锁、共享锁
    - 可重入锁、偏向锁
    - 轻量级锁、CAS与自旋锁
+ 并发容器
+ 原子类
+ JUC并发工具类
  • 网络编程
+ 学习路径


    - Socket API + 多线程 + 网络模型/IO模型 + IO复用
    - Netty
+ 核心点


    - 进程间通信方式:信号量、管道、共享内存、socket 等
    - 多线程编程:互斥锁、条件变量、读写锁、线程池等
    - 五大 IO 模型:同步、异步、阻塞、非阻塞、信号驱动
    - 高性能 IO 两种模式:Reactor 和 Proactor
    - IO 复用机制:epoll、select、poll(破解 C10K 问题的利器)
  • Java源码

数据库/SQL

  • 《SQL必知必会》、《高性能MySQL》
  • SQL语句
+ 手写SQL


    - 联表
    - 聚合
  • SQL语句优化
  • 事务、隔离级别
  • 索引
  • 锁

数据结构与算法

  • 《漫画算法》、《算法》
  • 数据结构
+ 字符串
+ 数组
+ 链表
+ 栈
+ 队列
+ 二叉树
+ 堆
+ 哈希
  • 算法
+ 十大排序
+ 查找、二分
+ 贪心
+ 分治
+ 动态规划
+ 回溯

设计模式

  • 《重学Java设计模式》
  • 单例
  • 工厂
  • 代理
  • 策略
  • 模板方法
  • 观察者
  • 适配器
  • 责任链
  • 建造者
  • 。。。。

计算机网络

  • 《计算机网络:自顶向下方法》
  • HTTP、TCP、IP、ICMP、UDP、DNS、ARP
  • IP地址、MAC地址、OSI七层模型(或者 TCP/IP 五层模型)
  • HTTPS安全相关的:数字签名、数字证书、TLS
  • 常见网络攻击:局域网ARP泛洪、DDoS、TCP SYN Flood、XSS等
  • TCP协议(最重要)
+ TCP协议


    - 三次握手、四次挥手
    - 状态转换
    - TCP状态中TIME\_WAIT
    - 拥塞控制
    - 快速重传、慢启动
+ 问题


    - TCP 如何实现可靠传输的(画外音:如何基于 UDP 实现可靠传输)
    - TCP 连接建立为什么不是两次握手(画外音:三次握手的充分必要性说明)
    - TIME\_WAIT 的存在解决了什么问题,等待时间为什么是 2 MSL
+ 核心


    - 可靠传输 + 高效传输(流量控制和窗口管理)
  • HTTP、HTTPS
+ 从 URL 输入到页面展现到底发生什么
  • 学习方法
+ 学习抓住一条主线


    - 一个数据包是如何发送出去的
+ 带着问题去思考为什么这么做

操作系统

  • 《现代操作系统》
  • 进程管理
  • 并发、同步互斥、锁
  • 内存管理
  • 文件系统
  • 重点
+ OS四大模块的理论知识


    - 进程与线程管理
    - 内存管理
    - IO与文件系统
    - 设备管理
+ 了解Linux内核部分实现原理


    - 内存管理
    - 进程管理
    - 虚拟文件系统
+ 与编程最密切


    - 内存
    - 进程
    - IO
  • 认知
+ 操作系统由哪些构成
+ 进程的状态、切换、调度
+ 进程间通信方式(共享内存、管道、消息)
+ 进程和线程的区别
+ 线程的实现方式(一对一、多对一等)
+ 互斥与同步(信号量、管程、锁)
+ 死锁检测与避免
+ 并发经典的问题:读者写者、哲学家就餐问题
+ 为什么需要虚拟内存,MMU 具体如何做地址转换的
+ 内存为什么分段、分页
+ 页面置换算法
+ 文件系统是如何组织的
+ 虚拟文件系统(VFS)是如何抽象的
+ 。。。。

应用框架

后端

  • JSP、Servlet
  • Spring家族
+ Spring


    - IOC
    - AOP
+ Spring MVC
+ MyBatis
+ SSM


    - 打war包
    - Tomcat运行
+ Spring Boot


    - 打jar包


        * 内嵌Tomcat


            + 微服务架构
    - 知识点


        * 自动配置、开箱即用
        * 整合Web
        * 整合数据库(事务问题)
        * 整合权限


            + Shiro
            + SpringSecurity
        * 各种中间件


            + 缓存
            + MQ
            + RPC框架


                - Dubbo
            + NIO框架


                - Netty
+ Spring Cloud


    - Netflix


        * Eureka


            + 服务治理组件,包括服务端的注册中心和客户端的服务发现机制。
        * Ribbon


            + 负载均衡的服务调用组件,具有多种负载均衡调用策略。
        * Hystrix


            + 服务容错组件,实现了断路器模式,为依赖服务的出错和延迟提供了容错能力。
        * Feign


            + 基于Ribbon和Hystrix的声明式服务调用组件。
        * Zuul


            + API 网关服务,过滤、安全、监控、限流、路由。
    - Alibaba


        * Nacos


            + 阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
        * Sentinel


            + 面向分布式服务架构的轻量级流量控制产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
        * RocketMQ


            + 一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
        * Dubbo


            + Apache Dubbo 是一款高性能 Java RPC 框架,用于实现服务通信。
        * Seata


            + 阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
    - Config


        * 分布式配置中心。配置管理工具,支持使用 Git 存储配置内容,支持应用配置的外部化存储,支持客户端配置信息刷新、加解密配置内容等。
    - Bus


        * 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。
    - Consul


        * 服务注册和配置管理中心。
    - Security


        * 安全工具包,对Zuul代理中的负载均衡OAuth2客户端及登录认证进行支持。
    - Sleuth


        * SpringCloud应用程序的分布式请求链路跟踪,支持使用Zipkin、HTrace和基于日志(例如ELK)的跟踪。
    - Stream


        * 轻量级事件驱动微服务框架,可以使用简单的声明式模型来发送及接收消息,主要实现为Apache Kafka及RabbitMQ。
    - Task


        * 用于快速构建短暂、有限数据处理任务的微服务框架,用于向应用中添加功能性和非功能性的特性。
    - Zookeeper


        * 服务注册中心。
    - Gateway(可替代 Zuul)


        * Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
    - OpenFeign(可替代 Feign)


        * OpenFeign 是 Spring Cloud 在 Feign 的基础上支持了 Spring MVC 的注解,如 @RequesMapping等等。OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
+ 项目经验


    - 总结
  • 中间件
+ 缓存


    - Redis


        * 5大数据类型
        * 事务
        * 消息通知
        * 管道
        * 持久化
        * 集群
+ 消息


    - RabbitMQ
    - RocketMQ
    - Kafka
+ 搜索


    - ElasticSearch

前端

  • 基础套餐
+ 三大件


    - HTML
    - CSS
    - JavaScript
+ 基础库


    - jQuery
    - Ajax
  • 模板框架
+ JSP
+ Thymeleaf
+ FreeMaker
  • 组件化框架
+ Node
+ VUE
+ React
+ Angular

开发工具

集成开发环境

  • Eclipse
  • IDEA
  • VSCode

Linux系统

  • Linux常用命令
  • 基本Shell脚本

代码管理工具

  • Git
+ Git命令和使用
  • SVN

项目管理/构建工具

  • Maven
  • Gradle

应用运维

Web服务器

  • Nginx

应用服务器

  • Tomcat

容器技术

  • Docker
  • Kubernetes(K8S)
    • 管理运维容器

持续集成、持续发布

  • Jenkins

代码质量检测

  • Sonar

日志收集、分析

  • ELK

CDN加速

本文转载自: 掘金

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

Redis GeoHash 测距

发表于 2021-04-27

Redis GeoHash 测距

GeoHash本质上是空间索引的一种方式,其基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码。以GeoHash方式建立空间索引,可以提高对空间poi数据进行经纬度检索的效率。由于采用的是base32编码方式,即Geohash中的每一个字母或者数字都是由5bits组成(2^5 = 32,base32),这5bits可以有32中不同的组合(0~31),这样我们可以将整个地图区域分为32个区域,通过00000 ~ 11111来标识这32个区域。

Redis 3.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
js复制代码/**
* 地方实体
*
* @author fengyn
* @since 2021/4/26 15:50
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Place {

/**
* 地名
*/
private String name;

/**
* 经度
*/
private Double longitude;

/**
* 经度
*/
private Double latitude;

初始化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码 @PostConstruct
private void setGeo(){
addGeo(new Place("浙港国际",120.093312,30.309403));
addGeo(new Place("丰潭路地铁口",120.109624,30.281936));
addGeo(new Place("杭州东站",120.212998,30.291331));
addGeo(new Place("杭州汽车西站",120.09105,30.261022));
}

private void addGeo(Place place){
String geo = "geo";
redisTemplate.opsForGeo().add(geo,
new Point(place.getLongitude(),place.getLatitude()),place.getName());
}
GeoHash实际上是存在zset里,score存geohash算法计算出来的值

企业微信截图_1619427763701.png

计算两个位置的距离

Distance distance(K var1, M var2, M var3, Metric var4);

Diststance有两个属性:double value(距离) Metric metric(单位)

var1 表示redis key值

var2 var3 表示zset两个位置的key值

var4 表示距离单位(可不传,默认单位为米)

1
2
js复制代码String geo = "geo";
Distance distance = redisTemplate.opsForGeo().distance(geo,"杭州东站", "杭州汽车西站", Metrics.KILOMETERS);

结果

1
2
3
4
js复制代码{
"value": 12.1894,
"metric": "KILOMETERS"
}

以中心位置为圆心找出指定半径范围内的位置

GeoResults radius(K var1, Circle var2, GeoRadiusCommandArgs var3);

GeoLocation有两个属性 : T name (zset中key值) ,Point point (返回 经纬度 double x,double y);

GeoResults有两个属性 : Distance averageDistance(平均距离) ,List content (返回GeoLocation属性);

var1 表示redis key值

var2 表示圆心坐标位置

var3 表示添加过滤排序条件

includeDistance 包含距离

includeCoordinates 包含经纬度

sortAscending 正序排序

limit 限定返回的记录数

1
2
3
4
5
6
js复制代码String geo = "geo";
GeoResults<RedisGeoCommands.GeoLocation<Object>> ret =
redisTemplate.opsForGeo().radius(geo, //丰潭路坐标
new Circle(new Point(120.109624,30.281936), new Distance(1, Metrics.KILOMETERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance().limit(3).includeCoordinates().sortAscending()));

结果

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
js复制代码{
"averageDistance": {
"value": 2.1217666666666664, //平均距离
"metric": "KILOMETERS"
},
"content": [
{
"content": {
"name": "丰潭路地铁口",
"point": {
"x": 120.10962277650833,
"y": 30.281934749809132
}
},
"distance": {
"value": 2.0E-4,
"metric": "KILOMETERS"
}
},
{
"content": {
"name": "杭州汽车西站",
"point": {
"x": 120.09105116128922,
"y": 30.26102076552342
}
},
"distance": {
"value": 2.9317,
"metric": "KILOMETERS"
}
},
{
"content": {
"name": "浙港国际",
"point": {
"x": 120.09330958127975,
"y": 30.309403523012882
}
},
"distance": {
"value": 3.4334,
"metric": "KILOMETERS"
}
}
]
}

本文转载自: 掘金

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

1…679680681…956

开发者博客

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