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

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


  • 首页

  • 归档

  • 搜索

五年Java经验,面试还是说不出日志该怎么写更好?——日志规

发表于 2020-01-07

本文是一个系列,欢迎关注

查看上一篇文章可以扫描文章下方的二维码,点击往期回顾-日志系列即可查看所有相关文章

概览

各位小伙伴大家好,我又回来更新了,上一篇我们讨论了为什么要使用日志框架,这次我们深入问题的根源,为什么我们需要日志,日志具体如何使用?

大多数开发人员会纠结日志该怎么输出,什么时候输出,输出了会不会有人看等问题,让我们跳出开发人员的局限来考虑这个问题:谁需要日志?日志有几种?日志都需要输出什么?如何输出日志?

谁需要日志?

  • 开发者 开发人员在开发过程中需要输出一些变量方便调试,正确的做法是使用日志来输出(使用 System.out来输出,一不小心发布到线上,会被项目经理痛批);其次线上问题很难重放,用户的表述一般都会失真,况且很多用户发现 bug 就删 app 关网页走人了
  • 运维人员 整个系统大部分时间都是运维人员来维护,日志可以帮助运维人员来了解系统状态(很多运维系统接入的也是日志),运维人员发现日志有异常信息也可以及时通知开发来排查
  • 运营人员 没错,就是运营人员,比如电商的转化率、视频网站的完播率、普通PV数据等都可以通过日志进行统计,随着大数据技术的普及,这部分日志占比也越来越高
  • 安全人员 虽然大多数企业不重视安全,但是安全也可以通过日志来进行预警,比如某个用户突然大额转账、再比如数据库突然出现大量无条件分页查库(拖库)等等

日志有几种?

  • 调试日志 用于开发人员开发或者线上回溯问题。
  • 诊断日志 一般用于运维人员监控系统与安全人员分析预警。
  • 埋点日志 一般用于运营决策分析,也有用作微服务调用链路追踪的(运维、调试)。
  • 审计日志 与诊断日志类似,诊断日志偏向运维,审计日志偏向安全。

日志都需要输出什么?

注:日志级别会在下面讲解

  • 调试日志
    • DEBUG 或者 TRACE 级别,比如方法调用参数,网络连接具体信息,一般是开发者调试程序使用,线上非特殊情况关闭这些日志
    • INFO 级别,一般是比较重要却没有风险的信息,如初始化环境、参数,清理环境,定时任务执行,远程调用第一次连接成功
    • WARN 级别,有可能有风险又不影响系统继续执行的错误,比如系统参数配置不正确,用户请求的参数不正确(要输出具体参数方便排查),或者某些耗性能的场景,比如一次请求执行太久、一条 SQL 执行超过两秒,某些第三方调用失败,不太可能被运行的if分支等
    • ERROR 级别,用于程序出错打印堆栈信息,不应该用于输出程序问题之外的其他信息,需要注意打印了日志异常(Exception)就不应该抛(throw)了
  • 诊断日志 一般输出 INFO 级别,请求响应时间,内存占用等等,线上接入监控系统时打开,建议输出到独立的文件,可以考虑 JSON 格式方便外部工具分析
  • 埋点日志 业务按需定制,比如上文提到的转化率可以在用户付款时输出日志,完播率可以在用户播放完成后请求一次后台输出日志,一般可输出 INFO 级别,建议输出到独立的文件,可以考虑JSON格式方便外部工具分析
  • 审计日志 大多 WARN 级别或者 INFO 级别,一般是敏感操作即可输出,登陆、转账付款、授权消权、删除等等,建议输出到独立的文件,可以考虑JSON格式方便外部工具分析

一般调试日志由开发者自定义输出,其他三种应该根据实际业务需求来定制。

日志的其他注意点

  1. 线上日志应尽量谨慎,要思考:这个位置输出日志能帮助排除问题吗?输出的信息与排查问题相关吗?输出的信息足够排除问题吗?做到不少输出必要信息,不多输出无用信息(拖慢系统,淹没有用信息)
  2. 超级 SessionId 与 RequestId,无论是单体应用还是微服务架构,应该为每个用户每次登陆生成一个超级 SessionId,方便跟踪区分一个用户;RequestId,每次请求生成一个 RequestId,用于跟踪一次请求,微服务也可以用于链路追踪
  3. 日志要尽量单行输出,一条日志输出一行,否则不方便阅读以及其他第三方系统或者工具分析
  4. 公司内部应该制定一套通用的日志规范,包括日志的格式,变量名(驼峰、下划线),分隔符(“=”或“:”等),何时输出(比如规定调用第三方前后输出INFO日志),公司的日志规范应该不断优化、调整,找到适合公司业务的最佳规范

OK,理论就聊到这里,接下来让我们回到技术层面。

使用概念

如果要想要学会使用日志框架,先要理解几个简单概念,Logger、Appenders、Layout、日志级别与级别继承(Level Inheritance)

Logger(日志实例)

用于输出日志,调用一次org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)或org.slf4j.LoggerFactory#getLogger(java.lang.String)就会产生一个日志实例,相同参数会共用同一个实例。

Appenders

日志输出器,logback 预定义了输出到控制台、文件、Socket 服务器、MySQL、PostgreSQL、Oracle 和其他数据库、JMS 和 UNIX Syslog 系统调用等实现,通过配置文件配置即可使用,当然我们常用的只有控制台和文件两种。

Layout

用于控制日志输出格式,前文所说的”自动输出日志相关信息,如:日期、线程、方法名称等等“就可以用 Layout 来控制,实际使用很简单,写一个 Layout 格式定义表达式(pattern)即可,使用方法类似于Java 的SimpleDateFomat。

日志级别

RFC 5424 (page 11)规定了 8 种日志级别,但是SLF4j 只定义了 5 种日志级别,分别是 ERROR、WARN、INFO、DEBUG、TRACE 这五个级别从高到低,配置级别越高日志输出就越少,如下图

日志级别

我们看到滑动条上五个点正好对应五个级别,滑动指示器可以左右移动,指示器作为分界点,指示器左侧都可以输出,右侧都不能输出,左右调整指示器就可以调整日志的输出,滑倒右侧就可以全部输出,滑倒左侧就可以减少输出,那么是否能够彻底关闭输出呢?答案是可以的,配置文件中还可以配置为 ALL 与 OFF,分别对应所有(等价于TRACE)与关闭。

级别继承

理解了日志级别,让我们来考虑如下场景:

  • 某些重要业务输出 INFO 级别,其他业务输出WARN级别的日志,同时关闭所有库、框架的日志

有需求就会有解决方案,其实很简单,logback 与 log4j 都支持按照日志实例来配置,现在问题解决了,但是新的问题又来了,如果线上所有日志都输出 INFO 级别,难道要一个一个配置吗?这时候就就要请出我们上面所提到的级别继承,如果 Java 一样,logback 与 log4j 中也都是单根继承模型,Java 中是 Object,日志中是 ROOT,如下图:

级别继承

有了继承机制,我们只需要将 ROOT 调整到 INFO 级别,再按照需求细化调整我们业务对应的 logger 实例级别即可满足绝大多数场景。

Codding 实战

问:把大象装冰箱分几步?分三步:1、引入依赖,2、编码输出日志,3、调整配置文件。前文已经讲过步骤一,如果没有看过的读者请移步公众号查看往期回顾,这里直接进入步骤二。

步骤二

如果项目中使用了Lombok,那么可以直接在类上面加@Slf4j注解既可获得日志实例,否则可以使用static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestLog.class);来获取日志实例

具体日志输出方法如下:

1
2
3
4
5
复制代码logger.trace("A TRACE Message");
logger.debug("A DEBUG Message");
logger.info("An INFO Message");
logger.warn("A WARN Message");
logger.error("An ERROR Message");

这里有个注意点,尽量使用参数占位而不要手动拼接字符串,如下

1
2
3
4
5
复制代码String level = "Trace";
// 反例
logger.trace("A " + level + " Message");
// 正确的做法
logger.trace("A {} Message", level);

这样做可以提高效率,如果不输出日志,第一种情况也会拼接字符串造成性能损耗,第二种就不会有此问题(阿里巴巴Java开发手册(华山版)这里表述有问题,占位符效率更高是因为尽量延迟进行字符串处理,如果不需要输出的日志就不处理了,下一篇原理分析会展开),另外我们也不需要if (logger.isTraceEnabled())来进行判断了(性能损耗不高,但是代码好看多了)。

步骤三

配置文件需要区分 logback 与 log4j2,两种框架在配置文件上有差别但很相似,来看具体配置文件。

logback 配置文件位置

logback 支持 xml 与 groovy 脚本两种配置方式,logback 查找配置文件位置规则如下(后续文章会讲如何修改位置)

  1. logback 尝试在类路径中找到一个名为 logback-test.xml 的文件。
  2. 如果找不到此类文件,则 logback 会尝试在类路径中找到名为 logback.groovy 的文件。
  3. 如果找不到这样的文件,它将在类路径中检查文件 logback.xml。
  4. 如果找不到此类文件,则通过查找文件 META-INF\services\ch.qos.logback.classic.spi.Configurator,如果有这个文件且内容是 com.qos.logback.classic.spi.Configurator 实现类的全类名,直接加载这个实现类。
  5. 如果以上方法均不能成功执行,则 logback 会使用 BasicConfigurator 自动进行自我配置,会将日志输出到控制台。

这段长长的文字其实不用看,我们就把 logback.xml 放入 Classpath 根目录就可以了。。

logback 配置文件编写规则

logback 配置文件主要分为三类,一个或多个 Appender,用于定义输出位置(不同文件位置,或者网络又或者数据库);一个或多个 Logger,用于细化配置不同 logger 的输出级别以及位置;一个 ROOT,是一个特殊的logger,用于配置根 Logger。

日志配置文件格式

我们一起来看下面的配置文件实例

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false" debug="false">

<!-- 定义日志文件的存储地 -->
<property name="LOG_PATH" value="/var/log"/>

<property name="CONSOLE_LOG_PATTERN"
value="%d{HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29}\(%4L\\) - %msg%n"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

<!-- 文件日志格式(打印日志,不打印行号) -->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%10.10thread] %-30.30logger{29} - %msg%n"/>

<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${LOG_PATH}/log.log</file>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- yyyy-MM-dd 按日滚动 -->
<fileNamePattern>${LOG_PATH}/log-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个文件最大50M -->
<maxFileSize>50MB</maxFileSize>
<!-- 最多占用5G磁盘空间,500个文件(总共不能超过该5G) -->
<maxHistory>500</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<!-- 追加方式记录日志 -->
<append>true</append>
<!-- 日志文件的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

<!-- 日志输出级别 STDOUT:控制台;FILE_ALL:文件 -->
<root level="warn">
<appender-ref ref="STDOUT"/>
</root>
<logger name="druid.sql" level="warn" additivity="true"/>
<logger name="druid.sql.ResultSet" level="warn" additivity="true"/>
<logger name="com.alibaba.druid.pool.DruidDataSource" level="debug" additivity="true">
<appender-ref ref="FILE_ALL"/>
</logger>
</configuration>

上面配置文件定义了两个 Appender,一个输出控制台,另一个输出到文件并且自动滚动。需注意的是property标签相当于定义一个变量,可以使用${xxx}进行引用,CONSOLE_LOG_PATTERN 与 FILE_LOG_PATTERN 定义了控制台与文件打印格式,具体编写方式类似于 Java 的SimpleDateFomat就不在此展开了,具体可以参考

  • logback:logback.qos.ch/manual/layo…
  • log4j:logging.apache.org/log4j/2.x/m…

log4j2 配置文件位置

log4j2 支持 XML、JSON、YAML 或者 properties 格式的配置文件,具体查找方式如下:

  1. 检查“ log4j.configurationFile”系统属性,如果有,尝试使用与文件扩展名匹配的ConfigurationFactory加载配置。
  2. 如果未设置系统属性,则属性 ConfigurationFactory 将在类路径中查找 log4j2-test.properties。
  3. 如果找不到此类文件,则 YAML ConfigurationFactory将在类路径中查找 log4j2-test.yaml或log4j2-test.yml。
  4. 如果找不到此类文件,则 JSON ConfigurationFactory 将在类路径中查找 log4j2-test.json或log4j2-test.jsn。
  5. 如果找不到这样的文件,XML ConfigurationFactory 将在类路径中查找 log4j2-test.xml。
  6. 如果找不到测试文件,则属性 ConfigurationFactory 将在类路径上查找 log4j2.properties。
  7. 如果找不到属性文件,则 YAML ConfigurationFactory 将在类路径上查找 log4j2.yaml或log4j2.yml。
  8. 如果无法找到 YAML 文件,则 JSON ConfigurationFactory 将在类路径上查找 log4j2.json或log4j2.jsn。
  9. 如果无法找到 JSON 文件,则 XML ConfigurationFactory 将尝试在类路径上找到 log4j2.xml。
  10. 如果找不到配置文件,使用 DefaultConfiguration 自动配置,将日志输出到控制台。

这段更长的文字当然也不用看,我们就把 log4j2.xml 放入 Classpath 根目录就可以了

log4j2 配置文件编写

log4j 也是 Logger 与 Appender 配置项,也有一个ROOT的特殊 Logger,Appender 比logback支持更多输出位置,如kafka、Cassandra、Flume等。

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true">
<!-- 定义变量,可以被${xxx}引用 -->
<Properties>
<Property name="baseDir">logs</Property>
</Properties>

<!-- 定义 Appenders 用来指定输出位置 -->
<Appenders>
<!-- 日志滚动 ?{date:yyyy-MM}:按月滚动文件夹 按小时、文件序号滚动,每次滚动都使用gz压缩 -->
<RollingFile name="RollingFile" fileName="${baseDir}/log.log"
filePattern="${baseDir}/?{date:yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.log.gz">
<!-- 日志格式 -->
<PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
<Policies>
<!-- 时间滚动(按月滚动目录,按小时滚动文件) -->
<TimeBasedTriggeringPolicy/>
<!-- 文件大小滚动(1小时内超过250M,强制滚动一次) -->
<SizeBasedTriggeringPolicy size="250 MB"/>
</Policies>
<!-- 每天最多100个文件 -->
<DefaultRolloverStrategy max="100">
<!-- 删除策略,超过三十天删除,如果总文件小于100G,文件数量小于10个,则不会被删除 -->
<Delete basePath="${baseDir}" maxDepth="2">
<IfFileName glob="*/app-*.log.gz">
<IfLastModified age="30d">
<IfAny>
<IfAccumulatedFileSize exceeds="100 GB"/>
<IfAccumulatedFileCount exceeds="10"/>
</IfAny>
</IfLastModified>
</IfFileName>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>

<Loggers>
<!-- 多个logger -->
<Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
<AppenderRef ref="RollingFile"/>
</Logger>

<!-- 一个ROOT -->
<Root level="trace">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

可以看得出 log4j2 与 logback 配置文件书写大同小异,甚至同样需要注意additivity="true"时导致的日志重复输出问题,毕竟 log4j1 与 logback 都是 Ceki大神都作品。

总结

得益于 Ceki 大佬的努力,日志使用几乎没有有差异(Logback 与 Log4j2,Google 于 2018年4月开源了流式(fluent)日志框架 Flogger,Slf4j 也将在 2.0 版本支持,而 log4j2 再次落后,不过笔者认为 log4j2 有很多优点,更多内容请关注后续文章)。关于日志如何输出本人也是经验之谈,免不了纰漏,欢迎补充指正,另外每个公司都有不同的应用场景,具体应该遵守公司统一规范。

本篇更多倾向基础使用,接下来的文章将展开对比、原理以及扩展日志框架,敬请各位小伙伴们期待。

本文相关代码

如果觉得写的不错,求关注、求点赞、求转发,如果有问题或者文中有错误,欢迎留言讨论。

扫描关注公众号,第一时间获得更新

扫码关注

参考

logging.apache.org/log4j/2.x/m…

logback.qos.ch/manual/conf…

本文转载自: 掘金

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

MySQL的万字总结(缓存,索引,Explain,事务,re

发表于 2020-01-07

hello,小伙伴们,好久不见,MySQL系列停更了差不多两个月了,也有小伙伴问我为啥不更了呢?其实我去看了MySQL的全集,准备憋个大招,更新篇长文(我不会告诉你是因为我懒的)。

好了,话不多说,直接开始吧。这篇文章将从查询缓存,索引,优化器,explain,redo日志,undo日志,事务隔离级别,锁等方面来讲,如果想了解某个方面,直接跳到指定目录。

开局一张图

这张图是重点!!!咱要先对MySQL有一个宏观的了解,知道他的执行流程。

一条SQL语句过来的流程是什么样的?那就follow me。哈哈哈哈,皮一下很开心。

1.当客户端连接到MySQL服务器时,服务器对其进行认证。可以通过用户名与密码认证,也可以通过SSL证书进行认证。登录认证后,服务器还会验证客户端是否有执行某个查询的操作权限。

2.在正式查询之前,服务器会检查查询缓存,如果能找到对应的查询,则不必进行查询解析,优化,执行等过程,直接返回缓存中的结果集。

3.MySQL的解析器会根据查询语句,构造出一个解析树,主要用于根据语法规则来验证语句是否正确,比如SQL的关键字是否正确,关键字的顺序是否正确。

而预处理器主要是进一步校验,比如表名,字段名是否正确等

4.查询优化器将解析树转化为查询计划,一般情况下,一条查询可以有很多种执行方式,最终返回相同的结果,优化器就是根据成本找到这其中最优的执行计划

5.执行计划调用查询执行引擎,而查询引擎通过一系列API接口查询到数据

6.得到数据之后,在返回给客户端的同时,会将数据存在查询缓存中

查询缓存

我们先通过show variables like '%query_cache%'来看一下默认的数据库配置,此为本地数据库的配置。

概念

have_query_cache:当前的MYSQL版本是否支持“查询缓存”功能。

query_cache_limit:MySQL能够缓存的最大查询结果,查询结果大于该值时不会被缓存。默认值是1048576(1MB)

query_cache_min_res_unit:查询缓存分配的最小块(字节)。默认值是4096(4KB)。当查询进行时,MySQL把查询结果保存在query cache,但是如果保存的结果比较大,超过了query_cache_min_res_unit的值,这时候MySQL将一边检索结果,一边进行保存结果。他保存结果也是按默认大小先分配一块空间,如果不够,又要申请新的空间给他。如果查询结果比较小,默认的query_cache_min_res_unit可能造成大量的内存碎片,如果查询结果比较大,默认的query_cache_min_res_unit又不够,导致一直分配块空间,所以可以根据实际需求,调节query_cache_min_res_unit的大小。

注:如果上面说的内容有点弯弯绕,那举个现实生活中的例子,比如咱现在要给运动员送水,默认的是500ml的瓶子,如果过来的是少年运动员,可能500ml太大了,他们喝不完,造成了浪费,那我们就可以选择300ml的瓶子,如果过来的是成年运动员,可能500ml不够,那他们一瓶喝完了,又开一瓶,直接不渴为止。那么那样开瓶子也要时间,我们就可以选择1000ml的瓶子。

query_cache_size:为缓存查询结果分配的总内存。

query_cache_type:默认为on,可以缓存除了以select sql_no_cache开头的所有查询结果。

query_cache_wlock_invalidate:如果该表被锁住,是否返回缓存中的数据,默认是关闭的。

原理

MYSQL的查询缓存实质上是缓存SQL的hash值和该SQL的查询结果,如果运行相同的SQL,服务器直接从缓存中去掉结果,而不再去解析,优化,寻找最低成本的执行计划等一系列操作,大大提升了查询速度。

但是万事有利也有弊。

  • 第一个弊端就是如果表的数据有一条发生变化,那么缓存好的结果将全部不再有效。这对于频繁更新的表,查询缓存是不适合的。

比如一张表里面只有两个字段,分别是id和name,数据有一条为1,张三。我使用select * from 表名 where name=“张三”来进行查询,MySQL发现查询缓存中没有此数据,会进行一系列的解析,优化等操作进行数据的查询,查询结束之后将该SQL的hash和查询结果缓存起来,并将查询结果返回给客户端。但是这个时候我有新增了一条数据2,张三。如果我还用相同的SQL来执行,他会根据该SQL的hash值去查询缓存中,那么结果就错了。所以MySQL对于数据有变化的表来说,会直接清空关于该表的所有缓存。这样其实是效率是很差的。

  • 第二个弊端就是缓存机制是通过对SQL的hash,得出的值为key,查询结果为value来存放的,那么就意味着SQL必须完完全全一模一样,否则就命不中缓存。

我们都知道hash值的规则,就算很小的变化,哈希出来的结果差距是很多的,所以select * from 表名 where name=“张三”和SELECT * FROM 表名 WHERE NAME=“张三”和select * from 表名 where name = “张三”,三个SQL哈希出来的值是不一样的,大小写和空格影响了他们,所以并不能命中缓存,但其实他们搜索结果是完全一样的。### 生产如何设置MySQL Query Cache

先来看线上参数:

我们发现将query_cache_type设置为OFF,其实网上资料和各大云厂商提供的云服务器都是将这个功能关闭的,从上面的原理来看,在一般情况下,他的弊端大于优点。

索引

例子

创建一个名为user的表,其包括id,name,age,sex等字段信息。此外,id为主键聚簇索引,idx_name为非聚簇索引。

1
2
3
4
5
6
7
8
复制代码CREATE TABLE `user` (
`id` varchar(10) NOT NULL DEFAULT '',
`name` varchar(10) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们将其设置10条数据,便于下面的索引的理解。

1
2
3
4
5
6
7
8
9
10
复制代码INSERT INTO `user` VALUES ('1', 'andy', '20', '女');
INSERT INTO `user` VALUES ('10', 'baby', '12', '女');
INSERT INTO `user` VALUES ('2', 'kat', '12', '女');
INSERT INTO `user` VALUES ('3', 'lili', '20', '男');
INSERT INTO `user` VALUES ('4', 'lucy', '22', '女');
INSERT INTO `user` VALUES ('5', 'bill', '20', '男');
INSERT INTO `user` VALUES ('6', 'zoe', '20', '男');
INSERT INTO `user` VALUES ('7', 'hay', '20', '女');
INSERT INTO `user` VALUES ('8', 'tony', '20', '男');
INSERT INTO `user` VALUES ('9', 'rose', '21', '男');

聚簇索引(主键索引)

先来一张图镇楼,接下来就是看图说话。

他包含两个特点:

1.使用记录主键值的大小来进行记录和页的排序。

页内的记录是按照主键的大小顺序排成一个单项链表。

各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。

2.叶子节点存储的是完整的用户记录。

1
复制代码注:聚簇索引不需要我们显示的创建,他是由InnoDB存储引擎自动为我们创建的。如果没有主键,其也会默认创建一个。

非聚簇索引(二级索引)

上面的聚簇索引只能在搜索条件是主键时才能发挥作用,因为聚簇索引可以根据主键进行排序的。如果搜索条件是name,在刚才的聚簇索引上,我们可能遍历,挨个找到符合条件的记录,但是,这样真的是太蠢了,MySQL不会这样做的。

如果我们想让搜索条件是name的时候,也能使用索引,那可以多创建一个基于name的二叉树。如下图。

他与聚簇索引的不同:

1.叶子节点内部使用name字段排序,叶子节点之间也是使用name字段排序。

2.叶子节点不再是完整的数据记录,而是name和主键值。

为什么不再是完整信息?

MySQL只让聚簇索引的叶子节点存放完整的记录信息,因为如果有好几个非聚簇索引,他们的叶子节点也存放完整的记录绩效,那就不浪费空间啦。

如果我搜索条件是基于name,需要查询所有字段的信息,那查询过程是啥?

1.根据查询条件,采用name的非聚簇索引,先定位到该非聚簇索引某些记录行。

2.根据记录行找到相应的id,再根据id到聚簇索引中找到相关记录。这个过程叫做回``表。

联合索引

图就不画了,简单来说,如果name和age组成一个联合索引,那么先按name排序,如果name一样,就按age排序。

一些原则

1.最左前缀原则。一个联合索引(a,b,c),如果有一个查询条件有a,有b,那么他则走索引,如果有一个查询条件没有a,那么他则不走索引。

2.使用唯一索引。具有多个重复值的列,其索引效果最差。例如,存放姓名的列具有不同值,很容易区分每行。而用来记录性别的列,只含有“男”,“女”,不管搜索哪个值,都会得出大约一半的行,这样的索引对性能的提升不够高。

3.不要过度索引。每个额外的索引都要占用额外的磁盘空间,并降低写操作的性能。在修改表的内容时,索引必须进行更新,有时可能需要重构,因此,索引越多,所花的时间越长。

4、索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);

5.一定要设置一个主键。前面聚簇索引说到如果不指定主键,InnoDB会自动为其指定主键,这个我们是看不见的。反正都要生成一个主键的,还不如我们设置,以后在某些搜索条件时还能用到主键的聚簇索引。

6.主键推荐用自增id,而不是uuid。上面的聚簇索引说到每页数据都是排序的,并且页之间也是排序的,如果是uuid,那么其肯定是随机的,其可能从中间插入,导致页的分裂,产生很多表碎片。如果是自增的,那么其有从小到大自增的,有顺序,那么在插入的时候就添加到当前索引的后续位置。当一页写满,就会自动开辟一个新的页。

1
复制代码注:如果自增id用完了,那将字段类型改为bigint,就算每秒1万条数据,跑100年,也没达到bigint的最大值。

万年面试题(为什么索引用B+树)

1、 B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

2、由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

优化器

在开篇的图里面,我们知道了SQL语句从客户端经由网络协议到查询缓存,如果没有命中缓存,再经过解析工作,得到准确的SQL,现在就来到了我们这模块说的优化器。

首先,我们知道每一条SQL都有不同的执行方法,要不通过索引,要不通过全表扫描的方式。

那么问题就来了,MySQL是如何选择时间最短,占用内存最小的执行方法呢?

什么是成本?

1.I/O成本。数据存储在硬盘上,我们想要进行某个操作需要将其加载到内存中,这个过程的时间被称为I/O成本。默认是1。

2.CPU成本。在内存对结果集进行排序的时间被称为CPU成本。默认是0.2。

单表查询的成本

先来建一个用户表dev_user,里面包括主键id,用户名username,密码password,外键user_info_id,状态status,外键main_station_id,是否外网访问visit,这七个字段。索引有两个,一个是主键的聚簇索引,另一个是显式添加的以username为字段的唯一索引uname_unique。

如果搜索条件是select * from dev_user where username=’XXX’,那么MySQL是如何选择相关索引呢?

1.使用所有可能用到的索引

我们可以看到搜索条件username,所以可能走uname_unique索引。也可以做聚簇索引,也就是全表扫描。

2.计算全表扫描代价

我们通过show table status like ‘dev_user’命令知道rows和data_length字段,如下图。

rows:表示表中的记录条数,但是这个数据不准确,是个估计值。

data_length:表示表占用的存储空间字节数。

data_length=聚簇索引的页面数量X每个页面的大小

反推出页面数量=1589248÷16÷1024=97

I/O成本:97X1=97

CPU成本:6141X0.2=1228

总成本:97+1228=1325

3.计算使用不同索引执行查询的代价

因为要查询出满足条件的所有字段信息,所以要考虑回表成本。

I/O成本=1+1X1=2(范围区间的数量+预计二级记录索引条数)

CPU成本=1X0.2+1X0.2=0.4(读取二级索引的成本+回表聚簇索引的成本)

总成本=I/O成本+CPU成本=2.4

4.对比各种执行方案的代价,找出成本最低的那个

上面两个数字一对比,成本是采用uname_unique索引成本最低。

多表查询的成本

对于两表连接查询来说,他的查询成本由下面两个部分构成:

  • 单次查询驱动表的成本
  • 多次查询被驱动表的成本(具体查询多次取决于对驱动表查询的结果集有多少个记录)

index dive

如果前面的搜索条件不是等值,而是区间,如select * from dev_user where username>'admin' and username<'test'这个时候我们是无法看出需要回表的数量。

步骤1:先根据username>’admin’这个条件找到第一条记录,称为区间最左记录。

步骤2:再根据username<’test’这个条件找到最后一条记录,称为区间最右记录。

步骤3:如果区间最左记录和区间最右记录相差不是很远,可以准确统计出需要回表的数量。如果相差很远,就先计算10页有多少条记录,再乘以页面数量,最终模糊统计出来。

Explain

产品来索命

产品:为什么这个页面出来这么慢?

开发:因为你查的数据多呗,他就是这么慢

产品:我不管,我要这个页面快点,你这样,客户怎么用啊

开发:。。。。。。。你行你来

哈哈哈哈,不瞎BB啦,如果有些SQL贼慢,我们需要知道他有没有走索引,走了哪个索引,这个时候我就需要通过explain关键字来深入了解MySQL内部是如何执行的。

id

一般来说一个select一个唯一id,如果是子查询,就有两个select,id是不一样的,但是凡事有例外,有些子查询的,他们id是一样的。

这是为什么呢?

那是因为MySQL在进行优化的时候已经将子查询改成了连接查询,而连接查询的id是一样的。

select_type

  • simple:不包括union和子查询的查询都算simple类型。
  • primary:包括union,union all,其中最左边的查询即为primary。
  • union:包括union,union all,除了最左边的查询,其他的查询类型都为union。

table

显示这一行是关于哪张表的。

type:访问方法

  • ref:普通二级索引与常量进行等值匹配
  • ref_or_null:普通二级索引与常量进行等值匹配,该索引可能是null
  • const:主键或唯一二级索引列与常量进行等值匹配
  • range:范围区间的查询
  • all:全表扫描

possible_keys

对某表进行单表查询时可能用到的索引

key

经过查询优化器计算不同索引的成本,最终选择成本最低的索引

rows

  • 如果使用全表扫描,那么rows就代表需要扫描的行数
  • 如果使用索引,那么rows就代表预计扫描的行数

filtered

  • 如果全表扫描,那么filtered就代表满足搜索条件的记录的满分比
  • 如果是索引,那么filtered就代表除去索引对应的搜索,其他搜索条件的百分比

redo日志(物理日志)

InnoDB存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作都是将页的数据加载到内存中,然后进行操作,再将数据刷回到硬盘上。

那么问题就来了,如果我要给张三转账100块钱,事务已经提交了,这个时候InnoDB把数据加载到内存中,这个时候还没来得及刷入硬盘,突然停电了,数据库崩了。重启之后,发现我的钱没有转成功,这不是尴尬了吗?

解决方法很明显,我们在硬盘加载到内存之后,进行一系列操作,一顿操作猛如虎,还未刷新到硬盘之前,先记录下,在XXX位置我的记录中金额减100,在XXX位置张三的记录中金额加100,然后再进行增删改查操作,最后刷入硬盘。如果未刷入硬盘,在重启之后,先加载之前的记录,那么数据就回来了。

这个记录就叫做重做日志,即redo日志。他的目的是想让已经提交的事务对数据的修改是永久的,就算他重启,数据也能恢复出来。

log buffer(日志缓冲区)

为了解决磁盘速度过慢的问题,redo日志不能直接写入磁盘,咱先整一大片连续的内存空间给他放数据。这一大片内存就叫做日志缓冲区,即log buffer。到了合适的时候,再刷入硬盘。至于什么时候是合适的,这个下一章节说。

我们可以通过show VARIABLES like 'innodb_log_buffer_size'命令来查看当前的日志缓存大小,下图为线上的大小。

redo日志刷盘时机

由于redo日志一直都是增长的,且内存空间有限,数据也不能一直待在缓存中,
我们需要将其刷新至硬盘上。

那什么时候刷新到硬盘呢?

  • log buffer空间不足。上面有指定缓冲区的内存大小,MySQL认为日志量已经占了
    总容量的一半左右,就需要将这些日志刷新到磁盘上。
  • 事务提交时。我们使用redo日志的目的就是将他未刷新到磁盘的记录保存起来,防止
    丢失,如果数据提交了,我们是可以不把数据提交到磁盘的,但为了保证持久性,必须
    把修改这些页面的redo日志刷新到磁盘。
  • 后台线程不同的刷新
    后台有一个线程,大概每秒都会将log buffer里面的redo日志刷新到硬盘上。
  • checkpoint
    下下小节讲

redo日志文件组

我们可以通过show variables like 'datadir'命令找到相关目录,底下有两个文件,
分别是ib_logfile0和ib_logfile1,如下图所示。

我们将缓冲区log buffer里面的redo日志刷新到这个两个文件里面,他们写入的方式
是循环写入的,先写ib_logfile0,再写ib_logfile1,等ib_logfile1写满了,再写ib_logfile0。
那这样就会存在一个问题,如果ib_logfile1写满了,再写ib_logfile0,之前ib_logfile0的内容
不就被覆盖而丢失了吗?
这就是checkpoint的工作啦。

checkpoint

redo日志是为了系统崩溃后恢复脏页用的,如果这个脏页可以被刷新到磁盘上,那么
他就可以功成身退,被覆盖也就没事啦。

冲突补习

从系统运行开始,就不断的修改页面,会不断的生成redo日志。redo日志是不断
递增的,MySQL为其取了一个名字日志序列号Log Sequence Number,简称lsn。
他的初始化的值为8704,用来记录当前一共生成了多少redo日志。

redo日志是先写入log buffer,之后才会被刷新到磁盘的redo日志文件。MySQL为其
取了一个名字flush_to_disk_lsn。用来说明缓存区中有多少的脏页数据被刷新到磁盘上啦。
他的初始值和lsn一样,后面的差距就有了。

做一次checkpoint分为两步

  • 计算当前系统可以被覆盖的redo日志对应的lsn最大值是多少。redo日志可以被覆盖,
    意味着他对应的脏页被刷新到磁盘上,只要我们计算出当前系统中最早被修改的oldest_modification,
    只要系统中lsn小于该节点的oldest_modification值磁盘的redo日志都是可以被覆盖的。
  • 将lsn过程中的一些数据统计。

undo日志(这部分不是很明白,所以大概说了)

基本概念

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

举个例子:

insert into a(id) values(1);(redo)
这条记录是需要回滚的。
回滚的语句是delete from a where id = 1;(undo)

试想想看。如果没有做insert into a(id) values(1);(redo)
那么delete from a where id = 1;(undo)这句话就没有意义了。

现在看下正确的恢复:
先insert into a(id) values(1);(redo)
然后delete from a where id = 1;(undo)
系统就回到了原先的状态,没有这条记录了

存储方式

是存在段之中。

事务

引言

事务中有一个隔离性特征,理论上在某个事务对某个数据进行访问时,其他事务应该排序,当该事务提交之后,其他事务才能继续访问这个数据。

但是这样子对性能影响太大,我们既想保持事务的隔离性,又想让服务器在出来多个事务时性能尽量高些,所以只能舍弃一部分隔离性而去性能。

事务并发执行的问题

  • 脏写(这个太严重了,任何隔离级别都不允许发生)

sessionA:修改了一条数据,回滚掉sessionB:修改了同一条数据,提交掉

对于sessionB来说,明明数据更新了也提交了事务,不能说自己啥都没干

  • 脏读:一个事务读到另一个未提交事务修改的数据

session A:查询,得到某条数据session B:修改某条数据,但是最后回滚掉啦

session A:在sessionB修改某条数据之后,在回滚之前,读取了该条记录

对于session A来说,读到了session回滚之前的脏数据

  • 不可重复读:前后多次读取,同一个数据内容不一样

session A:查询某条记录
session B : 修改该条记录,并提交事务
session A : 再次查询该条记录,发现前后查询不一致

  • 幻读:前后多次读取,数据总量不一致

session A:查询表内所有记录
session B : 新增一条记录,并查询表内所有记录
session A : 再次查询该条记录,发现前后查询不一致

四种隔离级别

数据库都有的四种隔离级别,MySQL事务默认的隔离级别是可重复读,而且MySQL可以解决了幻读的问题。

  • 未提交读:脏读,不可重复读,幻读都有可能发生
  • 已提交读:不可重复读,幻读可能发生
  • 可重复读:幻读可能发生
  • 可串行化:都不可能发生

但凡事没有百分百,emmmm,其实MySQL并没有百分之百解决幻读的问题。

举个例子:

session A:查询某条不存在的记录。

session B:新增该条不存在的记录,并提交事务。

session A:再次查询该条不存在的记录,是查询不出来的,但是如果我尝试修改该条记录,并提交,其实他是可以修改成功的。

MVCC

版本链:对于该记录的每次更新,都会将值放在一条undo日志中,算是该记录的一个旧版本,随着更新次数的增多,所有版本都会被roll_pointer属性连接成一个链表,即为版本链。

readview:

  • 未提交读:因为可以读到未提交事务修改的记录,所以可以直接读取记录的最新版本就行
  • 已提交读:每次读取之前都生成一个readview
  • 可重复读:只有在第一次读取的时候才生成readview
  • 可串行化:InnoDB涉及了加锁的方式来访问记录

求个关注

小老弟不容易,忙了好几天,终于写好。

参考文献

【原创】面试官:讲讲mysql表设计要注意啥

【原创】杂谈自增主键用完了怎么办

MySQL 是怎样运行的:从根儿上理解 MySQL

详细分析MySQL事务日志(redo log和undo log)

本文转载自: 掘金

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

一文彻底搞懂Kotlin中的委托

发表于 2020-01-06
1. 什么是委托?

委托,也就是委托模式,它是23种经典设计模式种的一种,又名代理模式,在委托模式中,有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项技巧,其他的几种设计模式如:策略模式、状态模式和访问者模式都是委托模式的具体场景应用。

委托模式中,有三个角色,约束、委托对象和被委托对象。

委托模式中的角色.png

  • 约束: 约束是接口或者抽象类,它定义了通用的业务类型,也就是需要被代理的业务
  • 被委托对象: 具体的业务逻辑执行者
  • 委托对象: 负责对真是角色的应用,将约束累定义的业务委托给具体的委托对象。
2. 委托的具体场景

上一节讲了委托的定义和它所包含的几个角色,那么具体该怎么运用呢?我们以一个实际的例子来看看。

现在很多年轻人都爱完游戏,不管是吃鸡、王者荣耀还是英雄联盟。它们都是有等级之分的:青铜->白银->黄金->铂金->钻石->宗师->王者,等级越高,代表你越厉害,就拿英雄联盟来说,我们多数混迹在白银黄金阶段,要上钻石宗师段位非常困难。比如你排位打了很久,就差几场就能上宗师了,老是打不上去,这个时候怎么办呢?好办,现在有很多游戏代练,委托游戏代练给你打上去就好了。这其实就是一个委托模式。代码该怎么写呢?一起来看看:

首先,我们定义约束类,定义我们需要委托的业务,就拿这个场景来说,我们的业务就是打排位赛,升级。因此,定义个约束类(接口)IGamePlayer:

1
2
3
4
5
6
7
复制代码// 约束类
interface IGamePlayer {
// 打排位赛
fun rank()
// 升级
fun upgrade()
}

约束类中,定义了我们要代理的业务rank(),upgrade(),然后,我们就定义被委托对象,也就是游戏代练:

1
2
3
4
5
6
7
8
9
10
11
复制代码// 被委托对象,本场景中的游戏代练
class RealGamePlayer(private val name: String): IGamePlayer{
override fun rank() {
println("$name 开始排位赛")
}

override fun upgrade() {
println("$name 升级了")
}

}

如上,我们定义了一个被委托对象RealGamePlayer, 它有一个属性name,它实现了我们约定的业务(实现了接口方法)。

接下来,就是委托角色:

1
2
复制代码// 委托对象
class DelegateGamePlayer(private val player: IGamePlayer): IGamePlayer by player

我们定义了一个委托类DelegateGamePlayer, 现在游戏代练有很多,水平有高有低,如果发现水平不行,我们可以随时换,因此,我们把被委托对象作为委托对象的属性,通过构造方法传进去。

注意:在kotlin 中,委托用关键字by 修饰,by后面就是你委托的对象,可以是一个表达式。因此在本例中,通过by player 委托给了具体的被委托对象。

最后,看一下场景测试类:

1
2
3
4
5
6
7
复制代码// Client 场景测试
fun main() {
val realGamePlayer = RealGamePlayer("张三")
val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
delegateGamePlayer.rank()
delegateGamePlayer.upgrade()
}

我们定义了一个游戏代练,叫张三,将它传递给委托类,然后就可以开始排位和升级的业务了,而最终谁完成了排位赛和升级了,当然是我们的被委托对象,也就是游戏代练–张三。

运行,结果如下:

1
2
复制代码张三 开始排位赛
张三 升级了

小结:以上就是委托的应用,再来回顾一下它的定义:2个对象参与处理同一请求,这个请求就是我们约束类的逻辑,因此委托类(DelegateGamePlayer)和被委托类(RealGamePlayer)都需要实现我们的约束接口IGamePlayer。

3. 属性委托

在Kotlin 中,有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们,但是很麻烦,各种样板代码存在,我们知道,Kotlin可是宣称要实现零样板代码的。为了解决这些问题呢?Kotlin标准为我们提供了委托属性。

1
2
3
4
复制代码class Test {
// 属性委托
var prop: String by Delegate()
}

委托属性的语法如下:

val/var <属性名>: <类型> by <表达式>

跟我们前面将的委托类似,只不过前面是类委托,这里属性委托。

3.1 属性委托的原理

前面讲的委托中,我们有个约束角色,里面定义了代理的业务逻辑。而委托属性呢?其实就是上面的简化,被代理的逻辑就是这个属性的get/set方法。get/set会委托给被委托对象的setValue/getValue方法,因此被委托类需要提供setValue/getValue这两个方法。如果是val 属性,只需提供getValue。如果是var 属性,则setValue/getValue都需要提供。

比如上面的Delegate类:

1
2
3
4
5
6
7
8
9
复制代码class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}

其中的参数解释如下:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
  • property —— 必须是类型 KProperty<*>或其超类型。
  • value —— 必须与属性同类型或者是它的子类型。

测试如下:

1
2
3
4
复制代码fun main() {
println(Test().prop)
Test().prop = "Hello, Android技术杂货铺!"
}

打印结果如下:

1
2
复制代码Test@5197848c, thank you for delegating 'prop' to me!
Hello, Android技术杂货铺! has been assigned to 'prop' in Test@17f052a3.
3.2 另一种实现属性委托的方式

上面我们讲了,要实现属性委托,就必须要提供getValue/setValue方法,对于比较懒的同学可能就要说了,这么复杂的参数,还要每次都要手写,真是麻烦,一不小心就写错了。确实是这样,为了解决这个问题, Kotlin 标准库中声明了2个含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

1
2
3
4
5
6
7
8
复制代码interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

被委托类 实现这两个接口其中之一就可以了,val 属性实现ReadOnlyProperty,var属性实现ReadOnlyProperty。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// val 属性委托实现
class Delegate1: ReadOnlyProperty<Any,String>{
override fun getValue(thisRef: Any, property: KProperty<*>): String {
return "通过实现ReadOnlyProperty实现,name:${property.name}"
}
}
// var 属性委托实现
class Delegate2: ReadWriteProperty<Any,Int>{
override fun getValue(thisRef: Any, property: KProperty<*>): Int {
return 20
}

override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
println("委托属性为: ${property.name} 委托值为: $value")
}

}
// 测试
class Test {
// 属性委托
val d1: String by Delegate1()
var d2: Int by Delegate2()
}

如上代码所示,定义了2个属性代理,都通过 ReadOnlyProperty / ReadWriteProperty 接口实现。

测试代码如下:

1
2
3
4
复制代码   val test = Test()
println(test.d1)
println(test.d2)
test.d2 = 100

打印结果:

1
2
3
复制代码通过实现ReadOnlyProperty实现,name:d1
20
委托属性为: d2 委托值为: 100

可以看到,与手动实现setValue/getValue效果一样,但是这样写代码就方便了很多了。

4. Kotlin 标准库中提供几个委托

Kotlin 标准库中提供了几种委托,例如:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
4.1 延迟属性 lazy

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

1
2
3
4
5
6
7
8
9
10
11
复制代码val lazyProp: String by lazy {
println("Hello,第一次调用才会执行我!")
"西哥!"
}

// 打印lazyProp 3次,查看结果
fun main() {
println(lazyProp)
println(lazyProp)
println(lazyProp)
}

打印结果如下:

1
2
3
4
复制代码Hello,第一次调用才会执行我!
西哥!
西哥!
西哥!

可以看到,只有第一次调用,才会执行lambda表达式中的逻辑,后面调用只会返回lambda表达式的最终值。

4.1.1 lazy 也可以接受参数

lazy延迟初始化是可以接受参数的,提供了如下三个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码/**
* Specifies how a [Lazy] instance synchronizes initialization among multiple threads.
*/
public enum class LazyThreadSafetyMode {

/**
* Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
*/
SYNCHRONIZED,

/**
* Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
* but only the first returned value will be used as the value of [Lazy] instance.
*/
PUBLICATION,

/**
* No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
*
* This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
*/
NONE,
}

三个参数解释如下:

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION: 初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

使用如下:

1
2
3
4
复制代码val lazyProp: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
println("Hello,第一次调用才会执行我!")
"西哥!"
}

如果你指定的参数为LazyThreadSafetyMode.SYNCHRONIZED,则可以省略,因为lazy默认就是使用的LazyThreadSafetyMode.SYNCHRONIZED。

4.2 可观察属性 Observable

如果你要观察一个属性的变化过程,那么可以将属性委托给Delegates.observable, observable函数原型如下:

1
2
3
4
5
复制代码public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}

接受2个参数:

  • initialValue: 初始值
  • onChange: 属性值被修改时的回调处理器,回调有三个参数property,oldValue,newValue,分别为: 被赋值的属性、旧值与新值。

使用如下:

1
2
3
4
5
6
7
8
9
复制代码var observableProp: String by Delegates.observable("默认值:xxx"){
property, oldValue, newValue ->
println("property: $property: $oldValue -> $newValue ")
}
// 测试
fun main() {
observableProp = "第一次修改值"
observableProp = "第二次修改值"
}

打印如下:

1
2
复制代码property: var observableProp: kotlin.String: 默认值:xxx -> 第一次修改值 
property: var observableProp: kotlin.String: 第一次修改值 -> 第二次修改值

可以看到,每一次赋值,都能观察到值的变化过程。

4.2.1 vetoable 函数

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效。

来看这样一个例子:声明一个Int类型的属性vetoableProp,如果新的值比旧值大,则生效,否则不生效。

代码如下:

1
2
3
4
5
复制代码var vetoableProp: Int by Delegates.vetoable(0){
_, oldValue, newValue ->
// 如果新的值大于旧值,则生效
newValue > oldValue
}

测试代码:

1
2
3
4
5
6
7
8
9
复制代码fun main() {
println("vetoableProp=$vetoableProp")
vetoableProp = 10
println("vetoableProp=$vetoableProp")
vetoableProp = 5
println("vetoableProp=$vetoableProp")
vetoableProp = 100
println("vetoableProp=$vetoableProp")
}

打印如下:

1
2
3
4
5
6
7
复制代码vetoableProp=0
0 -> 10
vetoableProp=10
10 -> 5
vetoableProp=10
10 -> 100
vetoableProp=100

可以看到10 -> 5 的赋值没有生效。

4.3 属性存储在映射中

还有一种情况,在一个映射(map)里存储属性的值,使用映射实例自身作为委托来实现委托属性,如:

1
2
3
4
复制代码class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

测试如下:

1
2
3
4
5
6
7
复制代码fun main() {
val user = User(mapOf(
"name" to "西哥",
"age" to 25
))
println("name=${user.name} age=${user.age}")
}

打印如下:

1
复制代码name=西哥 age=25

使用映射实例自身作为委托来实现委托属性,可以使用在json解析中,因为json本身就可以解析成一个map。不过,说实话,我暂时还没有发现这种使用场景的好处或者优势,如果有知道的同学,评论区告知,谢谢!

5. 总结

委托在kotlin中占有举足轻重的地位,特别是属性委托,lazy延迟初始化使用非常多,还有其他一些场景,比如在我们安卓开发中,使用属性委托来封装SharePreference,大大简化了SharePreference的存储和访问。在我们软件开发中,始终提倡的是高内聚,低耦合。而委托,就是内聚,可以降低耦合。另一方面,委托的使用,也能减少很多重复的样板代码。

参考:www.kotlincn.net/docs/refere…

如果你喜欢我的文章,就关注下我的公众号 Android技术杂货铺 、 简书 或者Github!
微信公众号:Android技术杂货铺

简书:www.jianshu.com/u/35167a70a…

GitHub:github.com/pinguo-zhou…

公众号.png

本文转载自: 掘金

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

TCP BBR v2

发表于 2020-01-05

前言

上次简单介绍了BBR的特点和基本的实现思路,但是BBR并不就是完美的吊打一切其他算法的存在。在2018年7月的时候google发布了BBR的相关改进的计划,目前已经有BBR v2 alpha版本的试用文档,正式版尚未发布。所以借助BBR v2的更新内容简单总结一下BBR的一些不足或者缺点。

BBR v2的计划更新内容

  • 改进与其他算法共存时的公平性:调整BBR探测带宽时的时间来和CUBIC/Reno共存
  • 降低排队压力(丢包和排队延时),在计算以下指标时将丢包和ECN考虑在内
    • in-flight 数据的安全范围
    • 退出STARTUP的时机
  • 加快min_rtt的收敛:增加PROBE_RTT的频率
  • 降低PROBE_RTT时的极端性

计算in-flight数据大小的新模型

v2版本使用新的模型来计算in-flight数据的大小范围,其中包含三个参数:inflight_lo、inflight_high、inflight_prob。

  • inflight_lo:基于丢包和ECN信号计算出来的in-flight数据包最小值
  • inflight_hi:出现丢包和ECN信号前的in-flight数据包最大值
  • inflight_prob:探测带宽时超过inflight_hi的增量

STARTUP阶段

BBR v1中,STARTUP会持续增加发送速度,直到探测到的最大带宽趋于平稳然后退出。但是这个阶段并不会把丢包考虑在内,所以在STARTUP阶段可能会出现丢包严重的现象。v2版本中在STARTUP阶段的退出条件中增加了一项:当发现丢包或者ECN时,也会提前退出STARTUP阶段,同时更新inflight_hi变量。STARTUP阶段模式的另一个修改是将拥塞窗口增益由2.89改为2,这个改动反而会加重因为ACK聚合而导致失速问题,而BBR对此的解决方案则是BBR Extra-CWND,这里还没有太弄明白,后续再详细了解下。

DRAIN阶段

DRAIN阶段会降低发送速度,尝试清空中间设备的缓存,直到inflight数据少于预估的带宽(”drain to target”)。这个阶段并没有改动。

PROBE_BW阶段

v2版本中PROBE_BW分为三个阶段:cruise(平稳)、up(探测更多带宽)和down(收敛到可用带宽)。同时为了与其他基于丢包的算法共存,PROBE_BW周期的时长不再是8个min_rtt,而是min(T_bbr, T_reno),T_bbr是时间范围为2-5s,T_reno是min(BDP, 50)* RTT。BDP过期时间不在是过去的十轮,而是更长的2个PROBE_BW周期时长。

cruise

v1版本中平稳阶段会使inflight保持在一个恒定的值,而v2版本则会预留一部分空间(让给其他连接),使inflight在inflight_lo和inflight_hi之间,并且会根据丢包和ECN事件减小inflight_lo的值。

up

v1版本中在探测更多带宽时简单地增加1/4的发送速度,而v2版本中采用了指数增长的方式,先慢后快地探测可用带宽,直到出现丢包或者新的可用带宽大于预估带宽的1.25倍,同时会在出现丢包时更新inflight_hi。

down

v1版本中每次收敛只会降低1/4,而v2版本中则直接采用了”drain to target”的策略,会直接收敛到预估带宽。后续这个阶段可能会直接代替DRAIN阶段。

PROBE_RTT阶段

v1版本中,在进入PROBE_RTT阶段时,为了探测min_RTT会直接将窗口降到4个,同时为了尽量减小PROBE_RTT带来的吞吐降低的影响,PROBE_RTT的频率比较低(10s一次)。这就使得BBR在收敛时的速度很慢(通常需要20~30s)。v2版本中对此作了两点改进:

  • 窗口降低的更温和,不再是4而是0.75*BDP
  • 探测得更频繁,不再是10s一次而是2.5s一次

通过这样的调整,使得PROBE_RTT不再那么激进,也可以有效提高收敛的速度。

总结

BBR v2版本总的来说更加保守了,把丢包和ECN加入了考虑范围,同时还考虑到与其他算法共存时的情况。

附:ECN即显式拥塞通知(Explicit Congestion Notification),它可以通过显式的通知来告知网络两端发生了拥塞。具体可以参考维基百科。

本文转载自: 掘金

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

最全面的Kotlin协程 Coroutine/Channe

发表于 2020-01-03

协程这个概念在1958年就开始出现, 比线程更早, 目前很多语言开始原生支, Java没有原生协程但是可以大型公司都自己或者使用第三方库来支持协程编程, 但是Kotlin原生支持协程.

本文更新自: 2021/8/18 协程_v1.5 kotlin_v1.5.21

我认为协程的核心就是一个词: 作用域, 理解什么是作用域就理解协程了

什么是协程

协程是协作式任务, 线程是抢占式任务, 本质上两者都属于并发

Kotlin协程就是线程库不是协程? 内部代码用的线程池?

  1. 最知名的协程语言Go内部也是维护了线程
  2. 协程只是方便开发者处理异步(可以减少线程数量), 线程才能发挥性能
  3. 协程是一种概念, 无关乎具体实现方式
  4. kotlin标准库中的协程不包含线程池代码, 仅扩展库才内部实现线程池

协程设计来源

  1. Kotlin的协程完美复刻了谷歌的Go语言的协程设计模式(作用域/channel/select), 将作用域用对象来具化出来; 且可以更好地控制作用域生命周期;
  2. await模式(JavaScript的异步任务解决方案)
  3. Kotlin参考RxJava响应式框架创造出Flow
  4. 使用协程开始就不需要考虑线程的问题, 只需要在不同场景使用不同的调度器(调度器会对特定任务进行优化)就好

特性

使用场景

假设首页存在七个接口网络请求(后端人员处理差)的情况一个个使用串行网络请求的时间比并发网络请求慢了接近七倍.

不是说这种并发只能协程实现, 但是协程实现是目前最优解

目前计算机都是通过多核CPU提升计算能力, 所以熟练掌握并发编程是未来的趋势

协程优势

  1. 并发实现方便
  2. 没有回调嵌套发生. 代码结构清晰
  3. 易于封装扩展
  4. 创建协程性能开销优于创建线程, 一个线程可以运行多个协程, 单线程即可异步

实验特性

协程在Kotlin1.3时候放出正式版本, 目前仍然存在不稳定函数(不影响项目开发), 通过注解标识

1
2
3
4
5
bash复制代码@FlowPreview 代表可能以后存在Api函数变动

@ExperimentalCoroutinesApi 代表目前可能存在不稳定的因素的函数

@ObsoleteCoroutinesApi 可能存在被废弃的可能

构成

Kotlin的协程主要构成分为三部分

  1. CoroutineScope 协程作用域: 每个协程体都存在一个作用域, 异步还是同步由该作用域决定
  2. Channel 通道: 数据如同一个通道进行发送和接收, 可以在协程之间互相传递数据或者控制阻塞和继续
  3. Flow 响应流: 类似RxJava等结构写法

推荐项目架构 MVVM + Kotlin + Coroutine + JetPack主要带来的优势;

  1. 简洁, 减少70%左右代码
  2. 双向数据绑定(DataBinding)
  3. 并发异步任务(网络)倍增速度
  4. 更健壮的数据保存和恢复

如果你想取代RxJava那么以下两个库我强烈推荐

框架 描述
Net 专为Android设计的协程并发网络请求库, 其中计时器/轮询器也有使用协程Channel设计
Channel 基于协程/LiveData实现的事件分发框架

依赖

这里我们使用协程扩展库, kotlin标准库的协程太过于简陋不适用于开发者使用

1
2
groovy复制代码implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"

创建协程

开启主协程的分为三种方式

生命周期和App一致, 无法取消(不存在Job), 不存在线程阻塞

1
2
3
4
5
6
7
kotlin复制代码fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L)
println("World!")
}
Thread.sleep(2000) // 防止JVM虚拟机退出
}

这里说的是GlobalScope没有Job, 但是启动的launch都是拥有Job的. GlobalScope本身就是一个作用域, launch属于其子作用域;

不存在线程阻塞, 可以取消, 可以通过CoroutineContext控制协程生命周期

1
2
3
4
5
kotlin复制代码fun main() {
CoroutineScope(Dispatchers.IO).launch {
}
Thread.sleep(1000)
}

线程阻塞, 适用于单元测试, 不需要延迟阻塞防止JVM虚拟机退出. runBlocking属于全局函数可以在任意地方调用

一般我们在项目中是不会使用runBlocking, 因为阻塞主线程没有开启的任何意义

1
2
3
kotlin复制代码fun main() = runBlocking { 
// 阻塞线程直到协程作用域内部所有协程执行完毕
}

创建作用域

协程内部还可以使用函数创建其他协程作用域, 分为两种创建函数:

  1. CoroutineScope的扩展函数, 只有在作用域内部才能创建其他的作用域
  2. suspend修饰的函数内部
  3. 协程永远会等待其内部作用域内所有协程都执行完毕后才会关闭协程

在主协程内还可以创建子协程作用域, 创建函数分为两种

  1. 阻塞作用域(串行): 会阻塞当前作用域
  2. 挂起作用域(并发): 不会阻塞当前作用域

同步作用域函数

都属于suspend函数

  • withContext 可以切换调度器, 有返回结果
  • coroutineScope 创建一个协程作用域, 该作用域会阻塞当前所在作用域并且等待其子协程执行完才会恢复, 有返回结果
  • supervisorScope 使用SupervisorJob的coroutineScope, 异常不会取消父协程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
// 返回结果; 可以和当前协程的父协程存在交互关系, 主要作用为来回切换调度器

public suspend inline operator fun <T> CoroutineDispatcher.invoke(
noinline block: suspend CoroutineScope.() -> T
): T = withContext(this, block)
// withContext工具函数而已

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T

异步作用域函数

这两个函数都不属于suspend, 只需要CoroutineScope就可以调用

  • launch: 异步并发, 没有返回结果
  • async: 异步并发, 有返回结果
1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job

public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>

并发

同一个协程作用域中的异步任务遵守顺序原则开始执行; 适用于串行网络请求, 在一个异步任务需要上个异步任务的结果时.

协程挂起需要时间, 所以异步协程永远比同步代码执行慢

1
2
3
4
5
6
7
kotlin复制代码fun main() = runBlocking<Unit> {
launch {
System.err.println("(Main.kt:34) 后执行")
}

System.err.println("(Main.kt:37) 先执行")
}

当在协程作用域中使用async函数时可以创建并发任务

1
2
3
4
5
kotlin复制代码public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>

示例

1
2
3
4
5
6
7
kotlin复制代码fun main() = runBlocking<Unit> {
val name = async { getName() }
val title = async { getTitle() }

System.err.println("(Main.kt:35) result = ${name.await() + title.await()}")
delay(2000)
}
  1. 返回对象Deferred; 通过函数await获取结果值
  2. Deferred集合还可以使用awaitAll()等待全部完成
  3. 不执行await任务也会等待执行完协程关闭
  4. 如果Deferred不执行await函数则async内部抛出的异常不会被logCat或tryCatch捕获, 但是依然会导致作用域取消和异常崩溃; 但当执 行await时异常信息会重新抛出

惰性并发

将async函数中的start设置为CoroutineStart.LAZY时则只有调用Deferred对象的await时才会开始执行异步任务(或者执行start函数)

启动模式

  1. DEFAULT 立即执行
  2. LAZY 直到Job执行start或者join才开始执行
  3. ATOMIC 在作用域开始执行之前无法取消
  4. UNDISPATCHED 不执行任何调度器, 直接在当前线程中执行, 但是会根据第一个挂起函数的调度器切换

异常

协程中发生异常, 则父协程取消并且父协程其他的子协程同样全部取消

Deferred

继承自Job

提供一个全局函数用于创建CompletableDeferred对象, 该对象可以实现自定义Deferred功能

1
2
3
4
5
6
7
8
9
kotlin复制代码public suspend fun await(): T 
// 结果
public val onAwait: SelectClause1<T>
// 在select中使用

public fun getCompleted(): T
// 如果完成[isCompleted]则返回结果, 否则抛出异常
public fun getCompletionExceptionOrNull(): Throwable?
// 如果完成[isCompleted]则返回结果, 否则抛出异常

示例

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun main() = runBlocking<Unit> {
val deferred = CompletableDeferred<Int>()

launch {
delay(1000 )
deferred.complete(23)
}

System.err.println("(Demo.kt:72) 结果 = ${deferred.await()}")
}

创建CompletableDeferred的顶层函数

1
2
kotlin复制代码public fun <T> CompletableDeferred(parent: Job? = null): CompletableDeferred<T>
public fun <T> CompletableDeferred(value: T): CompletableDeferred<T>

CompletableDeferred函数

1
2
3
4
5
6
7
8
kotlin复制代码public fun complete(value: T): Boolean
// 结果

public fun completeExceptionally(exception: Throwable): Boolean
// 抛出异常, 异常发生在`await()`时

public fun <T> CompletableDeferred<T>.completeWith(result: Result<T>): Boolean
// 可以通过标记来判断是否成功, 避免异常抛出

CoroutineScope

创建此对象表示创建一个协程作用域

结构化并发

如果你看协程的教程可能会经常看到这个词, 这就是作用域内部开启新的协程; 父协程会限制子协程的生命周期, 子协程承接父协程的上下文, 这种层级关系就是结构化并发

在一个协程作用域里面开启多个子协程进行并发行为

CoroutineContext

协程上下文, 我认为协程上下文可以看做包含协程基本信息的一个Context(上下文), 其可以决定协程的名称或者运行

创建一个新的调度器

1
2
kotlin复制代码fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher

创建新的调度器比较消耗资源, 建议复用且当不需要的时候使用close函数释放

调度器

Dispatchers继承自CoroutineContext, 该枚举拥有三个实现; 表示不同的线程调度; 当函数不使用调度器时承接当前作用域的调度器

  1. Dispatchers.Unconfined 不指定线程, 如果子协程切换线程那么接下来的代码也运行在该线程上
  2. Dispatchers.IO 适用于IO读写
  3. Dispatchers.Main 根据平台不同而有所差, Android上为主线程
  4. Dispatchers.Default 默认调度器, 在线程池中执行协程体, 适用于计算操作

立即执行

1
复制代码Dispatchers.Main.immediate

immediate属于所有调度器都有的属性, 该属性代表着如果当前正处于该调度器中不执行调度器切换直接执行, 可以理解为在同一调度器内属于同步协程作用域

例如launch函数开启作用域会比后续代码执行顺序低, 但是使用该属性协程属于顺序执行

示例

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码CoroutineScope(Job() + Dispatchers.Main.immediate).launch {
// 执行顺序 1
}

// 执行顺序 2

CoroutineScope(Job() + Dispatchers.Main).launch {
// 执行顺序 4
}

// 执行顺序 3

协程命名

通过创建一个CoroutineName对象, 在构造函数中指定参数为协程名称, CoroutineName继承自CoroutineContext.

1
2
3
javascript复制代码launch(CoroutineName("吴彦祖")){

}

协程上下文名称用于方便调试使用

协程挂起

yield函数可以让当前协程暂时挂起执行其他协程体, 如果没有其他正在并发的协程体则继续执行当前协程体(相当于无效调用)

1
kotlin复制代码public suspend fun yield(): Unit

看协程中可能经常提及挂起, 挂起可以理解为这段代码(作用域)暂停, 然后执行后续代码; 挂起函数一般表示suspend关键字修饰的函数, suspend要求只允许在suspend修饰的函数内部调用, 但是本身这个关键字是没做任何事的. 只是为了限制开发者随意调用

挂起函数调用会在左侧行号列显示箭头图标

image-20200106120117080

JOB

在协程中Job通常被称为作业, 表示一个协程工作任务, 他同样继承自CoroutineContext

1
2
3
ini复制代码val job = launch {

}

Job属于接口

1
kotlin复制代码interface Job : CoroutineContext.Element

函数

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
kotlin复制代码public suspend fun join()
// 等待协程执行完毕都阻塞当前线程
public val onJoin: SelectClause0
// 后面提及的选择器中使用

public fun cancel(cause: CancellationException? = null)
// 取消协程
public suspend fun Job.cancelAndJoin()
// 阻塞并且在协程结束以后取消协程

public fun start(): Boolean
public val children: Sequence<Job>
// 全部子作业

public fun getCancellationException(): CancellationException

public fun invokeOnCompletion(
onCancelling: Boolean = false,
invokeImmediately: Boolean = true,
handler: CompletionHandler): DisposableHandle
// p1: 当为true表示cancel不会回调handler
// p2: 当为true则先执行[handler]然后再返回[DisposableHandle], 为false则先返回[DisposableHandle]

public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
// 当其作用域完成以后执行, 主协程指定才有效, 直接给CoroutineScope指定时无效的
// 手动抛出CancellationException同样会赋值给cause

状态

通过字段可以获取JOB当前处于状态

1
2
3
kotlin复制代码public val isActive: Boolean
public val isCancelled: Boolean
public val isCompleted: Boolean

扩展函数

1
2
3
kotlin复制代码public fun Job.cancelChildren(cause: CancellationException? = null)

public suspend fun Job.cancelAndJoin()

每个协程作用域都存在coroutineContext. 而协程上下文中都存在Job对象

1
css复制代码coroutineContext[Job]

结束协程

如果协程作用域内存在计算任务(一直打日志也算)则无法被取消, 如果使用delay函数则可以被取消;

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码fun main() = runBlocking<Unit> {

val job = launch(Dispatchers.Default) {
while (true){
delay(100) // 这行代码存在则可以成功取消协程, 不存在则无法取消
System.err.println("(Main.kt:30) ")
}
}

delay(500)
job.cancel()
System.err.println("(Main.kt:42) 结束")
}

通过使用协程内部isActive属性来判断是否应该结束

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码fun main() = runBlocking<Unit> {

val job = launch(Dispatchers.Default) {
while (isActive) { // 一旦协程被取消则为false
System.err.println("(Main.kt:30) ")
}
}

delay(500)
job.cancel()
System.err.println("(Main.kt:42) 结束")
}

释放资源

协程存在被手动取消的情况, 但是有些资源需要在协程取消的时候释放资源, 这个操作可以在finally中执行

无论如何finally都会被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码fun main() = runBlocking<Unit> {

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
try {
repeat(1000){
System.err.println("(Main.kt:31) it = $it")
delay(500)
}
} finally {
// 已被取消的协程无法继续挂起
}
}
delay(1500)
job.cancel()
System.err.println("(Main.kt:42) ")
}

再次开启协程

通过withContext和NonCancellable可以在已被取消的协程中继续挂起协程; 这种用法其实可以看做创建一个无法取消的任务

1
2
3
4
5
kotlin复制代码withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}

上下文组合

协程作用域可以接收多个CoroutineContext作为上下文参数; CoroutineContext本身属于接口, 很多上下文相关的类都实现与他

配置多个CoroutineContext可以通过+符号同时指定多个协程上下文, 每个实现对象可能包含一部分信息可以存在覆盖行为故相加时的顺序存在覆盖行为

1
2
3
4
kotlin复制代码val a = CoroutineScope(SupervisorJob() + coroutineContext).launch(handler) {
delay(1000)
System.err.println("(Main.kt:51) ${Thread.currentThread()}")
}
1
kotlin复制代码launch(Dispatchers.IO + CoroutineName("吴彦祖")){	}

协程局部变量

使用ThreadLocal可以获取线程的局部变量, 但是要求使用扩展函数asContextElement转为协程上下文作为参数传入在创建协程的时候

该局部变量作用于持有该协程上下文的协程作用域内

1
kotlin复制代码public fun <T> ThreadLocal<T>.asContextElement(value: T = get()): ThreadContextElement<T> = ThreadLocalElement(value, this)

超时

1
2
3
4
5
6
7
kotlin复制代码public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T
// 超过指定时间timeMillis自动结束协程;
// 当没有超时时返回值获取并且继续执行协程;
// 当超时会抛出异常TimeoutCancellationException, 但是不会导致程序结束

public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T?
// 如果超时不会结束协程而是返回null

无法手动抛出TimeoutCancellationException, 因为其构造函数私有

全局协程作用域

全局协程作用域属于单例对象, 整个JVM虚拟机只有一份实例对象; 他的寿命周期也跟随JVM. 使用全局协程作用域的时候注意避免内存泄漏

1
2
3
4
5
6
7
kotlin复制代码public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext];
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

全局协程作用域不继承父协程作用域的上下文, 所以也不会因为父协程被取消而自身被取消

启动模式

  • DEFAULT 立即执行协程体
  • ATOMIC 立即执行协程体,但在开始执行协程之前无法取消协程
  • UNDISPATCHED 立即在当前线程执行协程体,第一个挂起函数执行在函数所在线程, 后面执行在函数指定线程
  • LAZY 手动执行start或join才会执行协程

协程取消

协程体如果已经执行实际上属于不可取消的, 在协程体中通过isActive来判断协程是否处于活跃中

通过取消函数的参数指定异常CancellationException可以自定义异常对象

不可取消的协程作用域

NonCancellable该单例对象用于withContext函数创建一个无法被取消的协程作用域

1
2
3
kotlin复制代码withContext(NonCancellable) {
delay(2000)
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码fun main() = runBlocking {
launch {
delay(1000)
System.err.println("(Main.kt:19) ")
}

launch {
withContext(NonCancellable) {
delay(2000)
System.err.println("(Main.kt:26) ")
}
}

delay(500) // 防止launch还未开启withContext就被取消
cancel()
}
  1. 当子作用域内包含没有终止的任务, 将等待任务完成后才会取消(delay不存在, Thread.sleep可以模拟未结束的任务)
  2. 抛出CancellationException视作结束异常, invokeOnCompletion也会执行(其中包含异常对象), 但是其他异常将不会执行invokeOnCompletion

取消GlobalScope

GlobalScope属于全局协程, 由他开启的协程都不拥有Job, 所以无法取消协程. 但是可以通过给GlobalScope开启的协程作用域指定Job然后就可以使用Job取消协程

协程异常

通过CoroutineExceptionHandler函数可以创建一个同名的对象, 该接口继承自CoroutineContext, 同样通过制定上下文参数传递给全局协程作用域使用, 当作用域抛出异常时会被该对象的回调函数接收到, 并且不会抛出异常

  1. CoroutineExceptionHandler 只有作为最外层的父协程上下文才有效, 因为异常会层层上抛, 除非配合SupervisorJob监督作业禁止异常上抛, 子作用域的异常处理器才能捕获到异常
  2. CoroutineExceptionHandler异常处理器并不能阻止协程作用域取消, 只是监听到协程的异常信息避免JVM抛出异常退出程序而已
  3. 只要发生异常就会导致父协程和其所有子协程都被取消, 这种属于双向的异常取消机制, 后面提到的监督作业(SupervisorJob)属于单向向下传递(即不会向上抛出)
  4. CoroutineExceptionHandler会被作用域一直作为协程上下文向下传递给子作用域(除非子作用域单独指定)

(如下示例)不要尝试使用try/catch捕捉launch作用域的异常, 无法被捕捉.

1
2
3
4
5
6
7
kotlin复制代码try {
launch {
throw NullPointerException()
}
} catch (e: Exception) {
e.printStackTrace()
}

后面专门介绍如何捕获协程异常避免抛出.

协程取消异常

取消协程的作业(Job)会引发异常, 但是会被默认的异常处理器给忽略, 但是我们可以通过捕捉可以看到异常信息

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码fun main() = runBlocking<Unit> {
val job = GlobalScope.launch {

try {
delay(1000)
} catch (e: Exception) {
e.printStackTrace()
}
}

job.cancel(CancellationException("自定义一个用于取消协程的异常"))
delay(2000)
}

Job取消函数

1
kotlin复制代码public fun cancel(cause: CancellationException? = null)
  • cause: 参数不传默认为JobCancellationException

全局协程作用域的异常处理

1
2
3
4
5
6
7
kotlin复制代码val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
System.err.println("(Main.kt:41):main coroutineContext = $coroutineContext, throwable = $throwable")
}

GlobalScope.launch(exceptionHandler) {

}

子协程设置异常处理器是无效的, 即使设置了错误依然会抛到父协程从而而没有意义. 除非同时使用异常处理器+监督作业(SupervisorJob), 这样就是让子协程的错误不向上抛(后面详解监督作业), 从而被其内部的异常处理器来处理.

异常聚合和解包

全局协程作用域也存在嵌套子父级关系, 故异常可能也会依次抛出多个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
// 第三, 这里的异常是第一个被抛出的异常对象
println("捕捉的异常: $exception 和被嵌套的异常: ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally { // 当父协程被取消时其所有子协程都被取消, finally被取消之前或者完成任务之后一定会执行
throw ArithmeticException() // 第二, 再次抛出异常, 异常被聚合
}
}
launch {
delay(100)
throw IOException() // 第一, 这里抛出异常将导致父协程被取消
}
delay(Long.MAX_VALUE)
}
job.join() // 避免GlobalScope作用域没有执行完毕JVM虚拟机就退出
}

监督作业

一般情况子协程发生异常会导致父协程被取消, 同时父协程发生异常会取消所有的子协程; 但是有时候子协程发生异常我们并不希望父协程也被取消, 而是仅仅所有子协程取消(仅向下传递异常), 这个使用就是用SupervisorJob作业

创建监督作业对象

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码fun main() = runBlocking<Unit> {
CoroutineScope(coroutineContext).launch {

launch(SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { _, _ -> }) {
throw NullPointerException()
}

delay(500)
println("( Process.kt:13 ) ")
}

println("( Process.kt:16 ) finish")
}
  1. 必须添加CoroutineExceptionHandler处理异常, 否则异常依然会向上传递取消父协程
  2. 直接创建 SupervisorJob() 对象传入作用域中会导致该作用域和父协程生命周期不统一的问题, 即父协程取消以后该子协程依然处于活跃状态, 故需要指定参数为coroutineContext[Job]即传入父协程的作业对象
  3. SupervisorJob仅能捕捉内部协程作用域的异常, 无法直接捕捉内部协程
1
2
3
4
5
6
7
kotlin复制代码supervisorScope {
// throw NoSuchFieldException() 抛出崩溃

launch {
throw NoSuchFieldException() // 不会抛出
}
}

监督作业在withContext和async中添加无效

直接创建一个异常向下传递监督作业的作用域

1
kotlin复制代码public suspend fun <R>  supervisorScope(block: suspend CoroutineScope.() -> R): R
  • 该函数属于阻塞
  • 具备返回值
  • supervisorScope函数使用的依然是当前作用域的Job, 所以跟随当前作用域生命周期, 可以被取消
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码fun main() = runBlocking<Unit> {
CoroutineScope(coroutineContext).launch {

// 在该作用域内只要设置CoroutineExceptionHandler都仅会向下传递
supervisorScope {
launch(CoroutineExceptionHandler { _, _ -> }) {
throw NullPointerException()
}

launch {
delay(1000) // 即使上面的launch抛出异常也会继续执行这里
println("( Process.kt:18 ) ")
}
}
}

println("( Process.kt:16 ) finish")
}

捕获异常

在作用域中的异常捕获和一般的异常捕获有所区别

  • CoroutineExceptionHandler可以捕获所有子作用域内异常
  • async可以使用监督作业可以捕获内部发生的异常, 但是其await要求trycatch
  • launch要求监督作业配合异常处理器同时使用, 缺一不可
  • withContext/supervisorScope/coroutineScope/select可以trycatch捕获异常

原始协程

函数 回调字段 描述
suspendCoroutine Continuation Result
suspendCancellableCoroutine CancellableContinuation 可取消
suspendAtomicCancellableCoroutine CancellableContinuation 可取消

[Continuation]

1
2
java复制代码public val context: CoroutineContext
public fun resumeWith(result: Result<T>)

[CancellableContinuation]
-| Continuation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean

public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit)
public fun tryResume(value: T, idempotent: Any? = null): Any?
public fun tryResumeWithException(exception: Throwable): Any?
public fun completeResume(token: Any)

public fun cancel(cause: Throwable? = null): Boolean

public fun invokeOnCancellation(handler: CompletionHandler)
public fun CoroutineDispatcher.resumeUndispatched(value: T)
public fun CoroutineDispatcher.resumeUndispatchedWithException(exception: Throwable)

线程不安全

解决线程不安全问题

  1. 互斥锁
  2. 切换线程实现单线程
  3. Channel

互斥

相当于Java中的Lock替代品: Mutex

创建互斥对象

1
2
kotlin复制代码public fun Mutex(locked: Boolean = false): Mutex
// p: 设置初始状态, 是否立即上锁

使用扩展函数可以自动加锁和解锁

1
2
kotlin复制代码public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T
// owner: 钥匙

函数

1
2
3
4
5
kotlin复制代码public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T
public fun holdsLock(owner: Any): Boolean
// owner是否被用于锁
public fun tryLock(owner: Any? = null): Boolean
// 使用owner来上锁, 如果owner已上锁则返回false

Channel

  1. 多个作用域可以通过一个Channel对象来进行数据的发送和接收
  2. Channel设计参考Go语言的chan设计, 可用于控制作用域的阻塞和继续(通过配合select)
  3. 在协程1.5开始出现废弃函数不在此处介绍

Channel属于接口无法直接创建, 我们需要通过函数Channel()来创建其实现类

源码

1
2
3
4
5
6
7
8
kotlin复制代码public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel() // 无缓存
UNLIMITED -> LinkedListChannel() // 无限制
CONFLATED -> ConflatedChannel() // 合并
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) // 64
else -> ArrayChannel(capacity) // 指定缓存大小
}
  • capacity
1
2
ini复制代码缓冲大小, 默认0
当Channel发送一条数据时就会挂起通道(不继续执行发送后续代码), 只有在接收这条数据时才会解除挂起继续执行; 但是我们可以设置缓存大小

通道允许被遍历获取当前发送数据

1
2
3
4
5
kotlin复制代码val channel = Channel<Int>()

for (c in channel){

}
1
kotlin复制代码public suspend fun yield(): Unit

Channel

Channel接口同时实现发送渠道(SendChannel)和接收渠道(ReceiveChannel)两个接口, 所以既能发送又能接收数据

1
kotlin复制代码public interface Channel<E> : SendChannel<E>, ReceiveChannel<E>

SendChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public val isClosedForSend: Boolean
// 是否关闭
public fun close(cause: Throwable? = null): Boolean
// 关闭发送通道

public fun trySend(element: E): ChannelResult<Unit>
// 发送消息, 非suspend函数

public suspend fun send(element: E)
// 发送消息

public fun invokeOnClose(handler: (cause: Throwable?) -> Unit)
// 当通道关闭时执行回调

public val onSend: SelectClause2<E, SendChannel<E>>
// 立即发送数据(如果允许), 在select中使用
  • 发送通道关闭后不能继续使用ReceiveChannel接收数据, 会导致ClosedReceiveChannelException抛出
  • 前缀try*等函数表示不是suspend挂起函数, 无需在协程作用域中调用

ReceiveChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码public val isClosedForReceive: Boolean
// SendChannel是否已经关闭通道, 如果关闭通道以后还存在缓存则会接收完缓存之后返回false

public val isEmpty: Boolean // 通道是否为空

public suspend fun receive(): E
public fun tryReceive(): ChannelResult<E>
public suspend fun receiveCatching(): ChannelResult<E>
// 接受通道事件

public val onReceive: SelectClause1<E> // 如果通道关闭, 抛出异常
public val onReceiveCatching: SelectClause1<ChannelResult<E>>// 此函数不会抛出异常
// 在select中使用的监听器, 推荐使用第三个函数

public suspend fun receiveOrClosed(): ValueOrClosed<E>
// `ValueOrClosed`对象可以判断通道是否已关闭

public fun cancel(cause: CancellationException? = null)
// 关闭通道

public fun <T> ReceiveChannel<T>.receiveAsFlow(): Flow<T> = ChannelAsFlow(this, consume = false)
// 将Chan转成Flow
  1. 通道的发送和接收都会导致作用域被阻塞, 但是发送消息可以通过设置缓存让他不阻塞, 或者取消通道可以让阻塞继续
  2. 通道只允许在挂起函数中发送和接收, 但是创建通道不限制
  3. 关闭通道会导致receive抛出异常
  4. SendChannel执行close函数后不允许再发送或者接收数据, 否则抛出异常
  5. Channel的send | receive函数所在作用域被取消cancel不会导致通道结束(isClosedForReceive返回false)
  6. receive接收而不是遍历则会导致卡住作用域

consume

ReceiveChannel不仅可以通过迭代器来接收事件, 还可以使用consume系列函数来接收事件

本质上consume和迭代没有任何区别只是consume会在发生异常时自动取消通道(通过cancel函数)

源码

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public inline fun <E, R> ReceiveChannel<E>.consume(block: ReceiveChannel<E>.() -> R): R {
var cause: Throwable? = null
try {
return block() // 直接返回
} catch (e: Throwable) {
cause = e
throw e
} finally {
cancelConsumed(cause) // 如果发生异常取消通道
}
}

consumeEach函数仅是迭代接收事件且异常自动取消; 一般建议使用consume函数来接收事件

BroadcastChannel

这个通道和一般的通道区别在于他的每个数据可以被每个作用域全部接收到; 默认的通道一个数据被接收后其他的协程是无法再接收到数据的

广播通道通过全局函数创建对象

1
kotlin复制代码public fun <E> BroadcastChannel(capacity: Int): BroadcastChannel<E>

本身广播通道继承自SendChannel, 只能发送数据, 通过函数可以拿到接收通道

1
kotlin复制代码public fun openSubscription(): ReceiveChannel<E>

取消通道

1
kotlin复制代码public fun cancel(cause: CancellationException? = null)

将Channel转成BroadcastChannel

1
2
3
4
kotlin复制代码fun <E> ReceiveChannel<E>.broadcast(
capacity: Int = 1,
start: CoroutineStart = CoroutineStart.LAZY
): BroadcastChannel<E>

通过扩展函数在协程作用域中快速创建一个广播发送通道

1
2
3
4
5
6
7
kotlin复制代码public fun <E> CoroutineScope.broadcast(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 1,
start: CoroutineStart = CoroutineStart.LAZY,
onCompletion: CompletionHandler? = null,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): BroadcastChannel<E>

迭代通道

接收通道实现操作符重载可以使用迭代

1
kotlin复制代码public operator fun iterator(): ChannelIterator<E>

示例

1
2
3
kotlin复制代码for (i in produce){
// 收到每个发型的消息
}

当多个协程接收同一个渠道数据会依次轮流接收到数据, 渠道对于多个协程是公平的

Produce

上面介绍的属于创建Channel对象来发送和接收数据, 但是还可以通过扩展函数快速创建并返回一个具备发送数据的ReceiveChannel对象

1
2
3
4
5
kotlin复制代码public fun <E> CoroutineScope.produce(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E>
  • context: 可以通过协程上下文决定调度器等信息
  • capacity: 初始化通道空间

ProducerScope 该接口继承自SendChannel以及CoroutineScope, 具备发送通道数据以及协程作用域作用

当produce作用域执行完成会关闭通道, 前面已经提及关闭通道无法继续接收数据

等待取消

该函数会在通道被取消时回调其函数参数, 前面提及协程取消时可以通过finally来释放内存等操作, 但是通道取消无法使用finally只能使用该函数

1
2
kotlin复制代码public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) 
// [SendChannel.close] or [ReceiveChannel.cancel] 代表取消通道

Actor

可以通过actor函数创建一个具备通道作用的协程作用域

1
2
3
4
5
6
7
kotlin复制代码public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>
  • context: 协程上下文
  • capacity: 通道缓存空间
  • start: 协程启动模式
  • onCompletion: 完成回调
  • block: 回调函数中可以进行发送数据

该函数和produce函数相似,

  1. produce返回ReceiveChannel, 外部进行数据接收; actor返回的SendChannel, 外部进行数据发送
  2. actor的回调函数拥有属性channel:Channel, 既可以发送数据又可以接收数据, produce的属性channel属于SendChannel
  3. 无论是produce或者actor他们的通道都属于Channel, 既可以发送又可以接收数据, 只需要类型强转即可
  4. 本身Channel可以进行双向数据通信, 但是设计produce和actor属于设计思想中的生产者和消费者模式
  5. 他们都属于协程作用域和数据通道的结合

轮循器

无论是RxJava还是协程都支持轮循器的功能, 在我的网络请求库中还赋予了轮循器暂停|继续|多个观察者|重置等功能

这里的协程轮循器就比较简陋

1
2
3
4
5
6
kotlin复制代码public fun ticker(
delayMillis: Long,
initialDelayMillis: Long = delayMillis,
context: CoroutineContext = EmptyCoroutineContext,
mode: TickerMode = TickerMode.FIXED_PERIOD
): ReceiveChannel<Unit>

该通道返回的数据是Unit

默认情况下可以理解为通道会在指定间隔时间后一直发送Unit数据

1
2
3
4
5
6
7
8
9
kotlin复制代码fun main() = runBlocking<Unit> {

val tickerChannel = ticker(delayMillis = 1000, initialDelayMillis = 0)

// 每秒打印
for (unit in tickerChannel) {
System.err.println("unit = $unit")
}
}

但是如果下游不是在发送数据以后立即接收数据, 而是延迟使用receive函数来接收通道数据

TickerMode该枚举拥有两个字段

  • FIXED_PERIOD 默认值, 动态调节通道发送数据的时间间隔, 时间间隔可以看做是上游发送数据的
  • FIXED_DELAY 只有当接收数据后才会开始计算间隔时间, 时间间隔可以看做是下游接收数据的

这个轮循器不支持多订阅|暂停|继续|重置|完成, 但是我的Net库中Interval对象已实现所有功能

Select

在select函数回调中监听多个Deferred/Channel的结果, 且只会执行最快接收数据的通道或者结果回调.

动作

在前面的函数介绍中可以看到一系列on{动作}变量, 他们的值全部是SelectClause{数字}接口对象;

[SelectBuilder]

1
2
3
4
5
6
7
8
kotlin复制代码public interface SelectBuilder<in R> {
public operator fun SelectClause0.invoke(block: suspend () -> R)
public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)
public operator fun <P, Q> SelectClause2<P, Q>.invoke(param: P, block: suspend (Q) -> R)
public operator fun <P, Q> SelectClause2<P?, Q>.invoke(block: suspend (Q) -> R) = invoke(null, block)
@ExperimentalCoroutinesApi
public fun onTimeout(timeMillis: Long, block: suspend () -> R)
}

根据这定义的扩展函数就可以直接使用动作

对象 使用的函数
SelectClause0 onJoin
SelectClause1 OnReceive
SelectClause2 onSend

示例

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
kotlin复制代码@ObsoleteCoroutinesApi
@UseExperimental(InternalCoroutinesApi::class)
suspend fun selectMulti(a: Channel<Int>, b: Channel<Int>): String = select<String> {

b.onReceive {
"b $it" // 优先执行第一个, 不是函数原因, 而是顺序
}

b.onReceiveOrClosed {
"b $it"
}

a.onSend(23) {
"发送 23"
}
}

fun main() = runBlocking<Unit> {
val a = Channel<Int>(1) // 缓冲数量, 避免发送数据时阻塞
val b = Channel<Int>(1)

launch {
b.send(24)
val s = selectMulti(a, b)
println("结果 = $s")
}
}
  • onReceive 在关闭通道时会导致抛出异常, 如果不想抛出异常应当使用onReceiveOrClosed来替换
  • onSend 该函数等效于Channel.send, 就是发送一个值, 假设注册多个onSend肯定是第一个先回调返回结果
  • 即使已经有成员被选中(select)也不会导致其他的成员协程作用域结束

[ValueOrClosed]

1
2
3
4
5
kotlin复制代码public val isClosed: Boolean // 通道是否已关闭

public val value: T
public val valueOrNull: T?
// 两者都是获取通道内的值, 但是第2个如果通道关闭不会抛出异常而是返回NULL
  1. 当在select中一个通道同时存在发送和接收监听时, 如果两者都执行到(即select没有被打断都执行到)会导致异常抛出
  2. 如果通道重复监听(多个动作), 优先执行第一个
  3. 关闭通道同样会收到数据, onReceive抛出异常, onReceiveOrClose数据为null

Flow

Flow相似于RxJava同样分为三个部分:

  1. 上游
  2. 操作符
  3. 下游

下游接收事件要求在协程作用域内执行(suspend函数)

创建Flow

1
kotlin复制代码public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>

示例

1
2
3
4
5
6
scss复制代码fun shoot() = flow {
for (i in 1.;3) {
delay(1000) // 假装我们在这里做了一些有用的事情
emit(i) // 发送下一个值
}
}
  • 集合或者Sequence都可以通过asFlow函数转成Flow对象
  • 也可以像创建集合一样通过fowOf直接创建Flow对象
  • Channel通道转成Flow
1
kotlin复制代码public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T>
  • 甚至挂起函数也可以转成Flow
1
kotlin复制代码public fun <T> (suspend () -> T).asFlow(): Flow<T>

collect和flow的回调函数本身属于suspend函数可以开启协程作用域

创建Flow的函数

函数 描述
flow 普通Flow
channelFlow 创建通道, 其支持缓冲通道, 允许不同的CorotineContext发送事件
callbackFlow 与channelFlow函数除了不使用awaitClose会报错以外没有区别
emptyFlow 空的Flow
flowOf 直接发送数据

flow的发射函数emit不是线程安全的不允许其他线程调用, 如果需要线程安全请使用channelFlow而不是flow

channelFlow使用send函数发送数据

发射数据示例

1
2
3
4
5
6
7
kotlin复制代码flow<Int> {
emit(23)
}

channelFlow<Int> {
send(23) // offer(23)
}
  1. offer可以在非suspend函数中使用, send必须在suspend函数中使用
  2. offer存在一个返回值, 假设没有元素空间则会直接返回false, send则会挂起阻塞等待新的元素空间.

Flow在取消作用域时释放资源可以使用callbackFlow. 这里演示注册和取消一个广播AppWidgetProvider

1
2
3
4
5
6
7
8
9
kotlin复制代码callbackFlow<Int> {
val appWidgetProvider = AppWidgetProvider()
registerReceiver(appWidgetProvider, IntentFilter()) // 注册
awaitClose { // 该回调会在协程作用域被取消时回调
unregisterReceiver(appWidgetProvider) // 注销
}
}.collect {

}

收集

收集数据

Flow是冷数据, 要求调用函数collect收集数据时才会进行数据的发射; 该系列函数也成为末端操作符;

1
2
3
4
5
kotlin复制代码flow {
emit(23)
}.collect {
System.err.println("(Demo.kt:9) it = $it")
}

查看源码会发现这个emit实际上就是执行collect的参数函数

collect函数表示接收上游发送的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public suspend fun Flow<*>.collect() 
// 不做任何处理的收集器, 仅仅为了触发发射数据

public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit
// 收集

public suspend fun <T> Flow<T>.collectLatest(action: suspend (value: T) -> Unit)
// 和上个函数的区别是: 如果在下游没有处理完情况下上游继续下个发射会导致上次的下游被取消

public suspend inline fun <T> Flow<T>.collectIndexed(
crossinline action: suspend (index: Int, value: T) -> Unit
): Unit
// 具备索引和值的收集器

public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job
// 将Flow运行在指定的协程作用域内

[FlowCollector] 发射器

1
2
3
4
5
kotlin复制代码public suspend fun emit(value: T)
// 发送一个数据

public suspend inline fun <T> FlowCollector<T>.emitAll(flow: Flow<T>)
// 发射另一个flow对象

调度器

调度器

Flow默认使用的是其所在的当前线程或者协程上下文, Flow不允许在内部使用withContext来切换调度器, 而是应该使用flowOn函数

1
kotlin复制代码public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T>

该函数改变的是Flow函数内部发射时的线程, 而在collect收集数据时会自动切回创建Flow时的线程

缓存

不需要等待收集执行就立即执行发射数据, 只是数据暂时被缓存而已, 提高性能

默认切换调度器时会自动缓存

1
java复制代码public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED): Flow<T>

合并函数, 这个函数实际上就是buffer, 当下游无法及时处理上游的数据时会丢弃掉该数据

1
kotlin复制代码public fun <T> Flow<T>.conflate(): Flow<T> = buffer(CONFLATED)

合并

将多个事件合并后发送给下游

zip

将两个Flow在回调函数中进行处理返回一个新的值 R

当两个flow的长度不等时只发送最短长度的事件

1
kotlin复制代码public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R>

示例

1
2
3
4
5
6
7
kotlin复制代码val nums = (1.;3).asFlow().onEach { delay(300) } // 发射数字 1.;3,间隔 300 毫秒
val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒发射一次字符串
val startTime = System.currentTimeMillis() // 记录开始的时间
nums.zip(strs) { a, b -> "$a -> $b" } // 使用“zip”组合单个字符串
.collect { value -> // 收集并打印
println("$value at ${System.currentTimeMillis() - startTime} ms from start")
}

combine

1
2
3
4
5
6
7
8
9
kotlin复制代码public fun <T1, T2, R> Flow<T1>.combine(
flow: Flow<T2>, transform: suspend (a: T1, b: T2) -> R
): Flow<R>
// 组合两个流,在经过第一次发射以后,任意方有新数据来的时候就可以发射,另一方有可能是已经发射过的数据

public fun <T1, T2, R> Flow<T1>.combineTransform(
flow: Flow<T2>,
@BuilderInference transform: suspend FlowCollector<R>.(a: T1, b: T2) -> Unit
): Flow<R>

集合

Flow直接转成集合函数

1
2
3
kotlin复制代码public suspend fun <T> Flow<T>.toList(destination: MutableList<T> = ArrayList()): List<T>
public suspend fun <T> Flow<T>.toSet(destination: MutableSet<T> = LinkedHashSet()): Set<T>
public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C

叠加

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public suspend fun <S, T : S> Flow<T>.reduce(operation: suspend (accumulator: S, value: T) -> S): S

public suspend inline fun <T, R> Flow<T>.fold(
initial: R,
crossinline operation: suspend (acc: R, value: T) -> R
): R
// `acc`为上次回调函数返回值, 第一次为初始值, 等同于叠加效果; 该函数和reduce的区别就是支持初始值; reduce累计两次元素才会回调函数

public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T>
public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R>
public fun <T, R> Flow<T>.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow<R>

转换

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
kotlin复制代码public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R>
public inline fun <T, R: Any> Flow<T>.mapNotNull(crossinline transform: suspend (value: T) -> R?): Flow<R>

public fun <T, R> Flow<T>.flatMapMerge(
concurrency: Int = DEFAULT_CONCURRENCY,
transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)
// 上游先发送所有的元素, 然后上游每个元素会导致回调函数中的Flow发送所有元素一次

public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R>
// 等同于RxJava的FlatMap

public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T>
// 串行收集数据

public fun <T> Flow<Flow<T>>.flattenMerge(
concurrency: Int = DEFAULT_CONCURRENCY
): Flow<T>
// 并发收集数据

public inline fun <T, R> Flow<T>.flatMapLatest(
@BuilderInference crossinline transform: suspend (value: T) -> Flow<R>
): Flow<R>
// 在每次 emit 新的数据以后,会取消先前的 collect

public fun <T> Flow<T>.withIndex(): Flow<IndexedValue<T>>
// 包含元素索引

生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
Kotlin复制代码public fun <T> Flow<T>.onStart(
action: suspend FlowCollector<T>.() -> Unit
): Flow<T>
// 开始

public fun <T> Flow<T>.onCompletion(
action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit
): Flow<T>
// 回调函数中的参数`cause`如果为null表示正常完成没有抛出异常, 反之则抛出异常非正常结束,
// 和catch函数一样只能监听到上游发生的异常, 但是无法避免异常抛出只能在异常抛出之前执行回调函数

public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T>
// 该函数只能捕获上游异常, 如果异常处于函数调用之下则依然会被抛出

过滤

限制流发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码public fun <T> Flow<T>.take(count: Int): Flow<T>
// 只接受指定数量事件
public fun <T> Flow<T>.takeWhile(predicate: suspend (T) -> Boolean): Flow<T>

public fun <T> Flow<T>.drop(count: Int): Flow<T>
// 丢弃指定数量事件
public fun <T> Flow<T>.dropWhile(predicate: suspend (T) -> Boolean): Flow<T>
// 回调函数判断是否丢弃或者接收, 只要丢弃或者接收后面就不会继续发送事件(结束流)

public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T>
public inline fun <T> Flow<T>.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow<T>
public inline fun <reified R> Flow<*>.filterIsInstance(): Flow<R> = filter { it is R } as Flow<R>
public fun <T: Any> Flow<T?>.filterNotNull(): Flow<T>

public suspend fun <T> Flow<T>.single(): T
// 期待只有一个元素, 否则抛出`IllegalStateException`
public suspend fun <T: Any> Flow<T>.singleOrNull(): T?
// 不抛出异常, 但如果不是仅有元素则返回null

public suspend fun <T> Flow<T>.first(): T
// 如果不存在一个元素则会抛出`NoSuchElementException`
public suspend fun <T> Flow<T>.first(predicate: suspend (T) -> Boolean): T
// 返回回调函数判断为true的第一个条件符合的元素

重试

1
2
3
4
5
6
7
8
kotlin复制代码public fun <T> Flow<T>.retry(
retries: Long = Long.MAX_VALUE, // 重试次数
predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T>

public fun <T> Flow<T>.retryWhen(
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T>

过滤

1
2
3
4
5
6
7
8
kotlin复制代码public inline fun <T, R> Flow<T>.transform(
@BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R>
// 转换函数, 可以在回调函数中发送新的元素

public fun <T, R> Flow<T>.transformLatest(
@BuilderInference transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R>

scan和reduce的区别在于

  • reduce是全部叠加计算完成后被收集
  • scan是每次叠加一次后收集一次数据

StateFlow/SharedFlow

类关系

SharedFlow

|- MutableSharedFlow

|- StateFlow

|- MutableStateFlow

SharedFlow属于热流数据, 既没有收集(collect)情况下也会发送, 然后在收集时进行重放(replay). 可以使用shareIn将冷流转成热流. 也可以直接使用以下函数创建

1
2
3
4
5
kotlin复制代码public fun <T> MutableSharedFlow(
replay: Int = 0, // 重放数量
extraBufferCapacity: Int = 0, // 缓存数量(不包含重放数量)
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

使用BufferOverflow

  1. DROP_LATEST 丢弃最新值
  2. DROP_OLDEST 丢失最旧值
  3. SUSPEND 挂起阻塞

StateFlow可以看做在Flow的基础上加上了LiveData的特性. 但是不存在生命周期跟随(除非使用lifecycleScope等生命周期作用域), 一直都可以收集数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {

// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState

init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}

示例

将flow从冷流转换成热流使用函数shareIn

1
2
3
4
5
kotlin复制代码public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
)

SharingStarted:

  1. WhileSubscribed 在第一个订阅者出现后开始共享数据,并使数据流永远保持活跃状态
  2. Lazily 存在订阅者时,将使上游提供方保持活跃状态
  3. Eagerly 立即启动提供方

Android

Google发行的Jetpack库中很多组件都附有KTX扩展依赖, 这种依赖主要是增加kotlin和协程支持

Lifecycle

官方提供生命周期协程作用域的快速创建实现;

  • 指定生命周期运行协程
  • 自动在onDestory取消协程

引入ktx依赖库

1
go复制代码implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03"

当执行到某个生命周期时运行协程

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job

fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job

fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job

suspend fun <T> Lifecycle.whenStateAtLeast(
minState: Lifecycle.State,
block: suspend CoroutineScope.() -> T
)

这些函数都属于Lifecycle和LifecycleOwner的扩展函数

LiveData

依赖

1
groovy复制代码implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc03"

提供开发者使用的只有这两个函数, 两个函数功能一样, 只是每个参数接收时间单位不一致

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeout: Duration,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeout.toMillis(), block)
  • timeout: 如果liveData的没有处于活跃的观察者则在指定的时间内(单位毫秒)会取消其作用域[block]
  • block: 该作用域只在活跃状态才会触发, 默认在Dispatchers.Main.immediate调度器

liveData作用域具备发射数据和LiveData的作用

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
kotlin复制代码interface LiveDataScope<T> {
/**
* Set's the [LiveData]'s value to the given [value]; If you've called [emitSource] previously,
* calling [emit] will remove that source.
*
* Note that this function suspends until the value is set on the [LiveData];
*
* @param value The new value for the [LiveData]
*
* @see emitSource
*/
suspend fun emit(value: T)

/**
* Add the given [LiveData] as a source, similar to [MediatorLiveData.addSource]; Calling this
* method will remove any source that was yielded before via [emitSource];
*
* @param source The [LiveData] instance whose values will be dispatched from the current
* [LiveData];
*
* @see emit
* @see MediatorLiveData.addSource
* @see MediatorLiveData.removeSource
*/
suspend fun emitSource(source: LiveData<T>): DisposableHandle

/**
* References the current value of the [LiveData];
*
* If the block never `emit`ed a value, [latestValue] will be `null`; You can use this
* value to check what was then latest value `emit`ed by your `block` before it got cancelled.
*
* Note that if the block called [emitSource], then `latestValue` will be last value
* dispatched by the `source` [LiveData];
*/
val latestValue: T?
}
  1. 如果emitSource在emit之前执行则无效
  2. 该作用域会在每次处于活跃状态时都执行一遍, 如果将应用从后台切换到前台则会返回执行该作用域, 但是观察者只会在活跃时才收到数据

本文转载自: 掘金

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

JAVA的断言 - Assert

发表于 2020-01-03

简介

断言是使用Java中的 assert 语句实现的。 在执行断言时,它被认为是正确的。 如果失败,JVM会抛出一个名为 AssertionError 的错误。 它主要用于开发过程中的测试目的。

assert 语句与布尔表达式一起使用,可以用两种不同的方式编写:

1
2
复制代码assert expression;
assert expression : errorMessage;

示例

1
2
3
4
5
6
7
8
复制代码public class AssertTest {
public static void main(String[] args) {

int value = 15;
assert value >= 20 : "Underweight";
System.out.println("value is " + value);
}
}

输出:

1
复制代码value is 15

可以看到 assert 语句并没有起作用,这是因为Java在执行程序的时候默认是不启动断言检查的,即所有的断言语句都将被忽略。

如果要开启断言检查,则需要使用 -enableassertions 或 -ea JVM参数来开启;如果要手动忽略断言检查,则可以通过使用 -disableassertions 或 -da JVM参数来忽略断言语句。

执行命令 java -ea AssertTest 开启断言后的输出:

1
2
复制代码Exception in thread "main" java.lang.AssertionError: Underweight
at AssertTest.main(AssertTest.java:11)

使用第一种无错误信息提示的语法编写:

1
2
3
4
5
6
7
8
复制代码public class AssertTest {
public static void main(String[] args) {

int value = 15;
assert value >= 20;
System.out.println("value is " + value);
}
}

输出:

1
2
复制代码Exception in thread "main" java.lang.AssertionError
at AssertTest.main(AssertTest.java:11)

可以看到这里仅是抛出了 AssertionError,没有任何错误信息提示

为什么使用断言

  1. 确保看起来无法到达的代码实际上是无法到达的
  2. 确保假设是正确的
  3. 确保 switch case 语句的 default 分支无法到达
  4. 检查对象的状态
  5. 在方法的开始检验
  6. 在方法调用后检验

注意事项

断言主要用于检查逻辑上不可能的情况。例如,它们可用于检查代码在开始运行之前期望的状态或在代码结束运行之后的状态。与普通的异常/错误处理不同,断言通常在运行时被禁用。

在哪里使用断言

  1. 私有方法的参数。私有参数仅由开发人员的代码提供,开发人员可能希望检查其关于参数的假设
  2. 分支条件,例如 switch case 语句
  3. 方法开头的条件

哪里不使用断言

  1. 断言不应用于替换错误/异常消息
  2. 断言不应用于 public 方法中的参数,因为它们可能由用户提供
  3. 断言不应在命令行参数上使用

小结

  1. 断言是一种调试方式,断言失败会抛出 AssertionError,只能在开发和测试阶段启用断言
  2. 对可恢复的错误不能使用断言,而应该抛出异常
  3. JAVA中断言很少被使用,更好的方法是编写单元测试

参考资料:

~~~~)~~~~GeeksforGeeks - Assertions in Java

~~~~)~~~~廖雪峰JAVA教程 - 使用断言

本文转载自: 掘金

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

程序员,练就哪些技能才胜任架构师?

发表于 2020-01-03

关注「 IT老兵哥」,赋能程序人生!本系列前序文章索引:

  1. 程序员为什么必须要懂架构?
  2. 架构到底是什么,你知道吗?
  3. 架构都有哪些,我该怎么选?
  4. 架构师都干什么,你知道吗?

架构师,我们程序员打怪升级的主要方向,它不像某些技能报个培训班就能获得。胜任架构工作需要具备许多技能,既有硬技能还有软技能。俗话说:一口吃不成胖子。从程序员到架构师也无法一蹴而就,它是一个循序渐进、稳步提升的进阶过程,每个阶段都有每个阶段要掌握的技能,多项技能之间还存在先后顺序。如果想尽快转型升级至架构师,那你必须在日常工作中有意识地储备这些技能,接下来老兵哥结合亲身经历来分享一下。

架构师之硬技能

  1. 硬技能

不像产品、管理等条线更加倚重通用技能,从技术条线转产品或管理,入门相对容易一些。但从产品或管理很难转型至架构,架构师必须从开发测试岗做起,在工作中不断提升专业技能和积累实践经验,从一个模块开始,到一个子系统,再到整个系统,最后到多个系统,这是一个循序渐进提升硬技能的过程,也可以看成构建架构师硬技能“点线面”。

1.1 点

老兵哥我刚入行时的岗位就是开发工程师,跟其他几个毕业生一起被安排在自动化测试平台项目组,整个系统由部门资深同事设计的,我们分别负责开发其中某个子系统的几个模块。这个阶段我主要关注函数、类和模块这个粒度,为了做好工作我要钻研编程语言 C/C++,以及熟悉 Visual C++ MFC、Socket 等代码库的使用。每周我们还会举行代码评审会议,邀请同事点评自己写的代码,那时候的自己年轻气盛,不管收到正面或负面的评价都会极大地激励自己。经过这个阶段的历练,我的编程技能得以较大的提升,也养成了较规范的编码习惯,掌握了如何设计好一个函数、类和模块。

这个项目前后做了两年左右时间,后面半年还做了些系统推广培训相关的事情。随后,我们又启动了采用脚本语言 Python 作为自动化测试脚本的自动化测试脚本,在这个项目我负责预研 Python 脚本解释引擎和开发测试代理子系统,这段项目经历让我跃升到子系统这个粒度。我需要考虑这个子系统在整个系统当中需要承担什么职责,以及跟其他子系统或被测系统之间的交互机制。同时,我还要负责这个子系统设计,确定它由哪些模块构成、每个模块内部包含哪些类等。

这个阶段让我具备了构建单个子系统的能力和信心,在后面的工作当中我还使用不同类型的编程语言构建过许许多多不同类型的子系统,但其实都是在强化构建单个点的能力,关联技能树包括:操作系统、编程语言、应用容器、开发框架、多线程等。

1.2 线

对于稍具规模的系统,它都免不了要划分成几个子系统,子系统之间或者与外部系统之间就需要连接通信,这相当于把两个孤立的点连接起来,即连点成线。这个过程跟开发单个子系统所需要的技能有所不同,它需要网络编程相关的知识技能。在老兵哥我刚参加工作的那些年,Web 应用还没有成为主流的应用形态,浏览器/服务器(B/S)架构还没有兴起,HTTP 协议尚未被广泛使用,当时最流行的就是客户端/服务器(C/S)架构,IP/TCP 才是最主要的通信协议,我就是在这个阶段积累下网络编程相关知识技能的。

最早我们要开发客户端或服务器程序,就需要熟悉掌握 Socket 网络编程,包括绑定监听端口、接受连接请求、并发处理请求等,从无到有全部自己编写,这些经验对于我后来理解网络通信机制有非常大帮助。为了满足子系统之间的交互需求,我们要基于 IP/TCP 协议来定制专门的应用层协议,包括制定报文头和报文体两层结构,虽然比 HTTP、FTP、SMTP等协议要简单,但这段经历让我对 HTTP 这类应用层协议的实现原理有了深刻的理解。另外,我们还要考虑报文内容过长时的分包组包、网络发生异常时的丢包重发,以及报文内容的编解码等。

因此,有志于在技术线发展的程序员都有必要补上这块技能。在近十五年的技术从业生涯中,老兵哥我前前后后解决过无数现网问题,其中有许多复杂的问题都跟系统交互通信有关,借助各种网络抓包分析工具抽丝剥茧,最后定位问题的根源都是没有正确使用网络协议,所以我很庆幸自己在过往的工作中有这段经历。

随着互联网的蓬勃发展,Web 应用成了最主要的应用形态,与此同时 HTTP 这种更人性化的网络通信协议成了最受欢迎的交互协议。从开发客户端/服务器(C/S)应用转到开发浏览器/服务器(B/S)应用的过程中,老兵哥我专门花时间学习了 HTTP 协议,了解其运行原理和控制机制,尤其是协议头中每个字段作用,包括请求方法、编码格式、超时机制、缓存机制等。印象最深刻的就是 Roy Thomas Fielding 博士发表了 REST 的论文《Architectural Styles and the Design of Network-based Software Architectures》,即《架构风格与基于网络的软件架构设计》,让我对 HTTP 有了全新的认知,这些知识技能对于理解掌握云计算时代的服务化架构非常有帮助。

除了 IP/TCP、HTTP 这两类协议之外,老兵哥我觉得还需要掌握消息队列(Message Queue)相关的协议,这类型协议更适合构建事件驱动架构的系统,不仅仅支持同步还支持异步。我曾经负责一个移动互联网的系统,其中有个子系统负责维护各种手机终端型号和设备信息,合作伙伴需要从这个子系统及时地获取最新信息,最初我们用 HTTP 协议轮询拉取的方式实现,但随着合作伙伴数量和信息更新频次的增加,这种信息同步机制就遭遇瓶颈了,后来我们通过引入消息队列中间件优雅地化解这个问题。

1.3 面

从点、线开始修炼,随着连线越来越多,最终将会形成平面,即分布式系统,这是我们程序员通往架构师路上必然经过的站点。刚开始我们仅仅利用互联网来发布搜索信息,接着我们的即时通信和社交也搬到了网上,再后来购物差旅等事情也可以通过互联网来完成了,现在跟我们衣食住行相关的所有事物都开始被互联网化了,这相当于虚拟世界被构建的越来越庞大越复杂。原先我们开发的软件系统复杂度还是有限的,它本身顶多被划分成几个子系统,需要关联交互的外部系统数量也非常有限。但随着业务越来越丰富,单个系统的复杂度也急剧增长,与之关联的外部系统也非常多,逐渐演变成一张纵横交错的网,也就是我们所说的“面”。如何在这样复杂的网络当中维护好复杂度,以及确保系统依旧满足易用性、性能、可靠性、稳定性、安全性等质量属性,这就需要程序员修炼分布式系统相关的技能。

老兵哥我在从事移动互联网相关系统研发的过程中遇到了更高复杂的场景,当时我们要构建一个苹果应用商店类似的生态体系,我们负责的系统本身由六七个子系统组成,它还需要跟许多上下游合作伙伴的系统对接交互。如果跟每个外部系统的对接都采用各自不同的标准,随着接入系统的数量越来越多,那对接相关的复杂度最终走向失控。另外,像应用订阅购买等典型业务场景都需要多个系统协作完成,其中涉及到分布式事务,怎样保证数据一致就是很大的挑战。按照常规逻辑,随着系统的复杂度不断提升,那么系统出现问题宕机的概率就会提升,但对于用户来说,他们依旧希望系统可以提供 7*24 小时的服务,不要出现服务超时或失效等异常情况,这就是“面”带来的挑战。

从那个阶段开始,我有了学习和实践分布式架构的机会。最早就是面向服务架构 SOA,即 Web Service、SOAP 等技术标准,站在现在回看那时候,这套技术栈是偏重偏繁琐的,但当时分布式系统对于整个业界都是全新的挑战,这套解决方案是由 Compaq、HP、IBM、Lotus、Microsoft、SAP 这些传统软件巨头们提出的。它通过 Web 服务描述语言 WSDL 来标准化分布式系统中的每个服务,再通过简单对象访问协议 SOAP 来规范服务之间的交互,从某个角度来看,越大规模的协作必须依赖统一的标准和规范。

但传统软件巨头很少有在互联网第一线实践的经验,不像 BAT 他们对互联网分布式系统的挑战有那么真切的感受,阿里巴巴就在实践中孵化出了比 Web Service、SOAP 更加轻量化的 Dubbo,它也是依托面向服务架构 SOA 这套理论,只是是线上更加接地气。这段工作经历让我对分布式架构有了体系化的认知,虽然近些年分布式技术从面向服务架构 SOA 演进至微服务架构 MicroService,技术中间件从 Dubbo 更替为 Spring Cloud,但我依然可以套用这套知识体系去理解新技术。

  1. 软技能

软硬技能到底是怎么区分呢?老兵哥我觉得一个人靠哪门手艺吃饭,那么这门手艺相关的技能就是硬技能,而辅助硬技能产生更大价值的技能就是软技能,这跟“T”字型人才的要求类似,既要求有足够精湛拔尖的主攻技艺,也要有各式各样的综合技能。硬技能很重要,这点我相信没有人会反对,但也有不少人意识不到软技能的重要性。对技术人来说,从开发到架构,从架构到 CTO,或者从开发转产品或管理,不管是晋升还是转型,我们都是在加强硬技能的同时提升软技能的比重,甚至原先的硬技能变成了软技能,而原先辅助作用的软技能却成了硬技能。接下来,我将结合个人从开发转型架构的经历来谈一谈哪些软技能很重要:

  • 沟通:相对于开发工程师,架构师的工作职责决定了他需要对接更多上下游客户,对沟通技能的要求就要高很多,毕竟不同角色的思维模式和立场角度各不相同,架构师必须懂得采用不同方式跟这些角色沟通互动,换位思考,从而挖掘到真实的需求,然后利用专业技能平衡好各方需求,最终输出各方都满意的架构方案。
  • 写作:做开发岗时,我的主要输出就是代码。虽然偶尔也要写一些技术文档,但通常是供自己看的技术文档或者凑数用的产品操作使用说明。在转型做架构之后,我写代码的比重降低了,为了让各个干系人理解认可我的架构方案,除了口头说明之外,最主要就是靠技术写作,写给他人看的文档跟纯粹记录的完全不同。
  • 设计:不管是口头沟通还是文档传播,语文或文字功底再好,也抵不过搭配上设计图例,图例中包含的信息是多个维度的,也更加直观易懂。通常,我习惯在写技术文档之前先把设计图画出来,画设计图的过程就是理顺思路的过程,在此基础上再来组织文字或语言变得更加简单容易,相当于是看图说话了。
  • 演讲:权力和非职权影响力,架构师开展工作主要依赖于非职权影响力。架构师跟各个干系人之间不存在上下级关系,要让团队及合作伙伴认可并执行你的架构方案,你必须要靠自己的专业能力让对方信服。在以往学校教育或成长过程中,我本身是缺乏这方面的积累的,为了成功转型架构师我刻意训练提升自己这方面的能力。

总结起来说,为了在架构师这个新平台上做好工作,我们需要提升自己输入、设计和输出等方面的能力。以往我们从外界获取信息主要靠阅读文档,现在还要加强立体多方位的沟通。在输出方面,以往我们的输出形式太过单一,现在我们要掌握文字、演讲等多媒体方式。当然,最核心的还是架构设计的专业技能,这个输入输出的中心。

  1. 知识体系

今天先分享到这里,老兵哥后续还会分享从程序员到架构师的进阶指南,包括每个阶段需要掌握的软硬技能图谱(如题图所示),文字提纲可以参考文章:从程序员到架构师,有捷径吗?如果你对这个主题感兴趣,千万要记得先 关注哦!

坚持原创不易,如果你觉得有价值,麻烦动动手指点下文 「 👍 」按钮,让更多小伙伴可以看到,老兵哥会更有动力坚持分享的。另外,我后续还会分享职业规划、应聘面试、技能提升、影响力打造等经验,欢迎 关注 本专栏或歪信公主号 「 IT老兵哥 」!

微信公众号「 IT老兵哥 」

软技能-热门文章:(首发公众号)

  • 如何在打造影响力的路上「码」不停?(新)
  • 2020 来了,你的 2019 晒好封存了吗?(新)
  • “花式”裁员套路深,你知道吗?
  • 遭遇裁员,如何渡过心理危机?
  • 程序员“求包养”攻略揭秘

硬技能-热门文章:

  • 如何设计出优美的Web API?(热)
  • 程序员必须掌握的性能调优 X Y Z (热)
  • 如何把单体式应用拆解成微服务?【上】
  • 如何把单体式应用拆解成微服务?【下】
  • 图解 Spring:HTTP 请求的处理流程与机制【1】

本文转载自: 掘金

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

美团面试官问我一个字符的Stringlength()是多少

发表于 2020-01-02

本文首发于微信公众号:程序员乔戈里))))))

1
2
3
4
5
6
复制代码public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}

以上结果输出为7。

)

))

小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。

1
2
3
4
5
6
7
8
9
10
11
复制代码    /**
* Returns the length of this string.
* The length is equal to the number of <a href="Character.html#unicode">Unicode
* code units</a> in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}

接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。

小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:

Java中 有内码和外码这一区分简单来说

  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)

而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。

)

代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。

UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。

你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!

而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!

所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!

)))

来看个例子!

1
2
3
4
5
6
7
8
9
10
复制代码public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}

可以看到通过codePointCount()函数得知这个音乐字符是一个字符!

几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是”uD834uDD1E”,这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?

一个一个解答:

第0个问题:

codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。

比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = “u1D11E”,机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。

但如果令字符str = “uD834uDD1E”,那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.

第1个问题

上图是对应的转换规则:

  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了

第2个问题

  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。

参考

  • 表情包出自微博@黄小B
  • blog.csdn.net/u012425381/…
  • xiaogd.net/
  • tool.oschina.net/hexconvert
  • baike.baidu.com/item/Unicod…

本文首发于微信公众号:程序员乔戈里

如果是头条用户,可以在我的头条号程序员乔戈里后台回复 资源获取价值59998元的编程和考研资料
觉得文章不错的欢迎关注我的WX公众号:程序员乔戈里
我是BAT大厂后台开发工程师,,专注分享技术干货/编程资源/求职面试/成长感悟等,关注送5000G编程资源和自己整理的一份帮助不少人拿下java的offer的面经附答案,免费下载CSDN资源。

本文转载自: 掘金

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

2020年Java面试题汇总篇——附答案

发表于 2019-12-31

话不多说直接放出面试题和答案的连接,准备好上车了吗?少年!

  1. 面试官 | Nginx 是什么?有什么作用?
  2. 面试官 | 为什么用了索引之后,查询就会变快?
  3. 面试官 | Java 对象不使用时为什么要赋值为 null?
  4. 美团面试题 | JVM 堆内存溢出后,其他线程是否可继续工作?
  5. 面试官 | 说一下 JVM 常用参数有哪些?
  6. 面试官 | 说一下什么是代理模式?
  7. 面试官 | AJAX请求为什么不安全?
  8. 经典面试题|ConcurrentHashMap 读操作为什么不需要加锁?
  9. 面试官问:一个Java字符串中到底能有多少个字符?
  10. 面试官 | JVM 为什么使用元空间替换了永久代?
  11. 面试官 | 说一下数据库如何分库分表?
  12. 面试官 | 讲一下如何给高并发系统做限流?
  13. 面试官:为什么 Spring 中的 bean 默认为单例?
  14. 面试官:HTTPS 为什么是安全的?说一下他的底层实现原理?
  15. 被一个熟悉的面试题问懵了:StringBuilder 为什么线程不安全?
  16. 拼多多面试|如何用 Redis 统计独立用户访问量?
  17. 面试官:使用SpringBoot如何开发邮件发送系统?
  18. 面试官:聊一下你对MySQL索引的理解?
  19. 面试官:如何实现幂等性校验?
  20. 如何有效的准备Java面试?
  21. 如何手撸一个队列?队列详解和面试题汇总(含答案)
  22. 面试题:如何设计一个高并发的系统?
  23. 面试题:图解深克隆和浅克隆
  24. 秋招注意事项和面试通关攻略
  25. 面试官:数据量大的情况下分页查询很慢,有什么优化方案?
  26. 面试珍藏:最常见的200多道Java面试题(2019年最新版)
  27. 面试题:聊一聊设计模式的基本原则
  28. 我被面试官给虐懵了,竟然是因为我不懂Spring中的@Configuration
  29. 面试官:谈谈数据库连接池的原理
  30. 经典面试题 | 讲一下垃圾回收器都有哪些?
  31. 面试干货 | Java 能否自定义一个类叫 java.lang.System?
  32. 面试官:聊一下二分法
  33. 阿里面试,我挂在了第四轮……
  34. 经典面试题:聊一聊垃圾回收算法
  35. 面试题:彻底搞懂 Cookie 和 Session
  36. 【面试收藏】Redis最全面试题
  37. 面试官问你MySQL的优化,看这篇文章就够了
  38. 经典面试题:如何保证缓存与数据库的双写一致性?
  39. 面试官:讲一下Jvm中如何判断对象的生死?
  40. 面试官:不使用synchronized和lock,如何实现一个线程安全的单例?
  41. 面试必备的分布式事物方案
  42. 面试题:为什么Java中的字符串对象是不可变的
  43. 经典面试题|讲一讲JVM的组成
  44. 记一次蚂蚁金服的面试经历
  45. Kafka面试题全套整理 | 划重点要考!
  46. 阿里面试题BIO和NIO数量问题附答案和代码
  47. Java 200+ 面试题补充③ Dubbo 模块
  48. Java 200+ 面试题补充② Netty 模块
  49. Java 200+ 面试题补充 ThreadLocal 模块
  50. 90% 的人都会答错的面试题 == 和 equals 的区别
  51. 面试经验分享|精华版
  52. 程序员专属精品简历合集—面试必备

关注下面二维码,发现更多精品Java面试内容。

Java中文社群公众号二维码

本文转载自: 掘金

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

SpringBoot 整合 SpringSecurity 之

发表于 2019-12-30

SpringBoot 整合 SpringSecurity 之起源篇(零)

本篇为SpringSecurity的第一篇,主要来介绍下什么是SpringSecurity,以及在springboot中如何使用它

I. 基本知识点

官方文档: docs.spring.io/spring-secu…

下面是官方介绍

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

用国语,简单抽象的说一下它的定义

  • 很🐂的认证和访问权限校验框架

那么具体能干嘛?

  • 用户登录认证:用户名+密码登录,确定用户身份
  • 用户访问鉴权(常见的ACL访问控制列表,RBAC角色访问控制):判定是否有权限访问某个资源
  • 安全保护(CSRF跨站点攻击,Session Fixation会话固定攻击…)

II. 初体验

接下来我们看一下再springboot中如何使用springsecurity

1. 配置

首先得是spring boot项目,然后添加上security的依赖即可,相对完整的pom配置如下(注意我们使用的springboot版本为2.2.1.RELEASE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

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

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

2. 实例demo

上面配置完之后,啥都不需要干,项目已经接入了spring security;项目中的服务都需要登录之后才能访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// 程序启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

// rest 服务
@RestController
public class IndexRest {

@GetMapping(path = {"/", "/index"})
public String index() {
return "hello this is index!";
}

@GetMapping(path = "hello")
public String hello(String name) {
return "welcome " + name;
}
}

当我们需要访问首页时,会发现直接302重定向到登录页面了,如下图

spring security默认给我们生成了一个用户名为user,密码为控制台中输出的一行日志如Using generated security password: aa410186-5c04-4282-b217-507ffb1f61eb

登录之后会重定向回我们之前访问的url,通过抓包可以看到,登录成功之后,会设置请求方的cookie,后续的请求携带cookie来表明用户身份

3. 基本配置

上面虽然演示了一个hello world的初体验项目,但是这个默认的用户名/密码有点鬼畜,默认的配置主要来自于org.springframework.boot.autoconfigure.security.SecurityProperties.User,下面是截图(所以前面的用户名为user)

接下来我们需要配置为对人类友好的方式,在项目的配置文件application.yml中,指定登录的用户名/密码

1
2
3
4
5
复制代码spring:
security:
user:
name: yihuihui
password: 123456

重启测试项目,使用新的用户名/密码(yihuihui/123456)就可以登录成功了;

4. 用户身份获取

上面虽然是一个简单的case,但还有一点不得不提一下,在我的接口中,虽然知道你登录了,但怎么知道你是谁呢?

我们可以直接通过HttpServletRequest#getRemoteUser()的方法来获取登录用户; 或者通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()来获取授权信息

我们来写一个通用方法

1
2
3
4
5
6
7
8
复制代码public String getUser() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getRemoteUser();
}

// or
public Object getUser() {
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

然后稍微改一下我们的服务接口

1
2
3
4
复制代码@GetMapping(path = {"/", "/index"})
public String index() {
return "hello this is index! welcome " + getUser();
}

再次访问之后,结果如下

5. 小结

本文主要是spring security系列的起源篇,第一节介绍了下什么是SpringSecurity,有什么特点

  • spring security是一个很🐂🍺的认证(可以简单理解为登录验证)和鉴权(可简单理解为访问控制)框架
  • 三大特点:登录 + 鉴权 + 安全防护

第二节介绍了一个简单入门的HelloWorld实例

  • springboot项目,添加依赖 spring-boot-starter-security; 所有的http接口访问都需要登录,默认提供一个用户名为user,密码为控制台输出的UUID字符串
  • 通过spring.security.user.name和spring.security.user.password来指定用户名密码
  • 通过HttpServletRequest#getRemoteUser()获取登录用户

那么问题来了,什么系统可能只有一个用户呢?要多用户怎么办?不同的用户不同的权限怎么办?某些接口所有人都可以访问又怎么办?

II. 其他

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 代码: github.com/liuyueyi/sp…

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

一灰灰blog

本文转载自: 掘金

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

1…838839840…956

开发者博客

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