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

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


  • 首页

  • 归档

  • 搜索

什么?面试官问我G1垃圾收集器?

发表于 2021-11-11

面试官:要不这次来聊聊G1垃圾收集器?

候选者:嗯嗯,好的呀

候选者:上次我记得说过,CMS垃圾收集器的弊端:会产生内存碎片&&空间需要预留

候选者:这俩个问题在处理的时候,很有可能会导致停顿时间过长,说白了就是CMS的停顿时间是「不可预知的」

候选者:而G1又可以理解为在CMS垃圾收集器上进行”升级”

候选者:G1 垃圾收集器可以给你设定一个你希望Stop The Word 停顿时间,G1垃圾收集器会根据这个时间尽量满足你

候选者:在前面我在介绍JVM堆的时候,是画了一张图的。堆的内存分布是以「物理」空间进行隔离

候选者:在G1垃圾收集器的世界上,堆的划分不再是「物理」形式,而是以「逻辑」的形式进行划分

候选者:不过,像之前说过的「分代」概念在G1垃圾收集器的世界还是一样奏效的

候选者:比如说:新对象一般会分配到Eden区、经过默认15次的Minor GC新生代的对象如果还存活,会移交到老年代等等…

候选者:我来画下G1垃圾收集器世界的「堆」空间分布吧

候选者:从图上就可以发现,堆被划分了多个同等份的区域,在G1里每个区域叫做Region

候选者:老年代、新生代、Survivor这些应该就不用我多说了吧?规则是跟CMS一样的

候选者:G1中,还有一种叫 Humongous(大对象)区域,其实就是用来存储特别大的对象(大于Region内存的一半)

候选者:一旦发现没有引用指向大对象,就可直接在年轻代的Minor GC中被回收掉

面试官:嗯…

候选者:其实稍微想一下,也能理解为什么要将「堆空间」进行「细分」多个小的区域

候选者:像以前的垃圾收集器都是对堆进行「物理」划分

候选者:如果堆空间(内存)大的时候,每次进行「垃圾回收」都需要对一整块大的区域进行回收,那收集的时间是不好控制的

候选者:而划分多个小区域之后,那对这些「小区域」回收就容易控制它的「收集时间」了

面试官:嗯…

面试官:那我大概了解了。那要不你讲讲它的GC过程呗?

候选者:嗯,在G1收集器中,可以主要分为有Minor GC(Young GC)和Mixed GC,也有些特殊场景可能会发生Full GC

候选者:那我就直接说Minor GC先咯?

面试官:嗯,开始吧

候选者:G1的Minor GC其实触发时机跟前面提到过的垃圾收集器都是一样的

候选者:等到Eden区满了之后,会触发Minor GC。Minor GC同样也是会发生Stop The World的

候选者:要补充说明的是:在G1的世界里,新生代和老年代所占堆的空间是没那么固定的(会动态根据「最大停顿时间」进行调整)

候选者:这块要知道会给我们提供参数进行配置就好了

候选者:所以,动态地改变年轻代Region的个数可以「控制」Minor GC的开销

面试官:嗯,那Minor GC它的回收过程呢?可以稍微详细补充一下吗

候选者:Minor GC我认为可以简单分为为三个步骤:根扫描、更新&&处理 RSet、复制对象

候选者:第一步应该很好理解,因为这跟之前CMS是类似的,可以理解为初始标记的过程

候选者:第二步涉及到「Rset」的概念

面试官:嗯…

候选者:从上一次我们聊CMS回收过程的时候,同样讲到了Minor GC,它是通过「卡表」(cart table)来避免全表扫描老年代的对象

候选者:因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉

候选者:同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决「跨代引用」的问题的存储一般叫做RSet

候选者:只要记住,RSet这种存储在每个Region都会有,它记录着「其他Region引用了当前Region的对象关系」

候选者:对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了)

候选者:而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用)

面试官:嗯…

候选者:那第二步看完RSet的概念,应该也好理解了吧?

候选者:无非就是处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉

候选者:到了第三步也挺好理解的:把扫描之后存活的对象往「空的Survivor区」或者「老年代」存放,其他的Eden区进行清除

候选者:这里要提下的是,在G1还有另一个名词,叫做CSet。

候选者:它的全称是 Collection Set,保存了一次GC中「将执行垃圾回收」的Region。CSet中的所有存活对象都会被转移到别的可用Region上

候选者:在Minor GC 的最后,会处理下软引用、弱引用、JNI Weak等引用,结束收集

面试官:嗯,了解了,不难

面试官:我记得你前面提到了Mixed GC ,要不来聊下这个过程呗?

候选者:好,没问题的。

候选者:当堆空间的占用率达到一定阈值后会触发Mixed GC(默认45%,由参数决定)

候选者:Mixed GC 依赖「全局并发标记」统计后的Region数据

候选者:「全局并发标记」它的过程跟CMS非常类型,步骤大概是:初始标记(STW)、并发标记、最终标记(STW)以及清理(STW)

面试官:确实很像啊,你继续来聊聊具体的过程呗?

候选者:嗯嗯,还是想说明下:Mixed GC它一定会回收年轻代,并会采集部分老年代的Region进行回收的,所以它是一个“混合”GC。

候选者:首先是「初始标记」,这个过程是「共用」了Minor GC的 Stop The World(Mixed GC 一定会发生 Minor GC),复用了「扫描GC Roots」的操作。

候选者:在这个过程中,老年代和新生代都会扫

候选者:总的来说,「初始标记」这个过程还是比较快的,毕竟没有追溯遍历嘛

面试官:…

候选者:接下来就到了「并发标记」,这个阶段不会Stop The World

候选者:GC线程与用户线程一起执行,GC线程负责收集各个 Region 的存活对象信息

候选者:从GC Roots往下追溯,查找整个堆存活的对象,比较耗时

面试官:嗯…

候选者:接下来就到「重新标记」阶段,跟CMS又一样,标记那些在「并发标记」阶段发生变化的对象

候选者:是不是很简单?

面试官:且慢

面试官:CMS在「重新标记」阶段,应该会重新扫描所有的线程栈和整个年轻代作为root

面试官:据我了解,G1好像不是这样的,这块你了解吗?

候选者:嗯,G1 确实不是这样的,在G1中解决「并发标记」阶段导致引用变更的问题,使用的是SATB算法

候选者:可以简单理解为:在GC 开始的时候,它为存活的对象做了一次「快照」

候选者:在「并发阶段」时,把每一次发生引用关系变化时旧的引用值给记下来

候选者:然后在「重新标记」阶段只扫描着块「发生过变化」的引用,看有没有对象还是存活的,加入到「GC Roots」上

候选者:不过SATB算法有个小的问题,就是:如果在开始时,G1就认为它是活的,那就在此次GC中不会对它回收,即便可能在「并发阶段」上对象已经变为了垃圾。

候选者:所以,G1也有可能会存在「浮动垃圾」的问题

候选者:但是总的来说,对于G1而言,问题不大(毕竟它不是追求一次把所有的垃圾都清除掉,而是注重 Stop The World时间)

面试官:嗯…

候选者:最后一个阶段就是「清理」,这个阶段也是会Stop The World的,主要清点和重置标记状态

候选者:会根据「停顿预测模型」(其实就是设定的停顿时间),来决定本次GC回收多少Region

候选者:一般来说,Mixed GC会选定所有的年轻代Region,部分「回收价值高」的老年代Region(回收价值高其实就是垃圾多)进行采集

候选者:最后Mixed GC 进行清除还是通过「拷贝」的方式去干的

候选者:所以,一次回收未必是将所有的垃圾进行回收的,G1会依据停顿时间做出选择Region数量(:

面试官:嗯,过程我大致是了解了

面试官:那G1会什么时候发生full GC?

候选者:如果在Mixed GC中无法跟上用户线程分配内存的速度,导致老年代填满无法继续进行Mixed GC,就又会降级到serial old GC来收集整个GC heap

候选者:不过这个场景相较于CMS还是很少的,毕竟G1没有CMS内存碎片这种问题(:

本文总结(G1垃圾收集器特点):

  • 从原来的「物理」分代,变成现在的「逻辑」分代,将堆内存「逻辑」划分为多个Region
  • 使用CSet来存储可回收Region的集合
  • 使用RSet来处理跨代引用的问题(注意:RSet不保留 年轻代相关的引用关系)
  • G1可简单分为:Minor GC 和Mixed GC以及Full GC
  • 【Eden区满则触发】Minor GC 回收过程可简单分为:(STW) 扫描 GC Roots、更新&&处理Rset、复制清除
  • 【整堆空间占一定比例则触发】Mixed GC 依赖「全局并发标记」,得到CSet(可回收Region),就进行「复制清除」
  • R大描述G1原理的时候,从宏观的角度看G1其实就是「全局并发标记」和「拷贝存活对象」
  • 使用SATB算法来处理「并发标记」阶段对象引用可能会修改的问题
  • 提供可停顿时间参数供用户设置(G1会尽量满足该停顿时间来调整 GC时回收Region的数量)

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

冲刺大厂每日算法&面试题,动态规划21天——第十三天 导读

发表于 2021-11-11

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

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。

下降路径
可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置
(row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1,
col + 1) 。

1
2
3
4
5
6
7
8
java复制代码示例 1:

输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:下面是两条和最小的下降路径,用加粗+斜体标注:
[[2,1,3], [[2,1,3],
[6,5,4], [6,5,4],
[7,8,9]] [7,8,9]]
1
2
3
4
5
6
7
java复制代码示例 2:

输入:matrix = [[-19,57],[-40,-5]]
输出:-59
解释:下面是一条和最小的下降路径,用加粗+斜体标注:
[[-19,57],
[-40,-5]]
1
2
3
4
java复制代码示例 3:

输入:matrix = [[-48]]
输出:-48
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
java复制代码分析

我们用 dp(r, c) 表示从位置为 (r, c) 的元素开始的下降路径最小和。根据题目的要求,位置 (r, c) 可以下降到 (r + 1, c - 1),(r + 1, c) 和 (r + 1, c + 1) 三个位置(先不考虑超出数组边界的情况),因此状态转移方程为:

dp(r, c) = A[r][c] + min{dp(r + 1, c - 1), dp(r + 1, c), dp(r + 1, c + 1)}

由于下降路径可以从第一行中的任何元素开始,因此最终的答案为 \min\limits_c \mathrm{dp}(0, c)
c
min
​
dp(0,c)。



class Solution {
public int minFallingPathSum(int[][] A) {
int N = A.length;
for (int r = N-2; r >= 0; --r) {
for (int c = 0; c < N; ++c) {
// best = min(A[r+1][c-1], A[r+1][c], A[r+1][c+1])
int best = A[r+1][c];
if (c > 0)
best = Math.min(best, A[r+1][c-1]);
if (c+1 < N)
best = Math.min(best, A[r+1][c+1]);
A[r][c] += best;
}
}

int ans = Integer.MAX_VALUE;
for (int x: A[0])
ans = Math.min(ans, x);
return ans;
}
}

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1
的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

1
2
3
4
5
6
7
8
9
10
java复制代码示例 1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
1
2
3
4
java复制代码示例 2:

输入:triangle = [[-10]]
输出:-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] f = new int[n][n];
f[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
f[i][0] = f[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; ++j) {
f[i][j] = Math.min(f[i - 1][j - 1], f[i - 1][j]) + triangle.get(i).get(j);
}
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
int minTotal = f[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, f[n - 1][i]);
}
return minTotal;
}
}

面试题

哈希 类

请说⼀说,Java中的HashMap的⼯作原理是什么? 参考回答:
HashMap类有⼀个叫做Entry的内部类。这个Entry类包含了key-value作为实例变量。 每当往
hashmap⾥⾯存放key-value对的时候,都会为它们实例化⼀个Entry对象,这个Entry对象就
会存储在前⾯提到的Entry数组table中。Entry具体存在table的那个位置是 根据key的
hashcode()⽅法计算出来的hash值(来决定)。

讲⼀讲,如何构造⼀致性哈希算法。 参考回答: 先构造⼀个⻓度为232的整数环(这个环被称为⼀致性Hash环),根据节点名称的Hash值
(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到 其Hash值(其分布也为[0,
232-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值 最近的服务器节点,完成Key到服务器的映射查找。
这种算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下 尽量有多的请求命中原来路由到的服务器。

本文转载自: 掘金

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

springboot基础入门之json转换框架 、全局异常捕

发表于 2021-11-11

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

一、Spring boot json转换框架

个人使用比较习惯的json框架是fastjson,所以spring boot默认的json使用起来就很陌生了,所以很自然我就想我能不能使用fastjson进行json解析呢?

1
2
3
4
5
6
7
8
9
10
11
xml复制代码 <dependencies>

        <dependency>

           <groupId>com.alibaba</groupId>

           <artifactId>fastjson</artifactId>

           <version>1.2.15</version>

</dependencies>

这里要说下很重要的话,官方文档说的1.2.10以后,会有两个方法支持HttpMessageconvert,一个是FastJsonHttpMessageConverter,支持4.2以下的版本,一个是FastJsonHttpMessageConverter4支持4.2以上的版本,具体有什么区别暂时没有深入研究。这里也就是说:低版本的就不支持了,所以这里最低要求就是1.2.10+。

配置fastjon

支持两种方法:

第一种方法:

(1)启动类继承extends WebMvcConfigurerAdapter

(2)覆盖方法configureMessageConverters

第二种方法:

(1)在App.java启动类中,注入Bean : HttpMessageConverters

具体代码如下:

代码:App.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码import java.util.List;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.http.converter.HttpMessageConverter;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.alibaba.fastjson.serializer.SerializerFeature;

import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;

//如果想集成其他的json框架需要继承WebMvcConfigurerAdapter,并重写configureMessageConverters

@SpringBootApplication

public class App extends WebMvcConfigurerAdapter {

       // 第一种方式,重写configureMessageConverters,并将FastJsonConverter设置到系统中

       @Override

       public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

              FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();

              converter.setFeatures(SerializerFeature.PrettyFormat);

              converters.add(converter);

              super.configureMessageConverters(converters);

       }

       // 第二种方法:注入beanHttpMessageConverters

       /*

        * @Bean public HttpMessageConverters faMessageConverters(){

* return new HttpMessageConverters(new FastJsonHttpMessageConverter()); }

        */

       public static void main(String[] args) {

              SpringApplication.run(App.class, args);

       }

}

二、springboot全局异常捕捉

在一个项目中的异常我们我们都会统一进行处理的,那么如何进行统一进行处理呢?

新建一个类GlobalDefaultExceptionHandler,

在class注解上@ControllerAdvice,

@ControllerAdvice:即把@ControllerAdvice注解内部使用@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。非常简单,不过只有当使用@ExceptionHandler最有用,另外两个用处不大。

在方法上注解上@ExceptionHandler(value = Exception.class),具体代码如下

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
kotlin复制代码
package com.hpit.base.exception;

 

import javax.servlet.http.HttpServletRequest;

 

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.bind.annotation.ExceptionHandler;

 

@ControllerAdvice

publicclass GlobalDefaultExceptionHandler {

   

    @ExceptionHandler(value = Exception.class)

    publicvoid defaultErrorHandler(HttpServletRequest req, Exception e)  {
      

      //打印异常信息:

       e.printStackTrace();

       System.out.println("GlobalDefaultExceptionHandler.defaultErrorHandler()");

 

       /*

        * 返回json数据或者String数据:

        * 那么需要在方法上加上注解:@ResponseBody

        * 添加return即可。

        */

       

       /*

        * 返回视图:

        * 定义一个ModelAndView即可,

        * 然后return;

        * 定义视图文件(比如:error.html,error.ftl,error.jsp);

        *

        */

  }

   }

com.hpit.test.web.DemoController 加入方法:

1
2
3
4
5
6
7
kotlin复制代码@RequestMapping("/zeroException")

    publicint zeroException(){

       return 100/0;

    }

访问:http://127.0.0.1:8080/zeroException 这个方法肯定是抛出异常的,那么在控制台就可以看到我们全局捕捉的异常信息了

三、Spring boot JPA连接数据库

​​​​​​​在任何一个平台都逃离不了数据库的操作,那么在spring boot中怎么接入数据库呢?

很简单,我们需要在application.properties进行配置一下,application.properties路径是src/main/resources下,对于application.properties更多的介绍请自行百度进行查找相关资料进行查看,在此不进行过多的介绍,以下只是mysql的配置文件。

   大体步骤:


   (1)在application.properties中加入datasouce的配置


   (2)在pom.xml加入mysql的依赖。


(3)获取DataSouce的Connection进行测试。

src/main/resouces/application.properties:

spring.datasource.url = jdbc:mysql://localhost:3306/test

spring.datasource.username = root

spring.datasource.password = root

spring.datasource.driverClassName = com.mysql.jdbc.Driver

spring.datasource.max-active=20

spring.datasource.max-idle=8

spring.datasource.min-idle=8

spring.datasource.initial-size=10

pom.xml配置:

1
2
3
4
5
6
7
xml复制代码<dependency>

       <groupId>mysql</groupId>

       <artifactId>mysql-connector-java</artifactId>

</dependency>

到此相关配置就ok了,那么就可以在项目中进行测试了,我们可以新建一个class Demo进行测试,实体类创建完毕之后,我们可能需要手动进行编写建表语句,这时候我们可能就会想起Hibernate的好处了。那么怎么在spring boot使用Hibernate好的特性呢?So easy,具体怎么操作,请看下篇之JPA – Hibernate。

​

本文转载自: 掘金

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

《Go 开发指南》-快速安装 Go 环境

发表于 2021-11-11

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

你好,我是看山。

本文源自并发编程网的翻译邀请,文章来自 Go 官方网站。

按照下面的步骤,你可以实现快速安装。

如果想要通过源码安装,可以访问:从源码安装 Go 环境。

如果想要安装多个版本的 Go 或者卸载,可以访问:管理 Go 环境。

下载

下面提供了 Linux、Mac、Windows 三种系统 Go 语言安装包的下载路径:

  • Linux:golang.org/dl/go1.16.5…
  • Mac:golang.org/dl/go1.16.5…
  • Windows:golang.org/dl/go1.16.5…

如果想要获取其他操作系统或者其他版本的,可以访问golang.org/dl/。

默认情况下,go 命令默认下载和验证模块时,使用的是 Google 提供的模块镜像服务和 checksum 验证。我们可以从golang.org/cmd/go/ 获取全面的 go 命令和配置。

安装

Linux

  1. 将下载的压缩包解压到/usr/local目录中,可以执行下面这条命令(需要切换到 root 用户或者使用 sudo):
1
2
bash复制代码rm -rf /usr/local/go
tar -C /usr/local -xzf go1.16.5.linux-amd64.tar.gz

注意:提供的这条命令会删除之前安装的 go 环境,在执行之前最好做下备份。
2. 将/usr/local/go/bin添加到PATH环境变量中,可以在$HOME/.profile或者/etc/profile中添加下面这条命令:

1
ruby复制代码export PATH=$PATH:/usr/local/go/bin

注意:对配置文件的修改,需要等到下次登录计算机的时候才会生效。如果想要立即生效,可以直接运行 shell 命令,或者使用命令source $HOME/.profile重新执行一下配置内容。
3. 打开终端,输入下面的命令,验证下是否按照成功:

1
go复制代码go version
  1. 确认一下打印的版本号,与下载的版本号是否一致。如果一致,说明按照成功,如果说命令找不到或者版本不一致,那就是安装有问题。【译者注:如果条件允许,最好重启系统,然后在验证一次,避免环境变量配置错误。】

Mac

  1. 双击打开下载的安装包,按照提示安装。这个安装包会直接将 Go 安装在/usr/local/go目录中,并将/usr/local/go/bin添加到PATH环境变量中。可能需要重启终端才能使环境变量生效。
  2. 打开终端,输入下面的命令,验证下是否按照成功:
1
go复制代码go version
  1. 确认一下打印的版本号

Windows

  1. 双击打开下载的安装包,按照提示安装。默认情况下,会安装在Program Files或者Program Files (x86)目录中,也可以根据自己的习惯修改。安装完成后,需要重启终端使环境变量生效。

  2. 验证安装是否成功

    1. 在 Windows 系统中,打开菜单

    2. 搜索框中键入cmd然后回车,打开终端

    3. 在终端中,输入下面命令

      1
      go复制代码go version
    4. 确认一下打印的版本号

开始 coding

经过上面步骤,你已经成功安装了 Go 环境,访问 Getting Started tutorial 开始 Coding 之旅吧。


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

本文转载自: 掘金

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

JDK 中居然也有反模式接口常量

发表于 2021-11-11

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

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

在实际开发过程中,经常会需要定义一个文件,用于存储一些常量,这些常量设计为静态公共常量(使用 public static final 修饰)。这个时候就出现两种选择:

  1. 在接口中定义常量,比如 JDK 1.1 中的 java.io.ObjectStreamConstans 接口;
  2. 在类中定义常量,比如 JDK 1.7 中的 java.nio.charset.StandardCharsets;

这两种方式都能够达到要求:存储常量、无需实例化。下面分情况讨论下两种方式孰优孰劣。

常量接口

首先从代码的角度分析,接口中定义的变量都必须是常量,即默认使用 public static final 修饰。也就是说,在写代码的时候直接写成下面这样:

1
2
3
4
java复制代码public interface ObjectStreamConstants {
    short STREAM_MAGIC = (short)0xaced;
    short STREAM_VERSION = 5;
}

但是在类中就必须乖乖的写成下面这种样子:

1
2
3
4
java复制代码public final class ObjectStreamConstants {
    public static final short STREAM_MAGIC = (short)0xaced;
    public static final short STREAM_VERSION = 5;
}

从直观的感受,接口写起来方便多了。第二个问题:因为类中写的字符比接口多,所以编译之后文件大小也是类文件比接口文件大。第三个问题:在JVM加载过程中,接口没有类提供的额外特种(如重载、方法的动态绑定等),所以接口加载比类快。分析到此,似乎没有什么理由不用接口定义常量了。但是,BUT,这种做法却是一种严重的反模式行为。引用《Effective Java》中的一段描述:

The constant interface pattern is a poor use of interfaces. That a class uses some constants internally is an implementation detail. Implementing a constant interface causes this implementation detail to leak into the class’s exported API. It is of no consequence to the users of a class that the class implements a constant interface. In fact, it may even confuse them. Worse, it represents a commitment: if in a future release the class is modified so that it no longer needs to use the constants, it still must implement the interface to ensure binary compatibility. If a nonfinal class implements a constant interface, all of its subclasses will have their namespaces polluted by the constants in the interface.

翻译出来就是:

常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口,这对于类的用户来讲并没有什么价值。实际上,这样做反而会使他们更加糊涂。更糟糕的是,它代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保兼容性。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。

这样说来就很明白透彻了:

  1. 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父、子接口(或实现) 去引用常量是可能不一致的;
  2. 同样的,由于被实现或继承,造成在继承树中可以用大量的接口、类或实例去引用同一个常量,从而造成接口中定义的常量污染了命名空间;
  3. 接口暗含的意思是:它是需被实现的,代表着一种类型,它的公有成员是要被暴露的API,但是在接口中定义的常量还算不上API。

综上所述:使用接口定义常量,是一种不可取的行为。JDK中定义的接口常量(例如java.io.ObjectStreamConstans)应该算是反面教材。

类接口

既然使用接口第一常量不可取,那就只能通过类定义常量了。虽然在JAVA中类不能够多继承,但是子类也能够“污染”父类定义常量的命名空间。所以为了常量不可变,需要将常量类定义为final的,然后再彻底点就是再定义个private的构造函数。就像java.nio.charset.StandardCharsets一样:

1
2
3
4
5
6
7
8
java复制代码public final class StandardCharsets {
    private StandardCharsets() {
        throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
    }
    public static final Charset US_ASCII = Charset.forName("US-ASCII");
    public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
    public static final Charset UTF_8 = Charset.forName("UTF-8");
}

在java.nio.charset.StandardCharsets中,为了阻止各种形式的实例化,甚至在构造函数中抛出错误,也是做个够彻底的了。

枚举类型

但是,BUT,还有一种情况,比如常量中定义性别:男、女,使用上面的类常量,需要写成:

1
2
3
4
5
6
7
java复制代码public final class Gender {
    private Gender() {
        throw new AssertionError("No x.y.z.Gender instances for you!");
    }
    public static final int MALE = 1;
    public static final int FEMALE = 0;
}

因为定义的性别类型实际是int,如果手贱写成m.setGender(3)也是没有错误的,那3又是什么鬼?是不是还要有4、5、6、7?那这种常量定义就失去价值了。对于这种可以归类的常量,最好的常量定义方法应该就是枚举了:

1
2
3
4
java复制代码public enum Gender {
    MALE, 
    FEMALE
}

根据编辑的字节码,Gender实际是:

1
2
3
4
java复制代码public final class Gender extends java.lang.Enum {
    public static final Gender MALE;
    public static final Gender FEMALE;
}

这样对于接受 Gender 类型参数的方法就只能传入 MALE 或 FEMALE 了,不再有其他选项,这就是枚举的意义。在后来的JDK中,也出现了像java.nio.file.StandardOpenOption等的枚举定义。而且枚举的定义也不只局限于这种,还有很多其他复杂定义,可以适用各种情况,以后再慢慢讨论。

结束语

定义常量不要使用接口常量,要在类中定义,最好是final类,并且定义private的构造方法,如果常量可以进行归类,最好使用枚举定义:枚举 > 类 > 接口。

本文被《Java 进阶》专栏收录。


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

本文转载自: 掘金

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

Executors:为什么阿里不待见我?

发表于 2021-11-11

大家好,我是Excutors,一个老实的工具类。

有个叫老三的程序员在文章 要是以前有人这么讲线程池,我早就该明白了!里挖了一个坑,说要把我介绍给大家认识认识。

我其实挺委屈的,作为一个没得感情,老实干活的工具类,我却上了阿里巴巴的黑名单。他们在一本叫《Java开发手册》的册子里写道:

禁止使用Excutors

作者画外音:人家为啥给你拉黑,不写的清清楚楚嘛,你有啥可委屈的。而且你这个家伙就是表面看起来老实,活是你干的吗?干活的不都是小老弟ThreadPoolExecutor。来,我一个个给你数。

  1. newFixedThreadPool

FixedThreadPool,是一个固定大小的线程池。

看一下它的源代码实现:

1
2
3
4
5
java复制代码    public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

直接调用ThreadPoolExecutor的构造方法。

  • 核心线程数和最大线程数相同
  • 使用LinkedBlockingQueue作为任务队列

FixedThreadPool的execute()运行示意图:

FixedThreadPool

整体运行过程:

  • 当前运行线程少于corePoolSize,则创建新线程执行任务
  • 当前运行线程大于corePoolSize,将任务加入LinkedBlockingQueue
  • 线程池中线程执行完任务后,会循环从LinkedBlockingQueue中获取任务执行

因为使用无界队列LinkedBlockingQueue来存储不能执行的任务,所以不会触发拒绝服务策略,可能会导致OOM。

  1. newSingleThreadExecutor

SingleThreadExecutor是使用单个线程工作的线程池。

实现源码如下:

1
2
3
4
5
6
java复制代码    public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

直接调用ThreadPoolExecutor的构造方法。

  • 核心线程数和最大线程数都是1
  • 使用LinkedBlockingQueue作为任务队列

SingleThreadExecutor的运行流程:

SingleThreadExecutor运行流程

  • 当前无运行线程,创建一个线程来执行任务
  • 当前有线程运行,将任务加入LinkedBlockingQueue
  • 线程执行完任务后,会循环从LinkedBlockingQueue中获取任务来执行

这里用了无界队列LinkedBlockingQueue,同样可能会导致OOM。

  1. newCachedThreadPool

CachedThreadPool是一个会根据需要创建新线程的线程池。

实现源码:

1
2
3
4
5
java复制代码    public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

直接调用ThreadPoolExecutor的构造方法。

  • 核心线程数为0,最大线程数是非常大的一个数字Integer.MAX_VALUE
  • 使用没有容量的SynchronousQueue作为工作队列
  • keepAliveTime设置为60L,空闲线程空闲60秒之后就会被终止

CachedThreadPool的运行流程:

CachedThreadPool执行流程

  • 如果当前有空闲线程,使用空闲线程来执行任务
  • 如果没有空闲线程,创建一个新线程来执行任务
  • 新建的线程执行完任务后,会执行poll(keepAliveTime,TimeUnit.NANOSECONDS),在SynchronousQueue里等待60s

这里线程池的大小没有限制,可能会无限创建线程,导致OOM。

  1. newScheduledThreadPool

ScheduledThreadPool是一个具备调度功能的线程池。

实现源码:

1
2
3
java复制代码    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

可以看到,这个线程池不太一样,它调用的是ScheduledThreadPoolExecutor的构造方法。

1
2
3
4
java复制代码    public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
  • 最大线程数是Integer.MAX_VALUE,无限大
  • 使用DelayedWorkQueue作为任务队列

ScheduledThreadPoolExecutor执行任务的流程:

ScheduledThreadPool执行流程

主要分为两大部分:

  1. 调用scheduleAtFixedRate()/scheduleWithFixedDelay()方法,会向DelayQueue添加一个ScheduledFutureTask。
  2. 线程池的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

它同样可以无限创建线程,所以也存在OOM的风险。

为了实现周期性执行任务,ScheduledThreadPoolExecutor对ThreadPoolExecutor进行了一些改造[4]:

  • ScheduledFutureTask来作为调度任务的实现

它主要包含了3个成员变量time(任务将要被执行的具体时间)、sequenceNumber(任务的序号)、period(任务执行的间隔周期)

  • 使用DelayQueue作为任务队列

DelayQueue封装了了一个PriorityQueue,会对对队列中的ScheduledFutureTask进行排序,排序的优先级time>sequenceNumber。

ScheduledThreadPoolExecutor执行流程

ScheduledThreadPoolExecutor的任务执行主要分为4步:

  1. 线程池里的线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())
  2. 线程1执行这个ScheduledFutureTask
  3. 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。
  4. 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())

Excutors自述:这,这……工具类出的问题不叫bug。虽然我偷懒不干活,还可能会OOM,但我还是一个好工具类,呜呜……

作者:是啊,其实Excutors有什么错呢?它只是一个没得感情的工具类,有错的只是不恰当地用它的人。所以,知其然且知其所以然,搞懂原理,灵活应用。我们应该像一个士兵一样,不只是会扣动扳机,还会拆解保养枪械。

我是三分恶,一个号称能文能武的全栈开发。

点赞、关注不迷路,咱们下期见!


参考:

[1]. 《Java并发编程的艺术》

[2]. 讲真 这次绝对让你轻松学习线程池

[3]. 小傅哥 《Java面经手册》

[4]. 《Java并发编程之美》

[5]. 阿里巴巴《Java开发手册》

本文转载自: 掘金

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

JWT 的过期时间为什么没有生效?

发表于 2021-11-11

在我第一次在 DRF(Django REST Framework)中使用 JWT 时,感觉 JWT 非常神奇,它即没有使用 session、cookie,也不使用数据库,仅靠一段加密的字符串,就解决了用户身份验证的烦恼。

直到我遇到了一个当时百思不得解的问题,才揭开了它的神秘面纱。

当时遇到的问题就是,无论怎么设置 JWT TOKEN 的过期时间,都没有生效,即使设置为 1 秒后过期,过了 1 分钟,TOKEN 还是可以正常使用,重启 Django 服务也不行。

没有别的办法,我就硬着头皮去追着源码,看看 JWT 是怎么判断 TOKEN 是否过期的。

具体的方法就是,深度优先追溯 JWT 代码的源头。在 DRF 中,配置了 DEFAULT_AUTHENTICATION_CLASSES 就是 JWT:

直接定位至这个类,发现它继承了 BaseJSONWebTOKENAuthentication

然后看 BaseJSONWebTOKENAuthentication,发现有一段判断过期的逻辑:

继续展开 jwt_decode_handler 这个函数,发现它调用了 jwt.decode 函数

展开 jwt.decode 函数,发现它调用了函数 _validate_claims

函数 _validate_claims 又调用了 _validate_exp,

然后展开 _validate_exp,找到了这段:

发现过期时间 exp 来自 payload,payload 又来自 TOKEN:

至此谜底揭开,原来,TOKEN 的过期时间其实被编码在了 TOKEN 本身,服务器收到 TOKEN 时先进行解码,解码出过期时间,然后和当前时间进行对比,如果当前时间比较小,说明没有过期,TOKEN 就是有效的,否则返回客户端 “Signature has expired.”

我 Debug 出了这个 TOKEN 的过期时间 exp,发现这个 exp 是修改 JWT_EXPIRATION_DELTA 之前的那个过期时间,原来修改 JWT_EXPIRATION_DELTA 之后需要重新生成 TOKEN,这样的过期时间才会按照新的来。

至此,JWT 的原理已经非常清晰了:

用户第一次登录时,服务器(JWT)会获得用户名、用户 id,在加上设置的过期时间构建 payload:

1
2
3
4
5
python复制代码payload = {
'user_id': user.pk,
'username': username,
'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
}

然后将 payload 用设置好的算法使用私钥加密成 token

1
2
3
4
5
6
7
python复制代码def jwt_encode_handler(payload):
key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload)
return jwt.encode(
payload,
key,
api_settings.JWT_ALGORITHM
).decode('utf-8')

token 返回至客户端后,客户端缓存该 token,然后每一次请求时都带上该 token。

服务器在收到请求是先验证该 token,验证的过程就是对 token 进行逆向解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码def jwt_decode_handler(token):
options = {
'verify_exp': api_settings.JWT_VERIFY_EXPIRATION,
}
# get user from token, BEFORE verification, to get user secret key
unverified_payload = jwt.decode(token, None, False)
secret_key = jwt_get_secret_key(unverified_payload)
return jwt.decode(
token,
api_settings.JWT_PUBLIC_KEY or secret_key,
api_settings.JWT_VERIFY,
options=options,
leeway=api_settings.JWT_LEEWAY,
audience=api_settings.JWT_AUDIENCE,
issuer=api_settings.JWT_ISSUER,
algorithms=[api_settings.JWT_ALGORITHM]
)

解密使用同样的算法,使用公钥或私钥进行解密,解密成功且不过期,则认为用户有权限访问,正常返回。

最后

这个问题至少花了我半个小时的时间,如果你遇到这种情况,能瞬间明白其中缘由,那本文的目的就达到了。

源码之下无秘密,遇到问题,去看源码可能不是解决问题最快的方法,却是提升自己最快的方法。很多开源软件设计模式的应用都非常值得我们学习,比如 DRF 的模块设计,通过 mixins 组合来实现灵活可扩展的 APIView,通过子类传入相关的 class 来实现用户自定义的功能。如何写出灵活可扩展、高内聚低耦合、符合开闭原则的程序,阅读开源代码,是一个非常高效的学习方式。

当然了,这需要先对设计模式有一个系统的学习,让自己有一双慧眼,不然就是守着金山不自知。

本文转载自: 掘金

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

Spring Boot(七):Swagger 接口文档 📚

发表于 2021-11-11

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

  1. Swagger 简介

1.1 Swagger 是什么?

Swagger 是一款 RESTful 风格的接口文档在线自动生成 + 功能测试功能软件。Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。目标是使客户端和文件系统作为服务器以同样的速度(同步)更新文件的方法,参数和模型紧密集成到服务器。

这个解释简单点来讲就是说,Swagger 是一款可以根据 resutful 风格生成的接口开发文档,API 文档与 API 同步更新,并且支持做测试的一款中间软件。

现在 Swagger 官网主要提供了几种开源工具,提供相应的功能。可以通过配置甚至是修改源码以达到你想要的效果。

Swagger Codegen:通过Codegen 可以将描述文件生成 html 格式和 wiki 形式的接口文档,同时也能生成多种语言的服务端和客户端的代码。支持通过 jar 包,docker,node 等方式在本地化执行生成。也可以在后面的 Swagger Editor 中在线生成。

Swagger UI:提供了一个可视化的 UI 页面展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署 UI 项目。

Swagger Editor:类似于 markendown 编辑器的编辑 Swagger 描述文件的编辑器,该编辑支持实时预览描述文件的更新效果。也提供了在线编辑器和本地部署编辑器两种方式。

Swagger Inspector:感觉和 postman 差不多,是一个可以对接口进行测试的在线版的 postman。比在 Swagger UI 里面做接口请求,会返回更多的信息,也会保存你请求的实际请求参数等数据。

Swagger Hub:集成了上面所有项目的各个功能,你可以以项目和版本为单位,将你的描述文件上传到Swagger Hub中。在 Swagger Hub 中可以完成上面项目的所有工作,需要注册账号,分免费版和收费版。

Springfox Swagger:Spring 基于 swagger 规范,可以将基于 SpringMVC 和 Spring Boot 项目的项目代码自动生成 JSON 格式的描述文件。本身不是属于 Swagger 官网提供的,在这里列出来做个说明,方便后面作一个使用的展开。

1.2 为什么要使用 Swagger?

相信无论是前端还是后端开发,都或多或少地被接口文档折磨过。

前端经常抱怨后端给的接口文档与实际情况不一致。

后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。

其实无论是前端调用后端,还是后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样,经常会抱怨别人写的代码没有写注释,然而自己写起代码起来,最讨厌的,也是写注释。

所以仅仅只通过强制来规范大家是不够的,随着时间推移,版本迭代,接口文档往往很容易就跟不上代码了。

总之,在这个前后端分离的时代,前后端联调会使得前后端开发人员无法做到即使协商,尽早解决。

发现了痛点就会去寻找更好的解决方案,所以 Swagger 接口文档就应运而生了。解决方案用的人多了,就成了标准的规范。通过这套规范,你只需要按照它的规范去定义接口及接口相关的信息。再通过 Swagger 衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,生成多种语言的客户端和服务端的代码,以及在线接口调试页面等等。

这样,如果按照新的开发模式,在开发新版本或者迭代版本的时候,只需要更新 Swagger 描述文件,就可以自动生成接口文档和客户端服务端代码,做到调用端代码、服务端代码以及接口文档的一致性。

但即便如此,对于许多开发来说,编写这个 yml 或 json 格式的描述文件,本身也是有一定负担的工作,特别是在后面持续迭代开发的时候,往往会忽略更新这个描述文件,直接更改代码。久而久之,这个描述文件也和实际项目渐行渐远,基于该描述文件生成的接口文档也失去了参考意义。

所以作为 Java 届服务端的大一统框架 Spring,迅速将 Swagger 规范纳入自身的标准,建立了 Spring-swagger 项目,后面改成了现在的 Springfox。通过在项目中引入 Springfox,可以扫描相关的代码,生成该描述文件,进而生成与代码一致的接口文档和客户端代码。这种通过代码生成接口文档的形式,在后面需求持续迭代的项目中,显得尤为重要和高效。

1.2.1 对于后端开发人员来说

  • 不用再手写 WiKi 接口拼大量的参数,避免手写错误
  • 对代码侵入性低,采用全注解的方式,开发简单
  • 方法参数名修改、增加、减少参数都可以直接生效,不用手动维护

缺点:增加了开发成本,写接口还得再写一套参数配置

1.2.2 对于前端开发人员来说

  • 后端只需要定义好接口,会自动生成文档,接口功能、参数一目了然
  • 联调方便,如果出问题,直接测试接口,实时检查参数和返回值,就可以快速定位是前端还是后端的问题

1.2.3 对于测试人员来说

  • 对于某些没有前端界面 UI 的功能,可以用它来测试接口
  • 操作简单,不用了解具体代码就可以操作
  1. Spring Boot 集成 Swagger2(Getting Started)

2.1 导入 Swagger 相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<dependencies>
<!-- 引入web才能打开浏览器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入Swagger2、SwaggerUI依赖 -->
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>

2.2 编写 Controller

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

@RequestMapping("/hello")
public String helloSwagger() {
return "Hello Swagger!";
}

}

2.3 编写 Swagger 配置类

1
2
3
4
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
}

2.4 访问接口文档

访问 http://localhost:8080/swagger-ui.html:

该 swagger-ui.html 界面是 Swagger 为我们提供的 UI 界面,可在引入的依赖中找到:

  1. 配置 Swagger(SwaggerConfig.java)

Swagger 有自己的 Bean 实例:Docket

3.1 配置 Swagger ApiInfo 信息

只需要在 SwaggerConfig 配置类中添加包含 ApiInfo 类信息的 Docket Bean 实例,就可以配置 Swagger 信息:

这里我们点进 Docket 源码中查看,发现大部分属性已有默认值,仅有一个构造函数且需要传入 DocumentationType 实例:

DocumentationType.java 是什么?点击进入,这里有三个可供选择的值:

同时若想自定义 Swagger Api 信息,则需要传入 Swagger ApiInfo,如下为默认配置:

⭐在 SwaggerConfig.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
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {

@Bean
public Docket swaggerInfo() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(getApiInfo());

return docket;
}

private ApiInfo getApiInfo() {
// 作者信息
Contact contact = new Contact("Scorpions", "github.com/Wu-yikun", "w577159462@163.com");
return new ApiInfo(
"Swagger2?!!!",
"Stay hungry",
"v2.0",
"gitee.com/Wu-Yikun",
contact,
"Apache 2.0",
"www.apache.org/licenses/LICENSE-2.0",
new ArrayList<>()
);
}

}

访问 http://localhost:8080/swagger-ui.html:

3.2 配置 Swagger 扫描接口

目前 Swagger 文档中有两个 Controller:

  • 一个默认的 /error:

  • 还有一个是我们自己写的 /hello 请求

由于 @RequestMapping 未指定提交方式 method:所以 Swagger 文档中就会罗列出所有的 method 供选择,如: GET、HEAD、POST、PUT、DELETE、OPTIONS、PATCH

3.2.1 select()、build()

⭐配置 Swagger 扫描接口的一般流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
// 配置Swagger的Docket实例
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis() // 指定扫描接口
.paths() // 过滤路径
.build();
}
}

Docket 中的 select() 返回 ApiSelectorBuilder 对象:

ApiSelectorBuilder 中的 build() 返回 Docket 对象,而 apis() 与 paths() 都返回 ApiSelectorBuilder 对象,可用于链式调用:

接下来介绍 apis() 与 paths() 的使用方法

3.2.2 ⭐apis()

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ApiSelectorBuilder {
private final Docket parent;
private Predicate<RequestHandler> requestHandlerSelector = ApiSelector.DEFAULT.getRequestHandlerSelector();
...

public ApiSelectorBuilder apis(Predicate<RequestHandler> selector) {
requestHandlerSelector = and(requestHandlerSelector, selector);
return this;
}

...
}

观察以上 ApiSelectorBuilder.java 源码,得知 apis 方法可传入以下参数:

① RequestHandlerSelectors.none(): 全都不扫描

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(ReqeustHandlerSelectors.none())
.build();
}
}

② ReqeustHandlerSelectors.any(): 扫描全部

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(ReqeustHandlerSelectors.any())
.build();
}
}

③ RequestHandlerSelectors.basePackage(): 扫描指定包

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(ReqeustHandlerSelectors.any())
.apis(RequestHandlerSelectors.basePackage("com.one.swagger.controller"))
.build();
}
}

④ RequestHandlerSelectors.withMethodAnnotation(): 扫描方法上的注解

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(ReqeustHandlerSelectors.withMethodAnnotation(GetMapping.class))
.build();
}
}

⑤ RequestHandlerSelectors.withClassAnnotation(): 扫描类上的注解

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(ReqeustHandlerSelectors.withClassAnnotation(RestController.class))
.build();
}
}

⭐综合实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean("docket")
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
/**
* apis():指定扫描的接口
* RequestHandlerSelectors:配置要扫描接口的方式
* basePackage:指定要扫描的包
* any:扫描全部
* none:不扫描
* withClassAnnotation:扫描类上的注解(参数是类上注解的class对象)
* withMethodAnnotation:扫描方法上的注解(参数是方法上的注解的class对象)
*/
.apis(RequestHandlerSelectors.basePackage("com.zsr.controller"))
.build();
}
}

3.2.3 ⭐paths()

paths() 与 apis() 相似,使用 PathSelectors,这里不再赘述:

① PathSelectors.ant(): 过滤 Spring 的 AntPathMatcher 提供的 match 方法匹配的路径

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.ant("/hello/**"))
.build();
}
}

过滤 /hello/** 请求:

② PathSelectors.regex(): 过滤正则表达式指定的路径

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
@EnableSwagger2 // 开启Swagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex("^/hello"))
.build();
}
}

过滤以 /hello 开头的请求:

③ PathSelectors.none()

④ PathSelectors.any()

⭐综合实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean("docket")
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
/**
* paths():过滤路径
* PathSelectors:配置过滤的路径
* any:过滤全部路径
* none:不过滤路径
* ant:过滤指定路径:按照按照Spring的AntPathMatcher提供的match方法进行匹配
* regex:过滤指定路径:按照String的matches方法进行匹配
*/
.paths(PathSelectors.ant("/hello/**"))
.build();
}
}

3.3 配置 API 文档分组

上文有提及 Docket 对象中的 groupName 属性,groupName 用于设置 API 文档的分组,默认分组为 default。

⭐可以为不同的分组配置不同的 Swagger 扫描接口!

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复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket docket1() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.none())
.build()
.groupName("X"); // 设置API文档为X组
}

@Bean
public Docket docket2() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.regex("^/swagger"))
.build()
.groupName("Y"); // 设置API文档为Y组
}

@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.paths()
.build()
.groupName("Scorpions"); // 设置Swagger的API文档分组
}

}

Scorpions 分组:

X 分组:

Y 分组:

3.4 配置是否启动 Swagger

⭐Docket 对象通过 enable() 方法来配置 Swagger 是否启用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean("docket")
public Docket getSwaggerDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// true表示启用swagger、false表示不启用swagger
.enable(false)
.select()
.paths()
.build();
}
}

⭐enable(false) 使得仅当前分组不启用 Swagger 文档,而其他分组仍然启用,若仅剩一个 group,则会出现如下的页面:


🔥 若只希望 Swagger 在开发环境中启用,在生产环境中不启用(发布的时候当然不能暴露 Swagger 文档,不然造成外部可以随意调用接口)

  • Environment 对象可作为参数由 Spring 容器自动传入
  • 通过 environment.acceptsProfiles(profiles) 来判断是否处于自己设定的环境当中
  • 将 flag 传入 enable() 方法的参数列表,如果处于自己设定的环境则开启 Swagger 接口文档

application-dev.yml:

1
2
3
yaml复制代码# 开发环境下默认使用该配置文件(约定俗成的名字)
server:
port: 8080

application-pro.yml:

1
2
3
yaml复制代码# 生产环境
server:
port: 8082

application.properties:

1
2
properties复制代码# 使得dev环境的配置生效: application-dev
spring.profiles.active=dev

SwaggerConfig.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复制代码@Configuration
@EnableSwagger2 // 开启Swagger2, 访问网址: http://localhost:8080/swagger-ui.html
public class SwaggerConfig {

@Bean("docket")
public Docket getSwaggerDocket(Environment environment) {

// 设置启用Scorpions分组下的Swagger文档的环境列表
Profiles profiles = Profiles.of("dev", "test", "otherEnv");
boolean flag = environment.acceptsProfiles(profiles);

return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/hello/**"))
.build()
.groupName("Scorpions");
}

@Bean
public Docket swaggerInfo(Environment environment) {

// 仅在 dev、test 环境下启用Z分组的Swagger接口文档!
Profiles profiles = Profiles.of("dev", "test");
boolean flag = environment.acceptsProfiles(profiles);

Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(getApiInfo())
.enable(flag)
.select()
.apis(RequestHandlerSelectors.any())
.build()
.groupName("Z");
return docket;
}
}

当前为开发环境:

  1. Swagger 接口注释&实体类注释

4.1 实体类注释

4.1.1 编写实体类

  • @ApiModel:为实体类添加注释
  • @ApiModelProperty:为实体类属性添加注释

User.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
java复制代码@ApiModel("用户实体类")     // 文档注释
public class User {
public User() {
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

// 属性设置为 public, 在 Swagger 中才可视
@ApiModelProperty("姓名")
public String username;
@ApiModelProperty("密码")
public String password;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

4.1.2 编写实体类对应的请求方法

编写完实体类后,我们还是无法在 Model 中看到 User 实体类信息,需在 HelloController 中新增一个返回 User 对象的请求方法:

1
2
3
4
5
6
7
java复制代码@RestController
public class HelloController {
@GetMapping("/swagger1")
public User getUser() {
return new User("Scorpions_", "123456");
}
}

4.1.3 测试访问

成功显示 Model 信息:

4.2 接口注释

  • @ApiOperation:为接口添加注释
  • @ApiParam:为接口参数列表添加注释

示例1

1
2
3
4
5
6
7
8
9
java复制代码@RestController
public class HelloController {
@GetMapping("/swagger2")
@ApiOperation("response返回错误")
public User swagger2(@ApiParam("接口形参num") int num) {
int i = num / 0;
return new User();
}
}

接口及其形参列表上标有注释:


这里将 /swagger2 请求改成 POST 请求而不是 GET 请求:

1
2
3
4
5
6
7
8
9
java复制代码@RestController
public class HelloController {
@PostMapping("/swagger22")
@ApiOperation("POST请求具备方法体, response返回错误")
public User swagger2(@ApiParam("接口形参num") int num) {
int i = num / 0;
return new User();
}
}

请求结果符合预期的 500 错误:

示例2

1
2
3
4
5
6
7
java复制代码// POST 表单才可以请求, 而 Swagger 在测试时会提供方法体, 仅需输入测试即可
// 注意传入的参数User实体类中必须要有 getter() 和 setter(), 方法才能正常赋值到形参user中!
@ApiOperation("Swagger3 POST 请求")
@PostMapping("/swagger3")
public User swagger3(@ApiParam("user参数, 必须设置属性的setter() & getter()") User user) {
return user;
}

填写完表单后会添加到方法体 body 中:

response 返回预期结果:

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

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

go语言——select初窥(二)

发表于 2021-11-10

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

上篇文章讲了sellock,接下来接着继续看

selunlock

1
2
3
4
5
6
7
8
9
go复制代码func selunlock(scases []scase, lockorder []uint16) {
for i := len(lockorder) - 1; i >= 0; i-- {
c := scases[lockorder[i]].c
if i > 0 && c == scases[lockorder[i-1]].c {
continue // will unlock it on the next iteration
}
unlock(&c.lock)
}
}

首先第一个看到的问题就是相对于sellock,他的遍历顺序恰恰相反,这里猜测是像for循环的大括号一样(不恰当的比喻,不过之前操作系统的加锁解锁方式也遵循这种规律)

照例也有了

1
2
3
go复制代码 if i > 0 && c == scases[lockorder[i-1]].c {
continue // will unlock it on the next iteration
}

这部分当然也是为了防止重复解锁,不过作者在注释中特意强调了一定要注意不要重复解锁,因为一个通道在被解锁后可能就被释放了。

selparkcommit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码func selparkcommit(gp *g, _ unsafe.Pointer) bool {

gp.activeStackChans = true

atomic.Store8(&gp.parkingOnChan, 0)

var lastc *hchan
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
if sg.c != lastc && lastc != nil {
unlock(&lastc.lock)
}
lastc = sg.c
}
if lastc != nil {
unlock(&lastc.lock)
}
return true
}

gp.activeStackChans = true,第一行就看不懂了,查了下gp可以理解为是一个stack,activeStackChans表示有未锁定的通道指向这个协程的堆栈,当他为true的时候,代表如果进行堆栈复制的话需要我们先锁通道再进行堆栈复制。

atomic.Store是往第一个参数里写入第二个参数的值,是一个原子操作。store8代表参数类型为unit8。

ParkingOnChan 表示 goroutine 即将停在 chansend 或 chanrecv 上。 用于表示堆栈收缩的不安全点。 它是一个布尔值,但会原子的更新。

一旦我们解锁这个通道,该通道里任何sudog中的字段都有可能会更改,由于多个sudog可能有相同的通道,因此我们只有在通道的最后一个实例才可以解锁。

本文转载自: 掘金

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

深入理解SpringBoot自动装配原理

发表于 2021-11-10

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

前言

一般Spring Boot应用的启动类都位于 src/main/java根路径下

image.png

其中 @SpringBootApplication开启组件扫描和自动配置,而 SpringApplication.run则负责启动引导应用程序。

注解@SpringBootApplication

@SpringBootApplication是一个复合 Annotation,它将三个有用的注解组合在一起:

image.png

2.1. @SpringBootConfiguration

@SpringBootConfiguration就是 @Configuration,它是Spring框架的注解,标明该类是一个 JavaConfig配置类。而 @ComponentScan启用组件扫描,前文已经详细讲解过,这里着重关注 @EnableAutoConfiguration。

2.2. @EnableAutoConfiguration

@EnableAutoConfiguration注解表示开启Spring Boot自动配置功能,Spring Boot会根据应用的依赖、自定义的bean、classpath下有没有某个类 等等因素来猜测你需要的bean,然后注册到IOC容器中。

2.2.1 @EnableAutoConfiguration 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

/**
* Environment property that can be used to override when auto-configuration is
* enabled.
*/
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};

/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};

}

@Import(EnableAutoConfigurationImportSelector.class) 是重点。

2.2.2 EnableAutoConfigurationImportSelector

@Import注解用于导入类,并将这个类作为一个bean的定义注册到容器中,这里它将把 EnableAutoConfigurationImportSelector作为bean注入到容器中,而这个类会将所有符合条件的@Configuration配置都加载到容器中

1
2
3
4
5
6
7
8
java复制代码@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

这个类会扫描所有的jar包,将所有符合条件的@Configuration配置类注入的容器中,何为符合条件,看看 META-INF/spring.factories的文件内容:
我们看下SPringBoot启动类里面的内容,org.springframework.boot.autoconfigure下的META-INF/spring.factories

image.png

比如我们前面介绍的flyway数据库配置管理工具。

1
复制代码org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\

EnableAutoConfigurationImportSelector#selectImports方法何时执行?

在容器启动过程中执行: AbstractApplicationContext#refresh()方法

本文转载自: 掘金

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

1…375376377…956

开发者博客

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