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

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


  • 首页

  • 归档

  • 搜索

杨辉三角 II LeetCode刷题笔记 二、思路分析

发表于 2021-11-05

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」


  • 相关文章

LeetCode刷题汇总:LeetCode刷题

一、题目描述


杨辉三角二

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

+ ![image.png](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/c7c9bbbb0a9b94163a41173c9f42a976fddb5e7b3aaa7bfcc1ff07d2ff8448c5)
+ 前面做了杨辉三角一,今天竟然又刷到了二,二话不说,盘他!
+ [大鱼刷题--杨辉三角一](https://dev.newban.cn/7001638318630764574)二、思路分析

======


+ 看看题目的示例,我们来理一理这个思路~
+ 示例 1:



1
2
ini复制代码输入: rowIndex = 3
输出: [1,3,3,1]
+ 示例2:
1
2
ini复制代码输入: rowIndex = 0
输出: [1]
+ 示例3: +
1
2
ini复制代码输入: rowIndex = 1
输出: [1,1]
+ 提示:`0 <= rowIndex <= 33` + **进阶:** - 你可以优化你的算法到 `*O*(*rowIndex*)` 空间复杂度吗? + 咋一看,好像和一没啥区别呀。别急,我们来理一理 - 一是给定一个非负整数 *`numRows`,* 生成「杨辉三角」的前 *`numRows`* 行。 - 二是给定一个非负索引 `rowIndex`,返回「杨辉三角」的第 `rowIndex` 行。 - 我第一反应就是,在第一个的前提上,得到n+1行不就完事了嘛? - 在一个list上取值替换值进行计算,参与下一个计算得到最终行。三、AC 代码

=======


+ 循环记录法:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码class Solution {
   public List<Integer> getRow(int rowIndex) {
       //定义list集合
       List<Integer> list = new ArrayList<>(1);
       list.add(1);
       for (int i = 1; i < rowIndex + 1; i++) {
           //定义临时存储集合
           List<Integer> newList = new ArrayList<>();
           newList.add(1);
           for (int j = 0; j < list.size() - 1; j++) {
               //计算累加量
               newList.add(list.get(j) + list.get(j + 1));
          }
           newList.add(1);
           list = newList;
      }
       return list;
  }
}
- ![image-20211104210219204.png](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/335db86d734e81b164929b5243609a3488add632aa3361bebaab13bc234a30f5) - 好像没啥毛病对吧? + 我这笨蛋脑子,高级的不适合我。。老规矩,学习下大神们的玩法,`研究`透了不就是我自己的东西了嘛?哇哈哈哈 + 官方线性递推解法:
1
2
3
4
5
6
7
8
9
10
csharp复制代码class Solution {
   public List<Integer> getRow(int rowIndex) {
       List<Integer> row = new ArrayList<Integer>();
       row.add(1);
       for (int i = 1; i <= rowIndex; ++i) {
           row.add((int) ((long) row.get(i - 1) * (rowIndex - i + 1) / i));
      }
       return row;
  }
}
- ![image-20211104210501001.png](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/98eb0a42595c9d677109797cc21f8dc082ae555a36dfb0578faca3cc70442756) - 官方解释: * ![image-20211104210644875.png](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/3da3220d7559b4c4fb3c115b08369d168dc530a3060db7fcbe47ac87998b3077) + 给定了索引值,可以直接知道是第几行了。 + 那么可以直接通过循环得到当前的内容。 + 比如: - rowIndex = 3; - 最左边和最右边肯定是1 。那么起始值肯定是1。 - 第一遍循环可以得到 * `row.get(i - 1)`= `row.get(1-1)`= `1` * `(rowIndex - i + 1) / i`= `(3- 1 + 1) / 1`= `3` - 第二遍循环自然结果就是 `3`、`1`。 - 那么组合之后自然而然就是我们要的结果啦!

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

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

本文转载自: 掘金

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

带你学Dubbo---Dubbo负载均衡 Dubbo负载均衡

发表于 2021-11-05

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

Dubbo负载均衡

  • 集群:集群是一种计算机系统,是一种服务器结构。把一组多个计算机,包括硬件和软件组织在一个网络中。相互连接起来共同完成某个工作。对用户来说使用的是一个计算机, 集群对用户是透明的。
  • 负载均衡:负载均衡是以集群为前提的。英文名称为 Load Balance,其意思就是将负载(工作任务)进行平衡、分摊到多个操作单元上进行执行。

对于网络应用而言,并不是一开始就需要负载均衡,当网络应用的访问量不断增长,单个处理单元无法满足负载需求时,网络应用流量将要出现瓶颈时,负载均衡才会起到作用。一般通过一个或者多个前端负载均衡器,将工作负载分发到后端的一组服务器上,从而达到整个系统的高性能和高可用性。

负载均衡有两方面的含义:首先,单个重负载的工作分配到多台服务器做并行处理,每个服务器处理结束后,将结果汇总,返回给用户,系统处理能力得到大幅度提高,这是集群(cluster)技术带来的优势。第二层含义是:大量的并发访问或数据流量分担到多台服务器分别处理,减少用户等待响应的时间。每个访问分配给不同的服务器处理。

Dubbo负载均衡

负载策略

Dubbo 提供了多种均衡策略,缺省为 random 随机调用。

A、Random LoadBalance随机,按权重设置随机概率。

在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

B、 RoundRobin LoadBalance轮循,按公约后的权重设置轮循比率。

存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

C、 LeastActive LoadBalance最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。

使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

D、ConsistentHash LoadBalance一致性 Hash,相同参数的请求总是发到同一提供者。

当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者, 不会引起剧烈变动。算法参见:en.wikipedia.org/wiki/Consis…

缺省只对第一个参数 Hash,如果要修改,请配置<dubbo:parameter key=”hash.arguments”

value=”0,1” />

配置方式

<dubbo:service interface="..." loadbalance="roundrobin" />或<dubbo:reference interface="..." loadbalance="roundrobin" />

  • 随机:loadbalance=” random”
  • 轮询:loadbalance=” roundrobin”
  • 最少活跃:loadbalance=” leastactive”
  • 一致性 Hash:loadbalance=” consistenthash”

本文转载自: 掘金

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

细说一下RedisTemplate的使用方法(六)

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

温故才能知新(复习一下)

上篇文章我们学习了三个方法,还记得是哪几个吗?分别是rename、type、dump三个方法,是否还记得是怎样的用途吗?

今天呢我们要来学习的是哪些呢?来一起学习一下吧。

开始学习,RedisTemplate系列方法之战

getExpire(K key)

功能描述:获取指定键值key的过期时间

具体代码使用:请看下一个方法的代码使用。

使用场景:当需要检测过期时间时使用,在业务中经常会在将要过期时对值进行更新或者进行续期。

源码截图:

image.png

getExpire(K key, TimeUnit timeUnit)

功能描述:获取指定键值key的过期时间,并且在这个基础上进行时间格式的转换。

具体代码使用:

1
2
3
4
5
6
7
8
9
vbnet复制代码/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

使用场景:这个与上面的方法使用场景大致相同,只不过在原来的基础上进行了更好的反馈支持。

TimeUnit类中也给到一些选项,比如NANOSECONDS、MICROSECONDS等等,大家可以自行去看一下,如果这个值传0的话,那么就是代表着永久有效,没有过期时间的标志。

源码截图:

image.png

move(K key, int dbIndex)

功能描述:该方法是将当前redis数据库中的相应key移动到我们指定redis中数据库索引下。

使用场景:这个的使用场景主要还是在迁移的时候会使用到,其他的情况倒是不多。

源码截图:

image.png

小结

今天我们学习了RedisTemplate中的getExpire(K key)、getExpire(K key, TimeUnit timeUnit)、move三个方法,你是否有所收获呢?

本文转载自: 掘金

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

编写代码最应该做好的事情是什么? CODELF

发表于 2021-11-05

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

  • 备战2022春招或暑期实习,祝大家每天进步亿点点!Day7
  • 本篇总结的是 《良好的命名规范》,后续会每日更新~
  • 关于《Redis入门到精通》、《并发编程》等知识点可以参考我的往期博客
  • 相信自己,越活越坚强,活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!

image.png

1、简介

著名的图灵奖得主:尼古拉斯·沃斯,提出一个著名的等式:程序 = 算法 +数据结构。因此我们程序员应该花精力做好学好算法和数据结构,但是这并不是我们编写代码时最应该做好的事情;因为在99.99%的场景下,不需要我们来设计算法和实现数据结构(即便是一般的算法工程师也是如此)。


所以在实际开发中,我们编写代码最应该做好的事情是什么呢?

小捌认为是良好的命名规范和详细的代码注释, 这两个看似简单的问题,但是想要做好真的太难太难了,以至于很多年老开发写的代码都会出现命名不规范,代码注释不全甚至没有注释的情况;因此这篇文章我们先来盘一盘Java程序员应该如何做好程序命名。

2、正文

2.1 目标

将Java程序的命名规范做好,需要达到的目标总结起来就只有四个字——望文生义。达到这个目标的命名能够让程序具有 “自解释” 的功能,仅仅从名字上就能理解某个包、类、方法、变量的含义,这样也可以缩减后续注释的工作量。这样的代码传阅给其他程序员时,也便于阅读和维护。

2.2 包

关于包名的命名有两个大致方向:

  1. 标准类库
  2. 用户类库

标准类库

在Java体系中,标准类库的命名涉及到java、javax等命名,这些命名是由定规范的人定的,因此人家想怎么玩就怎么玩,在核心类库rt.jar包下可以查看。

用户类库

用户类库代码建议使用公司的Internet域名开头,顶级域名在前面,比如com.google、edu.xxx、org.xxx等等。这并不强制要求,但是如果你编写的代码要在公司外部使用(比如开源),这是一个很好打广告、展现公司的机会。

除了包的前缀之外的其余部分,需要满足尽可能使用单个单词,比如time、security、math;也可以使用缩写,比如awt、io、sql。

不建议包名超过8个字母、不建议使用复数。

2.3 类

类由很多种,比如接口、抽象类、枚举类、普通类等等,这些类在命名上有相同点,也有不同点。

相同点:

  • 类名统一使用驼峰命名法则(大驼峰),比如HttpClient、HttpResponse、HttpRequest
  • 类名具有复数形式可以使用复数命名
  • 禁止使用拼音,即便是国际通用的拼音命名,比如alibaba、youku也不建议出现在类名命名中

不同点:

  • 接口命名一定要要言简意赅,这样的命名其实现类可以很好的包含接口的命名,比如Map、Set、List
  • 枚举类必须使用Enum结尾
  • 抽象类必须使用Abstract开头,比如AbstractMap、AbstractSet;也可以使用Base,比如BaseCalendar、BaseRowSet,建议使用Abstract。

2.4 变量

变量的命名遵守小驼峰命名,在名称中要体现具体的业务,描述变量的用途。千万不要学源码搞那些单个字母的命名,比如这种:

此外要注意变量名尽量不要使用is开头,比如isRemoved、isDeleted,因为这种命名往往在序列化场景中,被框架方向解析时错误的认为是removed和deleted,相信这种坑大家都踩过。最好的办法就是完全不用。

2.5 常量

常量就是在作用域内保持不变的值,常量一般用final关键字修饰。常量有很多种分别是全局常量、包内常量、类内常量、局部常量等等。

全局常量:(包内常量、类内常量都一样)

全局常量值得是访问修饰符为public,通常用public static final修饰,这种常量威力巨大,任何地方都可以访问,因此我们一定要把命名做好。

全局常量命名必须全部使用大写字母,如果是多个单词用下划线隔开,拿Java的BigDecimal来举例.

单个单词的常量:

1
java复制代码 public static final BigDecimal ZERO = zeroThroughTen[0];

多个单词的常量:

1
arduino复制代码public final static int ROUND_HALF_UP =      4;

局部常量:

局部常量分为方法常量和参数常量,这两种无需使用大写字母,使用小写即可。

参数常量:

1
2
3
4
5
arduino复制代码public void hello(final String name) {
// 编译错误
// name = "李子捌";
System.out.println(name);
}

方法常量:

1
2
3
4
5
6
ini复制代码public void hello() {
final String name = "李子捌";
// 编译错误
// name = "Liziba";
System.out.println(name);
}

3、彩蛋

最后给大家推荐一个在线命名神器。大家快去试试吧,随便输入一个单词,便会给你搜索很多由该单词组成的词。unbug.github.io/codelf/#cli…

CODELF

本文转载自: 掘金

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

深入理解JVM------垃圾收集器

发表于 2021-11-05

​

引言

如果说收集算法是垃圾回收的方法论,那么垃圾收集器就是内存回收的具体实现。基于我们平时使用的jvm都是HotSpot的实现,因此,本篇讨论的垃圾收集器也是基于HotSpot的jvm。

垃圾收集器分类

 **串行收集器**:只有一条GC线程,运行时需要停止用户程序。


![](https://gitee.com/songjianzaina/juejin_p18/raw/master/img/d889d6c64799a565a84e36277ca013cd8237f25b54fedae642b8da5a3b02460c)![]( "点击并拖拽以移动")​

并行收集器:有多条GC线程,运行时也需要停止用户程序。

![](https://gitee.com/songjianzaina/juejin_p18/raw/master/img/a42ba18baf41d28690d02a29870b1102e38e1cbfdab215139a57c41ebb15ba12)![]( "点击并拖拽以移动")​


    **并发收集器**:有一条或多条GC线程,每个收集周期都要经历:**初始标记、并发标记、重新标记、并发清除**。


![](https://gitee.com/songjianzaina/juejin_p18/raw/master/img/edf9e8894f7d7fa044bbd1862190a742147fb030713a5d4fb38faff57b05a714)![]( "点击并拖拽以移动")​           

Garbage First(G1):有一条或多条GC线程,收集动作类似并发:初始标记、并发标记、重新标记、清除、转移回收。

![](https://gitee.com/songjianzaina/juejin_p18/raw/master/img/9c9938e2fc0985ee36639c74ccd86fac63d5878f54525004bc5862b3419392ef)![]( "点击并拖拽以移动")​

HotSpot中的垃圾收集器

上面我们已经知道了垃圾收集器的分类,下面我们看下每种类型下对应的垃圾收集器:   

串行收集器的实现: serial(用于新生代,采用复制算法)、serial old(用于年老代,采用标记/整理算法)。

并行收集器的实现: ParNew(用于新生代,采用复制算法)、Parallel Scavenge(用于新生代,采用复制算法)、Parallel old(用于年老代,采用标记/整理算法)。

并发收集器的实现: concurrent mark sweep[CMS](用于年老代,采用标记/清除算法)。

**Garbage First(G1):** 用于新生代和老年代,新生代采用复制算法、老年代采用标记-整理 **。**


 可以看到,上面每一种垃圾搜集器都是针对不同内存区域所设计的,因为它们采用的算法不同,凡是用于新生代的都是使用的复制算法,而用于年老代的都是使用的标记/清除或者标记/整理算法。


 下面我们来一起看下jdk垃圾回收的大家庭以及它们之间的相互组合关系:


 ![](https://gitee.com/songjianzaina/juejin_p18/raw/master/img/c1ffb9bda0edf82406765f8991406152c0d70655076c01495c5acd1ed4ec17f6)![]( "点击并拖拽以移动")​


 针对上面简单说明下,上面三个是新生代收集器,下面三个是老年代收集器,两者之间的连线表示两者可以配合使用,中间的G1收集器。

结束语

本章大致介绍了7种垃圾收集器,关于它们的运行方式和特点我们下一章再一起讨论。  

​

本文转载自: 掘金

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

栈的实现

发表于 2021-11-05

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

一、栈

💦 栈的概念及结构

1
2
sql复制代码栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对应多种出栈顺序

栈有两个经典的操作

1️⃣ 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

2️⃣ 出栈:栈的删除操作叫做出栈。出数据也在栈顶 。

在这里插入图片描述

💦 栈的实现

这里对于栈的实现我们既可以选择数组也可以和选择链表两者的效率都差不多,但是还是建议使用数组
在这里插入图片描述

1.初始化

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
c复制代码void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
2.插入

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
c复制代码void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}
3.判空

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}
4.删除

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
c复制代码void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}
5.长度

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}
6.栈顶

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
c复制代码STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}
7.销毁

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

💦 完整代码

这里需要三个文件

1️⃣ Static.h,用于函数的声明

2️⃣ Static.c,用于函数的定义

3️⃣ Test.c,用于测试函数


🧿 Stack.h
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
c复制代码#pragma once

//头
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

//结构体
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a; //指向动态开辟的空间
int top; //栈顶
int capacicy; //容量
}ST;

//函数
//注意链表和顺序表我们写Print,但是栈不写,因为如果栈可以Print的话,就不符合后进先出了
//初始化
void StackInit(ST* ps);
//插入
void StackPush(ST* ps, STDatatype x);
//判空
bool StackEmpty(ST* ps);
//删除
void StackPop(ST* ps);
//长度
int StackSize(ST* ps);
//栈顶
STDatatype StackTop(ST* ps);
//销毁
void StackDestory(ST* ps);
🧿 Stack.c
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
c复制代码#include"Stack.h"

void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}
bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}
void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}
int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}
STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}
void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
🧿 Test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include"Stack.h"

int main()
{
ST st;
//初始化
StackInit(&st);
//插入+删除
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPop(&st);
StackPop(&st);
//长度
StackSize(&st);
//栈顶
StackTop(&st);
//销毁
StackDestory(&st);
return 0;
}

本文转载自: 掘金

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

微服务编程范式 原则(道) 标准(术) 及时清理技术债务 推

发表于 2021-11-05

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

目前很多互联网公司都采用微服务架构,微服务的优点和缺点被反复说到,这里不在重复赘述,只结合工作中的一些实践,说说要用微服务要注意的点,厚颜写做编程范式,其实就是一些具体实践而已。

原则(道)

原则是比较抽象的一个概念,简单说是一些指导意见,在不同的组之间可以共享,是为了实现一个共同的目的,必须遵守的一些规则,或者叫做规矩。

这些规则(或规矩)对我们开发过程中有一定的约束作用,不容置疑,必须遵守。如果发现有那个团队或者个人没有遵守,一定要及时纠正,否则,原则就没有任何存在的意义。

现在用的很广的一个原则是Heroku的 12-Factor 原则。具体内容如下:

I. 基准代码

一份基准代码,多份部署

II. 依赖

显式声明依赖关系

III. 配置

在环境中存储配置

IV. 后端服务

把后端服务当作附加资源

V. 构建,发布,运行

严格分离构建和运行

VI. 进程

以一个或多个无状态进程运行应用

VII. 端口绑定

通过端口绑定提供服务

VIII. 并发

通过进程模型进行扩展

IX. 易处理

快速启动和优雅终止可最大化健壮性

X. 开发环境与线上环境等价

尽可能的保持开发,预发布,线上环境相同

XI. 日志

把日志当作事件流

XII. 管理进程

后台管理任务当作一次性进程运行

在我们团队中有一些原则可以和大家分享:

  1. 不要为了用而用,程序猿(或者叫攻城狮)是最喜欢尝鲜的人,有了新技术出来,就想用。

这无疑是优点,但是在工作中这样,就有可能给系统带来灾难。

所以如果想用某项技术,必须充分调研之后,经过一系列的验证,才能引入。
2. 战略目标明确,业务部门愿景清晰。

作为开发团队,可能很少关心业务团队的想法,这是很致命的。

我们开发的东西,不是业务部门想要的,顾客想要一个吃饭的勺子,你给做了一个铁铲,铁铲做的再好又有什么用。
3. 服务之间调用必须使用HTTP/RESTful方案,不能引入其他方案。

不是说其他方案不好,而是最好协议一致,一致的协议能够减少系统的复杂性和沟通成本。

标准(术)

标准的定义会比原则更加具体,有点类似于道和术、战略和战术的关系,不同的技术栈、不同的团队可能会制定不同的标准。

  1. 我们团队都是使用的是Java技术栈,所以标准大体上采用的是《阿里巴巴Java开发手册》,有一部分内容作了修改,这里对孤尽表示感谢。

这个手册脱胎于《Effective Java》和《代码简洁之道》,其中加入了孤尽在阿里的一些实践,所以对于Java栈的同学是比较适用的。
2. 我们使用的是Spring Cloud,服务之间的调用,必须使用Feign Client形式,指定这个标准是为了以后使用K8s时改动最小。
3. 页面与服务之间的调用,HTTP返回状态码都是200,在返回的数据中,定义具体的状态码,这个状态码参照HTTP状态码,同时定义一个子级状态码,用来具体定义业务情况。

比如,状态码等于500表示服务异常,子级状态码等于5001,表示操作数据库异常等。
4. 监控系统、日志系统、任务调度等,如果需要,也要指定明确是标准。

比如打印日志时,日志开头必须以6位数字开头,因为我们的日志系统与监控系统对接,6位数字能够定位到不同的系统、模块、业务,直接定位问题位置。
5. 不一而足(每个团队有每个团队具体的标准,这里就不一一列举了)。

这些标准,最好形成文档,放在知识库中。这样,团队的成员可以随时查看,有新人加入时,也能避免老员工口口相传,传错了指令。

有些架构师认为,原则和标准就是一个东西。就我来看,这两个粒度不同,对于大的团队,最好分开。因为大的团队,技术栈更加多样化,标准没有办法做到一致,但是原则可以做到不会脱离大框。对于小的团队,因为技术栈比较单一,有可能就是一个技术栈(就像我现在的团队),因为标准只有一个,就是对原则的细化,所以两者就是一个东西。

标准的另外一个作用,就是告诉团队成员怎么做是对的,怎么做是更好的方案,更加直白的表述是,按照标准进行开发,bug更少。

有了开发标准之后,就需要将标准推广到所有人,但是每个人的理解力和执行力是有偏差的,所以需要一些手段来快速推广。具体的方法有:DEMO(示例)、代码模板(脚手架)。

DEMO(示例)

软件开发是一个比较有技术壁垒的行业,一个新人(没有经验或者有经验刚加入新团队的人)想要快速了解老系统所使用的标准,是比较困难的,毕竟每家团队采用的标准都不是百分之百一致。比较简单的做法就是,提供新人DEMO示例,然后告诉他,“就照着这个做”。

对于有经验的新人,一定是先接受,然后在了解清楚之后,才会提出自己的一些看法,而不是一上来,就搬出自己以前的经验,全盘否定团队指定的标准。

代码模板(脚手架)

代码模板的作用实现一个服务的集成方案,经过有效可靠的裁剪和定制。在需要新建服务时,就使用这个方案,直接进行业务代码开发即可,所以也被称为脚手架,比如SpringBoot的Starter和AutoConfiguration。

前面说过,我们团队使用的是Java技术栈,基于SpringCloud开发,所以我们对SpringCloud进行封装,定义了几根通用的组件,比如定义了一个web-misc组件,引入这个组件,就能够实现,引入实现WebMvc,并且提供了统一的获取Metric信息的接口,统一的异常处理的ExceptionHandler等。

及时清理技术债务

虽然我们都是代码洁癖,但是有时候迫于时间压力、业务压力,我们不得不背负一些技术债务。

比如我们知道硬编码会破坏系统灵活性,但是明天就要上线,根本没有时间做配置型服务,所以只能硬编码到系统中,这就是技术债务。第二天系统上线,正常运行,如果这个时候把硬编码事情抛之脑后,这个债务就会产生利息,不知道哪天就会变成炸弹。

对于技术债务,我们团队的做法是做一个技术债务清单,设置超时提醒功能,限期修复。这个清单是公开的,每一项都注明了,是谁,因为什么,于什么时间,产生的技术债务,需要在什么时候修复。如果是临时特性或功能产生的债务,也需要注明特性或功能过期时间,由专人检查债务是否已经没有了。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

微服务编程范式

本文转载自: 掘金

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

Spring Cloud Gateway实战之一:初探

发表于 2021-11-05

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):github.com/zq2599/blog…

关于《Spring Cloud Gateway实战》系列

《Spring Cloud Gateway实战》是欣宸在Java领域的系列原创,旨在通过项目实战与大家一起学习和掌握Spring Cloud Gateway,更好的为实际项目服务

本篇概览

作为《Spring Cloud Gateway实战》的开篇,本文的主要内容如下:

  1. 基础知识简介
  2. 确认环境涉及到的工具和服务的版本
  3. 启动nacos,作为后续实战的注册中心和配置中心
  4. 创建maven父工程,为后续实战的代码和依赖库版本做好管理
  5. 创建名为common的子工程,存放共用的常量和数据结构
  6. 创建名为provider-hello的web应用,用于gateway路由的目标
  7. 运行一个简单的demo,完成spring-cloud-gateway的初体验

关于Spring Cloud Gateway

  • 这是一个基于Spring技术栈构建的API网关,涉及到:Spring5、Spring Boot 2、Reactor等,目标是为项目提供简单高效的API路由,以及强大的扩展能力:安全、监控、弹性计算等
  • 官方架构图如下,可见请求到来后,由Handler Mapping决定请求对应的真实目标,然后交给Web Handler,由一系列过滤器(filter)执行链式处理,从红色箭头和注释可以发现,请求前后都有过滤器在运行:

在这里插入图片描述

版本信息

  • 《Spring Cloud Gateway实战》系列涉及的软件和库版本信息如下:
  • 本篇实战涉及到的主要版本情况如下:
  1. JDK:1.8.0_291
  2. IDEA:2021.1.3 (Ultimate Edition)
  3. maven:3.8.1
  4. 操作系统:win10 64位
  5. springboot:2.4.2
  6. spring-cloud:2020.0.1
  7. spring-cloud-alibaba:2021.1
  • 更详细的版本匹配关系请参考:github.com/alibaba/spr…

经典配置中的核心概念

  • 先通过一个典型的简化版配置来了解几个核心概念,假设Spring Cloud Gateway应用正在运行,监听8080端口,一旦有远程请求来到8080端口,下面的配置就会生效了,三个核心概念,以及每个配置的作用,请参考中文注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yml复制代码spring:
cloud:
gateway:
# 核心概念1:路由,一个路由代表一个处理逻辑,
# 该逻辑里面包含三个元素:匹配条件(是否该此路由处理)、真实处理地址、过滤器
routes:
# id要确保唯一性
- id: add_request_header_route
# 真实处理地址,请求一旦确定是当前路由处理,就会转发到这个地址去
uri: https://example.org
# 核心概念2:谓语或者断言,作用是判断请求是否由当前路由处理
predicates:
# 这是断言的一种,检查请求的Cookie中mycookie的值是否等于mycookievalue
- Cookie=mycookie,mycookievalue
# 核心概念3:过滤器,请求前和请求后都可以有过滤器处理请求响应数据
filters:
# 这个过滤器的作用是在请求header中添加一个键值对,值等于"aaabbbccc"
- AddRequestHeader=X-Request-Red, aaabbbccc
  • 上述配置信息中的predicates是简化版配置,和完整配置对比效果如下,简单的说就是把一行拆成了三项:name、args.name、args.regexp

在这里插入图片描述

  • 理论知识点到为止,咱们还是尽快动手吧

启动nacos-2.0.3

  • 整个《pring Cloud Gateway实战》系列,我们会涉及到多个服务,这就不可避免的会用到注册中心和配置中心,这里我选择了nacos,它可以很好地承担注册中心和配置中心的角色,接下来介绍如何部署和启动nacos
  • 下载nacos,地址是:github.com/alibaba/nac…
  • 解压后进入nacos\bin目录,执行以下命令启动nacos:
1
shell复制代码startup.cmd -m standalone
  • 如果您的电脑是mac或者linux,请执行以下命令启动nacos:
1
shell复制代码sh startup.sh -m standalone
  • 浏览器登录nacos,地址是http://localhost:8848/nacos,账号和密码都是nacos
  • 登录成功后显示如下:

在这里插入图片描述

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在spring-cloud-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

《Spring Cloud Gateway实战》系列的父工程

  • 新建名为spring-cloud-tutorials的maven工程,这就是《Spring Cloud Gateway实战》系列所有源码的父工程就,pom.xml内容如下,可见这里将springboot、spring-cloud、spring-cloud-alibaba库的版本号都已经确定,今后子工程就无需关注依赖库的版本号了:
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
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">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>hello-gateway</module>
<module>provider-hello</module>
<module>common</module>
</modules>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>

<groupId>com.bolingcavalry</groupId>
<artifactId>spring-cloud-tutorials</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
</properties>

<packaging>pom</packaging>
<description>Demo project for Spring Cloud </description>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.9</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

创建名为common的子工程,存放共用的常量和数据结构

  • 现在创建名为common的子工程,整个《Spring Cloud Gateway实战》系列涉及的常量和数据结构都放在这个子工程中,方便其他工程使用
  • 新增常量Constants.java:
1
2
3
4
5
java复制代码package com.bolingcavalry.common;

public interface Constants {
String HELLO_PREFIX = "Hello World";
}

创建web应用,作为服务提供方

  • 现在创建名为provider-hello的web应用,这是个极其普通的web应用,提供几个http接口服务,咱们在尝试Spring Cloud Gateway的基本功能时,都会将请求路由到provider-hello上来
  • provider-hello是个普通的springboot应用,会在nacos进行注册,其pom.xml内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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>spring-cloud-tutorials</artifactId>
<groupId>com.bolingcavalry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>provider-hello</artifactId>
<packaging>jar</packaging>

<dependencies>

<dependency>
<groupId>com.bolingcavalry</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--nacos:用于服务注册与发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<!-- 如果父工程不是springboot,就要用以下方式使用插件,才能生成正常的jar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.bolingcavalry.provider.ProviderApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
  • 工程的配置文件application.yml如下,web端口是8082,还有一处要注意的是nacos服务地址:
1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码server:
#服务端口
port: 8082

spring:
application:
name: provider-hello
cloud:
nacos:
discovery:
# nacos服务地址
server-addr: 127.0.0.1:8848
  • 启动类ProviderApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.bolingcavalry.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
  • 普通的Controller类Hello.java,对外提供一个http服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码package com.bolingcavalry.provider.controller;

import com.bolingcavalry.common.Constants;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;

@RestController
@RequestMapping("/hello")
public class Hello {

private String dateStr(){
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
}

/**
* 返回字符串类型
* @return
*/
@GetMapping("/str")
public String helloStr() {
return Constants.HELLO_PREFIX + ", " + dateStr();
}
}
  • 新增测试类HelloTest.java,用于检查应用的服务是否正常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码package com.bolingcavalry.provider.controller;

import com.bolingcavalry.common.Constants;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
class HelloTest {

@Autowired
private MockMvc mvc;

@Test
void hello() throws Exception {
String responseString = mvc.perform(MockMvcRequestBuilders.get("/hello/str").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString(Constants.HELLO_PREFIX)))
.andDo(print())
.andReturn()
.getResponse()
.getContentAsString();

log.info("response in junit test :\n" + responseString);
}
}
  • 执行单元测试(此时nacos是否启动无所谓,只是不启动的话控制台会有一些错误信息,但是没有影响),如下,测试通过表示服务是正常的:

在这里插入图片描述

开发一个简单的demo,完成spring-cloud-gateway的初体验

  • 前面做了那么多准备,接下来咱们会投入到Spring Cloud Gateway的开发中,先写个简单的demo快速体验一下
  • 新增名为hello-gateway的子工程,pom.xml如下,重点是依赖了spring-cloud-starter-gateway库,还有一处要重点小心的:测试库用的是reactor-test和spring-boot-starter-test,这和之前的单元测试很不一样,用的是webflux:
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
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>spring-cloud-tutorials</artifactId>
<groupId>com.bolingcavalry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>hello-gateway</artifactId>

<dependencies>
<dependency>
<groupId>com.bolingcavalry</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
  • 下面是重点,Spring Cloud Gateway的配置文件application.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码server:
#服务端口
port: 8081
spring:
application:
name: hello-gateway
cloud:
gateway:
routes:
- id: path_route
# 匹配成功后,会被转发到8082端口,至于端口后面的path,会直接使用原始请求的
# 例如http://127.0.0.1:8081/hello/str,会被转发到http://127.0.0.1:8082/hello/str
uri: http://127.0.0.1:8082
predicates:
# 根据请求路径中带有"/hello/",就算匹配成功
- Path=/hello/**
  • 如果要转发到其他域名下,需要创建配置类解决跨域问题:
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
java复制代码package com.bolingcavalry.hellogateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source);
}
}
  • 启动类:
1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.bolingcavalry.hellogateway;

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

@SpringBootApplication
public class HelloGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(HelloGatewayApplication.class,args);
}
}
  • 最后是单元测试类,请注意,由于Spring Cloud Gateway使用了webflux技术栈,因此不能用常见的MockMvc来模拟请求,几个注解也值得注意,另外也要注意WebTestClient的expectStatus、expectBody等API的用法:
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
java复制代码package com.bolingcavalry.hellogateway;

import com.bolingcavalry.common.Constants;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@AutoConfigureWebTestClient
public class HelloTest {

@Autowired
private WebTestClient webClient;

@Test
void testHelloPredicates() {
webClient.get()
.uri("/hello/str")
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 验证状态
.expectStatus().isOk()
// 验证结果,注意结果是字符串格式
.expectBody(String.class).consumeWith(result -> assertTrue(result.getResponseBody().contains(Constants.HELLO_PREFIX)));
}
}
  • 请确保provider-hello应用已经启动,再运行上面创建的HelloTest.java,得到结果如下,测试通过,证明hello-gateway的功能符合预期,成功的将请求转发到provider-hello应用,并且成功收到响应:

在这里插入图片描述

  • 至此,《Spring Cloud Gateway实战》系列的准备工作已经完成,而且开发了一个简单的应用体验最基本的Spring Cloud Gateway功能,接下来的文章,咱们一起实战更多基本功能。

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

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

还在用new Date计算任务执行时间?强烈建议使用这个AP

发表于 2021-11-05

在实践过程中,我们经常需要记录一个任务执行的耗时,这是评价代码好坏,评测代码性能,排查业务执行问题的重要操作。那么,你是如何来获取并计算任务执行耗时的呢?通过new Date获得时间进行换算?还是有更好的方案?本文给你答案。

获取任务耗时通常做法

获取任务耗时,最简单的方式就是打印当前时间与任务开始执行时间的差值,实例代码如下:

1
2
3
4
5
6
7
8
9
vbscript复制代码  @Test
public void testElapsedTimes() throws InterruptedException {
long startTime = new Date().getTime();
​
// do something
Thread.sleep(1000);
​
System.out.println("执行耗时:" + (new Date().getTime() - startTime) + "ms");
}

上述方式实现简单,逻辑也比较直观。但如果执行大量测试,测试中还有不同的代码逻辑块,那么需要改动的地方就比较多。

改进做法

在上述代码中,如果IDE安装有代码检查工具,则会提示采用System.currentTimeMillis()来获取时间,而不是new Date().getTime()的方式。

改造之后,实现代码如下:

1
2
3
4
5
6
7
8
9
csharp复制代码  @Test
public void testElapsedTimes1() throws InterruptedException {
long startTime = System.currentTimeMillis();
​
// do something
Thread.sleep(1000);
​
System.out.println("执行耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}

在这样的场景下(无需获取更多Date相关信息)也推荐使用System.currentTimeMillis()来获取时间戳。至于为什么,看一下Date的源码实现就知道了。

Date的构造方法:

1
2
3
csharp复制代码public Date() {
  this(System.currentTimeMillis());
}

Date在构造时,本质上也是先获得了System.currentTimeMillis(),然后再初始化其他信息。既然我们只需要时间戳,那就没必要再构建Date对象了。从性能层面来说,能优化则优化。

Spring的StopWatch

上述两种方式虽然性能和写法有所区别,但本质是一样的。下面我们来讲讲Spring提供的StopWatch类,它不仅可实现上述功能,而且还可以做类似任务执行时间控制,也就是封装了一个对开始时间、结束时间记录操作的Java类。

先通过StopWatch类来实现一下上述功能:

1
2
3
4
5
6
7
8
9
10
11
java复制代码  @Test
public void testStopWatch() throws InterruptedException {
StopWatch sw = new StopWatch();
​
sw.start("开始执行业务");
// do something
Thread.sleep(1000);
sw.stop();
​
System.out.println(sw.getTotalTimeMillis());
}

通过创建StopWatch对象,然后调用它的start、stop方法来区分执行任务区间,通过getTotalTimeMillis()方法获得总耗时。

乍一看,代码好像还比之前的方式多了,体现不出来什么优势啊!下面我们再来看一个复杂点的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码  @Test
public void testStopWatch1() throws InterruptedException {
StopWatch sw = new StopWatch();
​
sw.start("起床");
Thread.sleep(1000);
sw.stop();
​
sw.start("洗漱");
Thread.sleep(2000);
sw.stop();
​
sw.start("锁门");
Thread.sleep(500);
sw.stop();
​
System.out.println(sw.prettyPrint());
System.out.println(sw.getTotalTimeMillis());
System.out.println(sw.getLastTaskName());
System.out.println(sw.getLastTaskInfo());
System.out.println(sw.getTaskCount());
}

执行上述测试示例,打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
markdown复制代码StopWatch '': running time = 3509166972 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
1003330360 029% 起床
2001421734 057% 洗漱
504414878 014% 锁门
​
3509
锁门
org.springframework.util.StopWatch$TaskInfo@12f40c25
3

此时,看到StopWatch的魅力所在了吗?

  • 通过多组start、stop方法,将业务代码块进行区分,可获得不同代码块的执行耗时;
  • 可以通过start方法传入taskName,对每个代码块进行命名;
  • 可以对总任务耗时、每个任务耗时进行统计分析;
  • prettyPrint()方法,可以优雅的打印出统计分析信息;
  • getTotalTimeMillis()方法,打印出总耗时;
  • getLastTaskName()方法,打印最后一个任务名称;
  • getLastTaskInfo()方法,获得最后一个任务的TaskInfo,进而获得更多相关信息;
  • getTaskCount()方法,获得任务数;

现在再看,使用StopWatch是不是可以获得更多有用的信息?

StopWatch的实现原理

最后呢,我们再来看一眼源码,了解一下StopWatch的实现机制。

先看StopWatch的start方法实现:

1
2
3
4
5
6
7
java复制代码  public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}

start方法中记录了任务名称和任务执行的时间,基于System.nanoTime()获得。

stop方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
}
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
if (this.keepTaskList) {
this.taskList.add(this.lastTaskInfo);
}
++this.taskCount;
this.currentTaskName = null;
}

在stop方法中,通过两个时间戳相减获得lastTime,也就是一个任务的执行时间;lastTime累计相加获得总的执行时间;同时,记录任务列表、任务数统计。

而其他get方法,则是对start、stop中获取的数据的进一步统计、分析、格式化输出而已。

小结

有些功能当我们使用习惯了,可能就固守于一个实现方式,但如果去参考学习其他框架中类似功能的实现,往往会有些新的突破。如果你在使用Spring的框架,建议你尝试一下StopWatch这个API,可以让你的时间统计日志更加高端大气。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

统一接口返回数据格式(一)—搭建公共服务

发表于 2021-11-05

1 整体说明

在前后端分离的Web开发中,为便于开发,有必要统一后端接口的返回结果格式以及统一处理全局异常。常见的统一格式如下:

1
2
3
4
5
json复制代码{
 "status":"100",
 "message":"操作成功",
 "data":"hello,world"
}

为了避免各个微服务都自行实现返回结果的封装和全局异常的处理,可将上述功能抽取为公共服务,其他服务引入公共服务直接使用。

通过@RestControllerAdvice来拦截所有的@RestController,@RestControllerAdvice注解搭配ResponseBodyAdvice接口可以实现在接口调用正常时的封装返回数据,@RestControllerAdvice注解搭配@ExceptionHandler注解可以实现在接口调用异常时封装指定异常类型的结果。本文就逐步说明如何搭建公共服务,并在其他服务中引用该公共服务。

2 公共服务搭建

2.1 服务自建自用

参考资料:SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

2.1.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
arduino复制代码@Data
public class ResultData<T> {
   private int status;
   private String message;
   private T data;
   private long timestamp ;
​
   public ResultData (){
       this.timestamp = System.currentTimeMillis();
  }
​
   public static <T> ResultData<T> success(T data) {
       ResultData<T> resultData = new ResultData<>();
       resultData.setStatus(ReturnCode.RC100.getCode());
       resultData.setMessage(ReturnCode.RC100.getMessage());
       resultData.setData(data);
       return resultData;
  }
​
   public static <T> ResultData<T> fail(int code, String message) {
       ResultData<T> resultData = new ResultData<>();
       resultData.setStatus(code);
       resultData.setMessage(message);
       return resultData;
  }
}
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
scss复制代码/* 定义状态码 */
public enum ReturnCode {
  /**操作成功**/
  RC100(100,"操作成功"),
  /**操作失败**/
  RC999(999,"操作失败"),
  /**服务限流**/
  RC200(200,"服务开启限流保护,请稍后再试!"),
  /**服务降级**/
  RC201(201,"服务开启降级保护,请稍后再试!"),
  /**热点参数限流**/
  RC202(202,"热点参数限流,请稍后再试!"),
  /**系统规则不满足**/
  RC203(203,"系统规则不满足要求,请稍后再试!"),
  /**授权规则不通过**/
  RC204(204,"授权规则不通过,请稍后再试!"),
  /**access_denied**/
  RC403(403,"无访问权限,请联系管理员授予权限"),
  /**access_denied**/
  RC401(401,"匿名用户访问无权限资源时的异常"),
  /**服务异常**/
  RC500(500,"系统异常,请稍后重试"),
​
  INVALID_TOKEN(2001,"访问令牌不合法"),
  ACCESS_DENIED(2003,"没有权限访问该资源"),
  CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
  USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
  UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");
​
  /**自定义状态码**/
  private final int code;
  /**自定义描述**/
  private final String message;
​
  ReturnCode(int code, String message){
      this.code = code;
      this.message = message;
  }
​
  public int getCode() {
      return code;
  }
​
  public String getMessage() {
      return message;
  }
}

2.1.2 封装接口异常的返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
   /**
    * 默认全局异常处理。
    */
   @ExceptionHandler(Exception.class)
   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
   public ResultData<String> exception(Exception e) {
       log.error("全局异常信息 ex={}", e.getMessage(), e);
       return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
  }
}

2.1.3 封装接口正常的返回结果

借助@RestControllerAdvice注解和ResponseBodyAdvice接口实现。

@RestControllerAdvice是@RestController注解的增强,可以实现三个方面的功能:全局异常处理、全局数据绑定、全局数据预处理。

ResponseBodyAdvice接口的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typescript复制代码@RestControllerAdvice  //拦截Controller方法的返回值, 统一处理返回值/响应体, 一般用于统一返回格式、加解密、签名等等
public class RestResponseAdvice implements ResponseBodyAdvice<Object> {
   @Autowired
   private ObjectMapper objectMapper;
​
   /*
   * 是否支持advice功能
   * true支持, false不支持
    */
   @Override
   public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
       return true;
  }
​
   /*
    * 处理返回的结果数据
    */
   @SneakyThrows
   @Override
   public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
                                 Class<? extends HttpMessageConverter<?>> aClass,
                                 ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
       if(o instanceof String){
           return objectMapper.writeValueAsString(ResultData.success(o));
      }
       if (o instanceof ResultData) {  // 对于已经是ResultData类型的结果不再封装, 直接返回
           return o;
      }
       return ResultData.success(o);
  }
}

2.1.4 在当前服务中验证

经过2.1.1~2.1.3节的处理,已经完成统一接口的返回数据格式和封装全局异常,这里直接在当前服务中创建2个接口进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
@RequestMapping("/api/demo")
public class RestDemo {
​
   @GetMapping("/hello")
   public String Hello() {
       return "hello, world!";
  }
​
   @GetMapping("/wrong")
   public int Wrong() {
       return 3/0;
  }
}

项目结构:

image-20211105000059803.png

接口调用正常的结果:

image-20211105000238234.png

接口调用异常的结果:

image-20211105000325898.png

2.2 公共服务安装

参考资料:

Maven 构建生命周期

springboot项目如何打包给其他项目引用

为了在其他服务中能以maven依赖项的方式引用2.1节搭建的公共服务,需要把公共服务安装到本地maven仓库或者部署到远程仓库。这里采用安装到本地仓库的方式进行演示。

Spring Boot打包的是Spring Boot特有格式的jar包,即可以运行的fat jar(生成jar包中源码对应的class文件在BOOT-INF目录),并不是传统的maven的JAR包,为此需要修改公共服务的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
xml复制代码    <build>
<plugins>
<!-- <plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

在IDEA的Terminal中执行maven项目安装命令:mvn clean install -Dmaven.test.skip=true。

image-20211105001840547.png

3 公共服务引用

搭建另一个SpringBoot Web业务服务,在这个服务中引用第2节搭建的公共服务以实现统一接口的返回结果格式的目的。

3.1 引入公共服务依赖项

公共服务的pom.xml部分配置如下:

1
2
3
4
5
6
7
8
xml复制代码    <groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common-advice</name>
<description>Public service that wraps interface results</description>
<properties>
<java.version>1.8</java.version>
</properties>

在业务服务的pom.xml中添加依赖项:

1
2
3
4
5
xml复制代码        <dependency>
<groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

3.2 组件扫描路径添加公共服务

在业务服务的组件扫描路径(在服务入口增加@ComponentScan配置)中增加公共服务的包路径,否则无法使@RestControllerAdvice注解修饰的配置类生效。

查看@RestControllerAdvic源码可知,它是封装了@ControllerAdvice注解,而@ControllerAdvice封装了@Component注解,根据SpringBoot自动装配的规定,通过@ComponentScan配置可以将@RestControllerAdvic注解标记的组件加载。

1
2
3
4
5
6
7
less复制代码@ComponentScan({"com.example.demorest", "com.example.common.advice"})
@SpringBootApplication
public class DemorestApplication {
public static void main(String[] args) {
SpringApplication.run(DemorestApplication.class, args);
}
}

3.3 编写接口验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
@RequestMapping("/api/original")
public class RestOriginal {
​
   @GetMapping("/success")
   public String GetSuccess() {
       return "hello, don!";
  }
​
   @GetMapping("/error")
   public int GetError() {
       return 6/0;
  }
}

项目结构:

image-20211105005252932.png

接口访问正常的结果:

image-20211105004944976.png

接口访问异常的结果:

image-20211105005031105.png

4 补充说明

通过调试我们可以发现,@RestControllerAdvice注解搭配的ResponseBodyAdvice接口和ExceptionHandler注解都是在controller接口方法体已经执行完成后在开始应用。

本文转载自: 掘金

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

1…417418419…956

开发者博客

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