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

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


  • 首页

  • 归档

  • 搜索

【PyCharm中文教程 06】超全 PyCharm 代码调

发表于 2021-03-04
  1. 调试的过程

调试可以说是每个开发人员都必备一项技能,在日常开发和排查 bug 都非常有用。

调试的过程分为三步:

  1. 第一步:在你想要调试的地方,打上断点
  2. 第二步:使用调试模式来运行这个 python 程序
  3. 第三步:使用各种手段开始代码调试

首先第一步和第二步,我用下面这张图表示

点击上图中的小蜘蛛,开启调试模式后,在 PyCharm 下方会弹出一个选项卡。

这个选项卡的按键非常多,包括

  1. 变量查看窗口
  2. 调试控制窗口
  3. 线程控制窗口
  4. 程序控制窗口

在变量查看窗口,你可以查看当前程序进行到该断点处,所有的普通变量和特殊变量,你每往下执行一行代码,这些变量都有可能跟着改变。

如果你的程序是多线程的,你可以通过线程控制窗口的下拉框来切换线程。

以上两个窗口,都相对比较简单,我一笔带过,下面主要重点讲下调试控制按钮和程序控制按钮。

在调试控制窗口,共有 8 个按钮,他们的作用分别是什么呢?

  1. Show Execution Point:无论你的代码编辑 窗口的光标在何处,只要点下该按钮,都会自动跳转到程序运行的地方。
  2. Step Over:在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止,也就是把子函数整个作为一步。在不存在子函数的情况下是和step into效果一样的。简单的说就是,程序代码越过子函数,但子函数会执行,且不进入。
  3. Step Into:在单步执行时,遇到子函数就进入并且继续单步执行,有的会跳到源代码里面去执行。
  4. Step Into My Code:在单步执行时,遇到子函数就进入并且继续单步执行,不会进入到源码中。
  5. Step Out:假如进入了一个函数体中,你看了两行代码,不想看了,跳出当前函数体内,返回到调用此函数的地方,即使用此功能即可。
  6. Run To Cursor:运行到光标处,省得每次都要打一个断点。
  7. Evaluate Expression:计算表达式,在里面可以自己执行一些代码。

以上七个功能,就是最常用的功能,一般操作步骤就是,设置好断点,debug运行,然后 F8 单步调试,遇到想进入的函数 F7 进去,想出来在 shift + F8,跳过不想看的地方,直接设置下一个断点,然后 F9 过去。

看这张图就行了(下面第6点有误,应该是运行到光标处,而不是下一断点处)

在程序控制窗口,共有 6 个按钮,他们的作用分别又是什么呢?同时看下面这张图就行了。

  1. 调试相关的快捷键

  • ⇧ + F9:调试当前文件
  • ⌥ + ⇧ + F9:弹出菜单,让你选择调试哪一个文件
  • F8:单步执行,不进入函数
  • F7:单步执行,进入函数
  • ⌥ + ⇧ +F7:单步执行,只进入自己写的函数
  • ⇧ + F8:跳出函数体
  • F9:运行到下一断点
  • ⌥ + F9:运行到光标处
  • ⇧ + ⌘ + F8:查看所有设置的断点
  • ⌘ + F8:切换断点(有断点则取消断点,没有则加上断点)
  • ⌥ + F5:重新以调试模式运行
  • ⌥ + F8 计算表达式(可以更改变量值使其生效)

文章最后给大家介绍两个我自己写的在线文档:

第一个文档:PyCharm 中文指南 1.0 文档

整理了 100 个 PyCharm 的使用技巧,为了让新手能够直接上手,我花了很多的时间录制了上百张 GIF 动图,有兴趣的前往在线文档阅读。

第二个文档:PyCharm 黑魔法指南 1.0 文档

系统收录各种 Python 冷门知识,Python Shell 的多样玩法,令人疯狂的 Python 炫技操作,Python 的超详细进阶知识解读,非常实用的 Python 开发技巧等。

本文转载自: 掘金

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

Spring Boot 集成 ActiveMQ (Artem

发表于 2021-03-04

Apache ActiveMQ 是一款基于 Java 的消息服务器,它使用行业标准协议,支持我们把各种语言和平台开发的系统连接在一起。目前 ActiveMQ 分为两个版本:ActiveMQ 5 和 ActiveMQ Artemis (下一代ActiveMQ)。当 ActiveMQ Artemis 达到 ActiveMQ 功能时会变为 ActiveMQ 6 。

19f56a270008116e7e5d1b37c8970a6f.png

Spring JMS 是专门用来处理 Spring 消息的模块。它支持主流的消息中间键,能完美结合 ActiveMQ 。Spring Boot 应用通过集成 Spring JMS 模块整合 ActiveMQ ,本文涉及 ActiveMQ 5 和 ActiveMQ Artemis 两种配置方法,根据项目实际使用 ActiveMQ 的版本,采取一种配置即可。

引入依赖

  • 引入activemq
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
  • 引入activemq artemis (与 activemq 二选一)
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>

配置

在 application.yml 中增加:

  • 配置 activemq
1
2
3
4
5
6
7
8
9
10
11
yaml复制代码spring:
activemq:
broker-url: tcp://${ACTIVEMQ_HOST:localhost}:${ACTIVEMQ:61616} # activemq连接地址
user: ${ACTIVEMQ_USER:admin} # 用户名
password: ${ACTIVEMQ_PASSWORD:admin} # 密码
send-timeout: # 发送超时时间
pool:
enabled: false # 是否创建 JmsPoolConnectionFactory 连接池
idle-timeout: 30s # 空闲连接超时时间
max-connections: 50 # 连接池中最大连接数
max-sessions-per-connection: 100 # 每个连接最大会话
  • 配置 activemq artemis
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码spring:
artemis:
mode: native
host: ${ARTEMIS_HOST:localhost} # artermis连接地址
port: ${ARTEMIS_PORT:9876} # artermis连接端口
user: ${ARTEMIS_USER:admin} # 用户名
password: ${ARTEMIS_PASSWORD:admin} # 密码
send-timeout: # 发送超时时间
pool:
enabled: false # 是否创建 JmsPoolConnectionFactory 连接池
idle-timeout: 30s # 空闲连接超时时间
max-connections: 50 # 连接池中最大连接数
max-sessions-per-connection: 100 # 每个连接最大会话

Spring JMS 中默使用 CachingConnectionFactory 创建连接池,如果指定 JmsPoolConnectionFactory 连接池,则在 spring.jms.* 中配置连接池属性。

1
2
3
yaml复制代码spring:
jms:
session-cache-size: 5

关于 Activemq (Artemis) 和 Apring Jms 更多配置,可参考:Spring Boot Integration Properties

使用

JmsTemplate 是 Spring JMS 中用来发送消息的类。做完以上配置,在 Spring Boot 启动入口加上 @EnableJms 注解,项目在启动时会自动装配,在项目中直接注入 JmsTemplate 对象。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootApplication
@EnableJms
public class Application {

@Autowired
private JmsTemplate jmsTemplate;

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

}

发送文本消息示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void send(String msg) {

log.info("发送消息:{}", msg);

jmsTemplate.send(DEST_NAME, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
// 也可以创建对象 session.createObjectMessage()
TextMessage textMessage = session.createTextMessage();
textMessage.setText(msg);
return textMessage;
}
});
}

接收使用 @JmsListener 注解:

1
2
3
4
5
6
7
8
java复制代码/**
* 监听消息
* @param content
*/
@JmsListener(destination = DEST_NAME, concurrency = "消费线程数")
public void recive(String content) {
log.info("收到消息:{}", content);
}

发送消息和接收消息的 DEST_NAME 要保持一至。 concurrency 属性非必须,用来设置消费消息的程序数。关于 ActiveMQ 的详细介绍会在专门的章节讲解。


除非注明,否则均为”攻城狮·正“原创文章,转载请注明出处。
本文链接:engr-z.com/137.html

本文转载自: 掘金

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

从小厂逆袭快手,我是如何准备面试的

发表于 2021-03-04

我将文中提到的的 MySQL、Redis、Kafka 思维导图放到了我的公众号中,大家可以关注我的公众号【haxianhe】,回复 “思维导图” 领取高清pdf版思维导图。


在上一篇文章 涨薪50%,从小厂逆袭快手 - 附面经 中,我概述性的给出了社招跳槽有哪些环节要去准备,那么今天我会给出面试复习比较推荐的复习资料、如何复习以及有哪些常见的注意事项。

自我介绍

自我介绍是面试的一场面试第一个环节,而一个好的自我介绍是可以引导整场面试的节奏的,下面就简单介绍一下如何准备一份“合适”的自我介绍。

首先,我们要知道面试官想通过自我介绍了解什么信息。

在技术面试中,面试官除了想在你自我介绍的时间看应聘者的简历之外,一般会想了解以下几点信息:年龄,毕业院校,工作年限,工作经历,行业背景,项目经验,技术面等这些基础信息,然后根据自我介绍和简历信息就可以深入的聊一下具体的项目经验,技术问题等。

一般在进行自我介绍的时候概述性的介绍一下自己的学历背景、工作经历、项目经验,以及自己擅长的技术面即可。如果面试官对你介绍的哪部分内容感兴趣,他会具体问你的。

项目经验

这部分是社招面试的重头戏,总的原则是 以“项目经验”容纳“线上问题”,支撑“技术亮点”。

面试提到的技术亮点是需要项目经验来支撑的。

大多数人在日常的工作中,用到的技术是非常有限的,可能就是 CRUD 外带一些调优,这也是普遍现象。反之,如果一个初级开发,在面试中说,之前开发的模块既有jvm调优、又有分布式组件,再外带数据库性能优化,似乎可信度也不高。

对此,你需要用“解决过的线上问题”去支撑想要展开的技术亮点,需要你平时工作中积极主动的去参与线上问题的解决,比如有 oom 问题、redis缓存被击穿,或者其他分布式组件的case,你参与排查并解决,那么将来面试的时候,你自然可以以此为基础去展开你事先准备好的技术亮点。

这样的话,就像上面的那张图一样,你就有足够的支撑物去支撑你的分布式组件以及其他值钱的技能了。

总之,技术本身不值钱,面试官只关心你如何使用技术去解决线上问题的。

项目介绍

在面试时,经过寒暄后,一般面试官会让介绍项目经验,常见的问法是:“说下你最近的(或最拿得出手的)一个项目”。

在面试前准备项目描述,别害怕,因为面试官什么都不知道

面试官是人,不是神,拿到你的简历的时候,是没法核实你的项目细节的(一般公司会到录用后,用背景调查的方式来核实)。

更何况,你做的项目是以月为单位算的,而面试官最多用3分钟来从你的简历上了解你的项目经验,所以你对项目的熟悉程度要远远超过面试官,所以你一点也不用紧张。

如果你的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程。

下面给出了你和面试官的情况对比:

你 面试官
对你以前的项目和技能 很了解 只能听你说,只能根据你说的内容做出判断
在面试过程中的职责 在很短的时间内防守成功即可 如果找不出漏洞,就只能算你以前做过
准备时间 面试前你有充足的时间准备 一般在面试前用3分钟阅读你的简历
沟通过程 你可以出错,但别出关键性的错误 不会太为难你,除非你太差
技巧 你有足够的技巧,也可以从网上找到足够多的面试题 其实就问些通用的有规律的问题

既然面试官无法了解你的底细,那么他们怎么来验证你的项目经验和技术?

下面总结了一些常用的提问方式:

提问方式 目的
让你描述工作经验和项目(极有可能是最近的),看看你说的是否和简历上一致 看你是否真的做过这些项目
看你简历上项目里用到的技术,比如框架、数据库,然后针对这些技术提些基本问题 还是验证你是否做过项目,同时看你是否了解这些技术,为进一步提问做准备
针对某个项目,不断深入地问一些技术上的问题,或者从不同侧面问一些技术实现,看你前后回答里面是否有矛盾 深入核实你的项目细节
针对某技术,问些项目里一定会遇到的问题,比如候选人说做过数据库,那么就会问索引方面的问题 通过这类问题,核实候选人是否真的有过项目经验(或者还仅仅是学习经验)

准备项目的各种细节,一旦被问倒了,就说明你没做过

一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分你说了算,流利些,因为你经过充分准备后,可以知道你要说些什么。

而且这些是你实际的项目经验(不是学习经验,也不是培训经验),那么一旦让面试官感觉你都说不上来,那么可信度就很低了。

不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后继提问权直接交给面试官。

下表列出了一些不好的回答方式:

回答方式 后果
我在XX软件公司做了XX门户网站项目,这个项目做到了XX功能,具体是XX和XX模块,各模块做了XX功能,客户是XX,最后这个项目挣了XX钱 直接打断,因为业务需求我不需要了解,我会直接问他项目里的技术
(需要招聘一个Java后端开发,会Spring MVC)最近一个项目我是用C#(或其他非Java技术)实现的,实现了……或者我最近做的不是开发,而是测试……或者我最近的项目没有用到Spring MVC 提问,你最近用到SSH技术的项目是什么时候,然后在评语上写:最近XX时间没接触过SSH
在毕业设计的时候(或者在读书的时候,在学习的时候,在XX培训学校,在XX实训课程中),…… 直接打断,提问你这个是否是商业项目,如果不是,你有没有其他的商业经验。如果没商业项目经验,除非是校招,否则就直接结束面试
描述项目时,一些关键要素(比如公司、时间、所用技术等)和简历上的不匹配 我们会深究这个不一致的情况,如果是简历造假,那么可能直接中断面试,如果真的是笔误,那么就需要提供合理的解释

在避免上述不好的回答的同时,大家可以按下表所给出的要素准备项目介绍。

要素 样式
控制在1分钟里面,讲出项目基本情况,比如项目名称,背景,给哪个客户做,完成了基本的事情,做了多久,项目规模多大,用到哪些技术,数据库用什么,然后酌情简单说一下模块。重点突出背景,技术,数据库和其他和技术有关的信息。 我在XX公司做了XX外汇保证金交易平台,客户是XX银行,主要完成了挂盘,实盘成交,保证金杠杆成交等功能,数据库是Oracle,前台用到JS等技术,后台用到Java的SSH,几个人做了X个月。不需要详细描述各功能模块,不需要说太多和业务有关但和技术无关的。如果面试官感兴趣,等他问。
要主动说出你做了哪些事情,这部分的描述一定需要和你的技术背景一致。 我做了外汇实盘交易系统,挂单成交系统,XXX模块,做了X个月
描述你在项目里的角色 我主要是做了开发,但在开发前,我在项目经理的带领下参与了业务调研,数据库设计等工作,后期我参与了测试和部署工作。
可以描述用到的技术细节,特别是你用到的技术细节,这部分尤其要注意,你说出口的,一定要知道,因为面试官后面就根据这个问的。你如果做了5个模块,宁可只说你能熟练说上口的2个。 用到了Java里面的集合,JDBC,…等技术,用到了Spring MVC等框架,用技术连接数据库。
这部分你风险自己承担,如果可以,不露声色说出一些热门的要素,比如Linux,大数据,大访问压力等。但一旦你说了,面试官就会直接问细节。 这个系统里,部署在Linux上,每天要处理的数据量是XX,要求是在4小时,1G内存是的情况下处理完5千万条数据。平均访客是每分钟XXX。

线上问题

大家平时工作中一定要 把握住出现“线上问题”的机会。

因为,功能做了只是具备了项目经验,但是面试的时候真正“值钱”的其实是技术的难点与解决方案,而一般技术难点普遍伴随着“线上问题”。所以,平时工作中积极主动点,出现了线上问题不管是不是自己的都去查、去解决,事后围绕着“问题现象、问题分析、问题影响、解决方案、问题扩展”等去总结、记录到自己的笔记总,后续都是自己最宝贵的财富。

技术亮点

结合自己遇到过的线上问题,优先准备分布式组件方面的技术亮点,常用的分布式组件主要有 MySQL、Redis、Kafka等。

这部分内容可以参考JD上对技术要求,有针对性的去准备,也可以参考我下面介绍的关于中间件部分的内容。

编程语言

关于这部分内容,转语言的同学可能更需要关注一下。

首先,需要明确的是你想转到什么语言,那你就需要事先准备好那个语言面试可能要问的内容,因为企业招你进去是干活的而不是再像校招那样,先把你招进去再去培养你。投简历前你是能看到JD的,JD上一般都有关于需要掌握的技术的明确的要求。

关于Java的复习材料,推荐开源项目

  • JavaGuide

中间件

常见的中间件主要有:MySQL、Redis、Kafka,接下来我简单介绍一下我的复习经验。

复习材料

关于中间件的复习材料,推荐开源项目

  • advanced-java

MySQL复习的话,还有两本书给大家推荐一下:

  • 《MySQL 技术内幕:InnoDB 存储引擎》
  • 《高性能 MySQL》

Redis 复习的话,也给大家推荐一本书:

  • 《Redis 设计与实现》

复习方法

一般我会结合上面推荐的书和开源项目去整理一份自己的思维导图(思维导图我在下面放了截图)和笔记。

MySQL


Redis


Kafka


完整的思维导图太长了不好截图,有需要的同学可以 关注我的公众号【haxianhe】,回复 “思维导图” 领取高清pdf版思维导图。

场景题

推荐一个秒杀的开源学习项目

  • miaosha

一篇比较好的关于秒杀的场景设计的文章

  • 秒杀系统设计

算法题

算法的话推荐大家最起码刷一下牛客网上的 《剑指offer》67题

答案的话大家可以在网上搜一下“牛客网《剑指Offer》66题题解”,我本人参考的leetcode上的《画解剑指 Offer》不过这个要开会员,大家可以根据自己的需求进行选择。

之后要是还有时间和精力的话推荐大家再刷一下 牛客霸题,上面可以筛选企业和考察次数等

提问环节

最后,就是提问环节了,这个环节和第一个环节一样是唯二应聘者可以掌握主动权的环节,一般透过这个环节你可以表现自己的面试团队的兴趣,也可以表现出自己的虚心好学都可以。

下面我给出我给自己准备的几个经典问题,给大家用作参考:

1.能和我介绍一下你们的业务嘛?
2.能和我介绍一下你们的技术栈嘛?
3.如果有我有幸能拿到offer,你认为我入职之后最需要注意的点是什么?
4.这个职位在公司的发展前景是怎样的?有什么晋升机制?在什么条件下,可以获得晋升机会?
5.团队成员有多少人?大家怎么分工?目前团队的核心工作是哪些?

  • 当面试官说,“你有什么要问我?”怎样回答最加分?

小结

到这这篇文章大体上把我想要分享的东西都讲清楚了,但是其实这里面还是有很多细节没有展开去讲,比如我总结的 MySQL、Redis、Kafka的学习笔记等等,这部分内容后续我会简单整理一下陆陆续续到我的公众号和个人博客上,可能就不会放到牛客网这种讨论区了,所以有需要的同学可以关注我的公众号【haxianhe】第一时间阅读。


我将文中提到的的 MySQL、Redis、Kafka 思维导图放到了我的公众号中,大家可以关注我的公众号【haxianhe】,回复 “思维导图” 领取高清pdf版思维导图。

本文转载自: 掘金

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

一份简单的调查问卷数据库设计

发表于 2021-03-04

背景

参考示例腾讯问卷


项目涉及到有关调查问卷的功能,参考了一些问卷网站的示例,大概了解了一下,一份简单的调查问卷包含哪些元素,它们之间存在哪些关联关系,由此设计出一份简单的数据库表结构。


一份问卷的基本元素

  • 调查问卷主表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码CREATE TABLE `survey_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`survey_name` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '主题',
`survey_description` varchar(1000) CHARACTER SET utf8 DEFAULT NULL COMMENT '描述',
`start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '开始时间',
`end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '结束时间',
`status` char(1) CHARACTER SET utf8 NOT NULL DEFAULT '0' COMMENT '0 发布 1 暂存 2已结束 3已失效',
`survey_sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`top_flag` char(1) CHARACTER SET utf8 NOT NULL DEFAULT '1' COMMENT '0 置顶 1不置顶',
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator_id` int(11) NOT NULL COMMENT '创建人员ID',
`updator_id` int(11) NOT NULL COMMENT '更新人员ID',
`survey_pic_id` int(11) DEFAULT NULL COMMENT '图片id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8mb4 COMMENT='调查问卷主表';

问卷主表比较简单,相关描述已注释。

  • 问题主表
1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE `question_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`survey_id` int(11) NOT NULL COMMENT '关联调查问卷主表ID',
`question_type` char(1) CHARACTER SET utf8 NOT NULL DEFAULT '1' COMMENT '1 单选 2多选 3填空',
`question_name` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '问题主题',
`question_description` varchar(1000) CHARACTER SET utf8 DEFAULT NULL,
`question_sort` int(11) DEFAULT '0' COMMENT '排序',
`required_flag` char(1) CHARACTER SET utf8 DEFAULT '0' COMMENT ' 0 必填 1非必填',
`question_pic_id` int(11) DEFAULT NULL COMMENT '图片id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=212 DEFAULT CHARSET=utf8mb4 COMMENT='调查问卷问题主表';

问题主表,关联问卷主键ID

  • 选项表
1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `option_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`survey_id` int(11) NOT NULL COMMENT '调查问卷ID',
`question_id` int(11) NOT NULL COMMENT '问题ID',
`option_name` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '选项名称',
`option_sort` int(11) NOT NULL,
`option_pic_id` int(11) DEFAULT NULL COMMENT '图片id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=518 DEFAULT CHARSET=utf8mb4 COMMENT='调查问卷问题选项主表';

问题类型是天空的不需要往选项表里插入内容

到此一份简单的调查问卷基本完成

一份问卷不只是看看

有了问卷之后,我们就需要填写问卷,因此我们还需要一份调查问卷的答案表

  • 答案主表
1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `answer_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '成员id',
`survey_id` int(11) NOT NULL COMMENT '问卷主表ID',
`question_id` int(11) NOT NULL COMMENT '问题主表ID',
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=140 DEFAULT CHARSET=utf8mb4 COMMENT='用户答案表';
  • 答案子表
1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `answer_option_relation` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`answer_id` int(11) NOT NULL COMMENT '答案主表id',
`option_id` int(11) DEFAULT NULL COMMENT '选项主表id',
`option_content` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '答案内容',
`answer_pic_id` int(11) DEFAULT NULL COMMENT '图片id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=207 DEFAULT CHARSET=utf8mb4;

其实两张表可以合并为一张表。问题类型为填空时,答案也不仅限为问题描述,也可能是一张图,这里也可以直接放图片的链接地址,避免再关联去查图片的链接;

统计

作为用户来说,不能像纸质时代那样,一份份自己去统计,因此我们需要给用户一份统计结果展示。个人没有去设计统计表的数据表结构,仅仅是通过关联查询得出的结果来给用户展示。这里提供两个思路:

  • 像我一样关联去查
  • 设计一份调查问卷的结果统计表,你可以写一个job在调查问卷截止的时候去做结果统计然后插入到这张表中。用户查看结果的时候,直接从这张表里取数据即可。

本文转载自: 掘金

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

Redis最佳实践:7个维度+43条使用规范,带你彻底玩转R

发表于 2021-03-04

微信搜索关注「水滴与银弹」公众号,第一时间获取优质技术干货。7年资深后端研发,给你呈现不一样的技术视角。

大家好,我是 Kaito。

这篇文章我想和你聊一聊 Redis 的最佳实践。

你的项目或许已经使用 Redis 很长时间了,但在使用过程中,你可能还会或多或少地遇到以下问题:

  • 我的 Redis 内存为什么增长这么快?
  • 为什么我的 Redis 操作延迟变大了?
  • 如何降低 Redis 故障发生的频率?
  • 日常运维 Redis 需要注意什么?
  • 部署 Redis 时,如何做好资源规划?
  • Redis 监控重点要关注哪些指标?

尤其是当你的项目越来越依赖 Redis 时,这些问题就变得尤为重要。

此时,你迫切需要一份「最佳实践指南」。

这篇文章,我将从以下七个维度,带你「全面」分析 Redis 的最佳实践优化:

  • 内存
  • 性能
  • 高可靠
  • 日常运维
  • 资源规划
  • 监控
  • 安全

在文章的最后,我还会给你一个完整的最佳实践清单,不管你是业务开发人员,还是 DBA 运维人员,这个清单将会帮助你更加「优雅」地用好 Redis。

这篇文章干货很多,希望你可以耐心读完。

如何使用 Redis 更节省内存?

首先,我们来看一下 Redis 内存方面的优化。

众所周知,Redis 的性能之所以如此之高,原因就在于它的数据都存储在「内存」中,所以访问 Redis 中的数据速度极快。

但从资源利用率层面来说,机器的内存资源相比于磁盘,还是比较昂贵的。

当你的业务应用在 Redis 中存储数据很少时,你可能并不太关心内存资源的使用情况。但随着业务的发展,你的业务存储在 Redis 中的数据就会越来越多。

如果没有提前制定好内存优化策略,那么等业务开始增长时,Redis 占用的内存也会开始膨胀。

所以,提前制定合理的内存优化策略,对于资源利用率的提升是很有必要的。

那在使用 Redis 时,怎样做才能更节省内存呢?这里我给你总结了 6 点建议,我们依次来看:

1) 控制 key 的长度

最简单直接的内存优化,就是控制 key 的长度。

在开发业务时,你需要提前预估整个 Redis 中写入 key 的数量,如果 key 数量达到了百万级别,那么,过长的 key 名也会占用过多的内存空间。

所以,你需要保证 key 在简单、清晰的前提下,尽可能把 key 定义得短一些。

例如,原有的 key 为 user:book:123,则可以优化为 u:bk:123。

这样一来,你的 Redis 就可以节省大量的内存,这个方案对内存的优化非常直接和高效。

2) 避免存储 bigkey

除了控制 key 的长度之外,你同样需要关注 value 的大小,如果大量存储 bigkey,也会导致 Redis 内存增长过快。

除此之外,客户端在读写 bigkey 时,还有产生性能问题(下文会具体详述)。

所以,你要避免在 Redis 中存储 bigkey,我给你的建议是:

  • String:大小控制在 10KB 以下
  • List/Hash/Set/ZSet:元素数量控制在 1 万以下

3) 选择合适的数据类型

Redis 提供了丰富的数据类型,这些数据类型在实现上,也对内存使用做了优化。具体来说就是,一种数据类型对应多种数据结构来实现:

例如,String、Set 在存储 int 数据时,会采用整数编码存储。Hash、ZSet 在元素数量比较少时(可配置),会采用压缩列表(ziplist)存储,在存储比较多的数据时,才会转换为哈希表和跳表。

作者这么设计的原因,就是为了进一步节约内存资源。

那么你在存储数据时,就可以利用这些特性来优化 Redis 的内存。这里我给你的建议如下:

  • String、Set:尽可能存储 int 类型数据
  • Hash、ZSet:存储的元素数量控制在转换阈值之下,以压缩列表存储,节约内存

4) 把 Redis 当作缓存使用

Redis 数据存储在内存中,这也意味着其资源是有限的。你在使用 Redis 时,要把它当做缓存来使用,而不是数据库。

所以,你的应用写入到 Redis 中的数据,尽可能地都设置「过期时间」。

业务应用在 Redis 中查不到数据时,再从后端数据库中加载到 Redis 中。

采用这种方案,可以让 Redis 中只保留经常访问的「热数据」,内存利用率也会比较高。

5) 实例设置 maxmemory + 淘汰策略

虽然你的 Redis key 都设置了过期时间,但如果你的业务应用写入量很大,并且过期时间设置得比较久,那么短期间内 Redis 的内存依旧会快速增长。

如果不控制 Redis 的内存上限,也会导致使用过多的内存资源。

对于这种场景,你需要提前预估业务数据量,然后给这个实例设置 maxmemory 控制实例的内存上限,这样可以避免 Redis 的内存持续膨胀。

配置了 maxmemory,此时你还要设置数据淘汰策略,而淘汰策略如何选择,你需要结合你的业务特点来决定:

  • volatile-lru / allkeys-lru:优先保留最近访问过的数据
  • volatile-lfu / allkeys-lfu:优先保留访问次数最频繁的数据(4.0+版本支持)
  • volatile-ttl :优先淘汰即将过期的数据
  • volatile-random / allkeys-random:随机淘汰数据

6) 数据压缩后写入 Redis

以上方案基本涵盖了 Redis 内存优化的各个方面。

如果你还想进一步优化 Redis 内存,你还可以在业务应用中先将数据压缩,再写入到 Redis 中(例如采用 snappy、gzip 等压缩算法)。

当然,压缩存储的数据,客户端在读取时还需要解压缩,在这期间会消耗更多 CPU 资源,你需要根据实际情况进行权衡。

以上就是「节省内存资源」方面的实践优化,是不是都比较简单?

下面我们来看「性能」方面的优化。

如何持续发挥 Redis 的高性能?

当你的系统决定引入 Redis 时,想必看中它最关键的一点就是:性能。

我们知道,一个单机版 Redis 就可以达到 10W QPS,这么高的性能,也意味着如果在使用过程中发生延迟情况,就会与我们的预期不符。

所以,在使用 Redis 时,如何持续发挥它的高性能,避免操作延迟的情况发生,也是我们的关注焦点。

在这方面,我给你总结了 13 条建议:

1) 避免存储 bigkey

存储 bigkey 除了前面讲到的使用过多内存之外,对 Redis 性能也会有很大影响。

由于 Redis 处理请求是单线程的,当你的应用在写入一个 bigkey 时,更多时间将消耗在「内存分配」上,这时操作延迟就会增加。同样地,删除一个 bigkey 在「释放内存」时,也会发生耗时。

而且,当你在读取这个 bigkey 时,也会在「网络数据传输」上花费更多时间,此时后面待执行的请求就会发生排队,Redis 性能下降。

所以,你的业务应用尽量不要存储 bigkey,避免操作延迟发生。

如果你确实有存储 bigkey 的需求,你可以把 bigkey 拆分为多个小 key 存储。

2) 开启 lazy-free 机制

如果你无法避免存储 bigkey,那么我建议你开启 Redis 的 lazy-free 机制。(4.0+版本支持)

当开启这个机制后,Redis 在删除一个 bigkey 时,释放内存的耗时操作,将会放到后台线程中去执行,这样可以在最大程度上,避免对主线程的影响。

3) 不使用复杂度过高的命令

Redis 是单线程模型处理请求,除了操作 bigkey 会导致后面请求发生排队之外,在执行复杂度过高的命令时,也会发生这种情况。

因为执行复杂度过高的命令,会消耗更多的 CPU 资源,主线程中的其它请求只能等待,这时也会发生排队延迟。

所以,你需要避免执行例如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE 等聚合类命令。

对于这种聚合类操作,我建议你把它放到客户端来执行,不要让 Redis 承担太多的计算工作。

4) 执行 O(N) 命令时,关注 N 的大小

规避使用复杂度过高的命令,就可以高枕无忧了么?

答案是否定的。

当你在执行 O(N) 命令时,同样需要注意 N 的大小。

如果一次性查询过多的数据,也会在网络传输过程中耗时过长,操作延迟变大。

所以,对于容器类型(List/Hash/Set/ZSet),在元素数量未知的情况下,一定不要无脑执行 LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。

在查询数据时,你要遵循以下原则:

  1. 先查询数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
  2. 元素数量较少,可一次性查询全量数据
  3. 元素数量非常多,分批查询数据(LRANGE/HASCAN/SSCAN/ZSCAN)

5) 关注 DEL 时间复杂度

你没看错,在删除一个 key 时,如果姿势不对,也有可能影响到 Redis 性能。

删除一个 key,我们通常使用的是 DEL 命令,回想一下,你觉得 DEL 的时间复杂度是多少?

O(1) ?其实不一定。

当你删除的是一个 String 类型 key 时,时间复杂度确实是 O(1)。

但当你要删除的 key 是 List/Hash/Set/ZSet 类型,它的复杂度其实为 O(N),N 代表元素个数。

也就是说,删除一个 key,其元素数量越多,执行 DEL 也就越慢!

原因在于,删除大量元素时,需要依次回收每个元素的内存,元素越多,花费的时间也就越久!

而且,这个过程默认是在主线程中执行的,这势必会阻塞主线程,产生性能问题。

那删除这种元素比较多的 key,如何处理呢?

我给你的建议是,分批删除:

  • List类型:执行多次 LPOP/RPOP,直到所有元素都删除完成
  • Hash/Set/ZSet类型:先执行 HSCAN/SSCAN/SCAN 查询元素,再执行 HDEL/SREM/ZREM 依次删除每个元素

没想到吧?一个小小的删除操作,稍微不小心,也有可能引发性能问题,你在操作时需要格外注意。

6) 批量命令代替单个命令

当你需要一次性操作多个 key 时,你应该使用批量命令来处理。

批量操作相比于多次单个操作的优势在于,可以显著减少客户端、服务端的来回网络 IO 次数。

所以我给你的建议是:

  • String / Hash 使用 MGET/MSET 替代 GET/SET,HMGET/HMSET 替代 HGET/HSET
  • 其它数据类型使用 Pipeline,打包一次性发送多个命令到服务端执行

7) 避免集中过期 key

Redis 清理过期 key 是采用定时 + 懒惰的方式来做的,而且这个过程都是在主线程中执行。

如果你的业务存在大量 key 集中过期的情况,那么 Redis 在清理过期 key 时,也会有阻塞主线程的风险。

想要避免这种情况发生,你可以在设置过期时间时,增加一个随机时间,把这些 key 的过期时间打散,从而降低集中过期对主线程的影响。

8) 使用长连接操作 Redis,合理配置连接池

你的业务应该使用长连接操作 Redis,避免短连接。

当使用短连接操作 Redis 时,每次都需要经过 TCP 三次握手、四次挥手,这个过程也会增加操作耗时。

同时,你的客户端应该使用连接池的方式访问 Redis,并设置合理的参数,长时间不操作 Redis 时,需及时释放连接资源。

9) 只使用 db0

尽管 Redis 提供了 16 个 db,但我只建议你使用 db0。

为什么呢?我总结了以下 3 点原因:

  1. 在一个连接上操作多个 db 数据时,每次都需要先执行 SELECT,这会给 Redis 带来额外的压力
  2. 使用多个 db 的目的是,按不同业务线存储数据,那为何不拆分多个实例存储呢?拆分多个实例部署,多个业务线不会互相影响,还能提高 Redis 的访问性能
  3. Redis Cluster 只支持 db0,如果后期你想要迁移到 Redis Cluster,迁移成本高

10) 使用读写分离 + 分片集群

如果你的业务读请求量很大,那么可以采用部署多个从库的方式,实现读写分离,让 Redis 的从库分担读压力,进而提升性能。

如果你的业务写请求量很大,单个 Redis 实例已无法支撑这么大的写流量,那么此时你需要使用分片集群,分担写压力。

11) 不开启 AOF 或 AOF 配置为每秒刷盘

如果对于丢失数据不敏感的业务,我建议你不开启 AOF,避免 AOF 写磁盘拖慢 Redis 的性能。

如果确实需要开启 AOF,那么我建议你配置为 appendfsync everysec,把数据持久化的刷盘操作,放到后台线程中去执行,尽量降低 Redis 写磁盘对性能的影响。

12) 使用物理机部署 Redis

Redis 在做数据持久化时,采用创建子进程的方式进行。

而创建子进程会调用操作系统的 fork 系统调用,这个系统调用的执行耗时,与系统环境有关。

虚拟机环境执行 fork 的耗时,要比物理机慢得多,所以你的 Redis 应该尽可能部署在物理机上。

13) 关闭操作系统内存大页机制

Linux 操作系统提供了内存大页机制,其特点在于,每次应用程序向操作系统申请内存时,申请单位由之前的 4KB 变为了 2MB。

这会导致什么问题呢?

当 Redis 在做数据持久化时,会先 fork 一个子进程,此时主进程和子进程共享相同的内存地址空间。

当主进程需要修改现有数据时,会采用写时复制(Copy On Write)的方式进行操作,在这个过程中,需要重新申请内存。

如果申请内存单位变为了 2MB,那么势必会增加内存申请的耗时,如果此时主进程有大量写操作,需要修改原有的数据,那么在此期间,操作延迟就会变大。

所以,为了避免出现这种问题,你需要在操作系统上关闭内存大页机制。

好了,以上这些就是 Redis 「高性能」方面的实践优化。如果你非常关心 Redis 的性能问题,可以结合这些方面针对性优化。

我们再来看 Redis 「可靠性」如何保证。

如何保证 Redis 的可靠性?

这里我想提醒你的是,保证 Redis 可靠性其实并不难,但难的是如何做到「持续稳定」。

下面我会从「资源隔离」、「多副本」、「故障恢复」这三大维度,带你分析保障 Redis 可靠性的最佳实践。

1) 按业务线部署实例

提升可靠性的第一步,就是「资源隔离」。

你最好按不同的业务线来部署 Redis 实例,这样当其中一个实例发生故障时,不会影响到其它业务。

这种资源隔离的方案,实施成本是最低的,但成效却是非常大的。

2) 部署主从集群

如果你只使用单机版 Redis,那么就会存在机器宕机服务不可用的风险。

所以,你需要部署「多副本」实例,即主从集群,这样当主库宕机后,依旧有从库可以使用,避免了数据丢失的风险,也降低了服务不可用的时间。

在部署主从集群时,你还需要注意,主从库需要分布在不同机器上,避免交叉部署。

这么做的原因在于,通常情况下,Redis 的主库会承担所有的读写流量,所以我们一定要优先保证主库的稳定性,即使从库机器异常,也不要对主库造成影响。

而且,有时我们需要对 Redis 做日常维护,例如数据定时备份等操作,这时你就可以只在从库上进行,这只会消耗从库机器的资源,也避免了对主库的影响。

3) 合理配置主从复制参数

在部署主从集群时,如果参数配置不合理,也有可能导致主从复制发生问题:

  • 主从复制中断
  • 从库发起全量复制,主库性能受到影响

在这方面我给你的建议有以下 2 点:

  1. 设置合理的 repl-backlog 参数:过小的 repl-backlog 在写流量比较大的场景下,主从复制中断会引发全量复制数据的风险
  2. 设置合理的 slave client-output-buffer-limit:当从库复制发生问题时,过小的 buffer 会导致从库缓冲区溢出,从而导致复制中断

4) 部署哨兵集群,实现故障自动切换

只部署了主从节点,但故障发生时是无法自动切换的,所以,你还需要部署哨兵集群,实现故障的「自动切换」。

而且,多个哨兵节点需要分布在不同机器上,实例为奇数个,防止哨兵选举失败,影响切换时间。

以上这些就是保障 Redis「高可靠」实践优化,你应该也发现了,这些都是部署和运维层的优化。

除此之外,你可能还会对 Redis 做一些「日常运维」工作,这时你要注意哪些问题呢?

日常运维 Redis 需要注意什么?

1) 禁止使用 KEYS/FLUSHALL/FLUSHDB 命令

执行这些命令,会长时间阻塞 Redis 主线程,危害极大,所以你必须禁止使用它。

如果确实想使用这些命令,我给你的建议是:

  • SCAN 替换 KEYS
  • 4.0+版本可使用 FLUSHALL/FLUSHDB ASYNC,清空数据的操作放在后台线程执行

2) 扫描线上实例时,设置休眠时间

不管你是使用 SCAN 扫描线上实例,还是对实例做 bigkey 统计分析,我建议你在扫描时一定记得设置休眠时间。

防止在扫描过程中,实例 OPS 过高对 Redis 产生性能抖动。

3) 慎用 MONITOR 命令

有时在排查 Redis 问题时,你会使用 MONITOR 查看 Redis 正在执行的命令。

但如果你的 Redis OPS 比较高,那么在执行 MONITOR 会导致 Redis 输出缓冲区的内存持续增长,这会严重消耗 Redis 的内存资源,甚至会导致实例内存超过 maxmemory,引发数据淘汰,这种情况你需要格外注意。

所以你在执行 MONITOR 命令时,一定要谨慎,尽量少用。

4) 从库必须设置为 slave-read-only

你的从库必须设置为 slave-read-only 状态,避免从库写入数据,导致主从数据不一致。

除此之外,从库如果是非 read-only 状态,如果你使用的是 4.0 以下的 Redis,它存在这样的 Bug:

从库写入了有过期时间的数据,不会做定时清理和释放内存。

这会造成从库的内存泄露!这个问题直到 4.0 版本才修复,你在配置从库时需要格外注意。

5) 合理配置 timeout 和 tcp-keepalive 参数

如果因为网络原因,导致你的大量客户端连接与 Redis 意外中断,恰好你的 Redis 配置的 maxclients 参数比较小,此时有可能导致客户端无法与服务端建立新的连接(服务端认为超过了 maxclients)。

造成这个问题原因在于,客户端与服务端每建立一个连接,Redis 都会给这个客户端分配了一个 client fd。

当客户端与服务端网络发生问题时,服务端并不会立即释放这个 client fd。

什么时候释放呢?

Redis 内部有一个定时任务,会定时检测所有 client 的空闲时间是否超过配置的 timeout 值。

如果 Redis 没有开启 tcp-keepalive 的话,服务端直到配置的 timeout 时间后,才会清理释放这个 client fd。

在没有清理之前,如果还有大量新连接进来,就有可能导致 Redis 服务端内部持有的 client fd 超过了 maxclients,这时新连接就会被拒绝。

针对这种情况,我给你的优化建议是:

  1. 不要配置过高的 timeout:让服务端尽快把无效的 client fd 清理掉
  2. Redis 开启 tcp-keepalive:这样服务端会定时给客户端发送 TCP 心跳包,检测连接连通性,当网络异常时,可以尽快清理僵尸 client fd

6) 调整 maxmemory 时,注意主从库的调整顺序

Redis 5.0 以下版本存在这样一个问题:从库内存如果超过了 maxmemory,也会触发数据淘汰。

在某些场景下,从库是可能优先主库达到 maxmemory 的(例如在从库执行 MONITOR 命令,输出缓冲区占用大量内存),那么此时从库开始淘汰数据,主从库就会产生不一致。

要想避免此问题,在调整 maxmemory 时,一定要注意主从库的修改顺序:

  • 调大 maxmemory:先修改从库,再修改主库
  • 调小 maxmemory:先修改主库,再修改从库

直到 Redis 5.0,Redis 才增加了一个配置 replica-ignore-maxmemory,默认从库超过 maxmemory 不会淘汰数据,才解决了此问题。

好了,以上这些就是「日常运维」Redis 需要注意的,你可以对各个配置项查漏补缺,看有哪些是需要优化的。

接下来,我们来看一下,保障 Redis「安全」都需要注意哪些问题。

Redis 安全如何保证?

无论如何,在互联网时代,安全问题一定是我们需要随时警戒的。

你可能听说过 Redis 被注入可执行脚本,然后拿到机器 root 权限的安全问题,都是因为在部署 Redis 时,没有把安全风险注意起来。

针对这方面,我给你的建议是:

  1. 不要把 Redis 部署在公网可访问的服务器上
  2. 部署时不使用默认端口 6379
  3. 以普通用户启动 Redis 进程,禁止 root 用户启动
  4. 限制 Redis 配置文件的目录访问权限
  5. 推荐开启密码认证
  6. 禁用/重命名危险命令(KEYS/FLUSHALL/FLUSHDB/CONFIG/EVAL)

只要你把这些做到位,基本上就可以保证 Redis 的安全风险在可控范围内。

至此,我们分析了 Redis 在内存、性能、可靠性、日常运维方面的最佳实践优化。

除了以上这些,你还需要做到提前「预防」。

如何预防 Redis 问题?

要想提前预防 Redis 问题,你需要做好以下两个方面:

  1. 合理的资源规划
  2. 完善的监控预警

先来说资源规划。

在部署 Redis 时,如果你可以提前做好资源规划,可以避免很多因为资源不足产生的问题。这方面我给你的建议有以下 3 点:

  1. 保证机器有足够的 CPU、内存、带宽、磁盘资源
  2. 提前做好容量规划,主库机器预留一半内存资源,防止主从机器网络故障,引发大面积全量同步,导致主库机器内存不足的问题
  3. 单个实例内存建议控制在 10G 以下,大实例在主从全量同步、RDB 备份时有阻塞风险

再来看监控如何做。

监控预警是提高稳定性的重要环节,完善的监控预警,可以把问题提前暴露出来,这样我们才可以快速反应,把问题最小化。

这方面我给你的建议是:

  1. 做好机器 CPU、内存、带宽、磁盘监控,资源不足时及时报警,任意资源不足都会影响 Redis 性能
  2. 设置合理的 slowlog 阈值,并对其进行监控,slowlog 过多及时报警
  3. 监控组件采集 Redis INFO 信息时,采用长连接,避免频繁的短连接
  4. 做好实例运行时监控,重点关注 expired_keys、evicted_keys、latest_fork_usec 指标,这些指标短时突增可能会有阻塞风险

总结

好了,总结一下,这篇文章我带你全面分析了 Redis 最佳实践的优化路径,其中包括内存资源、高性能、高可靠、日常运维、资源规划、监控、安全 7 个维度。

这里我画成了思维导图,方便你在实践时做参考。

我还把这些实践优化,按照「业务开发」和「运维」2 个维度,进一步做了划分,并且以「强制」、「推荐」、「参考」3 个级别做了标注,这样你在实践优化时,就会更明确哪些该做,哪些需要结合实际的业务场景进一步分析。

这些级别的实施规则如下:

  • 强制:需严格遵守,否则危害极大
  • 推荐:推荐遵守,可提升性能、降低内存、便于运维
  • 参考:根据业务特点参考实施

如果你是业务开发人员,你需要了解 Redis 的运行机制,例如各个命令的执行时间复杂度、数据过期策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。

如果你是 DBA 运维人员,你需要做到未雨绸缪,在资源规划、运维、监控、安全层面做到位。

后记

如果你能耐心地读到这里,应该对如何「用好」Redis 有了新的认识。

这篇文章我们主要讲的是 Redis 最佳实践,对于「最佳实践」这个话题,我想再和你多聊几句。

如果你面对的不是 Redis,而是其它中间件,例如 MySQL、Kafka,你在使用这些组件时,会有什么优化思路吗?

你也可以沿用这篇文章的这几个维度来分析:

  • 性能
  • 可靠性
  • 资源
  • 运维
  • 监控
  • 安全

你可以思考一下,MySQL 和 Kafka 在这几个维度,需要注意哪些问题。

另外,从学习技能的角度来讲,我们在软件开发过程中,要尽可能地去思考和探索「最佳实践」的方式。

因为只有这样,我们才会不断督促自己去思考,对自己提出更高的要求,做到持续进步。

qr_search.png

想看更多硬核技术文章?欢迎关注我的公众号「水滴与银弹」。

我是 Kaito,是一个对于技术有思考的资深后端程序员,在我的文章中,我不仅会告诉你一个技术点是什么,还会告诉你为什么这么做?我还会尝试把这些思考过程,提炼成通用的方法论,让你可以应用在其它领域中,做到举一反三。

本文转载自: 掘金

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

Spring Boot 2x基础教程:使用MongoDB

发表于 2021-03-04

前段时间因为团队调整,大部分时间放在了团队上,这系列的更新又耽误了一下。但既然承诺持久更新,那就不会落下,今天开始继续更新这部分的内容!

过了年,重申一下这个系列的目标:目前主要任务就是把Spring Boot 1.x部分没有升级的内容做完升级。我会将因为版本升级而产生的变化做一些说明,这样不论低版本的读者还是高版本的读者都能找到自己想要的部分。这也是这次做2.x版本升级的重要原因,尽量避免或减少有读者用着高版本参考我这边低版本的实现而出现问题,然后开始问候我家人的情况。

在完成上述所有的更新之后,接下来很重要的更新内容将会集中在关于Spring Boot的一些进阶内容,比如:要做什么扩展的时候,该从哪里着手等。

如果是您是Spring Boot的使用者,那么一定要关注一下!后面的内容会越来越精彩!

下面回归今天的主题,如何在Spring Boot中使用MongoDB!

MongoDB简介

MongoDB是一个基于分布式文件存储的数据库,它是一个介于关系数据库和非关系数据库之间的产品,其主要目标是在键/值存储方式(提供了高性能和高度伸缩性)和传统的RDBMS系统(具有丰富的功能)之间架起一座桥梁,它集两者的优势于一身。

MongoDB支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型,也因为他的存储格式也使得它所存储的数据在Nodejs程序应用中使用非常流畅。

既然称为NoSQL数据库,Mongo的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

但是,MongoDB也不是万能的,同MySQL等关系型数据库相比,它们在针对不同的数据类型和事务要求上都存在自己独特的优势。在数据存储的选择中,坚持多样化原则,选择更好更经济的方式,而不是自上而下的统一化。

较常见的,我们可以直接用MongoDB来存储键值对类型的数据,如:验证码、Session等;由于MongoDB的横向扩展能力,也可以用来存储数据规模会在未来变的非常巨大的数据,如:日志、评论等;由于MongoDB存储数据的弱类型,也可以用来存储一些多变json数据,如:与外系统交互时经常变化的JSON报文。而对于一些对数据有复杂的高事务性要求的操作,如:账户交易等就不适合使用MongoDB来存储。

MongoDB官网:www.mongodb.org/

动手试试

第一步:引入依赖

Spring Boot中可以通过在pom.xml中加入spring-boot-starter-data-mongodb引入对mongodb的访问支持依赖。它的实现依赖spring-data-mongodb。是的,您没有看错,又是spring-data的子项目,之前介绍过spring-data-jpa、spring-data-redis,对于mongodb的访问,spring-data也提供了强大的支持,下面就开始动手试试吧。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

第二步:创建用户实体User

1
2
3
4
5
6
7
8
9
10
java复制代码@Data
public class User {

@Id
private Long id;

private String username;
private Integer age;

}

第三步:实现用户实体User的数据访问对象UserRepository

1
2
3
4
5
java复制代码public interface UserRepository extends MongoRepository<User, Long> {

User findByUsername(String username);

}

在Spring Data的抽象下,是不是同其他Spring Data子项目一样的简洁、好用、易学!

第四步:编写单元测试

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复制代码@SpringBootTest(classes = Chapter61Application.class)
public class ApplicationTests {

@Autowired
private UserRepository userRepository;

@Test
public void test() throws Exception {
userRepository.deleteAll();

// 创建三个User,并验证User总数
userRepository.save(new User(1L, "didi", 30));
userRepository.save(new User(2L, "mama", 40));
userRepository.save(new User(3L, "kaka", 50));
Assertions.assertEquals(3, userRepository.findAll().size());

// 删除一个User,再验证User总数
User u = userRepository.findById(1L).get();
userRepository.delete(u);
Assertions.assertEquals(2, userRepository.findAll().size());

// 删除一个User,再验证User总数
u = userRepository.findByUsername("mama");
userRepository.delete(u);
Assertions.assertEquals(1, userRepository.findAll().size());
}

}

这里注意所使用的Assertions是Spring Boot 2.4之后整合的版本,之前的版本还是使用Assert

第五步:参数配置

通过上面的例子,我们可以轻而易举的对MongoDB进行访问,但是实战中,应用服务器与MongoDB通常不会部署于同一台设备之上,这样就无法使用自动化的本地配置来进行使用。这个时候,我们也可以方便的配置来完成支持,只需要在application.properties中加入mongodb服务端的相关配置,具体示例如下:

1
bash复制代码spring.data.mongodb.uri=mongodb://name:pass@localhost:27017/test

在尝试此配置时,记得在mongo中对test库创建具备读写权限的用户(用户名为name,密码为pass),不同版本的用户创建语句不同,注意查看文档做好准备工作

若使用mongodb 2.x,也可以通过如下参数配置,该方式不支持mongodb 3.x。

1
ini复制代码spring.data.mongodb.host=localhost spring.data.mongodb.port=27017

题外话

MongoDB虽然在过去的一段时间,收到不少的关注,但由于其在各方面都表现中庸,最近似乎越来越少听到或者看到关于MongoDB的大规模应用场景。就笔者所接触的很多以往的使用场景也都开始在使用ES来取代,以获得更好的性能表现。

所以,下次我们就来讲讲Spring Boot中如何使用ES吧!关注我,持续获得更多Spring Boot的技术干货!

更多本系列免费教程连载「点击进入汇总目录」

代码示例

本文的相关例子可以查看下面仓库中的chapter6-1目录:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

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

Java真的要没落了? Q:为什么Go的web框架速度还不如

发表于 2021-03-04

最近也收到很多后端同学的提问,为什么Go的web框架速度还不如Java?为什么许多原本的 Java 项目都试图用 go 进行重写开源?Java会不会因为容器的兴起而没落?Java这个20多年的后端常青树难道真的要走下坡路了?橙子邀请了淘系技术部的同学对以上问题进行解答,也欢迎大家一起交流。

Q:为什么Go的web框架速度还不如Java?

**风弈:**华山论剑,让我们索性把各框架的性能分析跑一下再说话。

各种框架的应用场景不同导致其优化侧重点不同,下面我们展开详细分析。

http server 概述

首先描述一下一个简单的 web server 的请求处理过程:

Net 层读取数据包后经过 HTTP Decoder 解析协议,再由 Route 找到对应的 Handler 回调,处理业务逻辑后设置相应 Response 的状态码等,然后由 HTTP Encoder 编码相应的 Response,最后由 Net 写出数据。

而 Net 之下的一层由内核控制,虽然也有很多优化策略,但这里主要比较 web 框架本身,那么暂时不考虑 Net 之下的优化。

看了下 techempower 提供的压测框架源码,各类框架基本上都是基于 epoll 的处理,那么各类框架的性能差距主要体现在上述这些模块的性能了。

关于各类压测的简述

我们再看 techempower 的各项性能排名,有JSON serialization, Single query, Multiple queries, Cached queries, Fortunes, Data updates 和 Plaintext 这几大类的排名。

其中 JSON serialization 是对固定的 Json 结构编码并返回 (message: hello word), Single query 是单次 DB 查询,Multiple queries 是多次 DB 查询,Cached queries 是从内存数据库中获取多个对象值并以json返回,Fortunes 是页面渲染后返回,Data updates 是对 DB 的写入,Plaintext 是最简单的返回固定字符串。

这里的 json 编码,DB 操作,页面渲染和固定字符串返回就是相应的业务逻辑,当业务逻辑越重(耗时越大)时,则相应的业务逻辑逐渐就成为了瓶颈,例如 DB 操作其实主要是在测试相应 DB 库和 DB 本身处理逻辑的性能,而框架本身的基础功能消耗随着业务逻辑的繁重将越来越忽略不计(Round 19 中物理机下 Plaintext 下的 QPS 在七百万级,而 Data updates 在万级别,相差百倍以上),所以这边主要分析 Json serialization 和 Plaintext两种相对能比较体现出框架本身 http 性能的排名。

在 Round 19 Json serialization 中 Java 性能最高的框架是 firenio-http-lite (QPS: 1,587,639),而 Go 最高的是 fasthttp-easyjson-prefork(QPS: 1,336,333),按照这里面的数据是Java性能高。

从 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外, json (相当于 Business logic) 占了 4.5%,fasthttp 自身(HTTP Decoder, HTTP Encoder, Router)占了 15%,仅看 Json serialization 似乎会有一种 Java 比 Go 性能高的感觉。

那我们继续把业务逻辑简化,看一下 Plaintext 的排名,Plaintext 模式其实是在使用 HTTP pipeline 模式下压测的,在 Round 19 中 Java 和 Go 已经几乎一样的 QPS 了,在 Round 19 之后的一次测试中 gnet 已经排在所有语言的第二,但是前几个框架QPS其实差别很微小。

这时候其实主要瓶颈都在 net 层,而 go 官方的 net 库包含了处理 goroutine 相关的逻辑,像 gonet 之类的直接操作 epoll 的会少一些这方面的消耗,Java 的 nio 也是直接操作的 epoll 。

拿了 gnet 的测试源码跑了下压测,看到 pprof 如下,其实这里 gnet 还有更进一步的性能优化空间:time.Time.AppendFormat 占用 30% CPU。

可以使用如下提前 Format ,允许减少获取当前时间精度的情况下大幅减少这部分的消耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码var timetick atomic.Value

func NowTimeFormat() []byte {
return timetick.Load().([]byte)
}

func tickloop() {
timetick.Store(nowFormat())
for range time.Tick(time.Second) {
timetick.Store(nowFormat())
}
}

func nowFormat() []byte {
return []byte(time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
}

func init() {
timetick.Store(nowFormat())
go tickloop()
}

这样优化后接下来的瓶颈在于 runtime 的内存分配,是由于这个压测代码中还存在下面的部分没有复用内存:


其实 gnet 本身的消耗已经做到非常小了,而 c++ 的 ulib 也是类似这样使用的非常简单的 HTTP 编解码操作来压测。

分析

对于这里面测试的框架,影响因素主要如下:

1、直接基于epoll的简单http: 没有完整的 http decoder 和 route (如gnet, ulib 直接简单的字节拼接,固定的路由 handler回调)

2、zero copy 和内存复用: 内部处理字节的 0 拷贝(go 官方 http 库为了减少开发者的出错概率,没有使用 zero copy,否则开发者可能在无意中引用了已经放回 buff 池内的的数据造成没有意识到的并发问题等等),而内存复用,大部分框架或多或少都已经做了。

3、prefork:注意到 go 框架中有使用了 prefork 进程的方式(比如 fasthttp-prefork),这是 fork 出多个子进程,共享同一个 listen fd,且每个进程使用单核但并发(1 个 P)处理的逻辑可以避免 go runtime 内部的锁竞争和 goroutine 调度的消耗(但是 go runtime 中为了并发和 goroutine 调度而存在的相关“无用”代码的消耗还是会有一些)

4、语言本身的性能差异

对于第一点,其实简化了各种编解码和路由之后,虽然提高了性能,但是往往会降低框架的易用性,对于一般的业务而言,不会出现如此高的QPS,同时选择框架的时候往往还需要考虑易用性和可扩展性等,同时还需要考虑到公司内部原有中间件或者 SDK 所使用的框架集成复杂度。

对于第二点,如果是作为一个网络代理而言,没有业务方的开发,往往可以使用真正的完全 zero copy,但是作为业务开发框架提供出去的话是需要考虑一定的业务出错概率,往往牺牲一部分性能是划算的。

第三点 prefork , java netty 等是直接对于线程操作,可以更加定制化的优化性能,而 go 的 goroutine 需要的是一个通用协程,目的是降低编写并发程序的难度,在这个层次上难免性能比不上一个优化的非常出色的 Java 基于线程操作的框架;但是直接操作线程的话需要合理控制好线程数,这是个比较头疼的调优问题(特别是对于新手来说),而 goroutine 则可以不关心池子的大小,使得代码更加优雅和简洁,这对于工程质量保障其实是一个提升。另外这里存在 prefork 是由于 go 没法直接操作线程,而 fasthttp 提供了 prefork 的能力,使用多进程方式来对标 Java 的多线程来进一步提高性能。

第四点,语言本身来说 Java 还是更加的成熟,包括 JVM 的 Jit 能力也使得在热代码中和 Go 编译型语言的差异不大,何况 Go 本身的编译器还不是特别成熟,比如逃逸分析等方面的问题, Go 本身的内存模型和 GC 的成熟度也比不上 Java。还有很重要的一点,Go 的框架成熟度和 Java 也不在一个级别,但相信这些都会随着时间逐步成熟。

总之,对于这个框架压测数据意义在于了解性能天花板,判断继续优化的空间和ROI (投入产出比)。具体选择框架还是要根据使用场景,性能,易用性,可扩展性,稳定性以及公司内部的生态等作出选择,语言和性能分别只是其中一个因素。

各种框架的应用场景不同导致其优化侧重点不同,如 spring web 为了易用性,可扩展性和稳定性而牺牲了性能,但它同样拥有庞大的社区和用户。再比如 Service Mesh Sidecar 场景下 Go 的天然并发编程上的优势,以及小内存占用,快速启动,编译型语言等特点使得比 Java 更加适合。

(附:其实我使用上述代码和 dockerfile 构建,并且使用同样的压测脚本,在阿里云4核独享机器测试下 go fasthttp-easyjson-prefork 框架 Json serialization 的性能要高于 Java wizzardo-http 和 firenio-http-lite 30% 以上且延迟更低的,这可能和内核有关)。

Q:为什么许多原本的 Java 项目都试图用 go 进行重写开源?

空蒙: Java还是go核心是生态问题。

生态发展会经历起步、发展、繁荣、停滞、消亡几个阶段,Java目前至少还在繁荣阶段,go还是发展阶段,不同阶段在开发人员的数量与质量、开源能力丰富性、工程配套上是有巨大差异的,go是在狂补这三块。另外不同公司还有个公司内部小生态的所处阶段问题,也会影响技术的选型判断。

现阶段go的火热,很大因素是云原生裹挟着大家往前,k8s operator go语言实现的自带光环,各种中间件能力在下沉与k8s融合,带动着一波基础中间件能力的go实现潮头,但基础的中间件能力相对是有限集合,如RPC、config、messagequeue等,这些中间件能力,以及云原生k8s对上层业务而言应该做的是开发语言的中立性,让业务基于公司的小生态和整个语言技术的大生态去抉择,如果硬逼着业务也用go语言开发那就是耍流氓了。

总结来说,基础中间件能力需要与k8s的融合需要会有go语言的动力,但整个开源生态其他能力并不见得是必须;业务开发依据公司生态和技术大生态选择最合适的开发语言,不要盲目的追从而导致在人、开源能力、工程配套上的尴尬。go语言能否在业务研发上发力,还有待其生态的进一步发展。

Q:Java会不会因为容器的兴起而没落?

玄力: 近年来以容器为核心的云原生技术,让服务端部署的伸缩性、可协作性,得到巨大的提升。使得原本开发语言本身选取的重要性,有一定程度的减弱。但不妨碍Java语言本身继续保持活力。

毕竟,作为研发而言,研发输出效率也是蛮关键的一个考量点,得益于Java完善而有庞大的开发者生态,提供了比大多数语言都要丰富的类库/框架,也得益于Java强大的IDE工具,开发起来往往事半功倍。

而且,Java自身也有一些变种语言(如Scala),也是在朝更灵活更好用的方向发展;

另一方面,在大数据领域,Java仍在大放异彩,我们所熟知的 ES、Kafka、Spark、Hadoop。

我们评估和预测一个技术的生命力的时候,往往不会孤立地只看技术本身,同时也会结合它背后的整个生态。一个具有顽强生命力的技术的背后往往都有一个成熟的生态体系支撑,上面也提到Java在多个领域都有完善而庞大的生态,因此,我们认为Java的生命力仍然是顽强的。

但由于众所周知的原因,客观来讲,Java本身在使用上,也会有一定的限制性。并且,在容器场景中,Java进程的内存配置,是需要小心谨慎的。

总的来说,Java的地位仍难撼动,而且在云原生场景中,也仍绽放着生命力。

本文转载自: 掘金

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

Mybatis系列全解(八):Mybatis的9大动态SQL

发表于 2021-03-04

封面:洛小汐

作者:潘潘

2021年,仰望天空,脚踏实地。

这算是春节后首篇 Mybatis 文了~

跨了个年感觉写了有半个世纪 …

借着女神节 ヾ(◍°∇°◍)ノ゙

提前祝男神女神们越靓越富越嗨森!

上图保存可做朋友圈封面图 ~

前言

本节我们介绍 Mybatis 的强大特性之一:动态 SQL ,从动态 SQL 的诞生背景与基础概念,到动态 SQL 的标签成员及基本用法,我们徐徐道来,再结合框架源码,剖析动态 SQL (标签)的底层原理,最终在文末吐槽一下:在无动态 SQL 特性(标签)之前,我们会常常掉进哪些可恶的坑吧~

建议关注我们! Mybatis 全解系列一直在更新哦

Mybaits系列全解

  • Mybatis系列全解(一):手写一套持久层框架
  • Mybatis系列全解(二):Mybatis简介与环境搭建
  • Mybatis系列全解(三):Mybatis简单CRUD使用介绍
  • Mybatis系列全解(四):全网最全!Mybatis配置文件XML全貌详解
  • Mybatis系列全解(五):全网最全!详解Mybatis的Mapper映射文件
  • Mybatis系列全解(六):Mybatis最硬核的API你知道几个?
  • Mybatis系列全解(七):Dao层的两种实现之传统与代理
  • Mybatis系列全解(八):Mybatis的动态SQL
  • Mybatis系列全解(九):Mybatis的复杂映射
  • Mybatis系列全解(十):Mybatis注解开发
  • Mybatis系列全解(十一):Mybatis缓存全解
  • Mybatis系列全解(十二):Mybatis插件开发
  • Mybatis系列全解(十三):Mybatis代码生成器
  • Mybatis系列全解(十四):Spring集成Mybatis
  • Mybatis系列全解(十五):SpringBoot集成Mybatis
  • Mybatis系列全解(十六):Mybatis源码剖析

本文目录

1、什么是动态SQL

2、动态SQL的诞生记

3、动态SQL标签的9大标签

4、动态SQL的底层原理

1、什么是动态SQL ?

关于动态 SQL ,允许我们理解为 “ 动态的 SQL ”,其中 “ 动态的 ” 是形容词,“ SQL ” 是名词,那显然我们需要先理解名词,毕竟形容词仅仅代表它的某种形态或者某种状态。

SQL 的全称是:

Structured Query Language,结构化查询语言。

SQL 本身好说,我们小学时候都学习过了,无非就是 CRUD 嘛,而且我们还知道它是一种 语言,语言是一种存在于对象之间用于交流表达的 能力,例如跟中国人交流用汉语、跟英国人交流用英语、跟火星人交流用火星语、跟小猫交流用喵喵语、跟计算机交流我们用机器语言、跟数据库管理系统(DBMS)交流我们用 SQL。

想必大家立马就能明白,想要与某个对象交流,必须拥有与此对象交流的语言能力才行!所以无论是技术人员、还是应用程序系统、或是某个高级语言环境,想要访问/操作数据库,都必须具备 SQL 这项能力;因此你能看到像 Java ,像 Python ,像 Go 等等这些高级语言环境中,都会嵌入(支持) SQL 能力,达到与数据库交互的目的。

很显然,能够学习 Mybatis 这么一门高精尖(ru-men)持久层框架的编程人群,对于 SQL 的编写能力肯定已经掌握得 ss 的,平时各种 SQL 编写那都是信手拈来的事, 只不过对于 动态SQL 到底是个什么东西,似乎还有一些朋友似懂非懂!但是没关系,我们百度一下。

动态 SQL:一般指根据用户输入或外部条件 动态组合 的 SQL 语句块。

很容易理解,随外部条件动态组合的 SQL 语句块!我们先针对动态 SQL 这个词来剖析,世间万物,有动态那就相对应的有静态,那么他们的边界在哪里呢?又该怎么区分呢?

其实,上面我们已经介绍过,在例如 Java 高级语言中,都会嵌入(支持)SQL 能力,一般我们可以直接在代码或配置文件中编写 SQL 语句,如果一个 SQL 语句在 “编译阶段” 就已经能确定 主体结构,那我们称之为静态 SQL,如果一个 SQL 语句在编译阶段无法确定主体结构,需要等到程序真正 “运行时” 才能最终确定,那么我们称之为动态 SQL,举个例子:

1
2
3
4
5
6
xml复制代码<!-- 1、定义SQL -->
<mapper namespace="dao">
<select id="selectAll" resultType="user">
select * from t_user
</select>
</mapper>
1
2
java复制代码// 2、执行SQL
sqlSession.select("dao.selectAll");

很明显,以上这个 SQL ,在编译阶段我们都已经知道它的主体结构,即查询 t_user 表的所有记录,而无需等到程序运行时才确定这个主体结构,因此以上属于 静态 SQL。那我们再看看下面这个语句:

1
2
3
4
5
6
7
8
9
xml复制代码<!-- 1、定义SQL -->
<mapper namespace="dao">
<select id="selectAll" parameterType="user">
select * from t_user
<if test="id != null">
where id = #{id}
</if>
</select>
</mapper>
1
2
3
4
5
6
7
java复制代码// 2、执行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1); // 有 id

User user2 = new User();
sqlSession.select("dao.selectAll",user2); // 无 id

认真观察,以上这个 SQL 语句,额外添加了一块 if 标签 作为条件判断,所以应用程序在编译阶段是无法确定 SQL 语句最终主体结构的,只有在运行时根据应用程序是否传入 id 这个条件,来动态的拼接最终执行的 SQL 语句,因此属于动态 SQL 。

另外,还有一种常见的情况,大家看看下面这个 SQL 语句算是动态 SQL 语句吗?

1
2
3
4
5
6
xml复制代码<!-- 1、定义SQL -->
<mapper namespace="dao">
<select id="selectAll" parameterType="user">
select * from t_user where id = #{id}
</select>
</mapper>
1
2
3
4
java复制代码// 2、执行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1); // 有 id

根据动态 SQL 的定义,大家是否能判断以上的语句块是否属于动态 SQL?

答案:不属于动态 SQL !

原因很简单,这个 SQL 在编译阶段就已经明确主体结构了,虽然外部动态的传入一个 id ,可能是1,可能是2,可能是100,但是因为它的主体结构已经确定,这个语句就是查询一个指定 id 的用户记录,它最终执行的 SQL 语句不会有任何动态的变化,所以顶多算是一个支持动态传参的静态 SQL 。

至此,我们对于动态 SQL 和静态 SQL 的区别已经有了一个基础认知,但是有些好奇的朋友又会思考另一个问题:动态 SQL 是 Mybatis 独有的吗?

2、动态SQL的诞生记

我们都知道,SQL 是一种伟大的数据库语言 标准,在数据库管理系统纷争的时代,它的出现统一规范了数据库操作语言,而此时,市面上的数据库管理软件百花齐放,我最早使用的 SQL Server 数据库,当时用的数据库管理工具是 SQL Server Management Studio,后来接触 Oracle 数据库,用了 PL/SQL Developer,再后来直至今日就几乎都在用 MySQL 数据库(这个跟各种云厂商崛起有关),所以基本使用 Navicat 作为数据库管理工具,当然如今市面上还有许多许多,数据库管理工具嘛,只要能便捷高效的管理我们的数据库,那就是好工具,duck 不必纠结选择哪一款!

那这么多好工具,都提供什么功能呢?相信我们平时接触最多的就是接收执行 SQL 语句的输入界面(也称为查询编辑器),这个输入界面几乎支持所有 SQL 语法,例如我们编写一条语句查询 id 等于15 的用户数据记录:

1
sql复制代码select * from user where id = 15 ;

我们来看一下这个查询结果:

很显然,在这个输入界面内输入的任何 SQL 语句,对于数据库管理工具来说,都是 动态 SQL!因为工具本身并不可能提前知道用户会输入什么 SQL 语句,只有当用户执行之后,工具才接收到用户实际输入的 SQL 语句,才能最终确定 SQL 语句的主体结构,当然!即使我们不通过可视化的数据库管理工具,也可以用数据库本身自带支持的命令行工具来执行 SQL 语句。但无论用户使用哪类工具,输入的语句都会被工具认为是 动态 SQL!

这么一说,动态 SQL 原来不是 Mybatis 独有的特性!其实除了以上介绍的数据库管理工具以外,在纯 JDBC 时代,我们就经常通过字符串来动态的拼接 SQL 语句,这也是在高级语言环境(例如 Java 语言编程环境)中早期常用的动态 SQL 构建方式!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 外部条件id
Integer id = Integer.valueOf(15);

// 动态拼接SQL
StringBuilder sql = new StringBuilder();
sql.append(" select * ");
sql.append(" from user ");

// 根据外部条件id动态拼接SQL
if ( null != id ){
sql.append(" where id = " + id);
}

// 执行语句
connection.prepareStatement(sql);

只不过,这种构建动态 SQL 的方式,存在很大的安全问题和异常风险(我们第5点会详细介绍),所以不建议使用,后来 Mybatis 入世之后,在对待动态 SQL 这件事上,就格外上心,它默默发誓,一定要为使用 Mybatis 框架的用户提供一套棒棒的方案(标签)来灵活构建动态 SQL!

于是乎,Mybatis 借助 OGNL 的表达式的伟大设计,可算在动态 SQL 构建方面提供了各类功能强大的辅助标签,我们简单列举一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等,我随手翻了翻我电脑里头曾经保存的学习笔记,我们一起在第3节中温故知新,详细的讲一讲吧~

另外,需要纠正一点,就是我们平日里在 Mybatis 框架中常说的动态 SQL ,其实特指的也就是 Mybatis 框架中的这一套动态 SQL 标签,或者说是这一 特性,而并不是在说动态 SQL 本身。

3、动态SQL标签的9大标签

很好,可算进入我们动态 SQL 标签的主题,根据前面的铺垫,其实我们都能发现,很多时候静态 SQL 语句并不能满足我们复杂的业务场景需求,所以我们需要有适当灵活的一套方式或者能力,来便捷高效的构建动态 SQL 语句,去匹配我们动态变化的业务需求。举个栗子,在下面此类多条件的场景需求之下,动态 SQL 语句就显得尤为重要(先登场 if 标签)。

当然,很多朋友会说这类需求,不能用 SQL 来查,得用搜索引擎,确实如此。但是呢,在我们的实际业务需求当中,还是存在很多没有引入搜索引擎系统,或者有些根本无需引入搜索引擎的应用程序或功能,它们也会涉及到多选项多条件或者多结果的业务需求,那此时也就确实需要使用动态 SQL 标签来灵活构建执行语句。

那么, Mybatis 目前都提供了哪些棒棒的动态 SQL 标签呢 ?我们先引出一个类叫做 XMLScriptBuilder ,大家先简单理解它是负责解析我们的动态 SQL 标签的这么一个构建器,在第4点底层原理中我们再详细介绍。

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复制代码// XML脚本标签构建器
public class XMLScriptBuilder{

// 标签节点处理器池
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

// 构造器
public XMLScriptBuilder() {
initNodeHandlerMap();
//... 其它初始化不赘述也不重要
}

// 初始化
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
}

其实源码中很清晰得体现,一共有 9 大动态 SQL 标签!Mybatis 在初始化解析配置文件的时候,会实例化这么一个标签节点的构造器,那么它本身就会提前把所有 Mybatis 支持的动态 SQL 标签对象对应的处理器给进行一个实例化,然后放到一个 Map 池子里头,而这些处理器,都是该类 XMLScriptBuilder 的一个匿名内部类,而匿名内部类的功能也很简单,就是解析处理对应类型的标签节点,在后续应用程序使用动态标签的时候,Mybatis 随时到 Map 池子中匹配对应的标签节点处理器,然后进解析即可。下面我们分别对这 9 大动态 SQL 标签进行介绍,排(gen)名(ju)不(wo)分(de)先(xi)后(hao):


Top1、if 标签

常用度:★★★★★

实用性:★★★★☆

if 标签,绝对算得上是一个伟大的标签,任何不支持流程控制(或语句控制)的应用程序,都是耍流氓,几乎都不具备现实意义,实际的应用场景和流程必然存在条件的控制与流转,而 if 标签在 单条件分支判断 应用场景中就起到了舍我其谁的作用,语法很简单,如果满足,则执行,不满足,则忽略/跳过。

  • if 标签 : 内嵌于 select / delete / update / insert 标签,如果满足 test 属性的条件,则执行代码块
  • test 属性 :作为 if 标签的属性,用于条件判断,使用 OGNL 表达式。

举个例子:

1
2
3
4
5
6
7
8
9
xml复制代码<select id="findUser">
select * from User where 1=1
<if test=" age != null ">
and age > #{age}
</if>
<if test=" name != null ">
and name like concat(#{name},'%')
</if>
</select>

很明显,if 标签元素常用于包含 where 子句的条件拼接,它相当于 Java 中的 if 语句,和 test 属性搭配使用,通过判断参数值来决定是否使用某个查询条件,也可用于 Update 语句中判断是否更新某个字段,或用于 Insert 语句中判断是否插入某个字段的值。

每一个 if 标签在进行单条件判断时,需要把判断条件设置在 test 属性中,这是一个常见的应用场景,我们常用的用户查询系统功能中,在前端一般提供很多可选的查询项,支持性别筛选、年龄区间筛查、姓名模糊匹配等,那么我们程序中接收用户输入之后,Mybatis 的动态 SQL 节省我们很多工作,允许我们在代码层面不进行参数逻辑处理和 SQL 拼接,而是把参数传入到 SQL 中进行条件判断动态处理,我们只需要把精力集中在 XML 的维护上,既灵活也方便维护,可读性还强。

有些心细的朋友可能就发现一个问题,为什么 where 语句会添加一个 1=1 呢?其实我们是为了方便拼接后面符合条件的 if 标签语句块,否则没有 1=1 的话我们拼接的 SQL 就会变成 select * from user where and age > 0 , 显然这不是我们期望的结果,当然也不符合 SQL 的语法,数据库也不可能执行成功,所以我们投机取巧添加了 1=1 这个语句,但是始终觉得多余且没必要,Mybatis 也考虑到了,所以等会我们讲 where 标签,它是如何完美解决这个问题的。

注意:if 标签作为单条件分支判断,只能控制与非此即彼的流程,例如以上的例子,如果年龄 age 和姓名 name 都不存在,那么系统会把所有结果都查询出来,但有些时候,我们希望系统更加灵活,能有更多的流程分支,例如像我们 Java 当中的 if else 或 switch case default,不仅仅只有一个条件分支,所以接下来我们介绍 choose 标签,它就能满足多分支判断的应用场景。


Top2、choose 标签、when 标签、otherwise 标签

常用度:★★★★☆

实用性:★★★★☆

有些时候,我们并不希望条件控制是非此即彼的,而是希望能提供多个条件并从中选择一个,所以贴心的 Mybatis 提供了 choose 标签元素,类似我们 Java 当中的 if else 或 switch case default,choose 标签必须搭配 when 标签和 otherwise 标签使用,验证条件依然是使用 test 属性进行验证。

  • choose 标签:顶层的多分支标签,单独使用无意义
  • when 标签:内嵌于 choose 标签之中,当满足某个 when 条件时,执行对应的代码块,并终止跳出 choose 标签,choose 中必须至少存在一个 when 标签,否则无意义
  • otherwise 标签:内嵌于 choose 标签之中,当不满足所有 when 条件时,则执行 otherwise 代码块,choose 中 至多 存在一个 otherwise 标签,可以不存在该标签
  • test 属性 :作为 when 与 otherwise 标签的属性,作为条件判断,使用 OGNL 表达式

依据下面的例子,当应用程序输入年龄 age 或者姓名 name 时,会执行对应的 when 标签内的代码块,如果 when 标签的年龄 age 和姓名 name 都不满足,则会拼接 otherwise 标签内的代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<select id="findUser">
select * from User where 1=1
<choose>
<when test=" age != null ">
and age > #{age}
</when>
<when test=" name != null ">
and name like concat(#{name},'%')
</when>
<otherwise>
and sex = '男'
</otherwise>
</choose>
</select>

很明显,choose 标签作为多分支条件判断,提供了更多灵活的流程控制,同时 otherwise 的出现也为程序流程控制兜底,有时能够避免部分系统风险、过滤部分条件、避免当程序没有匹配到条件时,把整个数据库资源全部查询或更新。

至于为何 choose 标签这么棒棒,而常用度还是比 if 标签少了一颗星呢?原因也简单,因为 choose 标签的很多使用场景可以直接用 if 标签代替。另外据我统计,if 标签在实际业务应用当中,也要多于 choose 标签,大家也可以具体核查自己的应用程序中动态 SQL 标签的占比情况,统计分析一下。


Top3、foreach 标签

常用度:★★★☆☆

实用性:★★★★☆

有些场景,可能需要查询 id 在 1 ~ 100 的用户记录

有些场景,可能需要批量插入 100 条用户记录

有些场景,可能需要更新 500 个用户的姓名

有些场景,可能需要你删除 10 条用户记录

请问大家:

很多增删改查场景,操作对象都是集合/列表

如果是你来设计支持 Mybatis 的这一类集合/列表遍历场景,你会提供什么能力的标签来辅助构建你的 SQL 语句从而去满足此类业务场景呢?

额(⊙o⊙)…

那如果一定要用 Mybatis 框架呢?

没错,确实 Mybatis 提供了 foreach 标签来处理这几类需要遍历集合的场景,foreach 标签作为一个循环语句,他能够很好的支持数组、Map、或实现了 Iterable 接口(List、Set)等,尤其是在构建 in 条件语句的时候,我们常规的用法都是 id in (1,2,3,4,5 … 100) ,理论上我们可以在程序代码中拼接字符串然后通过 ${ ids } 方式来传值获取,但是这种方式不能防止 SQL 注入风险,同时也特别容易拼接错误,所以我们此时就需要使用 #{} + foreach 标签来配合使用,以满足我们实际的业务需求。譬如我们传入一个 List 列表查询 id 在 1 ~ 100 的用户记录:

1
2
3
4
5
6
7
8
xml复制代码<select id="findAll">
select * from user where ids in
<foreach collection="list"
item="item" index="index"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

最终拼接完整的语句就变成:

1
2
sql复制代码
select * from user where ids in (1,2,3,...,100);

当然你也可以这样编写:

1
2
3
4
5
6
7
8
xml复制代码<select id="findAll">
select * from user where
<foreach collection="list"
item="item" index="index"
open=" " separator=" or " close=" ">
id = #{item}
</foreach>
</select>

最终拼接完整的语句就变成:

1
2
sql复制代码
select * from user where id =1 or id =2 or id =3 ... or id = 100;

在数据量大的情况下这个性能会比较尴尬,这里仅仅做一个用法的举例。所以经过上面的举栗,相信大家也基本能猜出 foreach 标签元素的基本用法:

  • foreach 标签:顶层的遍历标签,单独使用无意义
  • collection 属性:必填,Map 或者数组或者列表的属性名(不同类型的值获取下面会讲解)
  • item 属性:变量名,值为遍历的每一个值(可以是对象或基础类型),如果是对象那么依旧是 OGNL 表达式取值即可,例如 #{item.id} 、#{ user.name } 等
  • index 属性:索引的属性名,在遍历列表或数组时为当前索引值,当迭代的对象时 Map 类型时,该值为 Map 的键值(key)
  • open 属性:循环内容开头拼接的字符串,可以是空字符串
  • close 属性:循环内容结尾拼接的字符串,可以是空字符串
  • separator 属性:每次循环的分隔符

第一,当传入的参数为 List 对象时,系统会默认添加一个 key 为 ‘list’ 的值,把列表内容放到这个 key 为 list 的集合当中,在 foreach 标签中可以直接通过 collection=”list” 获取到 List 对象,无论你传入时使用 kkk 或者 aaa ,都无所谓,系统都会默认添加一个 key 为 list 的值,并且 item 指定遍历的对象值,index 指定遍历索引值。

1
2
3
4
5
6
7
java复制代码// java 代码
List kkk = new ArrayList();
kkk.add(1);
kkk.add(2);
...
kkk.add(100);
sqlSession.selectList("findAll",kkk);
1
2
3
4
5
6
7
8
9
xml复制代码<!-- xml 配置 -->
<select id="findAll">
select * from user where ids in
<foreach collection="list"
item="item" index="index"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

第二,当传入的参数为数组时,系统会默认添加一个 key 为 ‘array’ 的值,把列表内容放到这个 key 为 array 的集合当中,在 foreach 标签中可以直接通过 collection=”array” 获取到数组对象,无论你传入时使用 ids 或者 aaa ,都无所谓,系统都会默认添加一个 key 为 array 的值,并且 item 指定遍历的对象值,index 指定遍历索引值。

1
2
3
4
5
6
java复制代码// java 代码
String [] ids = new String[3];
ids[0] = "1";
ids[1] = "2";
ids[2] = "3";
sqlSession.selectList("findAll",ids);
1
2
3
4
5
6
7
8
9
xml复制代码<!-- xml 配置 -->
<select id="findAll">
select * from user where ids in
<foreach collection="array"
item="item" index="index"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

第三,当传入的参数为 Map 对象时,系统并 不会 默认添加一个 key 值,需要手工传入,例如传入 key 值为 map2 的集合对象,在 foreach 标签中可以直接通过 collection=”map2” 获取到 Map 对象,并且 item 代表每次迭代的的 value 值,index 代表每次迭代的 key 值。其中 item 和 index 的值名词可以随意定义,例如 item = “value111”,index =”key111”。

1
2
3
4
5
6
7
8
9
java复制代码// java 代码
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3);

Map map1 = new HashMap<>();
map1.put("map2",map2);
sqlSession.selectList("findAll",map1);

挺闹心,map1 套着 map2,才能在 foreach 的 collection 属性中获取到。

1
2
3
4
5
6
7
8
9
xml复制代码<!-- xml 配置 -->
<select id="findAll">
select * from user where
<foreach collection="map2"
item="value111" index="key111"
open=" " separator=" or " close=" ">
id = #{value111}
</foreach>
</select>

可能你会觉得 Map 受到不公平对待,为何 map 不能像 List 或者 Array 一样,在框架默认设置一个 ‘map’ 的 key 值呢?但其实不是不公平,而是我们在 Mybatis 框架中,所有传入的任何参数都会供上下文使用,于是参数会被统一放到一个内置参数池子里面,这个内置参数池子的数据结构是一个 map 集合,而这个 map 集合可以通过使用 “_parameter” 来获取,所有 key 都会存储在 _parameter 集合中,因此:

  • 当你传入的参数是一个 list 类型时,那么这个参数池子需要有一个 key 值,以供上下文获取这个 list 类型的对象,所以默认设置了一个 ‘list’ 字符串作为 key 值,获取时通过使用 _parameter.list 来获取,一般使用 list 即可。
  • 同样的,当你传入的参数是一个 array 数组时,那么这个参数池子也会默认设置了一个 ‘array’ 字符串作为 key 值,以供上下文获取这个 array 数组的对象值,获取时通过使用 _parameter.array 来获取,一般使用 array 即可。
  • 但是!当你传入的参数是一个 map 集合类型时,那么这个参数池就没必要为你添加默认 key 值了,因为 map 集合类型本身就会有很多 key 值,例如你想获取 map 参数的某个 key 值,你可以直接使用 _parameter.name 或者 _parameter.age 即可,就没必要还用 _parameter.map.name 或者 _parameter.map.age ,所以这就是 map 参数类型无需再构建一个 ‘map’ 字符串作为 key 的原因,对象类型也是如此,例如你传入一个 User 对象。

因此,如果是 Map 集合,你可以这么使用:

1
2
3
4
5
6
java复制代码// java 代码
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3);
sqlSession.selectList("findAll",map2);

直接使用 collection=”_parameter”,你会发现神奇的 key 和 value 都能通过 _parameter 遍历在 index 与 item 之中。

1
2
3
4
5
6
7
8
9
xml复制代码<!-- xml 配置 -->
<select id="findAll">
select * from user where
<foreach collection="_parameter"
item="value111" index="key111"
open=" " separator=" or " close=" ">
id = #{value111}
</foreach>
</select>

延伸:当传入参数为多个对象时,例如传入 User 和 Room 等,那么通过内置参数获取对象可以使用 _parameter.get(0).username,或者 _parameter.get(1).roomname 。假如你传入的参数是一个简单数据类型,例如传入 int =1 或者 String = ‘你好’,那么都可以直接使用 _parameter 代替获取值即可,这就是很多人会在动态 SQL 中直接使用 # { _parameter } 来获取简单数据类型的值。

那到这里,我们基本把 foreach 基本用法介绍完成,不过以上只是针对查询的使用场景,对于删除、更新、插入的用法,也是大同小异,我们简单说一下,如果你希望批量插入 100 条用户记录:

1
2
3
4
5
6
7
8
xml复制代码<insert id="insertUser" parameterType="java.util.List">
insert into user(id,username) values
<foreach collection="list"
item="user" index="index"
separator="," close=";" >
(#{user.id},#{user.username})
</foreach>
</insert>

如果你希望更新 500 个用户的姓名:

1
2
3
4
5
6
7
8
9
10
xml复制代码<update id="updateUser" parameterType="java.util.List">
update user
set username = '潘潘'
where id in
<foreach collection="list"
item="user" index="index"
separator="," open="(" close=")" >
#{user.id}
</foreach>
</update>

如果你希望你删除 10 条用户记录:

1
2
3
4
5
6
7
8
9
xml复制代码<delete id="deleteUser" parameterType="java.util.List">
delete from user
where id in
<foreach collection="list"
item="user" index="index"
separator="," open="(" close=")" >
#{user.id}
</foreach>
</delete>

更多玩法,期待你自己去挖掘!

注意:使用 foreach 标签时,需要对传入的 collection 参数(List/Map/Set等)进行为空性判断,否则动态 SQL 会出现语法异常,例如你的查询语句可能是 select * from user where ids in () ,导致以上语法异常就是传入参数为空,解决方案可以用 if 标签或 choose 标签进行为空性判断处理,或者直接在 Java 代码中进行逻辑处理即可,例如判断为空则不执行 SQL 。


Top4、where 标签、set 标签

常用度:★★☆☆☆

实用性:★★★★☆

我们把 where 标签和 set 标签放置一起讲解,一是这两个标签在实际应用开发中常用度确实不分伯仲,二是这两个标签出自一家,都继承了 trim 标签,放置一起方便我们比对追根。(其中底层原理会在第4部分详细讲解)

之前我们介绍 if 标签的时候,相信大家都已经看到,我们在 where 子句后面拼接了 1=1 的条件语句块,目的是为了保证后续条件能够正确拼接,以前在程序代码中使用字符串拼接 SQL 条件语句常常如此使用,但是确实此种方式不够体面,也显得我们不高级。

1
2
3
4
5
6
7
8
9
xml复制代码<select id="findUser">
select * from User where 1=1
<if test=" age != null ">
and age > #{age}
</if>
<if test=" name != null ">
and name like concat(#{name},'%')
</if>
</select>

以上是我们使用 1=1 的写法,那 where 标签诞生之后,是怎么巧妙处理后续的条件语句的呢?

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<select id="findUser">
select * from User
<where>
<if test=" age != null ">
and age > #{age}
</if>
<if test=" name != null ">
and name like concat(#{name},'%')
</if>
</where>
</select>

我们只需把 where 关键词以及 1=1 改为 < where > 标签即可,另外还有一个特殊的处理能力,就是 where 标签能够智能的去除(忽略)首个满足条件语句的前缀,例如以上条件如果 age 和 name 都满足,那么 age 前缀 and 会被智能去除掉,无论你是使用 and 运算符或是 or 运算符,Mybatis 框架都会帮你智能处理。

用法特别简单,我们用官术总结一下:

  • where 标签:顶层的遍历标签,需要配合 if 标签使用,单独使用无意义,并且只会在子元素(如 if 标签)返回任何内容的情况下才插入 WHERE 子句。另外,若子句的开头为 “AND” 或 “OR”,where 标签也会将它替换去除。

了解了基本用法之后,我们再看看刚刚我们的例子中:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<select id="findUser">
select * from User
<where>
<if test=" age != null ">
and age > #{age}
</if>
<if test=" name != null ">
and name like concat(#{name},'%')
</if>
</where>
</select>

如果 age 传入有效值 10 ,满足 age != null 的条件之后,那么就会返回 where 标签并去除首个子句运算符 and,最终的 SQL 语句会变成:

1
2
sql复制代码select * from User where age > 10; 
-- and 巧妙的不见了

值得注意的是,where 标签 只会 智能的去除(忽略)首个满足条件语句的前缀,所以就建议我们在使用 where 标签的时候,每个语句都最好写上 and 前缀或者 or 前缀,否则像以下写法就很有可能出大事:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<select id="findUser">
select * from User
<where>
<if test=" age != null ">
age > #{age}
<!-- age 前缀没有运算符-->
</if>
<if test=" name != null ">
name like concat(#{name},'%')
<!-- name 前缀也没有运算符-->
</if>
</where>
</select>

当 age 传入 10,name 传入 ‘潘潘’ 时,最终的 SQL 语句是:

1
2
3
4
5
6
sql复制代码select * from User 
where
age > 10
name like concat('潘%')
-- 所有条件都没有and或or运算符
-- 这让age和name显得很尴尬~

由于 name 前缀没有写 and 或 or 连接符,而 where 标签又不会智能的去除(忽略)非首个 满足条件语句的前缀,所以当 age 条件语句与 name 条件语句同时成立时,就会导致语法错误,这个需要谨慎使用,格外注意!原则上每个条件子句都建议在句首添加运算符 and 或 or ,首个条件语句可添加可不加。

另外还有一个值得注意的点,我们使用 XML 方式配置 SQL 时,如果在 where 标签之后添加了注释,那么当有子元素满足条件时,除了 < !– –> 注释会被 where 忽略解析以外,其它注释例如 // 或 /**/ 或 – 等都会被 where 当成首个子句元素处理,导致后续真正的首个 AND 子句元素或 OR 子句元素没能被成功替换掉前缀,从而引起语法错误!

基于 where 标签元素的讲解,有助于我们快速理解 set 标签元素,毕竟它俩是如此相像。我们回忆一下以往我们的更新 SQL 语句:

1
2
3
4
5
6
7
xml复制代码<update id="updateUser">
update user
set age = #{age},
username = #{username},
password = #{password}
where id =#{id}
</update>

以上语句是我们日常用于更新指定 id 对象的 age 字段、 username 字段以及 password 字段,但是很多时候,我们可能只希望更新对象的某些字段,而不是每次都更新对象的所有字段,这就使得我们在语句结构的构建上显得惨白无力。于是有了 set 标签元素。

用法与 where 标签元素相似:

  • set 标签:顶层的遍历标签,需要配合 if 标签使用,单独使用无意义,并且只会在子元素(如 if 标签)返回任何内容的情况下才插入 set 子句。另外,若子句的 开头或结尾 都存在逗号 “,” 则 set 标签都会将它替换去除。

根据此用法我们可以把以上的例子改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<update id="updateUser">
update user
<set>
<if test="age !=null">
age = #{age},
</if>
<if test="username !=null">
username = #{username},
</if>
<if test="password !=null">
password = #{password},
</if>
</set>
where id =#{id}
</update>

很简单易懂,set 标签会智能拼接更新字段,以上例子如果传入 age =10 和 username = ‘潘潘’ ,则有两个字段满足更新条件,于是 set 标签会智能拼接 “ age = 10 ,” 和 “username = ‘潘潘’ ,” 。其中由于后一个 username 属于最后一个子句,所以末尾逗号会被智能去除,最终的 SQL 语句是:

1
sql复制代码update user set age = 10,username =  '潘潘'

另外需要注意,set 标签下需要保证至少有一个条件满足,否则依然会产生语法错误,例如在无子句条件满足的场景下,最终的 SQL 语句会是这样:

1
sql复制代码update user ;  ( oh~ no!)

既不会添加 set 标签,也没有子句更新字段,于是语法出现了错误,所以类似这类情况,一般需要在应用程序中进行逻辑处理,判断是否存在至少一个参数,否则不执行更新 SQL 。所以原则上要求 set 标签下至少存在一个条件满足,同时每个条件子句都建议在句末添加逗号 ,最后一个条件语句可加可不加。或者 每个条件子句都在句首添加逗号 ,第一个条件语句可加可不加,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<update id="updateUser">
update user
<set>
<if test="age !=null">
,age = #{age}
</if>
<if test="username !=null">
,username = #{username}
</if>
<if test="password !=null">
,password = #{password}
</if>
</set>
where id =#{id}
</update>

与 where 标签相同,我们使用 XML 方式配置 SQL 时,如果在 set 标签子句末尾添加了注释,那么当有子元素满足条件时,除了 < !– –> 注释会被 set 忽略解析以外,其它注释例如 // 或 /**/ 或 – 等都会被 set 标签当成末尾子句元素处理,导致后续真正的末尾子句元素的逗号没能被成功替换掉后缀,从而引起语法错误!

到此,我们的 where 标签元素与 set 标签就基本介绍完成,它俩确实极为相似,区别仅在于:

  • where 标签插入前缀 where
  • set 标签插入前缀 set
  • where 标签仅智能替换前缀 AND 或 OR
  • set 标签可以只能替换前缀逗号,或后缀逗号,

而这两者的前后缀去除策略,都源自于 trim 标签的设计,我们一起看看到底 trim 标签是有多灵活!


Top5、trim 标签

常用度:★☆☆☆☆

实用性:★☆☆☆☆

上面我们介绍了 where 标签与 set 标签,它俩的共同点无非就是前置关键词 where 或 set 的插入,以及前后缀符号(例如 AND | OR | ,)的智能去除。基于 where 标签和 set 标签本身都继承了 trim 标签,所以 trim 标签的大致实现我们也能猜出个一二三。

其实 where 标签和 set 标签都只是 trim 标签的某种实现方案,trim 标签底层是通过 TrimSqlNode 类来实现的,它有几个关键属性:

  • prefix :前缀,当 trim 元素内存在内容时,会给内容插入指定前缀
  • suffix :后缀,当 trim 元素内存在内容时,会给内容插入指定后缀
  • prefixesToOverride :前缀去除,支持多个,当 trim 元素内存在内容时,会把内容中匹配的前缀字符串去除。
  • suffixesToOverride :后缀去除,支持多个,当 trim 元素内存在内容时,会把内容中匹配的后缀字符串去除。

所以 where 标签如果通过 trim 标签实现的话可以这么编写:(

1
2
3
4
5
6
7
8
xml复制代码<!--
注意在使用 trim 标签实现 where 标签能力时
必须在 AND 和 OR 之后添加空格
避免匹配到 android、order 等单词
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
...
</trim>

而 set 标签如果通过 trim 标签实现的话可以这么编写:

1
2
3
4
5
6
7
8
9
xml复制代码<trim prefix="SET" prefixOverrides="," >
...
</trim>

或者

<trim prefix="SET" suffixesToOverride="," >
...
</trim>

所以可见 trim 是足够灵活的,不过由于 where 标签和 set 标签这两种 trim 标签变种方案已经足以满足我们实际开发需求,所以直接使用 trim 标签的场景实际上不太很多(其实是我自己使用的不多,基本没用过)。

注意,set 标签之所以能够支持去除前缀逗号或者后缀逗号,是由于其在构造 trim 标签的时候进行了前缀后缀的去除设置,而 where 标签在构造 trim 标签的时候就仅仅设置了前缀去除。

set 标签元素之构造时:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// Set 标签
public class SetSqlNode extends TrimSqlNode {

private static final List<String> COMMA = Collections.singletonList(",");

// 明显使用了前缀后缀去除,注意前后缀参数都传入了 COMMA
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", COMMA, null, COMMA);
}

}

where 标签元素之构造时:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// Where 标签
public class WhereSqlNode extends TrimSqlNode {

// 其实包含了很多种场景
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

// 明显只使用了前缀去除,注意前缀传入 prefixList,后缀传入 null
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}

}

Top6、bind 标签

常用度:☆☆☆☆☆

实用性:★☆☆☆☆

简单来说,这个标签就是可以创建一个变量,并绑定到上下文,即供上下文使用,就是这样,我把官网的例子直接拷贝过来:

1
2
3
4
5
xml复制代码<select id="selecUser">
<bind name="myName" value="'%' + _parameter.getName() + '%'" />
SELECT * FROM user
WHERE name LIKE #{myName}
</select>

大家应该大致能知道以上例子的功效,其实就是辅助构建模糊查询的语句拼接,那有人就好奇了,为啥不直接拼接语句就行了,为什么还要搞出一个变量,绕一圈呢?

我先问一个问题:平时你使用 mysql 都是如何拼接模糊查询 like 语句的?

1
sql复制代码select * from user where name like concat('%',#{name},'%')

确实如此,但如果有一天领导跟你说数据库换成 oracle 了,怎么办?上面的语句还能用吗?明显用不了,不能这么写,因为 oracle 虽然也有 concat 函数,但是只支持连接两个字符串,例如你最多这么写:

1
sql复制代码select * from user where name like concat('%',#{name})

但是少了右边的井号符号,所以达不到你预期的效果,于是你改成这样:

1
sql复制代码select * from user where name like '%'||#{name}||'%'

确实可以了,但是过几天领导又跟你说,数据库换回 mysql 了?额… 那不好意思,你又得把相关使用到模糊查询的地方改回来。

1
sql复制代码select * from user where name like concat('%',#{name},'%')

很显然,数据库只要发生变更你的 sql 语句就得跟着改,特别麻烦,所以才有了一开始我们介绍 bind 标签官网的这个例子,无论使用哪种数据库,这个模糊查询的 Like 语法都是支持的:

1
2
3
4
5
xml复制代码<select id="selecUser">
<bind name="myName" value="'%' + _parameter.getName() + '%'" />
SELECT * FROM user
WHERE name LIKE #{myName}
</select>

这个 bind 的用法,实打实解决了数据库重新选型后导致的一些问题,当然在实际工作中发生的概率不会太大,所以 bind 的使用我个人确实也使用的不多,可能还有其它一些应用场景,希望有人能发现之后来跟我们分享一下,总之我勉强给了一颗星(虽然没太多实际用处,但毕竟要给点面子)。


拓展:sql标签 + include 标签

常用度:★★★☆☆

实用性:★★★☆☆

sql 标签与 include 标签组合使用,用于 SQL 语句的复用,日常高频或公用使用的语句块可以抽取出来进行复用,其实我们应该不陌生,早期我们学习 JSP 的时候,就有一个 include 标记可以引入一些公用可复用的页面文件,例如页面头部或尾部页面代码元素,这种复用的设计很常见。

严格意义上 sql 、include 不算在动态 SQL 标签成员之内,只因它确实是宝藏般的存在,所以我要简单说说,sql 标签用于定义一段可重用的 SQL 语句片段,以便在其它语句中使用,而 include 标签则通过属性 refid 来引用对应 id 匹配的 sql 标签语句片段。

简单的复用代码块可以是:

1
2
3
4
xml复制代码<!-- 可复用的字段语句块 -->
<sql id="userColumns">
id,username,password
</sql>

查询或插入时简单复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<!-- 查询时简单复用 -->
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"></include>
from user
</select>

<!-- 插入时简单复用 -->
<insert id="insertUser" resultType="map">
insert into user(
<include refid="userColumns"></include>
)values(
#{id},#{username},#{password}
)
</insert>

当然,复用语句还支持属性传递,例如:

1
2
3
4
xml复制代码<!-- 可复用的字段语句块 -->
<sql id="userColumns">
${pojo}.id,${pojo}.username
</sql>

这个 SQL 片段可以在其它语句中使用:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!-- 查询时复用 -->
<select id="selectUsers" resultType="map">
select
<include refid="userColumns">
<property name="pojo" value="u1"/>
</include>,
<include refid="userColumns">
<property name="pojo" value="u2"/>
</include>
from user u1 cross join user u2
</select>

也可以在 include 元素的 refid 属性或多层内部语句中使用属性值,属性可以穿透传递,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!-- 简单语句块 -->
<sql id="sql1">
${prefix}_user
</sql>

<!-- 嵌套语句块 -->
<sql id="sql2">
from
<include refid="${include_target}"/>
</sql>

<!-- 查询时引用嵌套语句块 -->
<select id="select" resultType="map">
select
id, username
<include refid="sql2">
<property name="prefix" value="t"/>
<property name="include_target" value="sql1"/>
</include>
</select>

至此,关于 9 大动态 SQL 标签的基本用法我们已介绍完毕,另外我们还有一些疑问:Mybatis 底层是如何解析这些动态 SQL 标签的呢?最终又是怎么构建完整可执行的 SQL 语句的呢?带着这些疑问,我们在第4节中详细分析。

4、动态SQL的底层原理

想了解 Mybatis 究竟是如何解析与构建动态 SQL ?首先推荐的当然是读源码,而读源码,是一个技术钻研问题,为了借鉴学习,为了工作储备,为了解决问题,为了让自己在编程的道路上跑得明白一些… 而希望通过读源码,去了解底层实现原理,切记不能脱离了整体去读局部,否则你了解到的必然局限且片面,从而轻忽了真核上的设计。如同我们读史或者观宇宙一样,最好的办法都是从整体到局部,不断放大,前后延展,会很舒服通透。所以我准备从 Mybatis 框架的核心主线上去逐步放大剖析。

通过前面几篇文章的介绍(建议阅读 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道几个?》),其实我们知道了 Mybatis 框架的核心部分在于构件的构建过程,从而支撑了外部应用程序的使用,从应用程序端创建配置并调用 API 开始,到框架端加载配置并初始化构件,再创建会话并接收请求,然后处理请求,最终返回处理结果等。

我们的动态 SQL 解析部分就发生在 SQL 语句对象 MappedStatement 构建时(上左高亮橘色部分,注意观察其中 SQL 语句对象与 SqlSource 、 BoundSql 的关系,在动态 SQL 解析流程特别关键)。我们再拉近一点,可以看到无论是使用 XML 配置 SQL 语句或是使用注解方式配置 SQL 语句,框架最终都会把解析完成的 SQL 语句对象存放到 MappedStatement 语句集合池子。

而以上虚线高亮部分,即是 XML 配置方式解析过程与注解配置方式解析过程中涉及到动态 SQL 标签解析的流程,我们分别讲解:

  • 第一,XML 方式配置 SQL 语句,框架如何解析?

以上为 XML 配置方式的 SQL 语句解析过程,无论是单独使用 Mybatis 框架还是集成 Spring 与 Mybatis 框架,程序启动入口都会首先从 SqlSessionFactoryBuilder.build() 开始构建,依次通过 XMLConfigBuilder 构建全局配置 Configuration 对象、通过 XMLMapperBuilder 构建每一个 Mapper 映射器、通过 XMLStatementBuilder 构建映射器中的每一个 SQL 语句对象(select/insert/update/delete)。而就在解析构建每一个 SQL 语句对象时,涉及到一个关键的方法 parseStatementNode(),即上图橘红色高亮部分,此方法内部就出现了一个处理动态 SQL 的核心节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// XML配置语句构建器
public class XMLStatementBuilder {

// 实际解析每一个 SQL 语句
// 例如 select|insert|update|delete
public void parseStatementNode() {

// [忽略]参数构建...
// [忽略]缓存构建..
// [忽略]结果集构建等等..

// 【重点】此处即是处理动态 SQL 的核心!!!
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
SqlSource sqlSource = langDriver.createSqlSource(..);

// [忽略]最后把解析完成的语句对象添加进语句集合池
builderAssistant.addMappedStatement(语句对象)
}
}

大家先重点关注一下这段代码,其中【重点】部分的 LanguageDriver 与 SqlSource 会是我们接下来讲解动态 SQL 语句解析的核心类,我们不着急剖析,我们先把注解方式流程也梳理对比一下。

  • 第二,注解方式配置 SQL 语句,框架如何解析?

大家会发现注解配置方式的 SQL 语句解析过程,与 XML 方式极为相像,唯一不同点就在于解析注解 SQL 语句时,使用了 MapperAnnotationBuilder 构建器,其中关于每一个语句对象 (@Select,@Insert,@Update,@Delete等) 的解析,又都会通过一个关键解析方法 parseStatement(),即上图橘红色高亮部分,此方法内部同样的出现了一个处理动态 SQL 的核心节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 注解配置语句构建器
public class MapperAnnotationBuilder {

// 实际解析每一个 SQL 语句
// 例如 @Select,@Insert,@Update,@Delete
void parseStatement(Method method) {

// [忽略]参数构建...
// [忽略]缓存构建..
// [忽略]结果集构建等等..

// 【重点】此处即是处理动态 SQL 的核心!!!
final LanguageDriver languageDriver = getLanguageDriver(method);
final SqlSource sqlSource = buildSqlSource( languageDriver,... );

// [忽略]最后把解析完成的语句对象添加进语句集合池
builderAssistant.addMappedStatement(语句对象)

}
}

由此可见,不管是通过 XML 配置语句还是注解方式配置语句,构建流程都是 大致相同,并且依然出现了我们在 XML 配置方式中涉及到的语言驱动 LanguageDriver 与语句源 SqlSource ,那这两个类/接口到底为何物,为何能让 SQL 语句解析者都如此绕不开 ?

这一切,得从你编写的 SQL 开始讲起 …

我们知道,无论 XML 还是注解,最终你的所有 SQL 语句对象都会被齐齐整整的解析完放置在 SQL 语句对象集合池中,以供执行器 Executor 具体执行增删改查 ( CRUD ) 时使用。而我们知道每一个 SQL 语句对象的属性,特别复杂繁多,例如超时设置、缓存、语句类型、结果集映射关系等等。

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
java复制代码// SQL 语句对象
public final class MappedStatement {

private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;

// SQL 源
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;

}

而其中有一个特别的属性就是我们的语句源 SqlSource ,功能纯粹也恰如其名 SQL 源。它是一个接口,它会结合用户传递的参数对象 parameterObject 与动态 SQL,生成 SQL 语句,并最终封装成 BoundSql 对象。SqlSource 接口有5个实现类,分别是:StaticSqlSource、DynamicSqlSource、RawSqlSource、ProviderSqlSource、VelocitySqlSource (而 velocitySqlSource 目前只是一个测试用例,还没有用作实际的 Sql 源实现)。

  • StaticSqlSource:静态 SQL 源实现类,所有的 SQL 源最终都会构建成 StaticSqlSource 实例,该实现类会生成最终可执行的 SQL 语句供 statement 或 prepareStatement 使用。
  • RawSqlSource:原生 SQL 源实现类,解析构建含有 ‘#{}’ 占位符的 SQL 语句或原生 SQL 语句,解析完最终会构建 StaticSqlSource 实例。
  • DynamicSqlSource:动态 SQL 源实现类,解析构建含有 ‘${}’ 替换符的 SQL 语句或含有动态 SQL 的语句(例如 If/Where/Foreach等),解析完最终会构建 StaticSqlSource 实例。
  • ProviderSqlSource:注解方式的 SQL 源实现类,会根据 SQL 语句的内容分发给 RawSqlSource 或 DynamicSqlSource ,当然最终也会构建 StaticSqlSource 实例。
  • VelocitySqlSource:模板 SQL 源实现类,目前(V3.5.6)官方申明这只是一个测试用例,还没有用作真正的模板 Sql 源实现类。

SqlSource 实例在配置类 Configuration 解析阶段就被创建,Mybatis 框架会依据3个维度的信息来选择构建哪种数据源实例:(纯属我个人理解的归类梳理~)

  • 第一个维度:客户端的 SQL 配置方式:XML 方式或者注解方式。
  • 第二个维度:SQL 语句中是否使用动态 SQL ( if/where/foreach 等 )。
  • 第三个维度:SQL 语句中是否含有替换符 ‘${}’ 或占位符 ‘#{}’ 。

SqlSource 接口只有一个方法 getBoundSql ,就是创建 BoundSql 对象。

1
2
3
4
5
java复制代码public interface SqlSource {

BoundSql getBoundSql(Object parameterObject);

}

通过 SQL 源就能够获取 BoundSql 对象,从而获取最终送往数据库(通过JDBC)中执行的 SQL 字符串。

JDBC 中执行的 SQL 字符串,确实就在 BoundSql 对象中。BoundSql 对象存储了动态(或静态)生成的 SQL 语句以及相应的参数信息,它是在执行器具体执行 CURD 时通过实际的 SqlSource 实例所构建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class BoundSql { 

//该字段中记录了SQL语句,该SQL语句中可能含有"?"占位符
private final String sql;

//SQL中的参数属性集合
private final List<ParameterMapping> parameterMappings;

//客户端执行SQL时传入的实际参数值
private final Object parameterObject;

//复制 DynamicContext.bindings 集合中的内容
private final Map<String, Object> additionalParameters;

//通过 additionalParameters 构建元参数对象
private final MetaObject metaParameters;

}

在执行器 Executor 实例(例如BaseExecutor)执行增删改查时,会通过 SqlSource 构建 BoundSql 实例,然后再通过 BoundSql 实例获取最终输送至数据库执行的 SQL 语句,系统可根据 SQL 语句构建 Statement 或者 PrepareStatement ,从而送往数据库执行,例如语句处理器 StatementHandler 的执行过程。

墙裂推荐阅读之前第六文之 Mybatis 最硬核的 API 你知道几个?这些执行流程都有细讲。

到此我们介绍完 SQL 源 SqlSource 与 BoundSql 的关系,注意 SqlSource 与 BoundSql 不是同个阶段产生的,而是分别在程序启动阶段与运行时。

  • 程序启动初始构建时,框架会根据 SQL 语句类型构建对应的 SqlSource 源实例(静态/动态).
  • 程序实际运行时,框架会根据传入参数动态的构建 BoundSql 对象,输送最终 SQL 到数据库执行。

在上面我们知道了 SQL 源是语句对象 BoundSql 的属性,同时还坐拥5大实现类,那究竟是谁创建了 SQL 源呢?其实就是我们接下来准备介绍的语言驱动 LanguageDriver !

1
2
3
java复制代码public interface LanguageDriver {
SqlSource createSqlSource(...);
}

语言驱动接口 LanguageDriver 也是极简洁,内部定义了构建 SQL 源的方法,LanguageDriver 接口有2个实现类,分别是: XMLLanguageDriver 、 RawLanguageDriver。简单介绍一下:

  • XMLLanguageDriver :是框架默认的语言驱动,能够根据上面我们讲解的 SQL 源的3个维度创建对应匹配的 SQL 源(DynamicSqlSource、RawSqlSource等)。下面这段代码是 Mybatis 在装配全局配置时的一些跟语言驱动相关的动作,我摘抄出来,分别有:内置了两种语言驱动并设置了别名方便引用、注册了两种语言驱动至语言注册工厂、把 XML 语言驱动设置为默认语言驱动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 全局配置的构造方法
public Configuration() {
// 内置/注册了很多有意思的【别名】
// ...

// 其中就内置了上述的两种语言驱动【别名】
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

// 注册了XML【语言驱动】 --> 并设置成默认!
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);

// 注册了原生【语言驱动】
languageRegistry.register(RawLanguageDriver.class);
}
  • RawLanguageDriver :看名字得知是原生语言驱动,事实也如此,它只能创建原生 SQL 源(RawSqlSource),另外它还继承了 XMLLanguageDriver 。
1
2
3
4
5
6
7
8
9
10
java复制代码/**
* As of 3.2.4 the default XML language is able to identify static statements
* and create a {@link RawSqlSource}. So there is no need to use RAW unless you
* want to make sure that there is not any dynamic tag for any reason.
*
* @since 3.2.0
* @author Eduardo Macarron
*/
public class RawLanguageDriver extends XMLLanguageDriver {
}

注释的大致意思:自 Mybatis 3.2.4 之后的版本, XML 语言驱动就支持解析静态语句(动态语句当然也支持)并创建对应的 SQL 源(例如静态语句是原生 SQL 源),所以除非你十分确定你的 SQL 语句中没有包含任何一款动态标签,否则就不要使用 RawLanguageDriver !否则会报错!!!先看个别名引用的例子:

1
2
3
4
5
6
7
8
9
xml复制代码<select id="findAll"  resultType="map" lang="RAW" >
select * from user
</select>

<!-- 别名或全限定类名都允许 -->

<select id="findAll" resultType="map" lang="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver">
select * from user
</select>

框架允许我们通过 lang 属性手工指定语言驱动,不指定则系统默认是 lang = “XML”,XML 代表 XMLLanguageDriver ,当然 lang 属性可以是我们内置的别名也可以是我们的语言驱动全限定名,不过值得注意的是,当语句中含有动态 SQL 标签时,就只能选择使用 lang=”XML”,否则程序在初始化构件时就会报错。

1
2
3
java复制代码## Cause: org.apache.ibatis.builder.BuilderException: 
## Dynamic content is not allowed when using RAW language
## 动态语句内容不被原生语言驱动支持!

这段错误提示其实是发生在 RawLanguageDriver 检查动态 SQL 源时:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class RawLanguageDriver extends XMLLanguageDriver { 

// RAW 不能包含动态内容
private void checkIsNotDynamic(SqlSource source) {
if (!RawSqlSource.class.equals(source.getClass())) {
throw new BuilderException(
"Dynamic content is not allowed when using RAW language"
);
}
}
}

至此,基本逻辑我们已经梳理清楚:程序启动初始阶段,语言驱动创建 SQL 源,而运行时, SQL 源动态解析构建出 BoundSql 。

那么除了系统默认的两种语言驱动,还有其它吗?

答案是:有,例如 Mybatis 框架中目前使用了一个名为 VelocityLanguageDriver 的语言驱动。相信大家都学习过 JSP 模板引擎,同时还有很多人学习过其它一些(页面)模板引擎,例如 freemark 和 velocity ,不同模板引擎有自己的一套模板语言语法,而其中 Mybatis 就尝试使用了 Velocity 模板引擎作为语言驱动,目前虽然 Mybatis 只是在测试用例中使用到,但是它告诉了我们,框架允许自定义语言驱动,所以不只是 XML、RAW 两种语言驱动中使用的 OGNL 语法,也可以是 Velocity (语法),或者你自己所能定义的一套模板语言(同时你得定义一套语法)。 例如以下就是 Mybatis 框架中使用到的 Velocity 语言驱动和对应的 SQL 源,它们使用 Velocity 语法/方式解析构建 BoundSql 对象。

1
2
3
4
5
6
7
java复制代码/**
* Just a test case. Not a real Velocity implementation.
* 只是一个测试示例,还不是一个真正的 Velocity 方式实现
*/
public class VelocityLanguageDriver implements LanguageDriver {
public SqlSource createSqlSource() {...}
}
1
2
3
java复制代码public class VelocitySqlSource implements SqlSource {
public BoundSql getBoundSql() {...}
}

好,语言驱动的基本概念大致如此。我们回过头再详细看看动态 SQL 源 SqlSource,作为语句对象 MappedStatement 的属性,在 程序初始构建阶段,语言驱动是怎么创建它的呢?不妨我们先看看常用的动态 SQL 源对象是怎么被创建的吧!

通过以上的程序初始构建阶段,我们可以发现,最终语言驱动通过调用 XMLScriptBuilder 对象来创建 SQL 源。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// XML 语言驱动
public class XMLLanguageDriver implements LanguageDriver {

// 通过调用 XMLScriptBuilder 对象来创建 SQL 源
@Override
public SqlSource createSqlSource() {
// 实例
XMLScriptBuilder builder = new XMLScriptBuilder();
// 解析
return builder.parseScriptNode();
}
}

而在前面我们就已经介绍, XMLScriptBuilder 实例初始构造时,会初始构建所有动态标签处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// XML脚本标签构建器
public class XMLScriptBuilder{
// 标签节点处理器池
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

// 构造器
public XMLScriptBuilder() {
initNodeHandlerMap();
//... 其它初始化不赘述也不重要
}

// 动态标签处理器
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
}

继 XMLScriptBuilder 初始化流程之后,解析创建 SQL 源流程再分为两步:

1、解析动态标签,通过判断每一块动态标签的类型,使用对应的标签处理器进行解析属性和语句处理,并最终放置到混合 SQL 节点池中(MixedSqlNode),以供程序运行时构建 BoundSql 时使用。

2、new SQL 源,根据 SQL 是否有动态标签或通配符占位符来确认产生对象的静态或动态 SQL 源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public SqlSource parseScriptNode() {

// 1、解析动态标签 ,并放到混合SQL节点池中
MixedSqlNode rootSqlNode = parseDynamicTags(context);

// 2、根据语句类型,new 出来最终的 SQL 源
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

原来解析动态标签的工作交给了 parseDynamicTags() 方法,并且每一个语句对象的动态 SQL 标签最终都会被放到一个混合 SQL 节点池中。

1
2
3
4
5
6
java复制代码// 混合 SQL 节点池
public class MixedSqlNode implements SqlNode {

// 所有动态 SQL 标签:IF、WHERE、SET 等
private final List<SqlNode> contents;
}

我们先看一下 SqlNode 接口的实现类,基本涵盖了我们所有动态 SQL 标签处理器所需要使用到的节点实例。而其中混合 SQL 节点 MixedSqlNode 作用仅是为了方便获取每一个语句的所有动态标签节点,于是应势而生。

知道动态 SQL 标签节点处理器及以上的节点实现类之后,其实就能很容易理解,到达程序运行时,执行器会调用 SQL 源来协助构建 BoundSql 对象,而 SQL 源的核心工作,就是根据每一小段标签类型,匹配到对应的节点实现类以解析拼接每一小段 SQL 语句。

程序运行时,动态 SQL 源获取 BoundSql 对象 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 动态 SQL 源
public class DynamicSqlSource implements SqlSource {

// 这里的 rootSqlNode 属性就是 MixedSqlNode
private final SqlNode rootSqlNode;

@Override
public BoundSql getBoundSql(Object parameterObject) {

// 动态SQL核心解析流程
rootSqlNode.apply(...);

return boundSql;

}
}

很明显,通过调用 MixedSqlNode 的 apply () 方法,循环遍历每一个具体的标签节点。

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

// 所有动态 SQL 标签:IF、WHERE、SET 等
private final List<SqlNode> contents;

@Override
public boolean apply(...) {

// 循环遍历,把每一个节点的解析分派到具体的节点实现之上
// 例如 <if> 节点的解析交给 IfSqlNode
// 例如 纯文本节点的解析交给 StaticTextSqlNode
contents.forEach(node -> node.apply(...));
return true;
}
}

我们选择一两个标签节点的解析过程进行说明,其它标签节点实现类的处理也基本雷同。首先我们看一下 IF 标签节点的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// IF 标签节点
public class IfSqlNode implements SqlNode {

private final ExpressionEvaluator evaluator;

// 实现逻辑
@Override
public boolean apply(DynamicContext context) {

// evaluator 是一个基于 OGNL 语法的解析校验类
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}

IF 标签节点的解析过程非常简单,通过解析校验类 ExpressionEvaluator 来对 IF 标签的 test 属性内的表达式进行解析校验,满足则拼接,不满足则跳过。我们再看看 Trim 标签的节点解析过程,set 标签与 where 标签的底层处理都基于此:

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

// 核心处理方法
public void applyAll() {

// 前缀智能补充与去除
applyPrefix(..);

// 前缀智能补充与去除
applySuffix(..);
}
}

再来看一个纯文本标签节点实现类的解析处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 纯文本标签节点实现类
public class StaticTextSqlNode implements SqlNode {

private final String text;

public StaticTextSqlNode(String text) {
this.text = text;
}

// 节点处理,仅仅就是纯粹的语句拼接
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}

到这里,动态 SQL 的底层解析过程我们基本讲解完,冗长了些,但流程上大致算完整,有遗漏的,我们回头再补充。

总结

不知不觉中,我又是这么巨篇幅的讲解剖析,确实不太适合碎片化时间阅读,不过话说回来,毕竟此文属于 Mybatis 全解系列,作为学研者还是建议深谙其中,对往后众多框架技术的学习必有帮助。本文中我们很多动态 SQL 的介绍基本都使用 XML 配置方式,当然注解方式配置动态 SQL 也是支持的,动态 SQL 的语法书写同 XML 方式,但是需要在字符串前后添加 script 标签申明该语句为动态 SQL ,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class UserDao {

/**
* 更新用户
*/
@Select(
"<script>"+
" UPDATE user "+
" <trim prefix=\"SET\" prefixOverrides=\",\"> "+
" <if test=\"username != null and username != ''\"> "+
" , username = #{username} "+
" </if> "+
" </trim> "+
" where id = ${id}"
"</script>"
)
void updateUser( User user);

}

此种动态 SQL 写法可读性较差,并且维护起来也挺硌手,所以我个人是青睐 xml 方式配置语句,一直追求解耦,大道也至简。当然,也有很多团队和项目都在使用注解方式开发,这些没有绝对,还是得结合自己的实际项目情况与团队等去做取舍。

本篇完,本系列下一篇我们讲《 Mybatis系列全解(九):Mybatis的复杂映射 》。

文章持续更新,微信搜索「潘潘和他的朋友们」第一时间阅读,随时有惊喜。本文会在 GitHub github.com/JavaWorld 收录,关于热腾腾的技术、框架、面经、解决方案、摸鱼技巧、教程、视频、漫画等等等等,我们都会以最美的姿势第一时间送达,欢迎 Star ~ 我们未来 不止文章!想进读者群的朋友欢迎撩我个人号:panshenlian,备注「加群」我们群里畅聊, BIU ~

本文转载自: 掘金

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

国内有哪些顶级技术团队的博客值得推荐?

发表于 2021-03-03

「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide! : github.com/Snailclimb/… 。

上个周末基本 90%的时间都在弄星球的《Java 面试进阶指北》这个小册。

晚上纠结了半天,不知道写点啥。突然想到很多小伙伴都非常好奇我都订阅了哪些技术团队的博客。于是,我便写下了这篇文章。

我的 RSS 订阅器用的是 NetNewsWire (支持 macOS 和 iOS)。

另外,需要说明的是,下面推荐的有些博客只有公众号,并没有对应的网站。

美团技术团队

美团技术团队 (公号同名)是美团旗下的一个技术博客类型的网站。

美团技术团队上面的文章质量都非常高,涵盖 Java 后端、分布式、人工智能、个人成长等多个领域,并且还出过好几个爆款技术文。

我是通过《Java 线程池实现原理及其在美团业务中的实践》 这篇文章入坑的。毫不夸张,这篇文章当时刷爆朋友圈,很多朋友都转发了。

优质文章推荐:

  • Java 中 9 种常见的 CMS GC 问题分析与解决
  • 新一代垃圾回收器 ZGC 的探索与实践
  • 设计模式在外卖营销业务中的实践

阿里技术

阿里技术是(公号同名)阿里旗下的一个技术博客类型的网站,更新频率非常高,并且,文章质量有很高!

大部分文章都是阿里的一些技术文章写的,质量有保障!

优质文章推荐:

  • 如何做好技术 Team Leader?
  • 阿里毕玄:提升代码能力的 4 段经历
  • 如何编写有效的接口测试?
  • 一个线上 SQL 死锁异常分析:深入了解事务和锁
  • 多中心容灾实践:如何实现真正的异地多活?

阿里巴巴中间件

阿里巴巴中间件(公号同名)是阿里巴巴中间件团队的官方账号。

大部分文章都是阿里的一些技术文章写的,质量有保障!

好家伙!阿里一家就占了我 RSS 列表的两个名额!

优质文章推荐:

  • 阿里资深技术专家崮德:如何成就更好的自己
  • 如何避免让微服务测试成为研发团队最大的瓶颈?
  • 我对技术架构的理解与架构师角色的思考
  • 快手基于 RocketMQ 的在线消息系统建设实践

ThoughtWorks 洞见

ThoughtWorks 洞见是 ThoughtWorks 旗下的一个技术博客类型的网站。

ThoughtWorks 洞见上面的文章质量都非常高,涵盖中台、后台开发实践、领域驱动设计、个人成长等多个领域。

优质文章推荐:

  • 后端开发实践——开发者的第 0 个迭代
  • 后端开发实践系列——领域驱动设计(DDD)编码实践
  • 写了十年技术博客,我收获了什么
  • 重构的七宗罪

小米信息部技术团队

小米信息部技术团队是小米旗下的一个技术博客类型的网站。

文章质量还是挺不错的,就是最近更新频率貌似有点低。小米信息部技术团队的朋友,麻烦多多更新啊!

优质文章推荐:

  • 设计模式基础之——模板模式业务实战
  • 如何高效对接第三方支付

360 核心安全技术博客

360 核心安全技术博客是 360 旗下的一个技术博客类型的网站,主要分享安全相关的文章。

我平时是抱着猎奇的心态来看的,很多文章还有意思的,就比如我在下面推荐的几篇文章。

优质文章推荐:

  • 手机色情软件中的“偷拍者”
  • 手机借贷中的偷拍者

伴鱼技术团队

伴鱼技术团队 主要分享服务治理、全链路追踪与压测、分布式架构设计等领域的知识。

第一次了解到伴鱼技术团队是因为 《我们为什么放弃 MongoDB 和 MySQL,选择 TiDB》这篇文章。我当时对 TiDB 比较感兴趣,想了解一下 TiDB 在实际的生产环境上的使用情况。

优质文章推荐:

  • 伴鱼全局 ID 生成服务实践
  • 读 TiDB 论文有感:数据强一致性且资源隔离的 HTAP 数据库

其他

  • 字节跳动技术团队 :主要分享前端、APP 端相关的一些文章。
  • 滴滴技术(公号同名) : 主要分享后端方向的一些文章。

本文转载自: 掘金

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

因在Java中不会优雅地判空,被CTO屌的快哭了。。。

发表于 2021-03-03

判空灾难

作为搬砖党的一族们,我们对判空一定再熟悉不过了,不要跟我说你很少进行判空,除非你喜欢NullPointerException。

不过NullPointerException对于很多猿们来说,也是Exception家族中最亲近的一员了。
在这里插入图片描述

为了避免NullPointerException来找我们,我们经常会进行如下操作。

1
2
3
kotlin复制代码if (data != null) {
do sth.
}

如果一个类中多次使用某个对象,那你可能要一顿操作,so:
在这里插入图片描述

“世界第九大奇迹”就这样诞生了。Maybe你会想,项目中肯定不止你一个人会这样一顿操作,然后按下Command+Shift+F,真相就在眼前:
在这里插入图片描述

What,我们有接近一万行的代码都是在判空?
在这里插入图片描述

好了,接下来,要进入正题了。

NullObject模式

对于项目中无数次的判空,对代码质量整洁度产生了十分之恶劣的影响,对于这种现象,我们称之为“判空灾难”。

那么,这种现象如何治理呢,你可能听说过NullObject模式,不过这不是我们今天的武器,但是还是需要介绍一下NullObject模式。

什么是NullObject模式呢?

In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral (“null”) behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof).

以上解析来自Wikipedia。

NullObject模式首次发表在“ 程序设计模式语言 ”系列丛书中。一般的,在面向对象语言中,对对象的调用前需要使用判空检查,来判断这些对象是否为空,因为在空引用上无法调用所需方法。

空对象模式的一种典型实现方式如下图所示(图片来自网络):
在这里插入图片描述

示例代码如下(命名来自网络,哈哈到底是有多懒):

Nullable是空对象的相关操作接口,用于确定对象是否为空,因为在空对象模式中,对象为空会被包装成一个Object,成为Null Object,该对象会对原有对象的所有方法进行空实现…

1
2
3
java复制代码public interface Nullable {
boolean isNull();
}

这个接口定义了业务对象的行为。

1
2
3
java复制代码public interface DependencyBase extends Nullable {
void Operation();
}

这是该对象的真实类,实现了业务行为接口DependencyBase与空对象操作接口Nullable。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Dependency implements DependencyBase, Nullable {

@Override
public void Operation() {
System.out.print("Test!");
}

@Override
public boolean isNull() {
return false;
}

}

这是空对象,对原有对象的行为进行了空实现。

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

@Override
public void Operation() {
// do nothing
}

@Override
public boolean isNull() {
return true;
}

}

在使用时,可以通过工厂调用方式来进行空对象的调用,也可以通过其他如反射的方式对对象进行调用(一般多耗时几毫秒)在此不进行详细叙述。

1
2
3
4
5
6
7
8
java复制代码public class Factory {
public static DependencyBase get(Nullable dependencyBase){
if (dependencyBase == null){
return new NullObject();
}
return new Dependency();
}
}

这是一个使用范例,通过这种模式,我们不再需要进行对象的判空操作,而是可以直接使用对象,也不必担心NPE(NullPointerException)的问题。

1
2
3
4
5
6
java复制代码public class Client {

public void test(DependencyBase dependencyBase){
Factory.get(dependencyBase).Operation();
}
}

关于空对象模式,更具体的内容大家也可以多找一找资料,上述只是对NullObject的简单介绍,但是,今天我要推荐的是一款协助判空的插件NR Null Object,让我们来优雅地进行判空,不再进行一顿操作来定义繁琐的空对象接口与空独享实现类。

.NR Null Object

NR Null Object是一款适用于Android Studio、IntelliJ IDEA、PhpStorm、WebStorm、PyCharm、RubyMine、AppCode、CLion、GoLand、DataGrip等IDEA的Intellij插件。其可以根据现有对象,便捷快速生成其空对象模式需要的组成成分,其包含功能如下:

1、分析所选类可声明为接口的方法;
2、抽象出公有接口;
3、创建空对象,自动实现公有接口;
4、对部分函数进行可为空声明;
5、可追加函数进行再次生成;
6、自动的函数命名规范

让我们来看一个使用范例:

在这里插入图片描述

怎么样,看起来是不是非常快速便捷,只需要在原有需要进行多次判空的对象中,邮件弹出菜单,选择Generate,并选择NR Null Object即可自动生成相应的空对象组件。

那么如何来获得这款插件呢?

安装方式

可以直接通过IDEA的Preferences中的Plugins仓库进行安装。

选择 Preferences → Plugins → Browse repositories

在这里插入图片描述

搜索“NR Null Oject”或者“Null Oject”进行模糊查询,点击右侧的Install,restart IDEA即可。
在这里插入图片描述

Optional

还有一种方式是使用Java8特性中的Optional来进行优雅地判空,Optional来自官方的介绍如下:

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

一个可能包含也可能不包含非null值的容器对象。如果存在值,isPresent()将返回true,get()将返回该值。

话不多说,举个例子。
在这里插入图片描述

有如下代码,需要获得Test2中的Info信息,但是参数为Test4,我们要一层层的申请,每一层都获得的对象都可能是空,最后的代码看起来就像这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    public String testSimple(Test4 test) {
if (test == null) {
return "";
}
if (test.getTest3() == null) {
return "";
}
if (test.getTest3().getTest2() == null) {
return "";
}
if (test.getTest3().getTest2().getInfo() == null) {
return "";
}
return test.getTest3().getTest2().getInfo();
}

但是使用Optional后,整个就都不一样了。

1
2
3
4
5
6
java复制代码    public String testOptional(Test test) {
return Optional.ofNullable(test).flatMap(Test::getTest3)
.flatMap(Test3::getTest2)
.map(Test2::getInfo)
.orElse("");
}

1、Optional.ofNullable(test),如果test为空,则返回一个单例空Optional对象,如果非空则返回一个Optional包装对象,Optional将test包装;

1
2
3
java复制代码    public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

2、flatMap(Test::getTest3)判断test是否为空,如果为空,继续返回第一步中的单例Optional对象,否则调用Test的getTest3方法;

1
2
3
4
5
6
7
8
java复制代码    public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}

3、flatMap(Test3::getTest2)同上调用Test3的getTest2方法;

4、map(Test2::getInfo)同flatMap类似,但是flatMap要求Test3::getTest2返回值为Optional类型,而map不需要,flatMap不会多层包装,map返回会再次包装Optional;

1
2
3
4
5
6
7
8
java复制代码    public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}

5、orElse(“”);获得map中的value,不为空则直接返回value,为空则返回传入的参数作为默认值。

1
2
3
java复制代码public T orElse(T other) {
return value != null ? value : other;
}

怎么样,使用Optional后我们的代码是不是瞬间变得非常整洁,或许看到这段代码你会有很多疑问,针对复杂的一长串判空,Optional有它的优势,但是对于简单的判空使用Optional也会增加代码的阅读成本、编码量以及团队新成员的学习成本。毕竟Optional在现在还并没有像RxJava那样流行,它还拥有一定的局限性。

如果直接使用Java8中的Optional,需要保证安卓API级别在24及以上。
在这里插入图片描述

你也可以直接引入Google的Guava。(啥是Guava?来自官方的提示)

Guava is a set of core libraries that includes new collection types (such as multimap and multiset), immutable collections, a graph library, functional types, an in-memory cache, and APIs/utilities for concurrency, I/O, hashing, primitives, reflection, string processing, and much more!

引用方式,就像这样:

1
2
3
4
5
arduino复制代码    dependencies {
compile 'com.google.guava:guava:27.0-jre'
// or, for Android:
api 'com.google.guava:guava:27.0-android'
}

不过IDEA默认会显示黄色,提示让你将Guava表达式迁移到Java Api上。

在这里插入图片描述

当然,你也可以通过在Preferences搜索”Guava”来Kill掉这个Yellow的提示。
在这里插入图片描述

关于Optional使用还有很多技巧,感兴趣可以查阅Guava和Java8相关书籍和文档。

使用Optional具有如下优点:

  • 将防御式编程代码完美包装
  • 链式调用
  • 有效避免程序代码中的空指针

但是也同样具有一些缺点:

  • 流行性不是非常理想,团队新成员需要学习成本

安卓中需要引入Guava,需要团队每个人处理IDEA默认提示,或者忍受黄色提示

有时候代码阅读看起来可能会如下图所示:

在这里插入图片描述

Kotlin

当然,Kotlin以具有优秀的空安全性为一大特色,并可以与Java很好的混合使用,like this:

1
复制代码test1?.test2?.test3?.test4

如果你已经开始使用了Kotlin,可以不用再写缭乱的防御判空语句。如果你还没有使用Kotlin,并不推荐为了判空优雅而直接转向Kotlin。

本文转载自: 掘金

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

1…712713714…956

开发者博客

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