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

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


  • 首页

  • 归档

  • 搜索

SQL优化及多数据库支持分享(五)

发表于 2021-11-15

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

EXPLAIN的详细使用

​ 在排查慢SQL时,我们经常使用explain来查看SQL执行计划。本次我们就来详细介绍下explain如何使用及其参数的分析。

1、id

​ select语句查询序列号。代表select语句的编号。如果是连接查询,那么表之间是平等关系,select都是1。如果有子查询,那么编号递增。

2、select_type

​ 表示select语句的类型。有以下几种:

​ 1、simple:简单查询,不包含子查询个连接查询。

​ 2、primary:表示主查询,或者是最外面的查询语句(包含子查询或者派生查询)

​ 3、union:连接查询的第 2 个或后面的查询语句

​ 4、union result:连接查询的结果

​ 5、subquery:子查询中的第 1 个 SELECT 语句。
​ 6、dependent subquery:子查询中的第 1 个 SELECT 语句,取决于外面的查询。

3、table

表示查询的表,有可能是实际的表名,e.g. select * from t1;表的别名 如 select * from t2 as tmp;

4、type

​ 表示表的连接类型。以下的连接类型的顺序是从最优到最差排序:

1、 system

​ 表示仅有一行,这是 const 类型的特列,平时不会出现,也可以忽略不计。

2、 const

​ 数据表最多只有一个匹配行,因为只匹配一行数据,所以很快,通常用于 PRIMARY KEY 或者 UNIQUE 索引的查询,

可理解为 const 是最优的。

3、 eq_ref

​ mysql 手册中这样介绍eq_ref:”对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除

了 const 类型。它用在一个索引的所有部分被联接使用并且索引是 UNIQUE 或 PRIMARY KEY”。eq_ref 可以用于使

用等于比较带索引的列。

4、 ref

​ 查询条件索引既不是 UNIQUE 也不是 PRIMARY KEY 的情况。ref 可用于=或<或>操作符的带索引的列。

5、ref_or_null

​ 如同 ref,但是添加了 MySQL 可以专门搜索包含 NULL 值的行。在解决子查询中经常使用该联接类型

的优化。

6、 index_merge

​ 该联接类型表示使用了索引合并优化方法。在这种情况下,key 列包含了使用的索引的清单,key_len 包含了使

用的索引的最长的关键元素。

7、 unique_subquery

​ unique_subquery 是一个索引查找函数,效率更高。

8、 index_subquery

​ 类似于 unique_subquery,可以替换 IN 子查询。

9、 range

​ 只检索给定范围的行,使用一个索引来选择行

10、 index

​ 该联接类型与 ALL 相同,除了只有索引树被扫描,通常比 ALL 快,因为索引文件通常比数据文件小。

11、 ALL

​ 对于每个来自于先前的表的行组合,进行完整的表扫描。(性能最差,需要优化)

5、key

​ 最终用的索引.显示 MySQL 实际决定使用的键(索引)。如果没有选择索引,键是 NULL。可以强制使用索引或者忽略索引。

6、ref

​ 显示使用哪个列或常数与 key 一起从表中选择行。

7、rows

​ MySQL 执行查询时必须扫描的行数,越少说明索引命中率越高,SQL语句执行的越快。

8、Extra

​ 包含 MySQL 解决查询的详细信息

​ Distinct: 发现第 1 个匹配行后,会停止为当前的行组合搜索更多的行。

​ Not exists: 能够对查询进行 LEFT JOIN 优化,发现 1 个匹配 LEFT JOIN 标准的行后,不再为前面的的行组

合在该表内检查更多的行。

​ Using filesort:需要额外的一次传递,以找出如何按排序顺序检索数据。

​ Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。

​ Using temporary:表示需要创建一个临时表来容纳结果集。

​ Using where:WHERE 子句用于限制哪一个行匹配下一个表。

本文转载自: 掘金

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

SpringCloud升级之路20200x版-34验证

发表于 2021-11-15

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

本系列代码地址:github.com/JoJoTec/spr…

我们继续上一节针对我们的重试进行测试

验证针对可重试的方法响应超时异常重试正确

我们可以通过 httpbin.org 的 /delay/响应时间秒 来实现请求响应超时。例如 /delay/3 就会延迟三秒后返回。这个接口也是可以接受任何类型的 HTTP 请求方法。

我们先来指定关于 Feign 超时的配置 Options:

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
less复制代码//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//关闭 eureka client
"eureka.client.enabled=false",
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
//指定默认响应超时为 2s
"feign.client.config.default.readTimeout=2000",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service1Instance3 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
//微服务 testService1 有一个实例即 service1Instance1
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1));
return spy;
}
}
}

我们分别定义会超时和不会超时的接口:

1
2
3
4
5
6
7
8
kotlin复制代码@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/delay/1")
String testGetDelayOneSecond();

@GetMapping("/delay/3")
String testGetDelayThreeSeconds();
}

编写测试,还是通过获取调用负载均衡获取实例的次数确定请求调用了多少次。

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
ini复制代码@Test
public void testTimeOutAndRetry() throws InterruptedException {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
long l = span.context().traceId();
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
= (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
int start = atomicInteger.get();
//不超时,则不会有重试,也不会有异常导致 fallback
String s = testService1Client.testGetDelayOneSecond();
//没有重试,只会请求一次
Assertions.assertEquals(1, atomicInteger.get() - start);

//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
start = atomicInteger.get();
//超时,并且方法可以重试,所以会请求 3 次
try {
s = testService1Client.testGetDelayThreeSeconds();
} catch(Exception e) {}
Assertions.assertEquals(3, atomicInteger.get() - start);
}
}

验证针对不可重试的方法响应超时异常不能重试

对于 GET 方法,我们默认是可以重试的。但是一般扣款这种涉及修改请求的接口,我们会使用其他方法例如 POST。这一类方法一般请求超时我们不会直接重试的。我们还是通过 httporg.bin 的延迟接口进行测试:

1
2
3
4
5
kotlin复制代码@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@PostMapping("/delay/3")
String testPostDelayThreeSeconds();
}

编写测试,还是通过获取调用负载均衡获取实例的次数确定请求调用了多少次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码@Test
public void testTimeOutAndRetry() throws InterruptedException {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
long l = span.context().traceId();
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
= (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
int start = atomicInteger.get();
//不超时,则不会有重试,也不会有异常导致 fallback
String s = testService1Client.testPostDelayThreeSeconds();
//没有重试,只会请求一次
Assertions.assertEquals(1, atomicInteger.get() - start);
}
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

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

SpringBoot中集成Thymeleaf

发表于 2021-11-15

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

在项目开发前后端分离规范的今天,后端开发不再需要进行页面开发,但是作为Java开发人员一定对JSP不陌生,这也是早年间Web开发的一大利器。

Thymeleaf可以说是JSP的升级版,它不仅可以作为页面开发的模板被SpringBoot完美支持,而且模板格式是HTML文件,使静态页面不再依赖于服务端,可以直接在浏览器打开查看效果。

Thymeleaf作为后端页面开发模板,避免了前后端分离带来的跨域等问题,对于后台管理等简单页面可以快速进行开发迭代。
学习Thymeleaf,首先了解一下如何将Thymeleaf集成到SpringBoot项目中使用。

  1. 创建SpringBoot项目

1.1 选择起步依赖

在创建SpringBoot项目时,官方提供了很多的起步依赖,其中就有Thymeleaf相关的启动器,在Template Engines模板引擎下勾选Thymeleaf启动器,由于是Web页面的开发,所以还需要引入Web模块启动器。

image.png

勾选后点击下一步并完成项目创建。

1.2 手动添加依赖

如果在项目创建时没有选择相关的Thymeleaf启动器,则可以在maven的坐标配置pom.xml文件中添加thymeleaf的依赖信息。

1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

完成创建后,就得到了一个集成Thymeleaf的SpringBoot项目,结构上与普通项目一样,但是在resources文件夹下多了一个templates文件夹,这个文件夹就是用于存放Thymeleaf模板文件。

image.png

  1. Thymeleaf配置

2.1 SpringBoot自动配置Thymeleaf

SpringBoot框架中提供了对Thymeleaf模板引擎的自动加载配置,在SpringBoot的自动配置类包中,有一个org.springframework.boot.autoconfigure.thymeleaf包,定义的ThymeleafProperties类就是thymeleaf的自动配置类,其中指定了thymeleaf框架的一些默认属性,比如模板的默认路径前缀classpath:/templates/,以及模板文件格式后缀.html。

ThymeleafProperties类的部分定义内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//自动配置类ThymeleafProperties
@ConfigurationProperties( prefix = "spring.thymeleaf" )
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
...
}

可以看出,配置类读取的是SpringBoot配置文件中以spring.thymeleaf作为前缀的配置内容,因此,如果需要自定义thymeleaf的相关配置,只需要在application.properties文件中指定即可。

2.2 自定义配置项

自动配置类ThymeleafProperties初始化时会读取application.properties文件中配置的内容,配置文件中可以定义的thymeleaf配置项有:

1
2
3
4
5
6
7
8
9
10
11
12
properties复制代码# thymeleaf模板文件前缀,可以自定义文件夹如classpath:/templates/temp
spring.thymeleaf.prefix=classpath:/templates/
# thymeleaf模板文件后缀
spring.thymeleaf.suffix=.html
# 视图模板类型
spring.thymeleaf.mode=HTML
# 默认视图编码格式
spring.thymeleaf.encoding=UTF-8
# 响应类型
spring.thymeleaf.servlet.content-type=text/html
# 配置页面缓存,thymeleaf默认开启缓存,页面不能及时刷新,需要关闭
spring.thymeleaf.cache=false

通过自定义的配置相关属性的值,在实际使用时可以更好的控制对thymeleaf模板引擎的使用。

  1. Thymeleaf页面效果

创建了项目,添加了配置,下面就来看一下thymeleaf带来的页面效果。

3.1 定义HTML文件

使用thymeleaf模板引擎展示页面,首先要创建一个html文件,并按照thymeleaf语法编码与接口数据绑定。

  • 定义html文件时,要对其中的<html>标签使用xmlns:th="http://www.thymeleaf.org"声明,这样在当前html页面中才可以使用thymeleaf语法。
  • 对于服务接口绑定返回的数据集合,可以使用th:each="user : ${userEntityList}"遍历对应值
  • 对于每个属性的值,使用th:text="${user.getName()}"来获取展示
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复制代码<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<table>
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>电话</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${userEntityList}">
<td th:text="${user.getName()}">name</td>
<td th:text="${user.getGender()}">male</td>
<td th:text="${user.getPhone()}">13812341234</td>
</tr>
</tbody>
</table>
</body>
</html>

3.2 定义服务接口

定义服务接口来向thymeleaf模板传递数据,接口定义时需要注意:

  • 使用@Controller注解而不是@RestController,因此@RestController会将结果解析后直接展示字符串内容,而不能根据字符串获取对应的模板名称
  • 服务接口需要加入Model对象作为参数,并使用Model对象来绑定需要传递的数据,以便在thymeleaf中使用
1
2
3
4
5
6
7
8
9
10
11
typescript复制代码@Controller
public class ThymeleafController {
@RequestMapping("/index")
public String getIndex(Model model){
List<UserEntity> userList = new ArrayList<>();
UserEntity user = new UserEntity("tom","female", "17788996600");
userList.add(user);
model.addAttribute(userList);
return "index";
}
}

3.3 页面展示

如此这般之后,可以运行项目,并请求定义的服务接口,此时会根据接口将数据加入Model中,并根据接口的返回结果找到对应的thymeleaf模板文件,在模板文件中将数据渲染,最终展示在页面中。

image.png

本文转载自: 掘金

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

Go结构体基础学习 Go结构体基础学习

发表于 2021-11-15

Go结构体基础学习

** 直接看示例代码和注释 **

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

import "fmt"

// Person 创建一个结构体
type Person struct {
name string
age int
gender string
}

func main() {
fmt.Println("im go .")
// 实例化结构体
// 第一种方法
var p Person
p.name = "canyon"
p.age = 24
p.gender = "男"
fmt.Printf("first method: 姓名: %v 年龄: %v 性别: %v\n", p.name, p.age, p.gender)

// 第二种方法
p2 := Person{}
p2.name = "张三"
p2.age = 18
p2.gender = "男不男"
fmt.Printf("second method: 姓名: %v 年龄: %v 性别: %v\n", p2.name, p2.age, p2.gender)

// 第三种方法: 直接赋值
p3 := Person{
name: "李四",
age: 19,
gender: "男",
}
fmt.Printf("third method: 姓名: %v 年龄: %v 性别: %v\n", p3.name, p3.age, p3.gender)
p4 := Person{
"王五", 20, "女吧",
}
fmt.Printf("third-2 method: 姓名: %v 年龄: %v 性别: %v\n", p4.name, p4.age, p4.gender)
fmt.Println("——————————————————————————————————")

// 创建结构体指针
var pp1 *Person
fmt.Print("pp1: ", pp1) //
fmt.Printf("%T\n", pp1)
// p3的内存地址赋值给pp1
pp1 = &p3
fmt.Printf("%T\n", pp1)
fmt.Printf("%p, %T\n", pp1, pp1)
fmt.Println("p3: ",p3)
// 这个指针存储的地址对应的数据
fmt.Println("指针存的数据: ", *pp1)
pp1.name = "李六" 等价于 (*pp1).name = "李六"
fmt.Println("指针存的数据: ", *pp1)
fmt.Println("p3的数据: ", p3)
}

本文转载自: 掘金

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

golang 微服务的负载均衡

发表于 2021-11-15

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

上次我们说了一下 微服务的容错处理,是用的 断路器

这一次我们来一起看看 微服务的负载均衡是如何做的

负载均衡

负载均衡是什么呢?

他能够将大量的请求,根据负载均衡算法,将不同的请求分发到多台服务器上进行处理,使得所有的服务器负载都维持在一个高效稳定的状态,进而可以提高系统的吞吐量,和保证系统的可用性

例如我们的访问服务器是这样的

用户 – 网络 – 服务器 – 数据库

那么,如果这一个服务器的请求数很高,超过了服务器能能处理的极限,自身无法及时响应的时候,则会出现异常,甚至无法连接,用户就无法得到及时的期望结果

那么我们至少可以期望服务器部署是这个样子的

就是在服务器的前面加一个负载均衡器,这样外部请求的压力就可以又 多个服务器来分担,并且请求给到任何一个服务器,得到的响应都是一样的

那么我们一起来看看负载均衡的类型都有哪些

负载均衡的类型

负载均衡的类型有 2 类:

  • 软件负载均衡

一般是独立的负载均衡软件来实现外部请求的分发,一般这样的软件配置简单,使用成本很低,并且能够满足基本的负载均衡要求,例如 haproxy

那么这就要对重点关注在软件的质量和该软件部署在所属服务器的性能上面,若软件质量不行,或者部署的服务器性能不行,都会成为系统吞吐量的瓶颈

  • 硬件负载均衡

硬件的负载均衡,必然是依赖特殊负载均衡设备来做的,部署成本相对较高,可是对于软件的负载均衡,硬件的做法能够满足更多种场景的使用

例如常见的例子,DNS 负载均衡 和 反向代理负载均衡

DNS 负载均衡

例如在 DNS服务器中,我们会给一个同一个名称配置多个 IP,那么不同的 DNS 请求就会解析到不同的 IP 地址,进而这就可以达到 不同请求去访问不同的服务器的目的,这就是咱们的 DNS 负载均衡

反向代理负载均衡

我们平时项目中使用到的服务网关就是反向代理负载均衡

作为客户端,你是不知道你访问的这个地址是不是真正的服务器的地址,你访问了网关地址之后,网关会根据路由将你的请求发送给对应服务器去处理,最终返回结果,例如这样

负载均衡算法

如何保证能够让每一个服务器的都能够处于高效稳定的运行呢,这就需要优秀的负载均衡算法出马了

负载均衡算法定义了如何将外部请求分散到各个服务器实例中,它能够有效的提高吞吐量

一般会有这几种算法:

  • 随机法

随机从服务器集群中任选一台。这种方法确实很简单,保证了请求的分散性,可是这种方法无法做到当前的请求分配是否合理以及不同服务器自身的负载能力

  • 轮询或者加权轮询法

就是轮流的将请求分配给集群中每一个服务器,加权的话,就是按照比例轮询的方式将请求分配给集群中的每一个服务器,如:

轮询 3 个服务器

加权轮询 3 个服务器,若 A 占比 20%,B 占比 50 % ,C 占比 30%

  • Hash 法或者一致性 Hash 的方式

就是使用 hash 算法将请求分散到集群中每一个服务器上面

一致性 hash 指的是,在将请求分散到每个服务器时,若其中一个服务器挂掉了,这个算法能够将请求平摊到剩下的服务器上面,这样可以避免请求剧烈的变动

  • 最小连接数方法

就是将请求分配到连接数最少的服务器上面,但是负载均衡器需要如何知道呢?

因此使用这种算法,就需要负载均衡器与服务器之前产生数据交互,这样它在可以了解集群中服务器的连接数情况

今天就到这里,学习所得,若有偏差,还请斧正

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

JAVA异常处理一

发表于 2021-11-15

异常: 指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处
理异常的方式是中断处理。

  • 异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

异常体系

异常机制是为了帮助程序员找到程序中的问题,异常的根类是java.lang.Throwable ,其下有两个子类:
java.lang.Error 与java.lang.Exception ,平常所说的异常指java.lang.Exception 。
在这里插入图片描述

Throwable体系

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

Throwable中的常用方法:

  • public void printStackTrace() :打印异常的详细信息。
    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
  • public String getMessage() :获取发生异常的原因。
    提示给用户的时候,就提示错误原因。
  • public String toString() :获取异常的类型和异常描述信息(不用)。

异常分类

我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
在这里插入图片描述

异常产生的的过程

运行下面的程序,程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。
工具类

1
2
3
4
5
6
7
java复制代码public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

测试类

1
2
3
4
5
6
7
8
java复制代码public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}

程序执行过程图解:
在这里插入图片描述

本文转载自: 掘金

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

MySQL索引优化系列:(二)索引全用及最左法则 复合索引全

发表于 2021-11-15

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

优化MySQL的性能,主要从索引方面优化,本篇就主要讲解复合索引全用及最左前缀法则来对MySQL进行优化。

复合索引全用

第一篇MySQL索引优化系列:(一)索引的类型里面有说过复合索引是什么,也就是对表上的多个列进行索引。复合索引全用的意思就是对于建立的复合索引中包含了几个字段,查询的时候最好能全部用到,而且严格按照索引顺序,这样查询效率是最高的。当然实际使用中要按照具体情况来分析,以上只是说一个理想状况。

下面来实际操作一下:

SQL脚本:

1
2
3
4
5
6
7
8
9
10
java复制代码CREATE TABLE `people` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` char(4) DEFAULT NULL,
`class` varchar(10) DEFAULT NULL,
`birthday` date DEFAULT NULL,
PRIMARY KEY (`Id`),
KEY `idx_name_age_sex_class` (`name`,`age`,`sex`,`class`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1026 DEFAULT CHARSET=utf8;

然后插入了1024条

1
java复制代码INSERT INTO `test`.`people` (`name`, `age`, `sex`, `class`, `birthday`) VALUES ('张三', '17', '男', '1', '2021-03-02');

和1条

1
java复制代码INSERT INTO `test`.`people` ( `name`, `age`, `sex`, `class`, `birthday`) VALUES ('李四', '25', '女', '2', '2021-02-24');

不同的查询条件对比:

1
java复制代码EXPLAIN SELECT * from people where name="李四"

在这里插入图片描述

=>查询时间:0.027s key_len :33

1
java复制代码EXPLAIN SELECT * from people where name="李四" and age="25"

在这里插入图片描述

=>查询时间:0.026s key_len :38

1
java复制代码EXPLAIN SELECT * from people where name="李四" and age="25" and sex="女"

在这里插入图片描述
=>查询时间:0.024s key_len :51

1
java复制代码EXPLAIN SELECT * from people where name="李四" and age="25" and sex="女" and class="2"

在这里插入图片描述
=>查询时间:0.023s key_len :84

最左前缀法则

最左前缀也跟复合索引有关,索引的顺序要按照建立时的顺序来进行索引,不然就不会使用创立的复合索引。就是从左到右的顺序来写sqlu、语句。

还是以上面的例子来说:

1
java复制代码EXPLAIN SELECT * from people where name="李四" and age="25" and sex="女" and class="2"

假如我们改成以下的方式来查询,来看下还会不会使用创造的复合索引?

1
2
3
4
5
java复制代码EXPLAIN SELECT * from people where age="25" and sex="女" and class="2"
EXPLAIN SELECT * from people where sex="女"
EXPLAIN SELECT * from people where class="2" and name="张三" and class="1"
EXPLAIN SELECT * from people where name="张三" and sex="女" and class="2"
EXPLAIN SELECT * from people where class="2" and name="张三" and sex="女"

答案是第1、第2、第3不会,第4、第5会。

来看下1、2、3:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

再看下4、5:
在这里插入图片描述
在这里插入图片描述
我们可以看出来推导出最左前缀法则就是:带头大哥不准死、中间兄弟不可断

以上就是今天的分享,如有错误,请大佬们多多包涵,多多指正!谢谢~

本文转载自: 掘金

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

跟着老猫来搞GO-容器(1)

发表于 2021-11-15

前期回顾

前面的一章主要和大家分享了GO语言的函数的定义,以及GO语言中的指针的简单用法,那么本章,老猫就和大家一起来学习一下GO语言中的容器。

数组

数组的定义

说到容器,大家有编程经验的肯定第一个想到的就是数组了,当然也有编程经验的小伙伴会觉得数组并不是容器。但是无论如何,说到数组其实它就是存储和组织数据的一种方式而已,大家就不要太过纠结叫法了。

咱们直接上数组定义的例子,具体如下:

1
2
3
4
5
go复制代码var arr1 [5]int //定义一个长度为5的默认类型
arr2:=[3]int{1,2,3} //定义一个数组,并且指定长度为3
arr3:=[...]int{1,2,3,4,5,6} //定义一个数组,具体的长度交给编译器来计算
var grid [4][5] bool //定义一个四行五列的二维数组
fmt.Println(arr1,arr2,arr3,grid)

上面的例子输出的结果如下

1
go复制代码[0 0 0 0 0] [1 2 3] [1 2 3 4 5 6] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

大家可以总结一下,其实数组有这么几个特点

  • 在写法上,其实也是和其他编程语言是相反的,其定义的数组的长度写在变量类型的前面
  • 数组中所存储的内容必然是同一类型的

数组的遍历

那么我们如何遍历获取数组中的数据呢?其实看过老猫之前文章的小伙伴应该晓得可以用for循环来遍历获取,其中一种大家比较容易想到的方式如下(我们以遍历上面的arr3为例)

1
2
3
go复制代码for i:=0;i<len(arr3);i++ {
fmt.Println(arr3[i])
}

这种方式呢,我们当然是可以获取的。接下来老猫其实还想和大家分享另外一种方式,采用range关键字的方式

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码//i表示的是数据在数组中的位置下标,v表示实际的值
for i,v :=range arr3 {
fmt.Println(i,v)
}
//那么如果我们只想要value值呢,回顾一下老猫之前所说的就可以晓得,我们可以用_的方式进行对i省略
for _,v :=range arr3 {
fmt.Println(v)
}
//如果咱们只要位置下标,那么我们如下去写即可
for i:=range arr3 {
fmt.Println(i)
}

大家觉得上述两种方式哪种方式会比较优雅?显而易见是后者了,意义明确而且美观。

go语言中数组是值传递的

另外和大家同步一点是数组作为参数也是值传递。还是沿用之前的我们重新定义一个新的函数如下:

1
2
3
4
5
go复制代码func printArray(arr [5]int){
for i,v:=range arr {
println(i,v)
}
}

那么我们在main函数中进行相关调用(为了演示编译错误,老猫这里用图片)

编译错误演示

大家根据上面的图可以很清晰的看到调用printArray(arr2)的时候报了编译错误,其实这就是说明,在go语言中,即使同一个类型的数组,如果不同长度,那么编译器还是认为他们是不同类型的。

那么我们这个时候再对传入的数组进行值的变更呢,具体如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func main() {
arr3:=[...]int{1,2,3,4,5} //定义一个数组,并且长度可变
printArray(arr3)

for i,v:=range arr3 {
println(i,v)
}
}

func printArray(arr [5]int){
arr[0] = 300
for i,v:=range arr {
println(i,v)
}
}

大家可以看到,老猫在这里操作了两次打印,第一次打印是直接在函数中打印,此时已经更改了第一个值,其函数内部打印的结果为

1
2
3
4
5
go复制代码0 300
1 2
2 3
3 4
4 5

显然内部的值是变更了,然而我们再看一下外面的函数的打印的值,如下

1
2
3
4
5
go复制代码0 1
1 2
2 3
3 4
4 5

其实并没有发生变更,这其实说明了什么呢,这其实说明了在调用printArray的时候其实是直接将数组拷贝一份传入函数的,外面的数组并未被更新,这也直接说明了GO语言是值传递的参数传递方式。

大家在使用这个数组的时候一定要注意好了,说不准就被坑了。大家可能会觉得这个数组真难用,其实可以告诉大家一个好消息,在GO语言中,一般其实不会直接去使用数组的,咱们用的比较多的还是“切片”

切片

说到切片的话,咱们其实最好是基于上面数组的基础上去理解切片。咱们先来看一个例子

1
2
3
4
5
6
7
go复制代码func main() {
arr := [...]int{1,2,3,4,5,6,7}
fmt.Println("arr[2:6]",arr[2:6])
fmt.Println("arr[:6]",arr[:6])
fmt.Println("arr[2:]",arr[2:])
fmt.Println("arr[:]",arr[:])
}

其实像类似于’[]’这种定义我们就称呼其为切片,英文成为slice,它表示拥有相同类型元素的可变长度的序列。我们来看一下结果:

1
2
3
4
go复制代码arr[2:6] [3 4 5 6]
arr[:6] [1 2 3 4 5 6]
arr[2:] [3 4 5 6 7]
arr[:] [1 2 3 4 5 6 7]

其实这么说会比较好理解,slice咱们可以将其看作为视图,就拿arr[2:6]来说,我们其实在原来数组的基础上抽取了从第二个位置到第六个位置的元素作为值重新展现出来,当然我们的取值为左闭右开区间的。

slice其实是视图概念

上面我们说了slice相当于是数组的视图,那么接下来的例子,咱们来证实上述的说法,详细看下面的例子

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
arr := [...]int{1,2,3,4,5,6,7}
fmt.Println("arr[2:6]",arr[2:6])
updateSlice(arr[2:6])
fmt.Println("arr[2:6]",arr[2:6])
fmt.Println(arr)
}

func updateSlice(arr []int){
arr[0] = 100
}

老猫写了个函数,主要是更新slice第一个位置的值,大家可以先思考一下执行前后所得到的结果是什么,然后再看下面的答案。

其实最终执行的结果为:

1
2
3
go复制代码arr[2:6] [3 4 5 6]
arr[2:6] [100 4 5 6]
[1 2 100 4 5 6 7]

那么为什么是这样的?其实arr[2:6]很容易理解是上面的3456,第二个也比较容易理解,当我们slice的第一个值被更新成了100,所以编程了第二种,那么原始的数据为什么也会变成100呢?这里面其实是需要好好品一下,因为我们之前说slice是对原数组的视图,当我们第二种看到slice其实已经发生了更新变成了100,那么底层的数据肯定也发生了变更,变成了100了。(这里要注意的是,并没有谁说视图的操作不会反作用于原数组)。这里还是比较重要的,希望大家细品一下。

reslice以及扩展

说到reslice,说白了就是对原先的slice再做一次slice取值,那么我们看下面的例子。

1
2
3
4
5
6
go复制代码func main() {
arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
fmt.Println(s2)
}

以上例子可见s1是对数组的全量切片,然后我们对s1又进行了一次切片处理,很容易地可以推算出来我们第二次所得到的结果为[3,4],像这种行为我们就称为reslice,这个还是比较好理解的。

接下来咱们在这个基础上加深一下难度,我们在S2的基础上再次进行resilce,具体如下:

1
2
3
4
5
6
7
go复制代码func main() {
arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
s3 := s2[1:3]
fmt.Println(s3)
}

我们都知道s2所得到的值为[3,4],当我们在次对其进行reslice的时候,由于取的是[1:3],那么此时我们发现是从第一个位置到第三个位置,第一个位置还是比较好推算出来的,基于[3,4]的话,那么其第一个位置应该是4,那么后面呢?结果又是什么呢?这里将结果直接告诉大家吧,其实老猫运行之后所得到的结果是

1
go复制代码[4 5]

那么为什么会有这样的一个结果?5又是从哪里来的呢?

咱们来看一下老猫下面整理的一幅示意图。
示意图

  1. arr的一个数组,并且其长度为7,并且里面存储了七个数。
  2. 接下来s1对其去完全切片,所以我们得到的也是一个完整的7个数。
  3. 需要注意的是,这时候我们用的是下标表示,当s2对s1在此切片的时候,咱们发现其本质是对数组的第二个元素开始进行取值,由于是视图的概念,其实s2还会视图arr虚幻出另外两个位置,也就是咱们表示的灰色的3以及4下标。
  4. 同样的我们将s3表示出来,由此我们s3是在s2的基础上再次切片,理论上有三个下标值,分别是0、1、2下标取值,但是我们发现s2的3号位置指示虚幻出来的位置,并未真正存在值与之对应,因此,咱们取交集之后与数组arr对应只能取出两个,也就是最终的[4,5]。

此处还是比较难理解,希望大家好好理解一下,然后写代码自己推演一下,其实这个知识点就是slice的扩展,我们再来看一下下面的slice的底层实现。
底层结构

其实slice一般包含三个概念,slice的底层其实是空数组结构,ptr为指向数组第一个位置的指针,Len表示具体的slice的可用长度,而cap表示有能力扩展的长度。

其实关于len以及cap我们都有函数直接可以调用获取,我们看一下上面的例子,然后打印一下其长度以及扩展cap大家就清楚了。具体打印的代码如下。

1
2
3
4
5
6
7
8
9
10
go复制代码func main() {
arr := [...]int{1,2,3,4,5,6,7}
s1 := arr[:]
s2 := s1[2:4]
s3 := s2[1:3]
fmt.Printf("arr=%v\n",arr)
fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n",s1,len(s1),cap(s1))
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
fmt.Printf("s3=%v,len(s3)=%d,cap(s3)=%d\n",s3,len(s3),cap(s3))
}

上述代码输出的结果为

1
2
3
4
go复制代码arr=[1 2 3 4 5 6 7]
s1=[1 2 3 4 5 6 7],len(s1)=7,cap(s1)=7
s2=[3 4],len(s2)=2,cap(s2)=5
s3=[4 5],len(s3)=2,cap(s3)=4

当我们的取值超过cap的时候就会报错,例如现在s2为s2:=[2:4],现在我们发现其cap为5,如果我们超过5,那么此时s2可以写成s2:=[2:8],那么此时就会报以下异常

1
2
3
4
5
go复制代码panic: runtime error: slice bounds out of range [:8] with capacity 7

goroutine 1 [running]:
main.main()
E:/project/godemo/part6-slice.go:8 +0x7f

再者如果我们这么取值

1
go复制代码fmt.Printf("s3=%v",s3[4])

此时s3已经超过了len长度,那么也会报错,报错如下

1
2
3
4
5
go复制代码panic: runtime error: index out of range [4] with length 2

goroutine 1 [running]:
main.main()
E:/project/godemo/part6-slice.go:14 +0x49f

综上例子,我们其实可以得到这么几个结论。

  1. slice可以向后扩展,不可以向前扩展。
  2. s[i]不可以超越len(s),向后扩展不可以超越底层数组cap(s)

以上对slice的扩展其实还是比较让人头疼的,比较难理解,不过真正弄清里面的算法倒是也还好,希望大家也能理解上述的阐释,老猫已经尽最大努力了,如果还有不太清楚的,也欢迎大家私聊老猫。

切片的操作

向slice添加元素,如何添加呢?看一下老猫的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func main() {
arr :=[...]int{0,1,2,3,4,5,6,7}
s1 :=arr[2:6]
s2 :=s1[3:5]
s3 := append(s2,10) //[5,6,10]
s4 := append(s3,11) //[5,6,10,11]
s5 := append(s4,12)
fmt.Printf("arr=%v\n",arr)
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
fmt.Printf("s2=%v\n",s2)
fmt.Printf("s3=%v\n",s3)
fmt.Printf("s4=%v\n",s4)
fmt.Printf("s5=%v\n",s5)
}

如上述所示,我们往切片中添加操作的时候采用的是append函数,大家可以先不看老猫下面的实际结果自己推算一下最终的输出结果是什么。结合之前老猫所述的切片操作。结果如下:

1
2
3
4
5
6
go复制代码arr=[0 1 2 3 4 5 6 10]
s2=[5 6],len(s2)=2,cap(s2)=3
s2=[5 6]
s3=[5 6 10]
s4=[5 6 10 11]
s5=[5 6 10 11 12]

上述我们会发现append操作的话会有这样的一个结论

  • 添加元素的时候如果超过cap,系统会重新分配更大的底层数组
  • 由于值传递的关系,必须接收append的返回值

slice的创建、拷贝

之前老猫和大家分享的slice看起来都是基于arr的,其实slice的底层也确实是基于arry的,那么我们是不是每次在创建slice的时候都需要去新建一个数组呢?其实不是的,我们slice的创建方式有很多种,我们来看一下下面的创建方式

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func main() {
var s []int //1、空slice的创建方式,其实底层是基于Nil值的数组创建而来
for i := 0;i<100;i++ {
s = append(s,2*i+1)
}
fmt.Println(s)
s1 :=[]int {2,4,5,6} //2、创建一个带有初始化值得slice
s2 :=make([]int ,16) //3、采用make内建函数创建一个长度为16的切片
s3 :=make([]int,10,32) //4、采用make内建函数创建一个长度为10的切片,但是cap为32
//slice的拷贝也是相当简单的也是直接用内建函数即可,如下
copy(s2,s1) //这里主要表示的是将s1拷贝给s2,这里需要注意的是不要搞反了
}

slice元素的删除操作

为什么要把删除操作单独拎出来分享,主要是因为上述这些操作都有比较便捷的内建函数来使用,但是删除操作就没有了。咱们只能通过切片的特性来求值。如下例子

1
2
3
4
5
go复制代码func main() {
s1 :=[] int{2,3,4,5,6}
s2 :=append(s1[:2],s1[3:]...)
fmt.Println(s2)
}

上述有一个2到6的切片,如果我们要移除其中的4元素,那么我们就得用这种切片组合的方式去移除里面的元素,相信大家可以看懂,至于“s1[3:]…”这种形式,其实是go语言的一种写法,表示取从3号位置剩下的所有的元素。

最终我们得到的结果就得到了

1
go复制代码[2 3 5 6]

以上就是对slice的所有的知识分享了,花了老猫不少时间整理出来的,老猫也尽量把自己的一些理解说清楚,slice在语言中还是比较重要的。

写在最后

回顾一下上面的GO语言容器,其实重点和大家分享是slice(切片)的相关定义,操作以及底层的一些原理。弄清楚的话还是比较容易上手的。当然go语言的容器可不止这些,由于篇幅的限制,老猫就不分享其他的容器了,相信在写下去就没有耐心看了。后面的容器主要会和大家分享map以及字符和字符串的处理。更多内容欢迎大家关注公众号“程序员老猫”

本文转载自: 掘金

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

Golang Gin 框架参数解析介绍(三)

发表于 2021-11-15

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

目录

  • 前言
  • 正文
+ Query 参数解析
+ Multipart/Urlencoded Form 参数解析
+ query + post form 参数解析
+ Map 参数解析
  • 结尾

前言

Gin 是使用纯 Golang 语言实现的 HTTP Web 框架,Gin 的接口设计简洁,性能极高,现在被广泛使用。今天,我们就来详细看看 Gin 是如何进行参数解析的。

正文

Query 参数解析

在所有的请求中,Query 参数属于最常见的一种,下面通过一段代码来看一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main 

import "github.com/gin-gonic/gin"

func main() {
router := gin.Default()

router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname")

c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run(":8080")
}

上述 API 接口能够匹配类似如下请求的 URL:

/welcome?firstname=Jane&lastname=Doe

上述 Get 请求中,如果 Query 部分缺少参数 firstname,那么 firstname 的值将赋值为“Guest”;但是,如果缺少参数 lastname,那么 lastname 的值为空。

Multipart/Urlencoded Form 参数解析

在所有的请求中,还有 Multipart/Urlencoded Form 类型的参数,下面通过一段代码来理解一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main 

import "github.com/gin-gonic/gin"

func main() {
router := gin.Default()

router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")

c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}

上述 Post 请求中,如果 Body 部分缺少参数 nick,那么 nick 的值将赋值为“anonymous”;但是,如果缺少参数 message,那么 message 的值为空。

query + post form 参数解析

有些请求可能同时含有 query 参数和 post form 参数,比如:

POST /post?id=1234&page=1 HTTP/1.1

Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main 

import "github.com/gin-gonic/gin"

func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")

fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}

由上面的例子可以看出,解析 Query 类型的参数时,使用的是 Query 方法和 DefaultQuery 方法;解析 post form 类型的参数时,使用的是 PostForm 方法。

Map 参数解析

请求参数中还有一种类型就是 Map 类型,请求实例如下:

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1

Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou

这种情况该如何解析呢?请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func main() {
router := gin.Default()

router.POST("/post", func(c *gin.Context) {

ids := c.QueryMap("ids")
names := c.PostFormMap("names")

fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}

打印结果如下:

ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]

结尾

好啦,今天介绍了如何利用 Gin 框架解析四种不同参数请求的方法。希望对大家如何熟练使用 Gin 框架有所帮助。

作者简介:大家好,我是 liuzhen007,是一位音视频技术爱好者,同时也是CSDN博客专家、华为云社区云享专家、签约作者,欢迎关注我分享更多干货!

本文转载自: 掘金

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

记一次SpringMvc下HTTP 406问题排查

发表于 2021-11-15

问题背景

由于项目需要,需要将某个SpringMvc的Rest接口响应修改为json类型,结果发现原来正常的请求会报HTTP 406,这里记录一下追踪的过程。

先简单介绍一下HTTP 406。

HTTP 406 (Not Acceptable)

The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.

Accept

Accept代表发送端(客户端)希望接收的数据类型,*/*表示可以接收任何类型。

Content-Type

代表响应端(服务器)发送的实体数据的数据类型

如果双方不一致,也就是说客户端请求的accept和服务端响应的content-type不兼容,就会出现前面提到的406错误。

问题复现

原有接口示例
1
2
3
4
5
java复制代码@RequestMapping(value = "/hello/**")
@ResponseBody
public String helloWorld(HttpServletRequest httpServletRequest) {
return "hello world";
}

客户端对应的请求连接

1
html复制代码localhost:8080/hello/test.htm

使用postman模拟请求,可以看到请求时的Accept是*/*,而服务端返回的Content-Type是text/html。

image-20211115132848248

接口改造

按照业务需求,需要将响应统一修改为application/json类型,对于SpringMvc的Rest请求,我们做了如下修改,增加produces标识响应类型为application/json。

关于produces属性的含义

Narrows the primary mapping by media types that can be produced by the mapped handler(限制该方法的MediaType)

1
2
3
4
5
6
7
java复制代码// see MediaType.java
// public static final String APPLICATION_JSON_VALUE = "application/json";
@RequestMapping(value = "/hello/**", produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
public String helloWorld(HttpServletRequest httpServletRequest) {
return "hello world";
}

改造后重新使用Postman测试,发现响应的Content-Type虽然变成了application/json类型,但是出现了HTTP 406。如果将请求uri中的htm后缀去掉后,请求就变为正常了。

image-20211115134407729
image-20211115134633093
显然,问题出现在请求后缀上,需要进一步排查问题原因。

对于SpringMvc的请求过程,首先需要DispatchServlet根据请求HttpServletRequest,利用HandlerMapping获取到对应的HandlerChain。

获取HandlerChain

这里对于采用了@RequestMapping注解的方法,会使用RequestMappingHandlerMapping方法。

image-20211115135844714
当然,其中有些方法会存在于父类AbstractHandlerMethodMapping中,我们断点到lookupHandlerMethod方法。

可以看到当前请求的类就是RequestMappingHandlerMapping,它的mappingRegistry中包含了我们的请求接口”/hello/**“,对应的Produces是application/json类型。

image-20211115140626302
然后会进入到该类的addMatchingMappings方法中,寻找满足条件的mapping信息。

image-20211115140941065
继续进入getMatchingMappings方法,最终会进入到RequestMappingInfo的getMatchingCondition方法。这个方法会对请求中的很多属性进行校验,包括请求方法、参数、header,consumers以及produces,这里我们重点关注producesCondition的getMatchingCondition方法,通过后续的分析也可以得到,这是出问题的根本所在。

image-20211115141730032
关于这个方法,可以先看一下javaDoc的注释。

Checks if any of the contained media type expressions match the given request ‘Content-Type’ header and returns an instance that is guaranteed to contain matching expressions only.

方法内部会先根据request获取到acceptedMediaTypes,即getAcceptedMediaTypes方法。然后将获取到的同当前produces提供的进行匹配。

image-20211115142056568

获取请求的acceptedMediaTypes

在getAccepedMediaTypes内部会调用核心的ContentNegotiationManager解析请求的MediaTypes,这个类中会注册一些ContentNegotiationStrategy。在当前断点条件下有HeaderContentNegotiationStrategy和ServletPathExtensionNegotiationStrategy。

image-20211115143039199
我们进入到该方法内部,会循环遍历所有的Strategy解析到MediaTypes。

image-20211115143341902
首先会进入ServletPathExtensionNegotiationStrategy的解析,会先进入到父类中的resolveMediaTypes方法。

image-20211115143709976
注意到上面的getMediaTypeKey方法,该方法是一个抽象方法,拥有两个实现。

image-20211115143858546
当前情况下会进入PathExtensionContentNegotiationStrategy中

image-20211115144016948
这里会返回htm,然后进入到前面的AbstractMappingContentNegotiationStrategy的resolveMediaTypeKey方法。

image-20211115144445418
其中lookupMediaType位于MappingMediaTypeFileExtensionResolver中。

1
2
3
4
java复制代码	@Nullable
protected MediaType lookupMediaType(String extension) {
return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
}

image-20211115144607135
该方法的MediaType中只有xml和json,所以对于htm返回空。进而会进入到AbstractMappingContentNegotiationStrategy的handleNoMatch方法,这里会进入到ServletPathExtensionContentNegotiationStrategy的handleNoMatch方法中。它会根据文件后缀,得到MediaType为text/html类型。

image-20211115145130802

不匹配情况下抛出HttpMediaTypeNotAcceptableException

现在我们可以回到ProducesCondition方法中,获取到acceptMediaType后,和Produces的进行匹配,进入到getMatchingExpressions方法。可以看到当前类的expression是application/json,但是accepted是text/html,这里会返回空。进一步,produceCondition会返回null。

image-20211115145913855
继续向上返回RequestMapping.getMatchingCondition也会返回空。

image-20211115150145816
继续返回,会回到AbstractHandlerMethodMapping的lookupHandlerMethod方法

image-20211115150724820

由于上述的matches是空的,所以方法会执行到handleNoMatch方法,该方法是抽象方法。RequestMappingInfoHandlerMapping对该方法进行了重写。方法开始的PartialMathHelper初始化的时候,会对各种Condition进行校验,可以看到这里又执行了一遍之前的getMatchingCondition方法,并且同理producesMatch的结果是false。而我们看到在第267行,如果有produces不匹配的情况下,就会抛出HttpMediaTypeNotAcceptableException异常。

image-20211115151156323

image-20211115151511704
到这里问题已经基本明确了,那么对于原始的,没有添加produces属性的接口,为什么是可以的呢?

我们可以直接定位到ProducesRequesetCondition,直接debug到getMatchingCondition,可以看到它的expression是空的,isEmpty如果发现expression是空的,不会对accept的contentType做校验,后续也就不会抛出HttpMediaTypeNotAcceptableException异常了。

image-20211115152941901

解决办法

针对这种情况,目前最好的解决方法是禁掉根据后缀类型匹配MediaType。

image-20211115154715735
该配置可以通过查看ContentNegotiationManagerFactoryBean这个类中的favorPathExtension属性。

在Spring-webmvc的5.3.5中,该配置是默认关闭的。

image-20211115155606875
但是在4.3.16中,该配置是开启的。

image-20211115160306685
关闭配置的方法

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
super.configureContentNegotiation(configurer);
}
}

问题总结

在本次排查过程中,有几点需要注意。

  1. 不同版本的Spring ContentNegotiation配置存在差异。
  2. 在前面提到的MappingMediaTypeFileExtensionResolverd的lookupMediaType中,存在mediaTypes取值为application/json和application/xml,他们的来源在哪里呢?

这里可以查看WebMvcConfigurationSupport中的getDefaultMediaTypes,可以看到这里会根据一些变量做一些初始化的工作。

image-20211115164726570

​ 而上述变量的取值情况如下,也就是会根据classpath中的类情况做初始化工作

image-20211115164831090

问题延伸

还有一种情况接口返回HTTP 406的情况,这种会出现在使用到了HttpMessageConverter时。

接口会返回对象,如以下case

1
2
3
4
5
6
7
8
java复制代码@RequestMapping(value = "/listPerson")
@ResponseBody
public List<Person> listPerson() {
Person person = new Person();
person.setId(1L);
person.setName("zhangsan");
return Lists.newArrayList(person);
}

具体可以跟进到AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法中。

image-20211115171207241
首先可以看到body中是有正常值的,上图中的逻辑和之前有些类似,218行先根据request获取到acceptedMediaType,最终也会调用this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));

然后再获取produceMediaType即230行代码,然后在237行到241行,进行匹配。如果匹配失败,则会在246行抛出HttpMediaTypeNotAcceptableException。

这里再着重看一下getProducibleMediaTypes方法。

主要就是根据HttpMessageConverter的canWrite方法判断是否可以对返回结果进行write,可以的话,添加getSupportedMediaTypes即可。

image-20211115172003721

image-20211115172048790
这里有很多的HttpMessageConverter,最终会利用MappingJackson2HttpMessageConvertoer增加applicaiton/json和applicaiton*+json。

而它的getSupportedMediaTypes会进入AbstractJackson2HttpMessageConverter中,如果有自定义的objectMapper,那就使用自定义的。

image-20211115172539003
否则的话,调用AbstractHttpMessageConverter的getSupportedMediaTypes方法。

image-20211115172742548
​ 还是需要看一下this.supportedMediaTypes的来源。

​ 可以直接看一下MappingJackson2HttpMessageConverter的初始化函数,终于找到你。

​ image-20211115173309240

​ 所以,如果在这种情况下,客户端的请求accept如果是application/xml,也会返回HTTP 406。

​ image-20211115173449827

​ 最后,如果真要返回application/xml,怎么办呢? 还是需要看一下完成的WebMvcConfigurationSupport类。

​ 这次的方法是addDefaultHttpMessageConverters,添加默认的messageConverter(代码有些长,截取了前半部分)。

​ image-20211115173838337

​ 可以看到xml解析的条件是!shoudIgnoreXml,该值默认是false,那另外一个条件就是jackson2XmlPresent。是的,这个配置在第二个关注点中有描述,即

​ image-20211115174226455
​ 所以需要先添加maven依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.11.4</version>
</dependency>

​ 此时AbstractMessageConverterMethodProcessor的getProducibleMediaTypes终于看到了xml类型啦。

​ image-20211115174641587

再用Postman试一下,大功告成!

image-20211115174912648
最后再啰嗦一句,如果此时Accept为*/*的话,会以xml形式返回,因为它对应的HttpMessageConvertor先被加载到。尽管在AbstractMessageConverterMethodProcessor->writeWithMessageConverters的最后,如果有匹配多个mediaTypesToUse,会利用MediaType.sortBySpecifityAndQuality进行排序。

image-20211115204301638
针对这个方法,主要是两个comparator,

1
java复制代码mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR.thenComparing(MediaType.QUALITY_VALUE_COMPARATOR));

image-20211115210236647
image-20211115205842645
​ 通过debug可以看到,对于application/json和application/xml,他们属于Type一致,并且quality一致,但是子类型不一致的情况,会返回0,即排序认为是相等的,不会交换顺序,也就是以进入list的顺序为准。

本文转载自: 掘金

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

1…332333334…956

开发者博客

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