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

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


  • 首页

  • 归档

  • 搜索

spring中需要掌握的25个常用注解

发表于 2021-08-29

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

前言

注解本身没有功能的,就和 xml 一样。注解和 xml 都是一种元数据,元数据即解释数据的数据,这就是所谓配置。

常用注解

@Component

表示一个带注释的类是一个“组件”,成为Spring管理的Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component还是一个元注解。

@Service

组合注解(组合了@Component注解),应用在service层(业务逻辑层)。

@Repository

组合注解(组合了@Component注解),应用在dao层(数据访问层)。

@Controller

组合注解(组合了@Component注解),应用在MVC层(控制层),DispatcherServlet会自动扫描注解了此注解的类,然后将web请求映射到注解了@RequestMapping的方法上。

@RequestMapping

用于映射Web请求,包括访问路径和参数。(类或方法上)

@ResponseBody

支持将返回值放在response内,而不是一个页面,通常用户返回json数据。(返回值旁或方法上)

@RequestBody

允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前)

@PathVariable

用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。

@RestController

该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。

@ExceptionHandler

用于全局处理控制器里的异常。

@ModelAttribute

本来的作用是绑定键值对到 Model 里,在 @ControllerAdvice 中是让全局的@RequestMapping都能获得在此处设置的键值对。

@Autowired

Spring提供的工具(由Spring的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。

@Configuration

声明当前类是一个配置类(相当于一个Spring配置的xml文件)(类上)。

@Bean

注解在方法上,声明当前方法的返回值为一个Bean。返回的Bean对应的类中可以定义init()方法和destroy()方法,然后在@Bean(initMethod=”init”,destroyMethod=”destroy”)定义,在构造之后执行init,在销毁之前执行destroy。(方法上)

@ComponentScan

自动扫描指定包下所有使用@Service、@Component、@Controller、@Repository的类并注册(类上)。

@Aspect

声明一个切面(类上) 使用@After、@Before、@Around定义建言(advice),可直接将拦截规则(切点)作为参数。

@After :在方法执行之后执行(方法上)。

@Before: 在方法执行之前执行(方法上)。

@Around: 在方法执行之前与之后执行(方法上)。

@PointCut: 声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)。

@Scope

定义我们采用什么模式去创建Bean(方法上,得有@Bean) 其设置类型包括:

Singleton (单例,一个Spring容器中只有一个bean实例,默认模式), Prototype(每次调用新建一个bean), Request (web项目中,给每个http request新建一个bean), Session(web项目中,给每个http session新建一个bean), GlobalSession(给每一个 global http session新建一个Bean实例)。

@PostConstruct

标注在方法上,该方法在构造函数执行完成之后执行。

@PreDestory

标注在方法上,该方法在对象销毁之前执行。

@Value

经常与Sping EL表达式语言一起使用,注入普通字符,系统属性,表达式运算结果,其他Bean的属性,文件内容,网址请求内容,配置文件属性值等。

@EnableAsync

配置类中,通过此注解开启对异步任务的支持,叙事性AsyncConfigurer接口。(类上)

@Async

在实际执行的bean方法使用该注解来申明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync开启异步任务)

@EnableScheduling

在配置类上使用,开启计划任务的支持。(类上)

@Scheduled

来申明这是一个任务,包括cron,fixDelay,fixRate等类型。(方法上,需先开启计划任务的支持)

@Enable*注解说明

这些注解主要用来开启对xxx的支持。 @EnableAspectJAutoProxy 开启对AspectJ自动代理的支持。

@EnableAsync: 开启异步方法的支持。

@EnableScheduling: 开启计划任务的支持。

@EnableWebMvc :开启Web MVC的配置支持。

@EnableConfigurationProperties :开启对@ConfigurationProperties注解配置Bean的支持。

@EnableJpaRepositories:开启对SpringData JPA Repository的支持。

@EnableTransactionManagement:开启注解式事务的支持。

@EnableTransactionManagement: 开启注解式事务的支持。

@EnableCaching: 开启注解式的缓存支持。

本文转载自: 掘金

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

IDEA + Vim = 得劲

发表于 2021-08-29

图片

本教程并不是单纯的 vim 操作介绍,更多的是与 Intellj Idea 进行配合。需要同时具备 Intellj Idea 和 vim 使用基础的同学学习。


01. 简介

Vim 是一个高度可配置的文本编辑器,旨在高效地创建和更改任何类型的文本。它作为“vi”包含在大多数 UNIX 系统和 Apple OS X 中。Vim 非常稳定,并且正在不断开发以变得更好。

它的特点包括:

  1. 持久的、多级撤消树
  2. 广泛的插件系统
  3. 支持数百种编程语言和文件格式
  4. 强大的搜索和替换
  5. 与许多工具集成

为什么要在 Intellj Idea 中使用 Vim?在我们学习某一项新技能,或者新的编程语言,新的编程模式。我们应该自问让我们做出这样的行为的驱动力是什么?学习完它之后它能解决你什么样的疼点问题。这样我们学习起来会更有动力一些。

让我尝试学习 Vim 的理由:

Mac 平台下 Intellj Idea 的很多的常用的 action 都是需要 command + option 这个组合键,从我转到 Mac 下,就一直很懊恼,个人感觉这个按键很反人类。

作为一名服务端开发同学,还是会经常需要在 linux 服务器上去修改服务器的系统配置文件。比如 nginx 配置,这个时候不可避免的需要使用到 vim 编辑器。如果你不熟练 操作起来还是相当的难受的。

在 Intellj Idea 中安装了 vim 插件之后。是一个互补的状态 利用各自的优势 使得编程更有效率。


02. 安装

图片

在 Intellj Idea 中 安装以下三个插件

  • IdeaVim
  • IdeaVim-EasyMotion
  • IdeaVimExtension

图片

发现图标像上面一样编程一个小方块的时候 就代表插件安装成功


03. Vim

不得不说 vim 的学习成本还是有的,典型的入门很难 入门之后就会爱不释手。我这里对一些基础操作进行一些简单的总结和整理。

练习一:方向键联系 hjkl

图片

根据我自己的经验总结下来从原生编辑器过度到 Vim 最不习惯的当属方向键的移动从原本的 方向键变成了 hjkl

这个其实没有太多好说的 就是需要克服这一关。多练就行。(下图为 Gif 演示)

图片

练习二:复制 & 粘贴

图片

CV 大法好,毕竟大家有时候自嘲都是 C V 工程师 😈 所以 复制 粘贴 还是需要在第一时间学会,这样才能更好的 coding💩。

  • yy 复制当前行
  • p 粘贴

图片

练习三:在学多一点点

图片

在学习一些其他的命令 提交效率。

y p f h c i

图片

GIF 中步骤如下:

  1. yy 复制当前行 private String username;
  2. p 将复制的内容进行粘贴
  3. f; 跳转到字符 ; 处,这里其实就是相当于行位。
  4. h 向左移动一个光标
  5. ciw 删除当前处的词,并进入 插入模式
  6. password 输入 password 完成

v j d

图片

GIF 中步骤如下:

  1. V 进入内容选择模式
  2. kkk 向上移动 3 行,并进行选中
  3. d 删除选中部分

块级删除「常用」di* da*

图片

di" 删除" 内的内容,个人感觉这个还是非常常用了。也是初步在效率上优于传统的 intellj idea 的操作方式。如果想要删除之后马上进入插入模式 可以使用 ci"

图片

di( 删除( 内的内容,如果想要删除之后马上进入插入模式 可以使用 ci(

图片

di{ 删除{ 内的内容,如果想要删除之后马上进入插入模式 可以使用 ci{

各种进入插入模式

  • i在光标之前,进入插入模式
  • I在本行开头,进入插入模式
  • a在光标之后,进入插入模式
  • A在本行结尾,进入插入模式
  • o在本行之后新增一行,并进入插入模式
  • O在本行之前新增一行,并进入插入模式
  • s删除当前字符,并进入插入模式
  • S删除当前行中的所有文本,并进入插入模式

在我最开始的学习 vim 的时候只知道 i 进入插入模式。后面才知道还有这么多种方式。

如果你真的是切换过来之后 这里所有的方式都能用到。

下面是 gif 演示 可以直接跳过

图片

  • a在光标之后,进入插入模式

图片

  • i在光标之前,进入插入模式

图片

  • o在本行之后新增一行,并进入插入模式

图片

  • A 在本行结尾,进入插入模式

图片

  • I 在本行开头,进入插入模式

图片

  • O 在本行之前新增一行,并进入插入模式

练习四:jump

图片

不得不说我们使用 vim 更多的就是为了减少鼠标操作,因为使用鼠标你的手需要离开键盘。

一来一回 麻烦!!!所以我们需要了解更多光标移动操作

  • [n]f{word} f 用来查找在当前行 当前光标后是否存在某个字符 {word} 如果存在则将光标移动过去
  • $ | ^ $ 跳转到行尾 ^ 跳转到行首
  • % 括号匹配跳转 () [] {}
  • * 跳转到上一个字符的定位处
  • [n]h/j/k/l 数字加上方向键 代表重复运行多少次
  • 2jj 向下移动两行
  • 10l 向右移动 10 个字符
  • 3kk 向下移动 3 行
  • 20h 向左移动 20 字符
  • G | gg
  • G 移动到最后一行
  • gg 移动到第一行
  • H | M | L
  • H 移动到当前页的第一行
  • M 移动到编辑器中部
  • L 移动到当前页的最后一行
  • [n]w 移动到后面 word 的开始位置,前面加上数字 代表重复多少次
  • e 移动到下一个词语的末尾
  • b 移动到上一个单词的开头
  • zz 改变窗口的整体位置,将当前光标所在处 移动到窗口中部

下面是 gif 演示

图片

  • [n]f{word} f 用来查找在当前行 当前光标后是否存在某个字符 {word} 如果存在则将光标移动过去。
  • 如果将这里的 f 改为 F 则是反向查找

图片

  • $ | ^ $ 跳转到行尾 ^ 跳转到行首

图片

  • % 括号匹配跳转 () [] {}

练习五 学习一些通用场景

其他的比较好用的命令

  • zo | zc
  • dt{char}

04. 配置

在安装了 Vim 之后,其实官方只是给我们了一个基本的架子,能够实现 Vim 的基本操作。如果你想要用的更加顺手,那么你需要对 Vim 进行自己的个性化配置。这是一张白纸,它会被书写成什么样子由你自己决定。

如何对 Idea 中的 Vim 模块进行配置

图片

在右下角点击 Vim 图标,然后点击 Open ~/.ideavimrc

基础配置

老规矩,个人感觉对于新手对编辑器进行配置,还是参照社区的常用配置进行使用。当自己对 Vim 更加熟练的时候可以在加入自己的东西

1
python复制代码"语法高亮syntax on"显示行号set number relativenumber"显示光标所在位置的行号和列号set rulerset wrap                      "自动折行set shiftwidth=4set tabstop=4set softtabstop=4set expandtab                  "将tab替换为相应数量空格set smartindentset backspace=2set co=5"设置取消备份 禁止临时文件生成set nobackupset noswapfile" 为IdeaVim插件增加自动切换为英文输入法的功能," idea 需要安装 IdeaVimExtension pluginset keep-english-in-normal"设置匹配模式 类似当输入一个左括号时会匹配相应的那个右括号set showmatchset clipboard^=unnamed,unnamedplus"set laststatus=2   "命令行为两行"set fenc=utf-8     "文件编码"set mouse=a        "启用鼠标"set ignorecase     "忽略大小写set cursorline     "突出显示当前行"set cursorcolumn   "突出显示当前列set fdm=marker

上面当中比较特别的配置就是

  • 设置 relativenumber 这样可以看相对行号
  • keep-english-in-normal 这个设置开启之后可以在进入命令模式之后自动将输入法切换为英文。这个还是非常推荐设置的,不然每次从 插入模式 切换到 命令模式的时候 如果这个时候是中文输入法,体验就极差了
  • set clipboard^=unamed,unnamedplus 共享剪切板,Vim 的剪切板和系统的剪切板是隔离的。设置之后可以共享。

Action 配置

在 Idea 中大部分的操作都可以抽象为一个 Action 比如

  • 文件重命名 Rename File
  • 重命名变量 或者 类名 RenameElement
  • 去到接口实现 GotoImplementation
  • 复制 Class 引用 CopyReference
  • …

在 Vim 中可以通过插入命令模式 直接调用 action

图片

上图中 gif 中演示的就是通过 调用 RenameElement Action 重命名变量。

初步的介绍完 Intellj Idea 的 action 之后,下面分享我自己目前在用的几个比较常用的配置,通过快捷键 调用 action

1
xml复制代码"jk退出insert模式inoremap jk <ESC>let mapleader = '\<space>'"Idea action 配置" 重命令元素nnoremap <Leader>re :action RenameElement<CR>nnoremap <Leader>gi :action GotoImplementation<CR>nnoremap <Leader>im :action ImplementMethods<CR>nnoremap <Leader>rv :action IntroduceVariable<CR>nnoremap <Leader>cr :action CopyReference<CR>nnoremap <Leader>em :action ExtractMethod<CR>nnoremap <Leader>sw :action SurroundWith<CR>

action listIntellj Idea 中只是的所有的 action 列表

插件配置

其实本来 Vim 中的插件体系是非常强大的,但是因为我们是在 Intellj Idea 中使用 Vim 可以利用很多 Intellj Idea 原本的很多功能。目前我个人还没有发现有很亮眼的插件。所以如果你也正在使用 可以分享一下。


05. 参考文献

B 站 vim 入门讲解:bilibili.com/video/BV1Yt411X7mu

简明 VIM 练级攻略:coolshell.cn/articles/5426.html

learn-vim:github.com/iggredible/Learn-Vim

Vim 配置:ruanyifeng.com/blog/2018/09/vimrc.html

投稿作者:胡川港

知乎主页:zhihu.com/people/hu-chuan-gang-58

GitHub主页:github.com/xiaoxiuniqu…

本文转载自: 掘金

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

SpringCloudAlibaba全网最全讲解7️⃣之Ga

发表于 2021-08-29

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

🌈专栏简介

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩,这个专栏主要是介绍目前微服务最主流的解决方案,SpringCloudAlibaba,将会分组件介绍。专栏地址: [SpringCloudAlibaba](https://juejin.cn/column/7001291481705086990)。**
  • SpringCloudAlibaba全网最全讲解6️⃣之Sentinel(建议收藏)
  • SpringCloudAlibaba全网最全讲解5️⃣之Feign(建议收藏)
  • SpringCloudAlibaba全网最全讲解4️⃣之Ribbon(建议收藏)
  • SpringCloudAlibaba全网最全讲解3️⃣之Nacos(建议收藏)
  • SpringCloudAlibaba全网最全讲解2️⃣(建议收藏)
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)

九、服务网关:Gateway

image-20210507115602441

9.1、网关简介

大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。

image-20210507115048632

这样的架构会存在许多的问题:
  1. 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性。
  2. 认证复杂,每个服务都需要独立认证。
  3. 存在跨域请求,在一定场景下处理相对复杂。
网关就是为了解决这些问题而生的。所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。

image-20210507115517531

9.2、常用的网关

9.2.1、Ngnix+lua

使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用。


lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本

9.2.2、Kong

基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。

他的缺点:

  1. 只支持Http协议。
  2. 二次开发,自由扩展困难。
  3. 提供管理API,缺乏更易用的管控、配置方式。

9.2.3、Zuul

Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发。


他的缺点:
  1. 缺乏管控,无法动态配置。
  2. 依赖组件较多。
  3. 处理Http请求依赖的是Web容器,性能不如Nginx。

9.2.4、Spring Cloud Gateway

Spring公司为了替换Zuul而开发的网关服务,SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关

9.3、Gateway简介

Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。


他的主要功能是:
  1. 进行转发重定向。
  2. 在开始的时候,所有类都需要做的初始化操作。
  3. 进行网络隔离。

9.4、快速入门

需求:通过浏览器访问api网关,然后通过网关将请求转发到商品微服务。

9.4.1、基础版

创建一个api-gateway 模块,并且导入下面的依赖。

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
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>

编写配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码server:
port: 9000 # 指定网关服务的端口
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的标识, 要求唯一
uri: http://localhost:8081 # 请求要转发到的地址
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product-serv/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径

测试

image-20210507171142491

9.4.2、升级版

我们发现升级版有一个很大的问题,那就是在配置文件中写死了转发路径的地址,我们需要在注册中心来获取地址。

加入nacos依赖

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
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>

在主类上添加注解

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableDiscoveryClient
public class GateWayServerApp {
public static void main(String[] args) {
SpringApplication.run(GateWayServerApp.class,args);
}
}

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yaml复制代码server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
我们还可以自定义多个路由规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
application:
gateway:
routes:
- id: product_route
uri: lb://product-service
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1

9.4.3、简写版

我们的配置文件无需写的1那么复杂就可以实现功能,有一个简写版。
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务

image-20210507201625163

我们发现,就发现只要按照网关地址/微服务名称/接口的格式去访问,就可以得到成功响应。

9.5、Gateway核心架构

9.5.1、基本概念

路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
  1. id:路由标识符,区别于其他 Route。
  2. uri:路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
  3. order:用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
  4. predicate:断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
  5. filter:过滤器用于修改请求和响应信息。
  6. predicate:断言,用于进行条件判断,只有断言都返回真,才会真正的执行路由。

9.5.2、执行原理

image-20201030161652819

  1. 接收用户的请求,请求处理器交给处理器映射器,返回执行链。
  2. 请求处理器去调用web处理器,在web处理器里面对我们的路径1进行处理。假设1我们的路径1是:http://localhost:9000/product-serv/get?id=1 ,根据配置的路由规则,上本地找对应的服务信息:product-service对应的主机ip是192.168.10.130。
  3. 根据1ribbon的负载均衡策略去选择一个节点,然后拼接好,将路径中的product-serv替换成192.168.10.130:8081,如果你配置了filter,那么他还会走filter。
  4. 如果你没有自定义路由的话,默认Gateway会帮你把第一层去掉。网关端口从此一个/开始到第二个/开始算第一层。

image-20210507203258953

9.6、过滤器

Gateway的过滤器的作用是:是在请求的传递过程中,对请求和响应做一些手脚。


Gateway的过滤器的生命周期:
  1. PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择
    请求的微服务、记录调试信息等。
  2. POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
    Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter:
  1. GatewayFilter:应用到单个路由或者一个分组的路由上。
  2. GlobalFilter:应用到所有的路由上。

9.6.1、局部过滤器

局部过滤器是针对单个路由的过滤器。他分为内置过滤器和自定义过滤器。

9.6.1.1、内置过滤器

在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。
9.6.1.1.1、局部过滤器内容
过滤器工厂 作用 参数
AddRequestHeader 为原始请求添加Header Header的名称及值
AddRequestParameter 为原始请求添加请求参数 参数名称及值
AddResponseHeader 为原始响应添加Header Header的名称及值
DedupeResponseHeader 剔除响应头中重复的值 需要去重的Header名称及去重策略
Hystrix 为路由引入Hystrix的断路器保护 HystrixCommand 的名称
FallbackHeaders 为fallbackUri的请求头中添加具体的异常信息 Header的名称
PrefixPath 为原始请求路径添加前缀 前缀路径
PreserveHostHeader 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host 无
RequestRateLimiter 用于对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
RedirectTo 将原始请求重定向到指定的URL http状态码及重定向的url
RemoveHopByHopHeadersFilter 为原始请求删除IETF组织规定的一系列Header 默认就会启用,可以通过配置指定仅删除哪些Header
RemoveRequestHeader 为原始请求删除某个Header Header名称
RemoveResponseHeader 为原始响应删除某个Header Header名称
RewritePath 重写原始的请求路径 原始路径正则表达式以及重写后路径的正则表达式
RewriteResponseHeader 重写原始响应中的某个Header Header名称,值的正则表达式,重写后的值
SaveSession 在转发请求之前,强制执行WebSession::save操作 无
secureHeaders 为原始响应添加一系列起安全作用的响应头 无,支持修改这些安全响应头的值
SetPath 修改原始的请求路径 修改后的路径
SetResponseHeader 修改原始响应中某个Header的值 Header名称,修改后的值
SetStatus 修改原始响应的状态码 HTTP 状态码,可以是数字,也可以是字符串
StripPrefix 用于截断原始请求的路径 使用数字表示要截断的路径的数量
Retry 针对不同的响应进行重试 retries、statuses、methods、series
RequestSize 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large 请求包大小,单位为字节,默认值为5M
ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容
ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容
9.6.1.1.2、局部过滤器的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
- SetStatus=2000 # 这里使用内置的过滤器,修改返回状态

9.6.1.2、自定义局部过滤器

很多的时候,内置过滤器没办法满足我们的需求,这个时候就必须自定义局部过滤器。我们假定一个需求是:统计订单服务调用耗时。

编写一个类,用于实现逻辑

**名称是有固定格式xxxGatewayFilterFactory**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {

private static final String BEGIN_TIME = "beginTime";

//构造函数
public TimeGatewayFilterFactory() {
super(TimeGatewayFilterFactory.Config.class);
}

//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("show");
}

@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!config.show){
// 如果配置类中的show为false,表示放行
return chain.filter(exchange);
}
exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());
/**
* pre的逻辑
* chain.filter().then(Mono.fromRunable(()->{
* post的逻辑
* }))
*/
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute(BEGIN_TIME);
if (startTime != null) {
System.out.println(exchange.getRequest().getURI() + "请求耗时: " + (System.currentTimeMillis() - startTime) + "ms");
}
}));
}
};
}

@Setter
@Getter
static class Config{
private boolean show;
}

}

编写application.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
yaml复制代码server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
predicates:
- Path=/product-serv/** # 符合这个规定的才进行1转发
filters:
- StripPrefix=1 # 将第一层去掉
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
- Time=true

访问路径:http://localhost:9000/order-serv/getById?o=1&pid=1

在这里插入图片描述

9.6.2、全局过滤器

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理。

网关全局过滤器

开发中的鉴权逻辑:
  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)。
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证。
  • 以后每次请求,客户端都携带认证的token。
  • 服务端对token进行解密,判断是否有效。

image-20210507220009473

我们来模拟一个需求:实现统一鉴权的功能,我们需要在网关判断请求中是否包含token且,如果没有则不转发路由,有则执行正常逻辑。

编写全局过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Component
public class AuthGlobalFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}

9.6.3、网关限流

网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。

image-20210507220048921

从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
  • route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
  • 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组

9.6.3.1、网关集成Sentinel

添加依赖

1
2
3
4
xml复制代码<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

编写配置类进行限流

配置类的本质是用代码替代nacos图形化界面限流。
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复制代码@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;

public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//增加对商品微服务的限流
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3) // 三次
.setIntervalSec(1) // 一秒,表示一秒钟1超过了三次就会限流
);
GatewayRuleManager.loadRules(rules);
}
}

修改限流默认返回格式

如果我们不想在限流的时候返回默认的错误,那么就需要自定义错误,指定自定义的返回格式。我们只需在类中添加一段配置即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON).
body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}

测试

image-20210507221350190

9.6.3.2、自定义API分组

我们可以发现,上面的这种定义,对整个服务进行了限流,粒度不够细。自定义API分组是一种更细粒度的限流规则定义,它可以实现某个方法的细粒度限流。

在Shop-order-server项目中添加ApiController

1
2
3
4
5
6
7
8
java复制代码@RestController
@RequestMapping("/api")
public class ApiController {
@RequestMapping("/hello")
public String api1(){
return "api";
}
}

在GatewayConfiguration中添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/order-serv/api/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3)
.setIntervalSec(1)
);
rules.add(new GatewayFlowRule("order_api").
setCount(1).
setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}

测试

直接访问<http://localhost:8082/api/hello> 是不会发生限流的,访问<http://localhost:9000/order-serv/api/hello> 就会出现限流了。

本文转载自: 掘金

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

Spring5参考指南 组件扫描

发表于 2021-08-29

本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

组件扫描

上一篇文章我们讲到了annotation-config配置,它主要用于bean内部的属性注入。而bean本身则需要通过配置的方式来定义。如果想使用配置的方式来定义bean,则可以使用component-scan,如下:

1
xml复制代码<context:component-scan base-package="com.flydean"/>

component-scan会扫描类路径里面的注解注解,包括(@Component, @Repository, @Service,
@Controller, @RestController, @ControllerAdvice, 和@Configuration ), 当然component-scan默认包含了annotation-config,我们可以直接在这些配置bean中使用上篇文章讲到的注解。

@Component

@Component表示该bean是一个组件,@Component是任何Spring管理的组件的通用原型。@Repository、@Service和@Controller是@Component针对更具体的用例(分别在持久性、服务和表示层中)的特殊注解。因此,您可以用@Component注解组件类,但是,通过用@Repository、@Service或@Controller注解它们,您的类更具有语义性。通常更适合在AOP中做进一步的业务逻辑处理。

元注解和组合注解

所谓元注解就是可以用在其他注解中的注解。 像之前提到的@Component就是@Service的元注解。如下:

1
2
3
4
5
6
7
8
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

// ....
}

@Component会导致@Service和@Component一样被对待。
当然你也可以组合使用元注解,或者自定义元注解。例如,Spring的@SessionScope注解将作用域名称硬编码为session,但仍允许自定义proxyMode。以下列表显示了sessionScope注解的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @Interface SessionScope {

/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

你可以不定义@SessionScope里面的proxyMode, 如下:

1
2
3
4
5
java复制代码@Service
@SessionScope
public class SessionScopedService {
// ...
}

你也可以重写proxyMode,如下:

1
2
3
4
5
java复制代码@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}

#@ComponentScan和filters

上面我们讲到,要是要使用组件扫描,需要在XML配置context:component-scan, 其实也可以使用注解的形式,如下所示:

1
2
3
4
5
java复制代码@Configuration
@ComponentScan(basePackages = "com.flydean.beans")
public class AppConfig {

}

@ComponentScan可以配置一些filters用来过滤不需要的组件。如下所示:

1
2
3
4
5
6
java复制代码@Configuration
@ComponentScan(basePackages = "com.flydean.beans",
includeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @ComponentScan.Filter(BeanA.class))
public class BeanAConfig {
}

下表是支持的filter类型和例子:

Filter type 表达式例子 描述
annotation(默认) org.example.SomeAnnotation type基本的目标组件
assignable org.example.SomeClass 目标组件可分配给(扩展或实现)的类(或接口)。
aspectj org.example..*Service+ 匹配目标组件的AspectJ类型
regex org.example.Default.* 匹配目标主键内名的正则表达式
custom org.example.MyTypeFilter org.springframework.core.type .TypeFilter的自定义实现

组件内部定义Bean元数据

Spring组件还可以为容器提供bean定义元数据。您可以使用用于在@Configuration annotated类中定义bean元数据的相同@Bean注解来实现这一点。以下示例显示了如何执行此操作:

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

@Bean
@Qualifier("public")
public BeanA publicInstance() {
return new BeanA();
}

public void doWork() {
// Component method implementation omitted
}
}

InjectionPoint

从SpringFramework4.3开始,还可以声明InjectionPoint类型的工厂方法参数,来创建Bean。

注意,这只适用于bean实例的实际创建,而不适用于现有实例的注入。因此,这个特性对于原型范围的bean最有意义。

1
2
3
4
5
6
7
8
9
java复制代码@Component
public class InjectPointFactoryMethodComponent {

@Bean
@Scope("prototype")
public BeanA prototypeInstance(InjectionPoint injectionPoint) {
return new BeanA("prototypeInstance for " + injectionPoint.getMember());
}
}

常规Spring组件中的@Bean方法的处理方式与Spring@Configuration类中的对应方法不同。不同的是,@Component类没有用cglib来增强以截获方法和字段的调用。cglib代理是调用@Configuration classes中通过@Bean methods内的方法或字段创建对协作对象的bean元数据引用的方法。

你可以将@Bean方法声明为静态方法,允许在不将其包含的配置类创建为实例的情况下调用它们。在定义post-processor bean(例如,BeanFactoryPostProcessor或BeanPostProcessor类型)时,这是特别有意义的,因为这样的bean在容器生命周期的早期就被初始化,应该避免在此时触发配置的其他部分。

由于技术限制,对static @Bean方法的调用永远不会被容器截获,即使是在@Configuration类(如本节前面所述)中也是如此:cglib子类只能重写非静态方法。因此,直接调用另一个@Bean方法相当于标准Java的new方法,导致从工厂方法本身直接返回一个独立的实例。

要注意: @Configuration类中的常规@Bean方法必须是可重写的,也就是说,它们不能声明为私有或最终的。

为自动检测组件命名

默认情况下,可以提供value属性给@Component、@Repository、@Service和@Controller),从而为Bean命名。

如果这样的注解不包含value,则默认bean名称生成器将返回小写的非限定类名。例如,如果检测到以下组件类,则名称为myMovieLister和movieFinderImpl:

1
2
3
4
java复制代码@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
1
2
3
4
java复制代码@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

如果您不想依赖默认的bean命名策略,可以提供一个自定义的bean命名策略。首先,实现BeanNameGenerator接口,并确保包含一个默认的无参数构造函数。然后,在配置扫描器时提供完全限定的类名,如下面的示例注解和bean定义所示:

1
2
3
4
5
6
java复制代码public class MyNameGenerator implements BeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
return null;
}
}
1
2
3
4
java复制代码@Configuration
@ComponentScan(basePackages = "com.flydean", nameGenerator = MyNameGenerator.class)
public class BeanNameConfig {
}

为自动检测的组件提供作用域

与一般的Spring管理组件一样,自动检测组件的默认和最常见的作用域是singleton。但是,有时您需要一个可以由@Scope注解指定的不同范围。可以在注解中提供作用域的名称,如下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Scope("prototype")
@Component("beanA")
public class BeanA {

public BeanA(){

}

public BeanA(String name){

}
}

自定义范围解析

要为范围解析提供自定义策略,而不是依赖基于注解的方法,可以实现ScopeMetadataResolver接口。如下所示:

1
2
3
4
5
6
java复制代码public class MyScopeResolver implements ScopeMetadataResolver {
@Override
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
return null;
}
}
1
2
3
4
java复制代码@Configuration
@ComponentScan(basePackages = "com.flydean", scopeResolver = MyScopeResolver.class)
public class BeanScopeResolverConfig {
}

scoped-proxy

当使用某些非单例作用域时,可能需要为作用域对象生成代理。为此,组件扫描元素上可以有一个scoped-proxy 属性。三个可能的值是:no、interfaces和targetClass。例如,以下配置将生成标准JDK动态代理:

1
2
3
4
java复制代码@Configuration
@ComponentScan(basePackages = "com.flydean", scopedProxy = ScopedProxyMode.INTERFACES)
public class ScopedProxyConfig {
}

生成候选组件的索引

虽然类路径扫描速度非常快,但是可以通过在编译时创建一个静态候选列表来提高大型应用程序的启动性能。

要生成索引,需要每个模块添加一个附加依赖项,该模块包含作为组件扫描指令目标的组件。下面的示例说明如何使用Maven:

1
2
3
4
5
6
7
8
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.1.8.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>

这个过程生成一个包含在JAR文件中的META-INF/spring.components文件。

当在类路径上找到META-INF/Spring.components时,索引将自动启用。如果某个索引部分可用于某些库(或用例),但无法为整个应用程序生成,则可以通过将spring.index.ignore设置为true(作为系统属性或类路径根目录下的spring.properties文件)来回滚到常规类路径安排(就像根本没有索引一样)。

本节的例子可以参考 component-scan.

更多教程请参考 flydean的博客

本文转载自: 掘金

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

Kafka 认证配置实践笔记

发表于 2021-08-29

今天我们来实践 Kafka 安全配置,通过本篇教程,我们将配置一个需要使用用户名、密码访问的 Kafka。

本文使用 SASL/SCRAM 认证机制,是 Kafka 0.10.2 版本引入的。

测试环境: MacOs mojave,kafka_2.12-2.4.0

第一步,创建用户

说明:

admin 用于 broker 之间通信,writer 用于生产者,reader 用于消费者。

1
2
3
4
5
6
7
java复制代码bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=admin]' --entity-type users --entity-name admin


bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=writer]' --entity-type users --entity-name writer


bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=reader]' --entity-type users --entity-name reader

第二步,创建 JAAS 文件

说明:

这个文件用于启动 broker,文件可以命名为 kafka-broker.jaas,broker 启动时需要指定该文件。

1
2
3
4
5
6
java复制代码
KafkaServer {
org.apache.kafka.common.security.scram.ScramLoginModule required
username="admin"
password="admin";
};

第三步,配置 server.properties 文件

说明:

既然要进行认证,server.properties 文件是要进行配置的,参考下面的配置。

1
2
3
4
5
6
7
java复制代码sasl.enabled.mechanisms=SCRAM-SHA-256

sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256

security.inter.broker.protocol=SASL_PLAINTEXT

listeners=SASL_PLAINTEXT://localhost:9092

第四步,启动 broker

说明:

启动 broker 时,指定 jaas 文件,及修改后的 server.properties 文件。

1
java复制代码KAFKA_OPTS=-Djava.security.auth.login.config=<yourPath>/kafka-broker.jaas bin/kafka-server-start.sh config/server.properties

第五步,发送消息

说明:

开启了认证后,我们的生产者,需要提供用户名、密码,因此需要配置一个文件,启动时指定,内容如下:可以命名为 producer.conf

1
2
3
java复制代码security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="writer" password="writer";
1
java复制代码bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test  --producer.config <yourPath>/kafka_2.12-2.4.0/producer.conf

第六步,接收消息

说明:

消费者和生产者一样,启动时,需要指定配置文件,配置文件的内容和生产者类似,只有用户名、密码不一致。内容如下:可以命名为 consumer.conf

1
2
3
4
java复制代码
security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="reader" password="reader";
1
java复制代码bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning --consumer.config <yourPath>/kafka_2.12-2.4.0/consumer.conf

总结

我们配置了一个简单的 Kafka 认证,使用 SASL/SCRAM 认证机制。首先创建用户,设置密码,然后修改 server 配置文件,最后,producer 、consumer 在启动时,均需要提供用户名、密码。

这样一个简单的安全访问的 Kafka 就配置完成了。下篇继续介绍,如何进行授权配置。

参考文档及推荐阅读

Kafka核心技术与实战

Apache Kafka

本文转载自: 掘金

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

nginx跳转:总有一种方式满足你的需求

发表于 2021-08-29

前言

最近工作用到了nginx,但是路由配置特殊,业务场景复杂,举几个例子:一个机器上多个服务,使用同一个端口,不同的域名如何配置?同一个服务分子域名呢?location那么多,怎么判断它匹配哪个呢?重定向和proxy都能跳到指定地址,那用哪个呢?带着这些问题我集中整理了nginx跳转的配置方式,方便后进行技术选型和决策。本文按分类整理了四种方式,相信总有一种满足你的需求。

一、配置server对应的域名

server name 为虚拟服务器的识别路径。因此不同的域名会通过请求头中的HOST字段,匹配到特定的server块,转发到对应的应用服务器中去。server_name匹配规则:后面可以跟多个域名,第1个是主域名

1.1、精确匹配

如下nginx配置

1
2
3
4
5
6
7
8
9
10
11
12
13
server复制代码        listen       8080;
        server_name  test1.com;
        location / {
            return 200 "I am test1!\n";
        }
    }
    server {
        listen       8080;
        server_name  my.test.com;
        location / {
            return 200 "I am mytest!\n";
        }
    }

请求结果

1
2
arduino复制代码curl http://my.test.com:8080  返回:I am mytest!
curl http://test1.com:8080 返回:I am test1!

1.2、正则表达式

  • 以*通配符开始的最长字符串,如下示例
1
2
3
4
5
6
7
arduino复制代码server {
        listen       8080;
        server_name  test1.*;
        location / {
            return 200 "I am test1!\n";
        }
    }
  • 以*通配符结束的最长字符串
1
2
3
4
5
6
server复制代码        listen       8080;
        server_name  *.test.com;
        location / {
            return 200 "I am mytest!\n";
        }
    }
  • 通配符名字只可以在名字的起始处或结尾处包含一个星号,并且星号与其他字符之间用点分隔。所以,“my..com“都是非法的。
    例如 :server_name my.
    .com;
    报以下错误:
1
vbnet复制代码nginx: [emerg] invalid server name or wildcard "my.*.com" on 0.0.0.0:8080
  • 匹配正则表达式
1
2
3
4
5
6
7
perl复制代码server {
        listen     8080;
        server_name  ~^my(?<serno>.+).mydomain.com$;
        location / {
            return 200 $serno;
        }
    }

解释说明

  1. ~: 表示大小写敏感的正则;
  2. ^:匹配字符串的开始;
  3. {.+}:换行符以外的任意自读重复一次活更多次;
  4. (): 分组与取值;
  5. :表示转义;
  6. serno:设置提取的变量;
  7. $:匹配字符串的结束;

请求结果

1
2
arduino复制代码curl http://my02.mydomain.com:8080 返回:02% 
curl http://my03.mydomain.com:8080 返回:03%
  • server_name的配置顺序是怎样的呢?
    按照如下顺序匹配:
    匹配顺序->

->精确匹配
->*在前的域名
->*在后的域名
->按文件中的顺序匹配
->default server:第一个,listen指定default

二、配置location

2.1、Location 匹配规则:仅匹配URI,忽略参数

1
bash复制代码location [=|~|~*|^~] /uri/ { … }

匹配的正则符号如下:

  • = 严格匹配。如果请求匹配这个location,那么将停止搜索并立即处理此请求
  • ~ 区分大小写匹配(可用正则表达式)
  • ~* 不区分大小写匹配(可用正则表达式)
  • !~ 区分大小写不匹配
  • !~* 不区分大小写不匹配
  • ^~ 如果把这个前缀用于一个常规字符串,那么告诉nginx 如果路径匹配那么不测试正则表达式

2.2、举例

1
2
3
4
5
6
7
8
ruby复制代码1、匹配任意请求
location [=|~|~*|^~] /uri/ { … }

2、不区分大小写匹配以js、php结尾的请求
location ~* .(js|php)$ { … }

3、区分大小写匹配以.txt结尾的请求
location ~ ^.+\.txt$

2.3、匹配顺序如下图

A428414C-215D-4AA3-80E8-2F58D3406E56.png

按照上面的规则配置了如下location

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码location = /documents {
    return 200 'configuration A'
}
location /documents {
    return 200 'configuration B'
}
location /documents/txt1 {
    return 200 'configuration C'
}
location ^~ /documents/ {
    return 200 'configuration D'
}
location ~* /documents/(\w+)$ {
    return 200 'configuration E'
}
location ~ /documents/$ {
    return 200 'configuration F'
}
  • curl test1.com:8080/documents,精… configuration A
  • curl test1.com:8080/documents/ ^~匹配上后不在匹配,返回 configuration D
  • curl test1.com:8080/documents/t… 走到了正则匹配,不会走到/documents/txt1(正则没走完) 返回configuration E
  • curl test1.com:8080/documents/t… C,因为正则都不匹配

2.4、如何debug正则呢?

编译的时候加上 –with-debug选项,例如 ./configure –with-debug
conf文件加上要debug的host,debug_connection对应要debug的连接。

1
2
3
4
5
ini复制代码events {
    worker_connections  1024;
    debug_connection  192.168.1.3;
    debug_connection  127.0.0.1;
}

error.log查看debug日志,图中test location就是正则匹配的过程
E5B119B4-EEF7-475F-A00F-0BAFAEB1FC6B.png

三、配置rewrite

语法如下:

1
2
3
4
scss复制代码   指令语法:rewrite regex replacement[flag];
  默认值:none
  应用位置:server、location、if
  rewrite是实现URL重定向的重要指令,他根据regex(正则表达式)来匹配内容跳转到replacement,结尾是flag标记.
flag标记 说明
last 本条规则匹配完成后继续向下匹配新的location URI规则
break 本条规则匹配完成后终止,不在匹配任务规则
redirect 返回302临时重定向
permanent 返回301永久重定向

3.1、重定向

return三种code,code url和url。
返回状态码:444表示关闭连接 301表示http1。0中永久重定向,302表示临时重定向,进制缓存。http1.1后,303表示临时重定向,允许改变方法,进制缓存,307表示临时重定向,不允许改变方法,禁止被缓存,308表示永久重定向,不允许改变方法。

  • 返回code
1
2
3
bash复制代码location / {
    return 301 https://www.xxxx.com$request_uri;
}
  • 通过$request_uri变量匹配所有的URI。
1
bash复制代码rewrite ^ https://www.xxxx.com$request_uri? permanent;
  • 通过正则匹配所有的URI后再去掉开头第一个/(反斜线)。
1
javascript复制代码rewrite ^/(.*)$ https://www.xxxx.com/$1;
  • 与if指令结合
1
2
3
4
5
6
7
perl复制代码server {
        listen       80;
        server_name  test1.net test2.net;
        if ($host != 'test1.net' ) {
                rewrite ^/(.*)$ http://www.baidu.net/$1 permanent;
        }
}

3.2、如何查看rewrite日志

打开日志开关rewrite_log on;
可以配置到http,server,location和if上下文中
示例:curl test1.com:8080/first/2.txt

1
2
3
4
bash复制代码location /first {
        rewrite_log on;
        rewrite /first(.*) /second$1 last;
      }

效果图如下
CF42DE3F-670C-4520-8EEF-08DA05E148AB.png

四、配置 proxy

对上游服务使用http/https协议进行反向代理。proxy_pass后面跟url,可以仿造location,if in location和limit_except上下文中。 这个功能是默认编译到nginx中的。本文重点讨论http proxy。

url参数规则

  • url必须以http或者https开头,接下来是域名、ip、unix socket或者upstream名字,都可以就端口。后面是可选的uri

http示例
proxy_pass http://localhost:8000/uri/;
UNIX域套接字路径来定义示例
proxy_pass http://unix:/tmp/backend.socket:/uri/;

  • url中是否携带uri,结果也不一样,如果在proxy_pass后面的url加/,相当于是绝对根路径,则nginx不会把location中匹配的路径部分代理走;如果没有/,则会把匹配的路径部分给代理走。
    目录结构如下
1
2
3
4
5
6
7
8
9
sql复制代码├── first

│   └── index.html

├── index.html

└── second

    └── index.html

nginx配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码server {
        listen       8081;
        server_name  my.test.com;
    }
    server {
        listen       8082;
        # 第一种情况
        location  /first {
            proxy_pass http://my.test.com:8081;
            proxy_set_header Host   $host;
            proxy_set_header X-Real-IP      $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        # 第二种情况
        location  /first {
            proxy_pass http://my.test.com:8081/;
            proxy_set_header Host   $host;
            proxy_set_header X-Real-IP      $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

不带/,然后 curl http://127.0.0.1:8082/first/index.html 返回index html

带/,然后 curl http://127.0.0.1:8082/first/index.html 返回first index

  • Url参数中可以携带变量proxy_pass http://$host$uri;
  • 可以配合rewrite break语句
1
2
3
4
ini复制代码location /nameb/ { 
    rewrite /nameb/([^/]+) /test?nameb=$1 break;
    proxy_pass http://127.0.0.1:8801/; 
}

五、小结

配置nginx的路由,有多种方式,域名可以用server_name配置,uri可以用location配置,复杂的可以加rewrite配置修改请求。还有就是配置proxy代理,在代理中转发id等。本文开头的几个问题,同端口不同域名使用server_name配置就可以,可以放到不同的server中;location的匹配顺序可以参考文中的那张流程图;重定向的情况下客户端拿到302和url就会去请求这个url指定的资源,proxy不会,因此在nginx+tomcat的场景下,proxy更适合。

本文转载自: 掘金

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

Nginx系列:Nginx解决跨域和负载均衡 八月更文挑

发表于 2021-08-29

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

前言

心态好了,就没那么累了。心情好了,所见皆是明媚风景。

“一时解决不了的问题,那就利用这个契机,看清自己的局限性,对自己进行一场拨乱反正。”正如老话所说,一念放下,万般自在。如果你正被烦心事扰乱心神,不妨学会断舍离。断掉胡思乱想,社区垃圾情绪,离开负面能量。心态好了,就没那么累了。心情好了,所见皆是明媚风景。


Nginx正向代理

正向代理:是一个位于客户端和目标服务器之间的服务器,为了从目标服务器取得内容,客户端向代理发送一个请 求并指定目标(目标服务器),然后代理向目标服务器转交请求并将获得的内容返回给客户端。客户端必须要进行 一些特别的设置才能使用正向代理。(例如:我们访问谷歌网站,由于其他原因无法访问到,但是我们通过访问其 它的服务器最终访问到谷歌网站了,此时就是一个正向代理的过程)


Nginx反向代理

反向代理:在计算机世界里,由于单个服务器的处理客户端(用户)请求能力有一个极限,当用户的接入请求蜂拥 而入时,会造成服务器忙不过来的局面,可以使用多个服务器来共同分担成千上万的用户请求,这些服务器提供相 同的服务,对于用户来说,根本感觉不到任何差别。实际上是通过反向代理服务器接受客户端的请求,然后把请求 分发给具体的服务器进行处理,然后再将服务器的响应结果返回给代理服务器,由代理服务器反馈给客户端。 (例如:拨打10086客服电话,一个省的10086客服估计有成千上万个,实际上我们并不关心有多个客服,我们关心 的是只要拨通了10086 的号码能够有客服为我们提供服务就可以了。其实10086总机号码就是我们说的反向代理)


反向代理示例

我们通过nginx来代理访问该项目。我们只需要修改配置文件nginx.conf即可,具体修改如下:(下面截图中如weight = 3中间的空格记得去掉,我这个是格式化了不去掉会报错哦….)

在这里插入图片描述

重新启动nginx服务器(nginx -s reload),然后在浏览器中访问网址:http://localhost:8086/chenOne或者chenTwo 即可看到效果(会分别访问upstream配置下的路径)。


什么是负载均衡

**当一台服务器在单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩 溃。为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。

我们可以建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,让这个中 间服务器在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器中。如此以来,用户的每次 访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。如下:**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码    # 配置负载均衡的服务器Ip和负载均衡方式(此处为权重轮询)并且允许请求失败次数 max_fails 为3次以及 fail_timeout 请求3次失败后,暂停的时间。
upstream tomcate_server {
server localhost:3010 weight=2 max_fails=3 fail_timeout=10s;
server localhost:3011 weight=2 max_fails=3 fail_timeout=10s;
server localhost:3012 weight=2 max_fails=3 fail_timeout=10s;
# backup 不能和 ip_hash 关键字一起使用
server localhost:3100 max_fails=3 fail_timeout=10s backup;# 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
}

# 配置负载均衡的服务器Ip和负载均衡方式(此处为权重轮询)并且允许请求失败次数 max_fails 为3次以及 fail_timeout 请求3次失败后,暂停的时间。
upstream tomcate_serverTwo {
server localhost:4010 weight=2 max_fails=3 fail_timeout=10s;
server localhost:4011 weight=2 max_fails=3 fail_timeout=10s;
server localhost:4012 weight=2 max_fails=3 fail_timeout=10s;
# backup 不能和 ip_hash 关键字一起使用
server localhost:4100 max_fails=3 fail_timeout=10s backup;# 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
}

故障转移

在使用负载均衡时,假如集群中的某台服务器挂掉了,那么当访问到该服务器时会有很长的响应超时时间,
响应失败后才会去访问集群中正常的服务器,这样的话用户的体验就非常差了,那么如何来解决这个问题
呢,实际上可以通过在nginx服务器上配置故障转移来解决这个问题。 打开nginx/conf目录下 nginx.conf文件进行编辑。如下:

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
xml复制代码        # 超过超时时间则进行故障转移
location /chenOne {
# 放置静态资源的地方
root static;# (d:/nginx/static)
# 访问的首页
index index.html index.htm;
# 进行负载均衡的配置指向地址
proxy_pass http://tomcate_server;
proxy_connect_timeout 3;#默认单位是秒
proxy_read_timeout 3;
proxy_send_timeout 3;
}

# 超过超时时间则进行故障转移
location /chenTwo {
# 放置静态资源的地方
root static;# (d:/nginx/static)
# 访问的首页
index index.html index.htm;
# 进行负载均衡的配置指向地址
proxy_pass http://tomcate_serverTwo;
proxy_connect_timeout 3;#默认单位是秒
proxy_read_timeout 3;
proxy_send_timeout 3;
}

编辑完成后保存文件,重启nginx服务器nginx -s reload。停掉集群中的一台服务器,然后进行测试。在浏览器中访问:http://localhost:8086/,则可看到当访问停止的服务器时,访问不成功,3秒后则自动转移访问另一个正常运行的服务器。


代理用法完整配置:解决跨域、负载均衡和故障转移(请注意看下面配置的注释)

  • 同域:简单的解释就是相同域名、端口号和协议。
  • 跨域问题:跨域问题的产生是因为浏览器对于javascript的同源策略的限制导致的,例如a.com下面的js不能
    调用b.com中的 js、对象或数据,因为a.com和b.com是不同域,所以想要调用就出现了跨域问题。
  • 同源策略:请求的url地址必须与浏览器上url地址处于同域上,也就是域名、端口号、协议相同。配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
xml复制代码http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

# 配置负载均衡的服务器Ip和负载均衡方式(此处为权重轮询)并且允许请求失败次数 max_fails 为3次以及 fail_timeout 请求3次失败后,暂停的时间。
upstream tomcate_server {
server localhost:3010 weight=2 max_fails=3 fail_timeout=10s;
server localhost:3011 weight=2 max_fails=3 fail_timeout=10s;
server localhost:3012 weight=2 max_fails=3 fail_timeout=10s;
# backup 不能和 ip_hash 关键字一起使用
server localhost:3100 max_fails=3 fail_timeout=10s backup;# 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
}

# 配置负载均衡的服务器Ip和负载均衡方式(此处为权重轮询)并且允许请求失败次数 max_fails 为3次以及 fail_timeout 请求3次失败后,暂停的时间。
upstream tomcate_serverTwo {
server localhost:4010 weight=2 max_fails=3 fail_timeout=10s;
server localhost:4011 weight=2 max_fails=3 fail_timeout=10s;
server localhost:4012 weight=2 max_fails=3 fail_timeout=10s;
# backup 不能和 ip_hash 关键字一起使用
server localhost:4100 max_fails=3 fail_timeout=10s backup;# 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
}

server {
listen 8086;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

# 超过超时时间则进行故障转移
location /chenOne {
# 放置静态资源的地方
root static;# (d:/nginx/static)
# 访问的首页
index index.html index.htm;
# 进行负载均衡的配置指向地址
proxy_pass http://tomcate_server;
}

# 超过超时时间则进行故障转移
location /chenTwo {
# 放置静态资源的地方
root static;# (d:/nginx/static)
# 访问的首页
index index.html index.htm;
# 进行负载均衡的配置指向地址
proxy_pass http://tomcate_serverTwo;
}
#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

这样 http://tomcate_server 和 http://tomcate_serverTwo 两个服务器上的数据就全
都出现在了http://loaclhost:8086服务器上,从而解决了跨域的问题。


🎉总结:

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

如何保证接口的幂等性?

发表于 2021-08-29

今天我们来聊聊关于接口的幂等性问题。

什么是幂等性

所谓幂等,就是任意多次执行所产生的影响均与一次执行的影响相同。

在 restful 规范中,常见的请求方式和接口幂等性关系如下:

请求方式 操作 是否幂等
GET 查询数据 是
POST 新增数据 否
PUT 更新数据 直接更新为某个值,满足幂等,如:set a = 1;累加操作的更新,不满足,如:set a = a+1
DELETE 删除数据 根据唯一条件删除,满足幂等;否则,不满足,幂等,比如:根据某一条件删除一批数据后,又新增了一条满足该条件的数据,又执行了一次删除,那么就会删除掉新增的这条数据

为什么会产生接口幂等性问题

在计算机应用中,可能遇到网络抖动,临时故障,或者服务调用失败,尤其是分布式系统中,接口调用失败更为常见。为了保证服务的完整性,我们可能会发起接口的重试调用,如果接口不处理幂等,可能对系统造成很大的影响,因此接口的幂等设计尤其更为重要。

对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这种情况的发生有以下几种常见的场景:

  1. 前端重复提交: 用户在提交表单的时候,可能会因网络波动没有及时做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
  2. 接口超时重试: 第三方调用接口时候,为了超时等异常情况造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  3. 消息重复消费: 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

幂等性解决方案

那我们应该能怎样保证接口的幂等性呢?

可以思考一下,第一种场景下,既然是用户重复提交导致的,那我们可以想办法让用户没办法重复提交。

方案一:前端控制

在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。

方案二:Token机制

用户进入表单页面首先调用后台接口获取 token 并存入 redis,当用户提交表单时将 token 也作为入参,后端先删除 redis 中的 token,删除成功则保存表单数据,失败则提示用户重复提交。

img

这里为什么不先判断 redis 是否存在这个 token 再删除,是因为要保证操作的原子性,极端情况下,第一个请求查询到 redis 中存在这个 token,还没来得及删除,第二个请求进来,也查询到 redis 中存在这个 token,那么还是会造成重复提交的问题。

token 机制需要先请求获取 token 的接口,在有些情况下很明显并不合适。我们大部分请求都是要落到数据库的,所以我们可以从数据库着手。

方案三、唯一索引

这种方案就比较好理解了,使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。唯一索引可以支持插入、更新、删除业务操作。

方案四、悲观锁

这里所说的悲观锁是基于数据库层面的,在获取数据时进行加锁,当同时有多个重复请求时,其他请求都无法进行操作。悲观锁只适用于更新操作。

1
2
mysql复制代码// 例如
select name from t_goods where id=1 for update;

*注意: id 字段一定要是主键或者唯一索引,不然会锁住整张表,这是会死人的。悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用。*

在请求量比较大的情况下,使用悲观锁明显不合适,这时候就到乐观锁上场了。

方案五、乐观锁

可以通过版本号实现,为表增加一个 version 字段,当数据需要更新时,先去数据库里获取此时的version版本号。

1
mysql复制代码select version from t_goods where id=1

更新数据时首先要对比版本号,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。

1
mysql复制代码update t_goods set count=count+1,version=version+1 where version=#{version}

还有一种是通过状态机实现的,其实也是乐观锁的原理。这种方法适合在有状态流转的情况下,比如订单的创建和付款,订单的创建肯定是在付款之前,这时我们可以通过在设计状态字段时,使用 int 类型,并且通过值类型的大小来实现幂等性。

1
mysql复制代码update t_goods set status=#{status} where id=1 and status<#{status}

同样,乐观锁也只适用于更新操作。

方案六、分布式锁

有时候我们的业务不仅仅是操作数据库,也可能是发送短信、消息等等,那数据库层面的锁就不适合了。这种情况下就要考虑代码层面的锁了,而 java 的自带的锁在分布式集群部署的场景下并不适用,那么就可以采用分布式锁来实现(Redis 或 Zookeeper)。

拿 Redis 分布式锁举例,比如一个订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则以 Key 为订单号向 Redis 插入。查询订单是否已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过 Redis 做到了分布式锁,只有这次订单支付请求完成,下次请求才能进来。当然这里需要设置一个Key 的过期时间,在发生异常的时候还要注意删除 Redis 的 Key。

总结

接口的幂等性是一个很常见的问题,需要根据具体业务场景的不同,选择合适的解决方案。

END

往期推荐

你必须了解的分布式事务解决方案

就这?分布式 ID 发号器实战

略懂设计模式之工厂模式

就这?Spring 事务失效场景及解决方案

就这?一篇文章让你读懂 Spring 事务

本文转载自: 掘金

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

系统对接阿里云短信接口(Java开发实践) 一、前期准备 二

发表于 2021-08-29

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

随着互联网的兴起,各行各业的需求都在不断的增加。随着业务的扩大,企业给用户发送短信验证码的业务,也是如火如荼。在这里,calvin给各位开发者推荐阿里云短信平台。原因有二:1.接入较简单,开发成本低 2.非常稳定,短信可达率99%。

OK,那么接下来我们就可以开发阿里短信接口吧。主要包括两个步骤,第一个是前期准备,包括注册阿里云账号,获得access_key和access_secret,并且申请短信签名和短信模板。第二个步骤则是引入阿里短信服务的SDK,并正式开发。

一、前期准备

1.注册阿里云账号

进入网址 www.aliyun.com/ 点击免费注册,输入相关信息,则成为阿里云用户。

2.获取Access_key和Access_secret

3.签名管理与模板管理

首先点击产品与服务,选择短信服务。

我们需要关心的是应用开发这块。

分别申请签名管理和模板管理。

注意,如果是个人学习使用,尽量

二、开发步骤

前面的准备工作都做完并且都申请通过后,我们可以正式进入我们的开发工作。  

可以先在IDEA中新建一个Maven工程。通过依赖阿里短信服务的SDK或者在pom文件中添加相关依赖。

SDK及Demo下载地址为:

help.aliyun.com/document_de…

这里建议使用在pom文件中添加相关依赖,这样比较方便。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.7.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>

了解一下入参与出参

根据官方给出的demo,我们新建类AliDayunSms,在主方法中写入发送短信验证码的逻辑。代码如下:

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
java复制代码import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;


public class AliDayunSms {

public static void main(String[] args) throws ClientException {
//设置超时时间-可自行调整
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化ascClient需要的几个参数
final String product = "Dysmsapi";//短信API产品名称(短信产品名固定,无需修改)
final String domain = "dysmsapi.aliyuncs.com";//短信API产品域名(接口地址固定,无需修改)
//替换成你的AK
final String accessKeyId = "your accessKeyId";//你的accessKeyId
final String accessKeySecret = "your accessKeySecret";//你的accessKeySecret
//初始化ascClient,暂时不支持多region(请勿修改)
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象
SendSmsRequest request = new SendSmsRequest();
//使用post提交
request.setMethod(MethodType.POST);
//必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式;发送国际/港澳台消息时,接收号码格式为00+国际区号+号码,如“0085200000000”
request.setPhoneNumbers("your phoneNumber");
//必填:短信签名-可在短信控制台中找到
request.setSignName("your signName");
//必填:短信模板-可在短信控制台中找到,发送国际/港澳台消息时,请使用国际/港澳台短信模版
request.setTemplateCode("your templateCode");
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
//友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败
//request.setTemplateParam("{\"code\":\"988756\"}");
String msgCode = getMsgCode();
request.setTemplateParam("{\"code\":\"" + msgCode + "\"}");
//请求失败这里会抛ClientException异常
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")) {
//请求成功
System.out.println("=====success====");
} else {
System.out.println("=====fail=======");
}
}

/**
* 生成随机的6位数,短信验证码
* @return
*/
private static String getMsgCode() {
int n = 6;
StringBuilder code = new StringBuilder();
Random ran = new Random();
for (int i = 0; i < n; i++) {
code.append(Integer.valueOf(ran.nextInt(10)).toString());
}
return code.toString();
}
}

将代码中的your accessKeyId和your accessKeySecret替换成你刚才申请或者已有的access_key和access_secret;your phoneNumber替换成你想要接收短信的那个手机号码;your signName替换之前申请到的签名名称;your templateCode也替换成控制台上面显示的那个code。代码中,短信验证码code为变量,里面的值可以自己定义规则生成并替换,可以是随机生成的的6位或者其他位的数字或者字母。

image.png

本文转载自: 掘金

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

互联网公司应该怎么选择技术体系? 谈最优之前,先排除差的 统

发表于 2021-08-29

谈最优之前,先排除差的

我所知道的技术搞得差的公司,都有一个特点:技术体系不统一。每个团队各搞各的,语言不一样,架构不一样,部署方式不一样,水平更是参差不齐。

团队之间协作怎么办?大部分情况下服务提供个 Rest 接口,还有的通过数据库互通数据,甚至更夸张的用 FTP 传文件。

最后导致服务无法统一监控,无法构建链路追踪,日志格式都没办法统一,就更别说从中提取有价值的数据了。这种情况下服务治理基本上不可能,建立基本的观测平台也做不到,成本太高。

统一比最优重要

导致这种情况的原因,正是因为最初的妥协和纵容,到后来成了摆脱不掉的「历史包袱」。这些历史包袱还需要不断找人维护,人员越来越多,走的原来越远,最终连重构的可能性都失去了。

这一般源于没有技术自信的 CTO,怕自己所知的那一套不是最优方案,所以宁愿放手让大家自己去尝试,哪种效果最好再大规模应用也不迟。往往到时候才发现走得太远,已经回不了头了。

作为 CTO(或技术负责人),不一定需要知道最优的架构或方案,但一定要坚决防止技术团队各玩各的。

根据我做架构的经验,任何糟糕的架构,都可以演进优化,或者用新的方案逐步替换掉。唯独遇到多种不同的架构,还各有各的问题,这时候要再统一架构难于登天,不如开个新公司重新开始。

次优的选择,要好于多个最优的选择。1 > N。

统一的价值

随便列举几个要统一的东西:

1. 统一语言

Java 语言具有全世界最优的可观测性,可调试性,永远是互联网公司的不二之选。

在统一语言的基础上,可以构建统一的链路追踪,监控指标,动态追踪平台。大家都用 Java,就都可以用 BTrace、Arthas 做线上问题的快速定位。再比如发生问题时,所有人都能理解 NullPointerException 是什么意思。或一旦有什么技术上的优化,可快速应用到所有服务上去。

2. 统一日志

包括统一输出格式、输出方式、日志框架。通过这些,可以为日后搭建日志平台,研发日志分析工具。可以通过日志中埋入 userId、链路 ID 等,跨服务追踪问题。

3. 统一 RPC 调用框架

对 Java 来说,可以闭着眼用 Dubbo 就好了。Dubbo 是对 Java 语言最友好的框架,社区活跃,成熟度高,运维简单。服务的数量级在三五百以下的完全 cover 的住。

绝对不推荐的是 Spring Cloud。

首先,我所知道的任何一个大公司,没有大规模使用 Spring Cloud 的。其次,Spring Cloud 技术栈的组件太多太杂,想要搞懂就已经很难了,大规模使用出现问题要治理起来更复杂。

网上大多数都会分析 RPC 框架的性能优劣什么的。我认为在选型上,性能是最不值得考虑的。更重要的东西反而经常被忽略:

3.1 统一 RPC 接口定义

Dubbo 的接口定义非常友好。在一个公司内,就可以使用同一个项目定义接口,所有服务都将接口定义在一起,统一审批发布。这样可以避免大家对 Dubbo 的理解能力不一致,导致错误的 RPC 接口定义,或者发布一些不兼容的接口变更。

3.2 开发效率

最早使用 Rest 接口做 RPC 调用时,最麻烦的事,就在于对接口,写接口文档,处理接口响应格式。即使大家都用 JSON,对日期的格式可能各有各的想法,或者因为用了不一样的 JSON 库,最终导致一堆莫名其妙的问题,浪费时间。

对 Dubbo 来说,定义了接口直接继承后写代码就好了,调用方和提供方都不用担心序列化的问题。并且对之后换成 gRPC 也留有后路,接口定义改成 proto 就好了。

至于跨语言 RPC 的问题,压根不值得考虑。没有什么情况需要跨语言,当公司的规模大的时候,各方面的设施也在慢慢齐全,再引入另一门语言实际上是一种破坏。

3.3 RPC 接口管理

对 Dubbo 来说,每个接口的提供方调用方都是可以得知的。每个接口可以统计到调用方的次数,要下线或升级接口时,方便通知到服务调用方。

Dubbo 的问题确实有,就是大规模下的性能问题。如果一个接口的提供方有几十个节点,或者某个服务依赖了几十个接口,Dubbo 的模型就有点处理不来了。

比如在系统中有 100 个节点互相依赖,那么每个节点,都需要与其他 99 个节点建立最低 1 条连接,那他们之间就需要建立 99 * 99 条连接。这对系统来说无疑负载很高。更优的架构,当然是通过中间件与节点建立连接,中间件来转发流量,那么理论上整个系统最低只需要 99 * N(中间件的数量) 条连接。是 N x N -> N x M 的优化,连接数不会无限地高。

这种架构同样会带来问题,最显而易见的是内网流量会翻倍。中间件承载了所有南北流量,对性能要求很高,系统整体稳定性也会有所下降。而对中间件的运维监控同样要付出成本。总之是没有最好的架构,解决一个问题,要解决之后带来的更多问题。

4. 统一链路追踪

链路追踪是重要的观测手段,从链路中可以采集到的非常多有价值的数据。

比如在我司统一使用 Skywalking,我们改造了 Skywalking,使之具备配置动态下发,采样外持续追踪的能力,增加采集了线程级别的内存和 CPU,再进一步从链路中分析到了应用的所有行为,从而可以通过监控系统一眼看出问题的根因所在。

想深挖某个技术栈,前提一定是统一。

统一架构的演进法则

统一技术架构,不代表架构僵化,反而有利于架构演进。公司内应该有固定的架构委员会,持续或者定期地,为公司的技术栈和架构保持活力,跟进行业趋势。

本文转载自: 掘金

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

1…545546547…956

开发者博客

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