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

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


  • 首页

  • 归档

  • 搜索

怎么理解回调函数? 回调函数合集

发表于 2021-08-08

​

网上查了一通,有点体会,特来分享与讨论。


SECTION 1


回调函数

回调函数是一个时时听到的概念,比如在windows API编程时遇到的WinProc函数,就是我们编写而由操作系统调用的函数。现在,我们需要慢慢又详细的记录一下这个问题。

库与使用者的问题

在开始之前,首先我们想像这样一个情景,一个大型软件公司开发一套软件库提供给用户使用。在这句话中,出现两个对立面,一个是软件公司,一个是用户。显然,软件公司实力很强大,他们强大的地方在于他们很聪明。请看下图:

库开发者是提供方,他们不知道用户想做什么,库开发者对用户的信息是未知的,因为两边的人谁也没见过谁,就像一个人在美国一个人在中国。他们聪明的地方就在于即使不知道用户怎么操作,他们也可以用一种通用的方法来解决用户的问题。换句话说,库开发方不管用户怎么折腾变化,他都能适应。这就是问题的神奇之处,当然,为了达到这种效果,必须有一种规则来约束。

规则

现在来说规则,规则比较好理解,就是两方达成的约束。比如甲和乙两个人,甲对乙说,你只要提供给我一个塑料球,我都能给它涂上颜色。这里的约束是塑料球,(当然,为了简化问题这里的约束还是比较泛泛而不严谨的),这个时候如果乙给他一个水晶球,或者给他一辆汽车都不行,因为已经超越了约束。所以说,库开发者能适应用户的各种变化就在于他也给了用户一定的约束,是在约束之下的适应,这也是不言自明的。

代码之下无秘密

下面一段代码极好的解释了上述问题:

)​

#include <stdio.h>

typedef int student_id;

typedef int student_age;

typedef struct _Student{

student_id id;

student_age age;

}Student;

//类型重定义:函数指针类型

typedef bool (*pFun)(Student, Student);

//———————————————–

//冒泡排序法:能够按AGE或ID排序,用同一个函数实现

//———————————————–

void sort(Student stu[],const int num,pFun fun)

{

Student temp;

for(int i = 0; i < num; ++i)

{

for(int j = 0; j < num - i -1; ++j)

{

if((*fun)(stu[j],stu[j+1]))

{

temp = stu[j];

stu[j] = stu[j+1];

stu[j+1] = temp;

}

}

}

}

//———————————————–

//回调函数:比较年龄

//———————————————–

bool CompareAge(Student stu1,Student stu2)

{

//更改从大到小还是从小到大的顺序,只需反一下。

if(stu1.age < stu2.age)

return true;

return false;

}

//———————————————–

//回调函数:比较id

//———————————————–

bool CompareId(Student stu1,Student stu2)

{

//更改从大到小还是从小到大的顺序,只需反一下。

if(stu1.id < stu2.id)

return true;

return false;

}

int main()

{

Student stu[] = {

{1103,24},

{1102,23},

{1104,22},

{1107,25},

{1105,21}};

pFun fun = CompareAge;

int size = sizeof(stu)/sizeof(Student);

sort(stu,size,fun);

for(int i = 0; i < size; ++i){

printf(“%d %d\n”,stu[i].id,stu[i].age);

}

return 0;

}

)​

通过代码,我们必须分辨出哪个是库提供方,哪个是用户:

sort(…) 方法是库开者提供的,只是因为模拟问题所以把它写在一个文件中了,我们可以假想它存放在某个静态库lib里面,这样就和它有了界限。

库开发者提供方法sort(…)供我们调用,形参里面还包括一个函数指针,就是调用用户自己写的函数,也即回调函数。所以它提供了规则,也就是说这个回调函数的形参是什么,返回值又是什么,假如用户对于这个回调函数的形参和返回值啥都不知道,随意写了一个函数放进去,sort方法能接受吗?显然是不行的。

那么用户怎么知道回调函数的形参和返回值等信息呢?库开发方会告知,或者在它的文档里面会有说明,就像在windows API开发窗口程序时提供的WinProc() 函数一样,里面的形参必须一模一样。(这个地方会牵涉到接口)

什么是回?

根据上面的代码,我们知道,库开发方提供了一个方法,然后形参又带有一个函数指针,等着用户去写然后拿来调用,并在其实现中调用了这个函数。站在库开发方的立场,我们可以总结为一句话:库开发方调用了用户的函数(回调函数)。

现在,我们把视角移到回调函数的立场上来看,它被调用了。但同时它也有参数,且传进来的参数是库开发方提供的数据,这里我们又可以总结一句话:回调函数调用了库开发方的数据。

这就是 “回” 的意思,你调我我也调用你,双方互相调用。

)​

回调函数的重点是什么?

根据上文分析,发现回调函数其实也不是难于理解,我觉得重点在于一定要划分清楚谁是库开发方(也可称为提供方,调用者),谁是用户,把职能划分清楚是最关键的。


SECTION 2


tip1

你就想象你函数的一部分功能被外包给别人。至于被人怎么实现的你不用管,你的函数具有一个完整的功能,但是有的功能可以随你自己定制,参照stl中的for_each

tip2

简单点说,用户是实现方,实现方需要调用A()函数,但为了A()函数具有通用性,需要根据实现方的意愿调用实现方提供的函数cbB(),在这里cbB()即为回调函数。在Windows编程中回调函数用途很广泛。


SECTION 3


回调函数是通过函数指针调用的函数:把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就称为回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

通俗点说就是:在A类中调用B类中的某个方法C,然后B类反过来调用A类中的方法D,则D就是回调函数。

打个比方:

我们将A类看成一个人,就叫他小A;将B类也看成一个人,就叫他小B;

那么使用回调函数D的过程就可以这样理解:

小A在开发过程中遇到了一个麻烦,而这个麻烦只有小B才能解决,于是小A找到了小B求他帮忙,但是由于和小B不怎么熟悉于是带上了名片。小A向小B说明了遇到的麻烦后,正巧小B正在忙于其他事情,于是小B先收下了小A的名片,告诉小A回去等消息。由于这个麻烦不解决就无法继续开发,于是回去等消息的小A就只好先去做别的事情。过了一段时间小B忙完手上的事情,解决了小A的麻烦后,找出小A名片上的电话号码拨了过去,告诉小A,麻烦已经搞定了(小B只是顺着名片把解决方案告诉给小A,而并不关心自己给出的解决方案在小A那里会如何运用)。小A放下电话后,利用小B给他的解决方案继续开发。

简而言之:小A带着名片D通过途径C找到小B求他帮忙,小B不能立即解决于是收下名片D,之后的某天小B解决了小A的问题后又通过名片D告诉了小A解决方法。

即:

A类调用B类中的C方法,D作为函数指针当做C方法的一个参数(小A带着名片D通过途径C找到小B求他帮忙)

B类无法立即处理,就先进行回调函数标记(收下名片)

在未来的某一个时间点,当满足触发条件时(解决问题后)

通过回调函数D传递回信息给A类(通过名片告诉结果)

下面以一个例子说明上述过程(解释见注释):

  1. #include
  1. typedef void (*Fun)(int); // 定义一个函数指针类型
  1. Fun p = NULL; // 用 Fun 定义一个变量 p ,它指向一个返回值为空参数为 int 的函数
  1. void caller(Fun pCallback)
  1. {
  2. p = pCallback;
  1. // 达成某一条件后,通过名片(函数指针 p ),传回结果
  1. int result = 1;
  1. (*p)(result);
  2. }
  1. void callback(int a) // 回调函数
  1. {
  1. std::cout << “callback result = “ << a << std::endl;
  1. }
  1. int main(int argc, char* argv[])
  1. {
  2. caller(callback);
  3. getchar();
  1. return 0;
  1. }

​

本文转载自: 掘金

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

【Golang】并发编程包之 errgroup

发表于 2021-08-08

原文链接:并发编程包之 errgroup

前言

哈喽,大家好,我是asong,今天给大家介绍一个并发编程包errgroup,其实这个包就是对sync.waitGroup的封装。我们在之前的文章—— 源码剖析sync.WaitGroup(文末思考题你能解释一下吗?),从源码层面分析了sync.WaitGroup的实现,使用waitGroup可以实现一个goroutine等待一组goroutine干活结束,更好的实现了任务同步,但是waitGroup却无法返回错误,当一组Goroutine中的某个goroutine出错时,我们是无法感知到的,所以errGroup对waitGroup进行了一层封装,封装代码仅仅不到50行,下面我们就来看一看他是如何封装的?

errGroup如何使用

老规矩,我们先看一下errGroup是如何使用的,前面吹了这么久,先来验验货;

以下来自官方文档的例子:

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
go复制代码var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)

type Result string
type Search func(ctx context.Context, query string) (Result, error)

func fakeSearch(kind string) Search {
return func(_ context.Context, query string) (Result, error) {
return Result(fmt.Sprintf("%s result for %q", kind, query)), nil
}
}

func main() {
Google := func(ctx context.Context, query string) ([]Result, error) {
g, ctx := errgroup.WithContext(ctx)

searches := []Search{Web, Image, Video}
results := make([]Result, len(searches))
for i, search := range searches {
i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
result, err := search(ctx, query)
if err == nil {
results[i] = result
}
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}

results, err := Google(context.Background(), "golang")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
for _, result := range results {
fmt.Println(result)
}

}

上面这个例子来自官方文档,代码量有点多,但是核心主要是在Google这个闭包中,首先我们使用errgroup.WithContext创建一个errGroup对象和ctx对象,然后我们直接调用errGroup对象的Go方法就可以启动一个协程了,Go方法中已经封装了waitGroup的控制操作,不需要我们手动添加了,最后我们调用Wait方法,其实就是调用了waitGroup方法。这个包不仅减少了我们的代码量,而且还增加了错误处理,对于一些业务可以更好的进行并发处理。

赏析errGroup

数据结构

我们先看一下Group的数据结构:

1
2
3
4
5
6
7
8
go复制代码type Group struct {
cancel func() // 这个存的是context的cancel方法

wg sync.WaitGroup // 封装sync.WaitGroup

errOnce sync.Once // 保证只接受一次错误
err error // 保存第一个返回的错误
}

方法解析

1
2
3
go复制代码func WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)
func (g *Group) Wait() error

errGroup总共只有三个方法:

  • WithContext方法
1
2
3
4
go复制代码func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}

这个方法只有两步:

  • 使用context的WithCancel()方法创建一个可取消的Context
  • 创建cancel()方法赋值给Group对象
  • Go方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func (g *Group) Go(f func() error) {
g.wg.Add(1)

go func() {
defer g.wg.Done()

if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}

Go方法中运行步骤如下:

  • 执行Add()方法增加一个计数器
  • 开启一个协程,运行我们传入的函数f,使用waitGroup的Done()方法控制是否结束
  • 如果有一个函数f运行出错了,我们把它保存起来,如果有cancel()方法,则执行cancel()取消其他goroutine

这里大家应该会好奇为什么使用errOnce,也就是sync.Once,这里的目的就是保证获取到第一个出错的信息,避免被后面的Goroutine的错误覆盖。

  • wait方法
1
2
3
4
5
6
7
go复制代码func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}

总结一下wait方法的执行逻辑:

  • 调用waitGroup的Wait()等待一组Goroutine的运行结束
  • 这里为了保证代码的健壮性,如果前面赋值了cancel,要执行cancel()方法
  • 返回错误信息,如果有goroutine出现了错误才会有值

小结

到这里我们就分析完了errGroup包,总共就1个结构体和3个方法,理解起来还是比较简单的,针对上面的知识点我们做一个小结:

  • 我们可以使用withContext方法创建一个可取消的Group,也可以直接使用一个零值的Group或new一个Group,不过直接使用零值的Group和new出来的Group出现错误之后就不能取消其他Goroutine了。
  • 如果多个Goroutine出现错误,我们只会获取到第一个出错的Goroutine的错误信息,晚于第一个出错的Goroutine的错误信息将不会被感知到。
  • errGroup中没有做panic处理,我们在Go方法中传入func() error方法时要保证程序的健壮性

踩坑日记

使用errGroup也并不是一番风顺的,我之前在项目中使用errGroup就出现了一个BUG,把它分享出来,避免踩坑。

这个需求是这样的(并不是真实业务场景,由asong虚构的):开启多个Goroutine去缓存中设置数据,同时开启一个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
go复制代码func main()  {
g, ctx := errgroup.WithContext(context.Background())

// 单独开一个协程去做其他的事情,不参与waitGroup
go WriteChangeLog(ctx)

for i:=0 ; i< 3; i++{
g.Go(func() error {
return errors.New("访问redis失败\n")
})
}
if err := g.Wait();err != nil{
fmt.Printf("appear error and err is %s",err.Error())
}
time.Sleep(1 * time.Second)
}

func WriteChangeLog(ctx context.Context) error {
select {
case <- ctx.Done():
return nil
case <- time.After(time.Millisecond * 50):
fmt.Println("write changelog")
}
return nil
}
// 运行结果
appear error and err is 访问redis失败

代码没啥问题吧,但是日志一直没有写入,排查了好久,终于找到问题原因。原因就是这个ctx。

因为这个ctx是WithContext方法返回的一个带取消的ctx,我们把这个ctx当作父context传入WriteChangeLog方法中了,如果errGroup取消了,也会导致上下文的context都取消了,所以WriteChangelog方法就一直执行不到。

这个点是我们在日常开发中想不到的,所以需要注意一下~。

总结

因为最近看很多朋友都不知道这个库,所以今天就把他分享出来了,封装代码仅仅不到50行,真的是很厉害,如果让你来封装,你能封装的更好吗?

欢迎关注公众号:【Golang梦工厂】

推荐往期文章:

  • 学习channel设计:从入门到放弃
  • 编程模式之Go如何实现装饰器
  • Go语言中new和make你使用哪个来分配内存?
  • 源码剖析panic与recover,看不懂你打我好了!
  • 空结构体引发的大型打脸现场
  • 面试官:你能聊聊string和[]byte的转换吗?
  • 面试官:两个nil比较结果是什么?
  • 面试官:你能用Go写段代码判断当前系统的存储方式吗?
  • 赏析Singleflight设计

本文转载自: 掘金

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

【Spring Boot 快速入门】二、我的第一个Sprin

发表于 2021-08-08

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

前言

  上一片文章介绍了Spring Boot的基本概念,并通过官方的网站建立了第一个Spring Boot的项目。本文针对Spring Boot最简项目进行介绍,并启动运行。好了,开始新的一文的介绍。

项目结构

  本文介绍的是最简单的Spring Boot 项目。主要是由启动类、配置文件和依赖所组成。这三部分组成Spring Boot项目的基本配置,集成业务功能代码之后,就组成一个完整的项目了。

@SpringBootApplication

  @SpringBootApplication注解包含:@Target、@Retention、@Documented、@Inherited、@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。SpringBootApplication是springboot的基本注解,是写在springboot的启动类上的注解,目的是开启spring Boot 的自动配置。
其中:

  • @ComponentScan:可以配置多个需要扫描的包
  • @Target:用于设定注解使用范围。
  • @Retention:被它所注解的注解保留多久。
  • @Documented:在自定义注解的时候可以使用@Documented来进行标注,如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。
  • @EnableAutoConfiguration:开启自动配置。
  • @SpringBootConfiguration:指示此类提供了应用程序配置。
  • @SpringBootApplication的目的只是为了简化,让开发人员少写代码,实现相同的目标,这也算Java封装思想的提现。

POM依赖包

1
2
3
4
js复制代码       <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • spring-boot-starter-web:依赖启动器的主要作用是提供Web开发场景所需的底层所有依赖。
1
2
3
4
5
js复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  • spring-boot-starter-test:进行单元测试的相关依赖。
1
2
3
4
5
js复制代码         <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
  • commons-lang3:集成的相关工具类。

配置文件

  配置文件application.properties,为了演示项目,本文仅配置一个简单的服务端口。在application.properties中可以配置更多的服务的相关参数,包含:SQL、MQ、Redis、项目名称等等。

项目原码

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>simple</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>simple</name>
<description>Demo project for Spring Boot and MyBatis and Swagger</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- commons start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!-- commons end -->
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* @ClassName TestController
* @Description: 测试接口
* @Author JavaZhan @公众号:Java全栈架构师
* @Date 2020/6/13
* @Version V1.0
**/
@Controller
@RequestMapping("/test")
public class TestController {

@RequestMapping("hello")
@ResponseBody
public String getTest(){
return "Hello World ,This is Test";
}
}

启动类DemoSimpleApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typescript复制代码package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @MethodName: DemoSimpleApplication
* @Description: 启动类
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2020/6/13
**/
@SpringBootApplication
public class DemoSimpleApplication {

/**
* @MethodName: main
* @Description:
* @param args
* @Return: void
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2020/6/13
**/
public static void main(String[] args) {
SpringApplication.run(DemoSimpleApplication.class, args);
}

}

配置文件

配置文件application.properties,为了演示项目,本文仅配置一个简单的服务端口。

1
ini复制代码server.port=8888

结语

  本次基于Spring Boot建立的第一个项目就完成了,执行DemoSimpleApplication类,在浏览器中输入http://localhost:8888/test/hello 就会返回正常的“Hello World ,This is Test”信息。好了,本次第一个Spring Boot项目启动啦!

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

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

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

Vue3+TypeScript+Django Rest Fr

发表于 2021-08-08

本文适合对有 Python 语言有一定基础的人群,希望利用 Python 做更多有意思的事情,比如搭建个人博客,记录自己的所思所想,或者想找一个项目实践前后端分离技术等等。跟着本文可以了解和运行项目,本项目是在 Window 10 Professional 系统下开发

大家好,我是落霞孤鹜,上一篇介绍了开发博客的背景、技术栈,并介绍了如何搭建开发环境。这一篇介绍后端和前端的基础框架代码初始化,基于Django和Vue初始化项目框架代码,跑通Hello world。

一、后端框架代码搭建

后端 Python 代码通过 PyCharm 能比较快速的搭建 Django 项目,因为在 PyCharm 的专业版里面,已经内置了 Django 框架

1.1 通过 PyCharm 初始化 Django 项目

  1. 通过 pip 安装Django 包

为了更好的兼容性,我们自己安装 Django 2 版本,不采用最新版本。在命令行输入如下命令:

1
bash复制代码pip install django==2.2.23
  1. 在 PyCharm 的首屏界面,点击 New Project 对话框,在左侧选择 Django,在右侧的 Location中选择项目地址,项目命令为Blog 并将我们之前安装的 Python 路径选择为 Interpreter ,如下图:

image-20210717165800189
3. 点击Create,等待 PyCharm 执行创建。

如果选择的 Python Interpreter 环境中没有安装 Django,PyCharm 会自动安装 Django 最新版本,由于我们已经安装了Django,PyCharm 会自动使用环境中的 Django 版本

完成后左侧的导航区域会自动生成Django框架项目所需的文件,结构如下图:

image-20210717170550892

  1. 在 Pycharm 右下角点击 Terminal,通过 pip 安装 Django Rest Framework
1
bash复制代码pip install djangorestframework==3.12.4
  1. 验证框架是否可以运行

运行点击 PyCharm 右上角的运行按钮,如果正常,在PyCharm的运行控制台会打印如下信息

1
2
3
4
5
6
7
8
9
10
11
bash复制代码Performing system checks...

Watching for file changes with StatReloader
System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
July 17, 2021 - 17:42:28
Django version 2.2.23, using settings 'project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

打开 Edge 或 Chrome 浏览器,输入 http://127.0.0.1:8000,回车,如下图,说明框架搭建成功

image-20210717201616697

1.2 配置 Django Rest Framework

  1. 启用 Django Rest Framework

在 Blog 文件夹下,打开 settings.py 文件,在 INSTALLED_APPS 的 list 中增加 rest_framework

1
2
3
4
5
6
7
8
9
python复制代码INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
]
  1. 在 settings.py 中增加 Rest Framework 的配置
1
2
3
4
python复制代码REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}

1.3 配置 Sqlite 数据库

  1. 在项目路径下,创建data文件夹
  2. 在 settings.py 中修改 DATABASES 中 default 下的 NAME 的值,增加 data 路径,接入如下
1
2
3
4
5
6
python复制代码DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'data/db.sqlite3'),
}
}

1.4 调整项目结构

  1. 修改 Blog 文件夹名称为 project

通过 PyCharm 自动生成的项目结构,会自动生成一个和项目名称一样的子文件夹,为了有效的组织后端的各个模块,这里我们将自动生成的 Blog 文件夹修改为 project

操作如图:

image-20210716174351302
image-20210716174434089
点击 Refector,然后点击左下角的 Do Refector 完成修改。

image-20210716174553811
2. 在 settings.py 文件中,将 ROOT_URLCONF 中的 Blog 修改为 project

1
python复制代码ROOT_URLCONF = 'project.urls'
  1. 在 settings.py 文件中,将 TEMPLATES 中的 DIRS 的值修改为 [BASE_DIR + '/templates']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码TEMPLATES = [    
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR + '/templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
  1. 创建 common APP,在 terminal 中输入如下命令:
1
bash复制代码python manage.py startapp common
  1. 完成后,整个项目结构如下图:

image-20210717173357712

1.5 编写 User 对象的 API

  1. 在 common/models.py 中编写基础模型抽象类 AbstractBaseModel

用来帮助构建所有业务模型自动增加创建人,创建时间,修改人,修改时间

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from django.contrib.auth.models
import AbstractUser
from django.db import models


class AbstractBaseModel(models.Model):
creator = models.IntegerField('creator', null=True)
created_at = models.DateTimeField(verbose_name='Created at', auto_now_add=True)

modifier = models.IntegerField('modifier', null=True)
modified_at = models.DateTimeField(verbose_name='Modified at', auto_now=True)

class Meta:
abstract = True

其中两个属性的入参需要说明

  • auto_now_add=True 表示在新增的时候,自动将该字段的值设置为当前时间
  • auto_now=True 表示在记录更新的时候,自动设置为当前时间
  • abstract = True 表示该类是抽象类,不需要生成物理模型
  1. 在 common/models.py 中重写User类

由于我们需要在博客中记录注册用户的昵称,头像等扩展信息,因此 Django 自带的User 模型字段无法满足,所以通过集集成Django 提供的 AbstractUser 来扩展,通过Meta类中定义我们想要的表名 blog_user 。

1
2
3
4
5
6
7
python复制代码class User(AbstractUser, AbstractBaseModel):
avatar = models.CharField('头像', max_length=1000, blank=True)
nickname = models.CharField('昵称', null=True, blank=True, max_length=200)

class Meta(AbstractUser.Meta):
db_table = 'blog_user'
swappable = 'AUTH_USER_MODEL'

如果我们不单独定义,则会用 Django 中设定的表名,这样不利于我们有效的识别和管理数据库中的表

  1. 在 common 下新增 serializers.py ,在serializers.py 中新增类 UserSerialiazer,继承 Rest Framework 提供的 serializers.ModelSerializer
1
2
3
4
5
6
7
8
python复制代码from rest_framework import serializers
from common.models import User


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'avatar', 'email', 'is_active', 'created_at', 'nickname']

这里定义需要在 API 接口中出现的字段,包括新增、修改、查询接口字段。

  1. 在 common/views.py 文件中,编写 UserViewSet 类,继承 Rest Framework 提供的 viewsets.ModelViewSet
1
2
3
4
5
6
7
8
python复制代码from rest_framework import viewsets, permissions
from common.models import User
from common.serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('username')
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]

这里只需要 Override 三个类属性,查询集合 queryset、序列器类 serialiazer_class,权限校验 permission_classes。

我们这里设置权限校验为 AllowAny,表示这个对象下的接口可以不用登录就可以访问,这么做的目的是为了下面测试接口的连通性。

  1. 修改 project/settings.py 配置

在 INSTALL_APPS 中增加 common

1
2
3
4
5
6
7
8
9
10
python复制代码INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'common',
]

新增一行代码,定义User 鉴权对应的模型,因为我们改写的默认的 User 表

1
2
python复制代码# User
AUTH_USER_MODEL = 'common.User'
  1. 定义 API 路由规则

在 common 下新增urls.py 中,并增加如下代码,这里需要定义 app_name = 'common',用于路由 Rest Framework 区别路由

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码from rest_framework import routers
from django.urls import include, path
from common import views

router = routers.DefaultRouter()
router.register('user', views.UserViewSet)

app_name = 'common'

urlpatterns = [
path('', include(router.urls)),
]

在 project/url 中引入 common APP中的路由,并加入 Rest Framework 用户鉴权路由api-auth/

1
2
3
4
5
6
7
8
python复制代码from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('common.urls', namespace='common')),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

1.6 模型迁移和配置

  1. 通过Django 做模型迁移

在 PyCharm 提供的 Terminal 中输入如下命令,完成模型创建

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
bash复制代码python manage.py makemigrations
# Migrations for 'common':
# common\migrations\0001_initial.py
# - Create model User
python manage.py migrate
# Operations to perform:
# Apply all migrations: admin, auth, common, contenttypes, sessions
# Running migrations:
# Applying contenttypes.0001_initial... OK
# Applying contenttypes.0002_remove_content_type_name... OK
# Applying auth.0001_initial... OK
# Applying auth.0002_alter_permission_name_max_length... OK
# Applying auth.0003_alter_user_email_max_length... OK
# Applying auth.0004_alter_user_username_opts... OK
# Applying auth.0005_alter_user_last_login_null... OK
# Applying auth.0006_require_contenttypes_0002... OK
# Applying auth.0007_alter_validators_add_error_messages... OK
# Applying auth.0008_alter_user_username_max_length... OK
# Applying auth.0009_alter_user_last_name_max_length... OK
# Applying auth.0010_alter_group_name_max_length... OK
# Applying auth.0011_update_proxy_permissions... OK
# Applying common.0001_initial... OK
# Applying admin.0001_initial... OK
# Applying admin.0002_logentry_remove_auto_add... OK
# Applying admin.0003_logentry_add_action_flag_choices... OK
# Applying sessions.0001_initial... OK
  1. 配置数据库管理工具

这个时候,可以在 data 文件下看到生成的 Sqlite 数据库文件 db.sqlite3,和我们在 project/settings.py 的 DATABASE 中定义的名称一致。

image-20210717200523546

双击这个文件,PyCharm 会自动在右侧的 Database 工具类中创建一个 Sqlite 的数据库记录。点击下图中表红的按钮,进入数据库配置页面。

image-20210717195549453

点击下方的 Download missing driver files,下载 Sqlite 驱动,修改 name 为 blog,在Schemas 标签页中,勾选 All shemas,点击确定。

image-20210717195453923

然后右侧就出现了我们刚刚通过 migrate 命令生成的表:

image-20210717200126701

标红的表就是我们在 common/models.py 中定义的 User 类映射出来的表,其他的表是 Django 框架内置的表,主要用在管理权限,Session,日志等。

1.7 创建管理员账号

这里通过 Django 自带的命令完成管理员账号的创建,在 PyCharm 提供的 Terminal 中,输入如下命令:

1
2
3
4
bash复制代码python manage.py createsuperuser --email admin@example.com --username blog-admin
# Password:
# Password (again):
# Superuser created successfully.

依据提示输入密码 12345678.Abc,确认输入密码,回车,完成管理员账号的创建

1.8 测试API

  1. 点击 PyCharm 右上角的运行按钮

image-20210717201808127

  1. 打开浏览器,在地址栏中输入 http://127.0.0.1:8000/

看到如下界面,说明API配置已经成功

image-20210717201333539

  1. 测试用户查询接口

点击上图中的 http://127.0.0.1:8000/user/

1
2
3
4
5
6
7
8
json复制代码HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
"user": "http://127.0.0.1:8000/user/"
}

得到如下图界面,说明接口已经完全编写成功,恭喜你,记得给你自己一个大拇指哦

image-20210717201959887

1.9 Django Rest Framework 官方英文示例教程

1
http复制代码https://www.django-rest-framework.org/tutorial/quickstart/

二、前端框架代码搭建

在介绍篇,我们已经安装了 Vite,这里我们就通过 Vite 来初始化 Vue 的项目

2.1 通过 Vite 初始化 Vue 项目

  1. 在 C:\Users\Administrator 路径下,创建文件夹 VSCodeProjects
1
2
bash复制代码cd C:\Users\Administrator
mkdir VSCodeProjects
  1. 在 VscodeProjects 文件夹下创建 blog 项目
1
2
bash复制代码cd VSCodeProjects
yarn create vite blog --template vue-ts
  1. 用 VS Code 项目

打开 C:\Users\Administrator\VSCodeProjects\Blog 文件夹,可以看到,通过 Vite 模板,已经帮我们生成好了所有基础文件,包括 TypeScript 相关的依赖和 shims 文件。

image-20210717223043228

2.2 依赖安装

  1. 安装 Less 依赖

这里的 -D 参数表示是在开发阶段的依赖,上线运行时不需要该依赖。

1
bash复制代码yarn add less@4.1.1 -D
  1. 安装 Element-Plus 依赖
1
bash复制代码yarn add element-plus
  1. 安装基础依赖,并运行

在 VS Code 中,通过快捷键 Ctrl + J 打开 Terminal,输入如下命令:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码yarn
yarn dev
# yarn run v1.22.10
# warning package.json: No license field
# $ vite
# [vite] Optimizable dependencies detected:
# vue
#
# Dev server running at:
# > Network: http://192.168.2.14:3000/
# > Local: http://localhost:3000/
  1. 在浏览器中输入地址 http://localhost:3000/, 效果如下

image-20210717223231841

三、前后端代码联调

前后端联调时,需要先在前端配置路由,Vite 代理,Axios 网络请求。

3.1 配置 Axios

  1. 安装 Axios ,在 VS Code 的Terminal 中执行命令
1
bash复制代码yarn add axios
  1. 配置 vite.config.ts

在项目根目录下,修改 vite.config.ts,代码如下:

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
typescript复制代码import vue from '@vitejs/plugin-vue'
import {defineConfig} from 'vite'


export default defineConfig({
plugins: [
vue(),
],

base: '/',

server: {
host: "localhost",
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000/',
changeOrigin: true,
ws: false,
rewrite: (pathStr) => pathStr.replace('/api', ''),
timeout: 5000,
},
},
}
});
  1. 配置 Axios 实例

在 src 目录下,新建文件夹 api,在 api 下新建文件 index.ts,编写如下代码:

1
2
3
4
5
6
7
typescript复制代码import axios, {AxiosRequestConfig, AxiosResponse} from "axios";

const request = axios.create({
baseURL: import.meta.env.MODE !== 'production' ? '/api' : '',
})

export default request;
  1. 编写请求接口的代码

在 api 下新建文件 service.ts,编写如下代码:

1
2
3
4
5
6
7
8
9
typescript复制代码import request from "./index";

export function getUserList(params: any) {
return request({
url: '/user/',
method: 'get',
params,
})
}

3.2 创建用户列表页面

修改在 src 下创建 App.vue ,在文件里面编写如下代码,获取用户列表的接口调用,调用后得到的数据加载到表格中展示,同时通过分页展示列表。

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
vue复制代码<template>
<div>
<div>
<el-table
:data="state.userList"
:header-cell-style="{ background: '#eef1f6', color: '#606266' }"
stripe
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" width="80" />
<el-table-column label="账号" prop="username" width="200" />
<el-table-column label="昵称" prop="nickname" width="200" />
<el-table-column label="状态" prop="is_active" />
</el-table>
</div>
<div class="pagination">
<el-pagination
:page-size="10"
:total="state.total"
background
layout="prev, pager, next"
></el-pagination>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
import { getUserList } from "./api/service";

export default defineComponent({
name: "App",
setup: function () {
const state = reactive({
userList: [],
params: {
page: 1,
page_size: 10,
},
total: 0,
});

const handleSearch = async (): Promise<void> => {
try {
const data: any = await getUserList(state.params);
state.userList = data.data.results;
state.total = data.data.count;
} catch (e) {
console.error(e);
}
};

handleSearch();
return {
state,
handleSearch,
};
},
});
</script>

<style scoped>
.pagination {
text-align: right;
margin-top: 12px;
}
</style>

3.3 配置 Router

  1. 安装 vue-router ,在 VS Code 的Terminal 中执行命令
1
bash复制代码yarn add vue-router@next
  1. 配置 route

在项目 src 目录下面新建 route 目录,并添加 index.ts 文件,文件中添加以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
import App from "../App.vue";

const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "User",
component: App,
meta: {}
},
]

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});


export default router;

3.4 调整 main.ts

增加 Element-Plus的组件,加载 router,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码import { createApp } from 'vue'
import App from './App.vue'
import router from "./route";
import {
ElPagination,
ElTable,
ElTableColumn
} from 'element-plus';

const app = createApp(App)

app.component(ElTable.name, ElTable);
app.component(ElTableColumn.name, ElTableColumn);
app.component(ElPagination.name, ElPagination);


app.use(router).mount('#app')

3.5 测试界面

  1. 在 PyCharm 中启动后端服务
  2. 在 VS Code 中启动前端
1
bash复制代码yarn dev
  1. 在浏览器中输入http://127.0.0.1:8080 得到如下效果

image-20210718175955373

到这一步,我们已经完成了前后端联调,第一个接口已经通过界面方式完成调用和数据展示,给自己一个赞哦。

四、代码纳入Git 版本管理

还记得我们在第一篇文章中创建的代码仓库吧,现在我们要把刚刚创建的前端代码和后台代码提交到代码仓库中。

这里以后端为例,我之前是在Gitee上创建了一个公开仓库:https://gitee.com/Zhou_Jimmy/blog-backend.git,现在就把后端代码提交到这个仓库中。

命令在 PyCharm 提供的 Terminal 中输入

4.1 配置本地的 git 信息

1
2
bash复制代码git config --global user.name "Zhou_Jimmy"
git config --global user.email "331352343@qq.com"

在设置的时候记得修改成你自己的名字和邮箱哦

4.2 本地代码初始化git

1
2
3
bash复制代码git init
# Initialized empty Git repository in C:/Users/Administrator/PycharmProjects/Blog/.git/
git remote add origin https://gitee.com/Zhou_Jimmy/blog-backend.git

在设置的时候,记得修改成你自己的仓库地址

4.3 拉取远程仓库上的代码

1
bash复制代码git pull origin master

4.4 建立忽略文件

在项目根路径下增加或编辑 .gitignore 文件,忽略不用加入到版本管理的文件,文件中的内容可以参考 Gitee 或 GitHub 提供的模板,这里主要增加 PyCharm 配置信息的文件夹

1
ini复制代码.idea

如果是前端代码,则增加这么一行

1
ini复制代码.vscode

4.5 提交代码

4.5.1 通过 Git 命令提交

可以通过如下命令完成提交,最后需要你输入仓库账号和密码,完成后,代码就提交到了远程仓库了。

1
2
3
bash复制代码git add .
git commit -m "Django框架初始化代码提交"
git push origin master

4.5.2 通过 PyCharm 界面功能提交

可以点击 PyCharm 右上角的提交按钮

image-20210717205924922

点击后,左侧出现提交文件选择框,勾选文件,并填写comment,点击 Commit and Push 按钮,完成提交。

image-20210717210041771

后端提交完成后,远程仓库效果如下:

image-20210717210532928

前端提交完成后,远程仓库效果如下:

image-20210718181424047

本文转载自: 掘金

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

在Docker中对Oracle进行容器化部署,看这一篇就够了

发表于 2021-08-08

​

写在前

由于本机mac空间不够(哈哈,促进我学习),所以想搭建一个云Oracle环境。

之前写过windows搭建的,但是之前11g(项目要求)一直安装不上,索性就把服务器换成linux,准备改用docker的方式来进行搭建。


总体步骤

  1. 阿里云申请 linux 服务器,这里我的是 CentOS 7.4 。
  2. 安装 docker 。
  3. 下载 Oracle 镜像并安装。
  4. 导入数据(通过DMP导入,需创建表空间,用户等)。

Docker简介

Docker简单来说,就是一个虚拟机,你要什么应用,就直接输入命令安装,不用就直接删掉,沙盒傻瓜式。官方logo就是鲸鱼上面载着货物,这里我主要是用来安装 Oracle,mysql,tomcat什么的。logo如下图:

image.png

废话不多说,就让我们开始吧。


一,安装docker

对于 linux 服务器来说,安装docker非常简单,不过需要 CentOS 内核版本高于 3.10,使用uname -r 来查看。如下图:

​

具体的安装步骤,可以参考这篇博文 blog.csdn.net/qq_36892341…,写得很清楚,就不再赘述,如有不懂,可以留言。


二,下载Oracle镜像并安装

使用docker search oracle 来搜索 oracle 的版本,我这里选用的是 registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g ,这是阿里云的一个 oracle 11g 版本,需要其他版本的小伙伴可以自行选择。

​

使用如下命令来进行镜像的拉取

1
bash复制代码docker pull registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g

拉取完后,可以使用 docker images 来查看已有镜像

​

可以看到,REPOSITORY 就是镜像名称,TAG 是版本号,IMAGE ID 是镜像的编号,后面的见名知意了。

启动 Oracle 服务,使用如下命令即可,-p 将容器的 8080 端口映射到主机的 8080 端口(-p 主机端口:容器端口)

1
css复制代码docker run --name registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g -p 1521:1521

之后使用 docker ps 来看启动中的容器。

​

这里可以看到,oracle 服务已经启动(即 ff94a17f84a7)


三,导入数据、用户及表空间创建

3.1,导入 dmp 数据文件

首先,需要从本机拷贝 dmp(从其他库导出的数据文件,包含数据 + 表结构等) 数据到 Oracle 容器中

格式:docker cp /本地文件地址 容器ID:/容器文件存储地址,如下:

1
bash复制代码docker cp /home/oracle/data_20181104.dmp ff94a17f84a7:/home/cloudera

【注:容器ID(这里我的是ff94a17f84a7)请换成自己的】

3.2,创建表空间及用户

使用如下命令进入 docker 容器内

1
bash复制代码docker exec -it ff94a17f84a7 bash

​

使用数据库管理员方式进行登录

这里我使用 sqlplus,提示 command not found ,不用担心,使用 su - oracle 切换下用户到 oracle 用户 ,然后 sqlplus,之后 sys/sys as sysdba 进行登录,如下图,登录成功!

​

进行表空间的创建:

这里分为四小步:

1,创建临时表空间:

1
lua复制代码create temporary tablespace TEST_DBF_TEMP tempfile '/home/oracle/data/TEST_DBF_TEMP.dbf' size 50m autoextend on next 50m maxsize 2048m;

​

2,创建正式表空间:

1
lua复制代码create  tablespace TEST_DBF datafile '/home/oracle/data/TEST_DBF.dbf' size 50M autoextend on next 50m maxsize 2048m;

​

3,创建用户并且指定表空间:

1
sql复制代码create user TEST identified by "123" default tablespace TEST_DBF temporary tablespace TEST_DBF_TEMP;

​

4,用户授权:

1
sql复制代码grant create session,connect,resource,dba to TEST;

​

3.3,导入 dmp 文件:

1
ini复制代码imp TEST/123 file = /data_20181210.dmp full=y;

静静等待导入完成即可。


PS:阿里云记得去云服务器控制台开放数据库对应的端口即可外网连接。

​

配置规则

​

选择克隆一个,输入 docker ps 中 oracle 对应的主机端口即可:

​

配上成功图:

​

Bingo!


常见问题(不定时更新):

Oracle服务无法连接&挂掉重启

今天起来准备工作,Oracle无法连接了,可能原因:

  1. 连接时使用的sid不正确,进入容器,连接后使用 SELECT name FROM v$database; 查看SID。
  2. dokcer容器中 Oracle 挂了。
  3. 其他未知原因(如电脑没有连网,磁盘空间满了,用户名密码错误等)。

马上进服务器去docker ps 查询在线容器,但是发现Oracle还在。

)​

于是,docker exec -it ff94a17f84a7 bash 进入到容器 。

lsnrctl start启动监听程序,如下图,提示已经启动了。

​

sqlplus /nolog进入,conn / as sysdba管理员连接,然后startup ,如下图,提示启动成功,数据库打开。

​

Bingo!


重启容器后,端口变更,设置固定端口

  1. 查看需要修改的容器,记住 container id

docker ps -a

​

  1. 停止容器

docker stop xxx

  1. 修改容器的端口映射配置文件

vim /var/lib/docker/containers/{container_id}/hostconfig.json

1
2
3
4
5
6
7
8
json复制代码"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "8080"//宿主机ip
}
]
},

4.重启docker服务

service docker restart

5.启动容器

docker start xxx


Error response from daemon: Cannot XX container

这个错误还真的是奇葩,我放个元旦回来,就什么都操作不了了?

)​

解决方案

重启 docker 服务

1
复制代码systemctl restart docker

重启容器成功

)​

​

本文转载自: 掘金

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

内部类(成员内部类、局部内部类(包括匿名内部类))

发表于 2021-08-08

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

一个事物的内部包含另一个事物。一个类里面包含另一个类,这个类叫内部类,包含它的叫它外部类。

例如:身体和心脏的关系;汽车和引擎的关系。

心脏、引擎只有在身体和汽车中才有用。内部类也一样。

分类:

  1.成员内部类;

  2.局部内部类(包括匿名内部类);

1.成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/*
修饰符 class 外部类名称{
修饰符 class 内部类名称{
......
}
......
}
*/
public class Outer {
private String name;

public class Inter{
public void InterMethod(){
System.out.println("内部方法");
System.out.println("姓名:" + name); // 成员内部类可以访问外部类属性
}
}
public void fun1(){
Inter in = new Inter();
in.InterMethod();
}
}

编译后,这个类的class文件保存在磁盘里

内部类的使用方式

  在外部类中可以直接通过 new 对象的方式使用。

  在其他类中访问:

    1.间接方式:在外部类的方法中使用内部类,而在其他类中使用就 new 外部类调用这个方法;

1
2
3
4
5
6
7
8
9
java复制代码/**
* 其他类使用内部类
*/
public class OuterDemo1 {
public static void main(String[] args) {
Outer out = new Outer();
out.fun1();
}
}

    2.直接方式,直接创建出内部类

    公式: 外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();

1
2
3
4
5
6
7
java复制代码public class OuterDemo2 {
public static void main(String[] args) {
Outer.Inter in = new Outer().new Inter();

in.InterMethod();
}
}

问题:在外部类、内部类、内部类方法体出现相同名称属性,如何输出相应的值呢?

解决:

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

private String name ="外";

class Inter{
private String name = "内";
public void interMethod(){
String name = "方法";
System.out.println(name); // 输出: 方法,就近原则
System.out.println(this.name); // 输出: 内
System.out.println(OuterDemo3.this.name); // 输出: 外
}
}
}

2.局部内部类

  在方法体内定义一个类,出了这个方法,就无法使用这个类(所以其他类无法使用【局部内部类】)

  普及:

  权限修饰符的使用规则:

  public > protected > (default) > private

  1.外部类:能使用 public / (defautl) 修饰

  2.成员内部类: public / protected / (default) / private

  3.局部内部类:什么都不写

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

public void fun(){
final int num = 100;
class Fun{
private void fun2(){
System.out.println(num);
}
}
Fun fun = new Fun();
fun.fun2();
}
}

问题:为什么访问所在方法的局部变量,必须要有final修饰?

原因(本质是生命周期问题):

  1.内部类 new 出来的对象在堆内存中;

  2.局部变量跟着方法,在栈内存中;

  3.方法运行完,立刻出栈,局部变量跟着消失;

  4.但 new 出来的对象,会在堆内存持续存在,直到垃圾回收;

  5.所以,要将该内存复制到常量池才能保存继续使用。

3.匿名内部类(重要)

  往常,我们要使用接口方法,得先定义该接口的实现类 -> 重写该接口的所有抽象方法 -> new 实现类使用。

  而如果接口的实现类只是用唯一的一次,那么这种情况就可以省略该实现类的定义,而改为使用 【匿名内部类】

接口

1
2
3
java复制代码public interface MyInteface {
void method();
}

使用【匿名内部类】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 格式:
* 接口名称 对象名 = new 接口名称(){
* // 覆盖重写所有抽象方法
* };
*/
public class AnonymityTest2 {
public static void main(String[] args) {
MyInteface my = new MyInteface(){
@Override
public void method() {
System.out.println("匿名内部类方法");
}
};
my.method();
}
}

  很多人一开始可能会有误解:不是【匿名内部类】吗? MyInteface my = new MyInteface(){…} 这不是有名字么?

  先看,对于”new MyInteface(){…};” 的解析:

    1). new 代表对象创建的动作;

    2). 接口名称 【匿名内部类】要实现的接口;

    3). {…} 这才是【匿名内部类】的内容,里面重写着接口的所有抽象方法

  它光秃秃的,的确没名没姓的。

  而 MyInteface my = new MyInteface(){…} 中的 my 是对象名,它是供你调用匿名类方法的对象。

ps:匿名内部类、匿名对象

1、【匿名内部类】表示,在创建对象是,只能使用唯一一次,一般用于书写接口的实现类。

    如果希望多次创建对象,而且类的内容一样的话,那还是单独定义实现类更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class AnonymityTest2 {
public static void main(String[] args) {
MyInteface my1 = new MyInteface(){
@Override
public void method() {
System.out.println("匿名内部类方法");
}
};
my1.method();
MyInteface my2 = new MyInteface(){
@Override
public void method() {
System.out.println("匿名内部类方法");
}
};
my2.method();
}
}

2.【匿名对象】表示,在调用方法时,只能调用唯一一次。

    如果希望同一对象,调用多次方法,那么还是给对象起个名把。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码new MyInteface(){
@Override
public void method1() {
System.out.println("匿名内部类方法1");
}

@Override
public void method2() {
System.out.println("匿名内部类方法2");
}
}.method1();

new MyInteface(){
@Override
public void method1() {
System.out.println("匿名内部类方法1");
}
@Override
public void method2() {
System.out.println("匿名内部类方法2");
}
}.method2();

3.两者不是一回事

【匿名内部类】是省略了 <实现类 / 子类>

【匿名对象】是省略了 <对象名称>

两者不是一回事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class AnonymityTest {
public static void main(String[] args) {
fun1();
}

private static void fun1() {
// 对于 Thread 来说,这就是【匿名对象】
// 对于 Runnable 来说,这就是【匿名内部类】
new Thread( new Runnable(){
@Override
public void run() {

}
}).start();
}
}

本文转载自: 掘金

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

CAS之ABA问题的解决方法

发表于 2021-08-08

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

AtomicReference

java.util.concurrent.atomic包下的AtomicInteger类可以对整数进行包装,使得类似于i++的操作可以变成原子操作,那么对于一般的对象类型要怎么实现原子操作呢,从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class AtomicReferenceDemo {
static AtomicReference<Student> stu = new AtomicReference<>();

public static void main(String[] args) {
Student lemon = new Student("lemon", 17);
stu.set(lemon);
Student blue = new Student("blue", 19);
boolean res = stu.compareAndSet(lemon, blue);
System.out.println(res + "\tstu=" + stu.get());
}
}

运行结果:

true stu=Student{name=’blue’, age=19}

但是和原来使用AtomicInteger一样,CAS的时候仍旧会产生ABA问题

产生ABA问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class ABADemo {
public static void main(String[] args) {
//ABA问题的代码
AtomicReference<Integer> atomicReference = new AtomicReference<>();
atomicReference.set(100); //原来的值是100
new Thread(() -> {
boolean res1 = atomicReference.compareAndSet(100, 101);
boolean res2 = atomicReference.compareAndSet(101, 100);
System.out.println("res1=" + res1);
System.out.println("res2=" + res2);
}).start();
new Thread(() -> {
boolean res3 = atomicReference.compareAndSet(100, 200);
System.out.println("res3=" + res3);
}).start();

}

}

运行结果:res1,res2,res3都是true,这说明虽然线程t1修改了atomicReference中的值,但是由于修改后值与原来的一样,所以线程t2在判断的时候认为该值没有被修改过CAS操作成功。

AtomicStampedReference解决ABA问题

为了解决ABA问题,引入了AtomicStampedReference,AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(版本号)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。

AtomicStampedReference的构造器

在初始化的时候传入两个参数,初始化的引用值和版本号

1
2
3
java复制代码public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}

AtomicStampedReference的compareAndSet方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public boolean compareAndSet(V   expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
  • expectedReference : 期望值
  • newReference : 想要更新成的新的值
  • expectedStamp : 期望的版本号
  • newStamp : CAS操作成功要更新成的版本号
  • 然后每次操作的时候都会先比较版本号,版本号一致才能操作成功,每次操作成功后都将版本号增加+1(版本号只加不减)*

测试代码

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
java复制代码import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolve {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
int stamp = atomicStampedReference.getStamp();
new Thread(() -> {
System.out.println("t1线程拿到的初始版本号:" + stamp);
System.out.println("t1线程拿到的初始值:" + atomicStampedReference.getReference());
//
boolean res1 = atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp() + 1);
System.out.println("修改结果:" + res1);
System.out.println("t1线程修改之后的版本号:" + atomicStampedReference.getStamp());
System.out.println("t1线程修改之后的值:" + atomicStampedReference.getReference());
boolean res2 = atomicStampedReference.compareAndSet(101, 100, 2, atomicStampedReference.getStamp() + 1);
System.out.println("修改结果:" + res2);

}).start();
new Thread(() -> {
System.out.println("t2线程拿到的初始版本号:" + stamp);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
boolean b1 = atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp() + 1);
System.out.println("t2线程期望的版本号为" + stamp + ",t2线程当前查看主内存中atomicStampedReference真实的版本号为:" + atomicStampedReference.getStamp());
System.out.println("t2线程CAS操作的结果为:" + b1);
}).start();
}
}

该代码模拟了线程1将atomicStampedReference值修改后又改回成原来的值的过程,观察版本号的变化以及最后线程2的CAS操作成功与否,运行结果如下:

1
2
3
4
5
6
7
8
9
java复制代码t1线程拿到的初始版本号:1
t1线程拿到的初始值:100
修改结果:true
t1线程修改之后的版本号:2
t1线程修改之后的值:101
修改结果:true
t2线程拿到的初始版本号:1
t2线程期望的版本号为1,t2线程当前查看主内存中atomicStampedReference真实的版本号为:3
t2线程CAS操作的结果为:false

总结

1.AtomicReference使得对象类型可以想整数类型一样保证包装,实现原子操作

2.AtomicStampedReference在原来AtomicReference基础上加入了stamp(版本号)这一属性,每次操作成功必定增加版本号,使得在CAS的时候不会出现ABA问题(因为数据被修改过版本号肯定不一样)。

本文转载自: 掘金

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

可视化监控应用性能-skywalking

发表于 2021-08-08

微服务体系下需要对各个服务进行监控,或者在对服务进行压测的时候都需要观察服务性能,我们选择使用skywalking来完成这个功能,skywalking简单操作就可以使用,也方便监控服务运行情况。

官网:skywalking.apache.org/docs/

中文文档:skyapm.github.io/

一. 概述

基于Skywalking,提供分布式服务链路追踪,服务依赖拓扑绘制,慢端点、慢SQL等面板展示能力,以及服务告警能力,同时自带显示面板,可以提供前后端调用链整合能力。支持多种语言:如 Java, C# , Node.js , Go , PHP 以及 Nginx LUA。

SkyWalking 为 服务(service) , 服务实例(service instance) , 以及 端点(endpoint) 提供了可观测能力。

二. 架构

SkyWalking 逻辑上分为四部分: 探针, 平台后端, 存储和用户界面.

image.png

  • 探针 基于不同的来源可能是不一样的, 但作用都是收集数据, 将数据格式化为 SkyWalking 适用的格式.
  • 平台后端, 支持数据聚合, 数据分析以及驱动数据流从探针到用户界面的流程。分析包括 Skywalking 原生追踪和性能指标以及第三方来源,包括 Istio 及 Envoy telemetry , Zipkin 追踪格式化等。
  • 存储 通过开放的插件化的接口存放 SkyWalking 数据. 你可以选择一个既有的存储系统, 如 ElasticSearch, H2 或 MySQL 集群(Sharding-Sphere 管理),也可以选择自己实现一个存储系统. 当然, 我们非常欢迎你贡献新的存储系统实现。
  • UI 一个基于接口高度定制化的Web系统,用户可以可视化查看和管理 SkyWalking 数据。

三. 安装-java版

1. 基本环境

需要的jdk和elasticsearch的下载安装自行百度

下载对应的apm包,skywalking下载地址:skywalking.apache.org/downloads/
安装:

1
2
3
复制代码jdk1.8
elasticsearch-7.8.1
apache-skywalking-apm-es7-8.3.0

image.png

2. 解压apache-skywalking-apm-es7-8.3.0.tar.gz并配置
  • 解压后的内容

  • 修改config/application.yml中配置存储(本次测试使用elasticsearch7)
    存储有多种方式:elasticsearch6,7/h2/mysql/tidb/influxdb

配置如下

1
2
3
4
5
6
7
css复制代码storage:
selector: ${SW_STORAGE:elasticsearch7}
elasticsearch7:
  nameSpace: ${SW_NAMESPACE:""}
  clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}
  user: ${SW_ES_USER:""}
  password: ${SW_ES_PASSWORD:""}
3. 启动skywalking

启动之前确认elasticsearch已经启动且正常运行

在bin目录下执行 startup.sh脚本启动

访问localhost:8080即可打开UI界面

四,启动应用程序

  • -Dskywalking.agent.namespace 命名空间—可以区分不同的环境
  • -Dskywalking.agent.service_name 服务的名字
  • -Dskywalking.collector.backend_service skywalking的接口地址(启动默认端口11800)

jar启动:(包含skywalking-agent.jar完整路径,而且不能移动jar)

1
bash复制代码java -javaagent:/developSoftware/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dskywalking.agent.namespace=ajisun-dev -Dskywalking.agent.service_name=ajisun-dev:ajisun-platform -Dskywalking.collector.backend_service=localhost:11800 -jaryourApp.jar

idea中启动:(在VM options中写入)

1
bash复制代码-javaagent:/developSoftware/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dskywalking.agent.namespace=ajisun-dev -Dskywalking.agent.service_name=ajisun-dev:ajisun-platform -Dskywalking.collector.backend_service=localhost:11800

如图:

image.png

启动后效果:

五,功能介绍

1. 仪表盘
  1. 吞吐量CPM,表示每分钟的调用.
  2. Apdex分数:衡量服务器性能的标准
  3. 响应时间百分比,包括 p99, p95, p90, p75, p50.
  4. SLA表示成功率。对于HTTP,表示响应为200的请求.

监控数据汇总:

image.png

服务维度的数据:

image.png

2. 拓扑图

拓扑图用指标显示服务和实例之间的关系.,点击服务显示监控数据

image.png

  1. 拓扑显示包含所有服务的默认全局拓扑.
  2. 服务选择器 支持显示直接关系,包括上游和下游.
  3. 自定义组 提供服务组的任意子拓扑功能.
  4. 服务下钻 当单击任何服务时打开。该图形可以对所选择的服务进行度量、跟踪和告警查询.
  5. 服务指标的关系 提供服务RPC交互的度量以及这两个服务的实例.
3. 跟踪查询

跟踪查询是与skywalk提供的分布式代理一样的典型特性.

image.png

  1. 跟踪部分列表 不是跟踪列表。每个跟踪都有几个属于不同服务的段。 如果通过所有服务或通过跟踪id进行查询,可以在其中列出具有相同跟踪id的不同段.
  2. 跨度 是否可单击,每个跨度的细节将在左侧弹出.
  3. 跟踪视图 提供3个典型的和不同的使用视图来可视化跟踪.
  4. 关联的服务 显示接口调用中经过的所有服务
4. 性能剖析

一个交互特性。提供了方法级的性能诊断.

  1. 选择特定的服务。
  2. 设置端点名称。这个端点名通常是第一个span的操作名。在追踪查询上找到这个 段列表视图。
  3. 监控时间可以从现在开始,也可以从未来的任何时间开始。
  4. 监视持续时间定义了观察时间窗口,以查找合适的请求进行性能分析。 即使概要文件对目标系统的性能影响非常有限,但它仍然是一个额外的负载。这个时间 使冲击可控。
  5. 最小持续时间阈值提供了一个过滤器机制,如果给定端点响应的请求很快,它就不会异形。这可以确保配置的数据是预期的数据。
  6. 最大抽样计数表示agent将收集的最大数据集。它有助于减少内存和网络负载。
  7. 一个隐式条件,在任何时候,skywalk只接受一个配置文件任务的每个服务。
  8. 代理可以有不同的设置来控制或限制此特性,请阅读文档设置以了解更多细节。
  9. 并不是所有的SkyWalking生态系统代理都支持此特性,7.0.0中的java代理默认支持此特性。

一旦配置文件完成,配置的跟踪段就会显示出来。你可以要求分析任意张成的空间。 通常,我们分析跨度具有较长的自持续时间,如果跨度及其子跨度都具有较长的持续时间,则可以进行选择 “包括子跨度”或“排除子跨度”来设定分析界限。

选择正确的跨度后,单击“analysis”按钮,您将看到基于堆栈的分析结果。最慢的方法 已被高亮显示


参考:skyapm.github.io/

本文转载自: 掘金

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

SpringBoot+Redis 实现消息订阅发布 什么是

发表于 2021-08-08

什么是 Redis

Redis 是一个开源的使用 ANSI C语言编写的内存数据库,它以 key-value 键值对的形式存储数据,高性能,读取速度快,也提供了持久化存储机制。

img

Redis 通常在项目中用的最多的功能是缓存,然而今天靓仔将为你介绍它的另一个功能,轻量级的消息队列。

Redis 发布订阅

机制

Redis 提供了发布订阅功能,可以用于消息的传输,Redis 的发布订阅机制包括三个部分,发布者,订阅者和 Channel(称之为频道或主题)。

img

发布者将消息发布到指定频道,订阅该频道的订阅者就能够接受到这条消息,如果有多个订阅者,那么他们会接受到相同的消息。

功能实现

发布消息

Redis 采用 PUBLISH 命令发送消息,其返回值为接收到该消息的订阅者的数量。

img

订阅频道

Redis 采用 SUBSCRIBE 命令订阅某个频道,其返回值包括客户端订阅的频道,目前已订阅的频道数量,以及接收到的消息,其中subscribe表示已经成功订阅了某个频道。

img

模式匹配

模式匹配功能允许客户端订阅符合某个模式的频道,Redis采用PSUBSCRIBE订阅符合某个模式所有频道,用“ * ”表示模式,“ * ”可以被任意值代替。

img

假设客户端同时订阅了某种模式和符合该模式的某个频道,那么发送给这个频道的消息将被客户端接收到两次,只不过这两条消息的类型不同,一个是 message 类型,一个是 pmessage 类型,但其内容相同。

取消订阅

Redi s采用 UNSUBSCRIBE 和 PUNSUBSCRIBE 命令取消订阅,其返回值与订阅类似。由于Redis的订阅操作是阻塞式的,因此一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。在 SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE 和 PUNSUBSCRIBE 命令中,其返回值都包含了该客户端当前订阅的频道和模式的数量,当这个数量变为0时,该客户端会自动退出订阅状态。

SpringBoot+Redis 实现发布订阅

springboot 如何整合 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
typescript复制代码@Configuration
public class RedisSubConfig {

public static final String SUB_KEY = "chat";//频道channel

/**
* redis消息监听器容器
* 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
* 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);

//订阅了一个频道
container.addMessageListener(listenerAdapter, new PatternTopic(RedisSubConfig.SUB_KEY));
return container;
}

/**
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}

/**
* redis 读取内容的template
* @param connectionFactory
* @return
*/
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}

接收消息

1
2
3
4
5
6
7
typescript复制代码@Service
public class RedisReceiver {

public void receiveMessage(String message) {
System.out.println("接收消息:" + message);
}
}

采用定时器发布消息

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@EnableScheduling //开启定时器功能
@Component
public class MessageSender {

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Scheduled(fixedRate = 5000) //间隔5s 通过StringRedisTemplate对象向redis消息队列chat频道发布消息
public void sendMessage(){
stringRedisTemplate.convertAndSend("chat", "hello "+ new Date());
}
}

运行结果

img

对比RabbitMQ发布订阅模式

  • Redis:轻量级,低延迟,高并发,低可靠性;
  • RabbitMQ:重量级,高可靠,异步,不保证实时;
  • RabbitMQ是一个专门的 AMQP 协议队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而Redis主要是用于缓存的,Redis 的发布订阅模式,可用于实现及时性,且可靠性低的功能。

往期推荐

最详细的图文解析Java各种锁(终极篇)

一定要收藏的5个优秀的SpringCloud开源项目

一定要收藏的5个后台管理系统的前端框架

MySQL 三大日志你了解多少?

图文详解 23 种设计模式

本文转载自: 掘金

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

Spring Boot 回顾(七):告别臃肿的字段校验--

发表于 2021-08-07

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

前言

在我们Spring Boot开发项目过程中,往往可以通过使用一些注解,让我们的代码看上去更加地优雅且高效,接下来的系类更文将介绍一些这样的注解给大家。

@Valid

大家在写业务代码时一定都有遇到需要对前端传来的参数进行校验的场景。虽然说一般前端都会在页面输入时进行了一次校验,但是为了保证功能的健壮性,后端这边在接收到前端参数时需要再次进行校验。如果只是一两个参数还好,写起来简单,一般几个if判断就行了,代码看上去还是很简洁的。但是遇到有十几个参数的表单提交过来,每个都写上一个if校验,恐怕代码review时技术经理的脸色要扭曲了吧。下面我们了解下@Valid注解,看看它是如何帮助我们消去这些臃肿的校验的。首先,我们在项目中引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
java复制代码<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>

然后就能在项目中愉快的使用相关校验的注解了

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 链名称
*/
@NotBlank(message = "{required}")
@Size(max = 100, message = "{noMoreThan}")
private String chainName;

/**
* 链id
*/
@NotBlank(message = "{required}")
@Size(max = 100, message = "{noMoreThan}")
private String chainId;

可以看到,我们在字段上加入了@NotBlank和@Size,分别进行了非空和字段长度的校验。最后,我们在controller中加上@valid来使校验生效。

image.png
写完这些,是不是感觉整个代码清爽了很多,再也不用写令人头疼的if校验了。
当然@valid中支持相当多的字段校验逻辑,我们简单了解下

注解名称 含义
@NotBlan 必须不能为空
@Size 长度校验,里面可以加入max、min
@URL 必须是一个URL
@Email 必须是格式正确的电子邮箱地址
@Pattern 符合指定的正则表达式
更多内容可以看下jar包里已有的注解

image.png

总结

通过这样一个简单的注解,可以让我们的代码看起来更加的简洁,也让校验逻辑更加清晰。所以我们写功能时,可以多思考如何用更少的代码来实现,避免臃肿的代码堆砌在项目中。

本文转载自: 掘金

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

1…573574575…956

开发者博客

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