一直都知道 grep 很强大,但是一直都没有办法来定义它,直到看到 man 介绍第一行,非常简洁精炼 “print lines matching a pattern”,一下子就知道了 grep 的作用。
grep 全称是 Global Regular Expression Print。grep 的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。
grep 可用于 shell 脚本,因为 grep 通过返回一个状态值来说明搜索的状态,如果模板搜索成功,则返回 0,如果搜索不成功,则返回 1,如果搜索的文件不存在,则返回 2。我们利用这些返回值就可进行一些自动化的文本处理工作。
在 UNIX
早期的编辑器中,如果要查找比如 junk
这个单词,需要输入 /junk/p
来打印找到的一个行,如果要找所有行,就使用 g/junk/p
,g 是 global search 的意思。
这个功能被独立出来作为了命令,只做一件事情,就是全局搜索,并打印,叫做 grep,其实是 g/regular expression/p
的缩写,可以理解为 g/re/p
,在 vi
或者 sed
中类似的功能也经常能见到。
下面这些选项非常常用,记住:
-i 忽略大小写
-v invert match
-n 行号
-B NUM -A NUM
-C NUM
比如说在当前目录下,搜索文件中包含的 password 词
grep password *
grep 会自动在当前目录下搜索并将包含 password
行打印出来,所以千万不要在本地文件中存放密码一类的敏感信息,这一无异于将密码写在显示器上。
使用 -i
来忽略大小写
grep -i password *
这一行命令会匹配,比如 Password,PASSWORD 等等。
将 grep 作为过滤器来过滤标准输出的内容
cat /etc/passwd | grep 'sshd'
强制让 grep 输出文件名
grep 'sshd' /etc/passwd /dev/null
这时 grep 会打印文件名,冒号,结果
-n
参数会显示行号
grep -n pattern file.txt
正常情况下 grep 会过滤出匹配 正则的行,使用 -v
参数反之,显示不包含正则的行
grep -v 'junk' *
再比如,如果想要设置 every
但是不想搜索 everyone
,everybody
和 everywhere
可以使用
grep every * | grep -v one | grep -v body | grep -v where
grep -B 10 -A 20 pattern file.txt
输出匹配 PATTERN 行的前 10 行和 后 20 行。
很多时候一行日志中有非常多的内容,我们往往只关心特定的部分,这个时候可以使用 grep 的正则来过滤出我们关心的部分,比如日志中的耗时我们可能会打印出 cost 10ms
这样的内容
grep -o -P "cost [0-9]+ms"
说明:
-o
表示只输出匹配到的内容,每一行显示一个-P "regex"
表示开启正则过滤当然如果组合使用 sed 也能够做到同样的事情。
在 grep 之上,后人又开发了很多很有用的命令,比如不解压的情况下搜索压缩包中的内容,再比如简便版的 ack-grep
jinja2 是基于 Python 的模板引擎。
注意下面的 \ 是因为模板需要转义,使用时需要去掉,另外大括号和 % 之间的空格也需要去掉。
\{\% ... \%\} Statements 控制结构,比如 if/elif/else for-loops,macros,block 等等
\{\{ ... \}\} Expressions 表达式,用来输出结果
\{\# ... \#\} Comments 注释
\# ... \#\# Line Statements
\{\{ name \}\}
结构表示一个变量
过滤器用法
Hello, \{\{ name|capitalize \}\}
常用的过滤器
过滤器 | 说明 |
---|---|
safe | 渲染值时不转义 |
capitalize | 把值的首字母转换成大写,其他字母转换成小写 |
lower | 把值转换成小写形式 |
upper | 把值转换成大写形式 |
title | 把值中每个单词的首字母都转换成大写 |
trim | 把值的首尾空格去掉 |
striptags | 渲染之前把值中所有的 HTML 标签都删掉 |
length | 输出长度 |
完整的过滤器清单可以在文档中找到:http://jinja.pocoo.org/docs/templates/#builtin-filters
用来控制流程
常见的 if-else
{ % if user % }
Hello, !
{ % else % }
Hello, Stranger!
{ % endif % }
for 循环
<ul>
{ % for comment in comments % }
<li></li>
{ % endfor % }
</ul>
宏,类似方法
{ % macro render_comment(comment) % }
<li></li>
{ % endmacro % }
<ul>
{ % for comment in comments % }
{ % endfor % }
</ul>
保存到文件中使用 import
{ % import 'macros.html' as macros % }
<ul>
{ % for comment in comments % }
{ % endfor % }
</ul>
需要多次使用的模板可以单独放在文件中,然后 使用 include
{ % include 'common.html' % }
模板继承,先定义父模板
<html>
<head>
{ % block head % }
<title>{ % block title % }{ % endblock % } - My Application</title>
{ % endblock % }
</head>
<body>
{ % block body % }
{ % endblock % }
</body>
</html>
然后继承
{ % extends "base.html" % }
{ % block title % }Index{ % endblock % }
{ % block head % }
<style>
</style>
{ % endblock % }
{ % block body % }
<h1>Hello, World!</h1>
{ % endblock % }
WTForms 支持的字段类型
字段类型 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为 datetime.date 格式 |
DateTimeField | 文本字段,值为 datetime.datetime 格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为 decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为 True 和 False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
WTForms 支持的验证函数
验证函数 | 说明 |
---|---|
验证电子邮件地址 | |
EqualTo | 比较两个字段的值;常用于要求输入两次密码进行确认的情况 |
IPAddress | 验证 IPv4 网络地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入值时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证 URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确保输入值不在可选值列表中 |
在模板中使用
<form method="POST">
</form>
赋值语句比较简单,Learning Python 这本书中对赋值语句介绍比较详细,分类也讲述的比较细,这篇文章就只简单的记录一些容易混乱的知识点,并不记录所有赋值语句需要注意的点。
在之前的文章中就交代过共享引用是需要特别注意的,在 augmented assignment 中也是
>>> L = [1, 2]
>>> M = L # L M 共享同一个对象
>>> L = L + [3, 4] # Concatenation 会创建新的对象
>>> L, M # 所以他们的值不相同
([1, 2, 3, 4], [1, 2])
>>> L = [1, 2]
>>> M = L # Share
>>> L += [3, 4] # 但是 `+=` 其实用的是 extend 方法,所以是原地修改
>>> L, M
([1, 2, 3, 4], [1, 2, 3, 4])
表达式同样也分为很多种
>>> L = [1,2]
>>> L.append(3)
>>> L
[1, 2, 3]
不过需要注意的是,比如 append()
方法并没有返回值,所有返回的 L 会是 None
>>> L = L.append(4)
>>> L
None
Maven 是一个项目管理工具,主要用于项目构建,依赖管理,项目信息管理。自动化构建过程,从清理、编译、测试和生成报告、再到打包和部署。Maven 通过一小段描述信息来管理项目。
安装之前先把 JDK 环境配置好。
Debian/Ubuntu/Linux Mint 下
sudo apt install maven
如果要手动安装则按照下面步骤,选择一个合适的版本
tar zxvf apache-Maven-3.3.9-bin.tar.gz
sudo mv apache-maven-3.3.9/ /opt/
sudo ln -s /opt/apache-maven-3.3.9/ /opt/maven
root 身份修改配置命令 sudo vi ~/.bashrc
在文件最后添加:
#set Maven environment
#export Maven_OPTS="-Xms256m -Xmx512m"
export M2_HOME=/opt/maven
export M2=$M2_HOME/bin
export PATH=$M2:$PATH
保存并关闭。
mvn -version
如果进行了上面步骤在任意目录中 mvn 命令不能使用,可以在 /etc/profile
文件后面加入下面三行 sudo vim ~/.bashrc
然后输入以下内容
Maven_HOME=/usr/local/apache-maven-3.3.9
export Maven_HOME
export PATH=${PATH}:${Maven_HOME}/bin
设置好 Maven 的路径之后,需要运行下面的命令 source ~/.bashrc
使刚刚的配置生效
Maven 最熟悉的一个概念就是 POM,Maven 项目会有一个 pom.xml 文件, 在这个文件里面添加相应配置,Maven 就会自动帮你下载相应 jar 包
<dependency>
<groupId>com.google.firebase</groupId> 项目名
<artifactId>firebase-admin</artifactId> 项目模块
<version>5.3.1</version> 项目版本
</dependency>
项目名 - 项目模块 - 项目版本
三个坐标定义了项目在 Maven 世界中的基本坐标,任何 jar,pom, war 都是基于这些坐标进行区分的。
groupId
定义了项目组,组和项目所在组织或公司,或者开源项目名称,一般为公司域名反写,比如 com.google.firebase 等等。Maven 项目和实际项目并不一定是一对一关系,比如 SpringFramework 实际项目,对应的 Maven 项目会有很多,spring-core, spring-context 等等,更进一步推荐 groupId 应当定义项目隶属的实际项目,如果定义到组织或者公司,那么一个组织下可能会有很多实际项目,造成混乱artifactId
定义了 Maven 项目的名称,在组中的唯一 ID,在同一个项目中可能有不同的子项目,可以定义不同的 artifactId,可以理解为 Maven 项目的模块。artifactId 也是构建完成项目后生成的 jar 包或者 war 包的文件名的一部分。version
顾名思义,就是项目的版本号,如果项目维发布,一般在开发中的版本号习惯性加上 SNAPSHOT, 比如 1.0-SNAPSHOT根据上面的例子,比如上面定义的 Maven 坐标,可以在对应的中央仓库中 https://repo1.maven.org/maven2/com/google/firebase/firebase-admin/5.3.1/
目录下找到对应的文件。
scope
定义了依赖范围,如果依赖范围为 test ,那么该依赖只对测试有效,也就是说在测试代码中引入 junit 有效,在主代码中用 junit 会造成编译错误。如果不声明依赖范围则默认为 compile ,表示该依赖对主代码和测试代码都有效。Maven 有以下几种依赖范围:
依赖范围和 classpath 的关系
依赖范围 Scope | 编译 classpath 有效 | 测试 classpath 有效 | 运行 classpath 有效 | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | - | Y | - | junit |
provided | Y | Y | - | servlet-api |
runtime | - | - | Y | JDBC 驱动 |
system | Y | Y | - | 本地的, Maven 仓库之外的类库 |
Maven 依赖调解 Dependency Mediation ,第一原则:路径最近者优先;第二原则:第一声明者优先。
SNAPSHOT 快照版本只应该在组织内部的项目或者模块之间的依赖使用,组织对于这些快照版本的依赖具有完全的理解和控制权。项目不应该依赖于任何组织外部的快照版本,由于快照的不稳定性,依赖会产生潜在的危险,即使项目构建今天是成功的,由于外部快照版本可能变化,而导致未来构建失败。
在上面介绍 Maven 的作用的时候提到了 Maven 的两个核心概念:坐标和依赖,这也是 Maven 首要解决的问题。这里要引入 Maven 另外一个核心概念:仓库。 Maven 时间中通过坐标来定位依赖,Maven 通过仓库来统一管理这些依赖。
Maven 项目的每一个构件对应着仓库的路径为: groupId/artifactId/version/artifactId-version.packageing
Maven 的仓库分为远程仓库和本地仓库,当第一次运行 Maven 命令时,需要网络连接,从远程仓库下载可用的依赖和插件。而当以后在运行 Maven 命令的时候,Maven 会自动先检查本地 ~/.m2/repository
目录下的依赖,如果本地有缓存优先从本地获取,在找不到的情况下去远程仓库寻找。
常用的在线 maven 依赖查看网站
Maven 的生命周期是抽象的,实际行为都有插件完成。
清理项目,包含三个阶段
pre-clean
执行清理前需要完成的工作clean
清理上一次构建生成的文件post-clean
执行一次清理后需要完成的工作定义了真正构建时所需要执行的步骤
建立和发布项目站点
具体的流程可以参考下图
下面是一个标准的 Maven 工程
src/main/java - 存放项目.java 文件;
src/main/resources - 存放项目资源文件;
src/test/java - 存放测试类.java 文件;
src/test/resources - 存放测试资源文件;
target - 项目输出目录;
pom.xml - Maven 核心文件(Project Object Model);
mvn archetype:create 创建 Maven 项目
mvn compile 编译源代码
mvn deploy 发布项目
mvn test-compile 编译测试源代码
mvn test 运行应用程序中的单元测试
mvn site 生成项目相关信息的网站
mvn clean 清除项目目录中的生成结果
mvn package 根据项目生成的 jar
mvn install 在本地 Repository 中安装 jar
mvn eclipse:eclipse 生成 eclipse 项目文件
mvn jetty:run 启动 jetty 服务
mvn tomcat:run 启动 tomcat 服务
mvn clean package -DMaven.test.skip=true 清除以前的包后重新打包,跳过测试类
mvn clean package 清除以前的包后重新打包
创建一个 Maven 项目
mvn -B archetype:generate
-DarchetypeGroupId=org.apache.maven.archetypes
-DgroupId=com.log4j.maven
-DartifactId=dependency-example
平时开发环境使用的是 jetty,而 Java Web 有一个更好更快的服务器 Resin,这篇文章就来说一下什么是 Resin,以及在 Debug 中如何使用。
Resin 是一个提供高性能的,支持 Java/PHP 的应用服务器。目前有两个版本:一个是 GPL 下的开源版本,提供给一些爱好者、开发人员和低流量网站使用;一种是收费的专业版本,增加了一些更加适用于生产环境的特性。
Resin 也可以和许多其他的 web 服务器一起工作,比如 Apache Server 和 IIS 等。Resin 支持 Servlets 2.3 标准和 JSP 1.2 标准。熟悉 ASP 和 PHP 的用户可以发现用 Resin 来进行 JSP 编程是件很容易的事情。Resin 支持负载平衡,可以增加 WEB 站点的可靠性。方法是增加服务器的数量。比如一台 Server 的错误率是 1% 的话,那么支持负载平衡的两个 Resin 服务器就可以使错误率降到 0.01%。到目前为止,Resin 对 WEB 应用的支持已经远远超过 Tomcat 等各种大型的 Server。
在 Resin 的官方 quick start 教程中有各大平台详细的安装指导。我在使用 apt 安装时没有成功,这里就记录下手工安装的过程。
在 http://caucho.com/download 网址下载, Resin 有两个版本, Pro 版和 GPL 开源版,个人使用开源基础版本已经足够。安装过程如下:
JAVA_HOME
中tar -vzxf resin-4.0.x.tar.gz
根据下载的最新版本解压cd resin-
./configure
更多参数参考 configure optionsmake
sudo make install
sudo resinctl start
, 或者运行 java -jar lib/resin.jar start
http://localhost:8080
可以使用 sudo resinctl stop
来停止运行,更多内容可以参考官方指南 。
第一步,添加 Resin Local 选项,在 IDEA 中 Run/Debug Configuration
中添加 Resin Local 选项
点击 configure 按钮,在弹出窗 Application Servers 中选择部分一中安装的 Resin 目录路径和目录下 Resin 的配置文件路径。
Run/Debug Configurations 中 Server 页面配置,基本都是默认。
Run/Debug Configurations 中 Deployment 页面配置,注意红色方框部分选择。选择 resin.xml 而不是 JMX 否则项目的 index 路径是 localhost:8080/appname/ 而不是 localhost:8080/
在 Resin 的服务器配置下 Depolyment 中 Depolyment method:有 JMX 和 resin.xml 两种选择,JMX 是把项目打包的文件放在 resin 服务器下 webapp 下 只有在服务器启动时 才把项目给拷贝过去 无法在 intellij 中实时更新;resin.xml 是在 C 盘 Temp 目录下 copy 了一份 resin.xml 的配置文件 然后把服务器目录空间指向了你的项目工作空间 可以实现 IntelliJ 修改实时更新。IntelliJ 默认的选择是 JMX 所以我们要选中 resin.xml 模式。同时当项目 Artifacts 指向的目录是 ROOT 时 上图中的 Use default context name(always true if depolyment method is JMX) 取消勾选
执行 build,得到 war 文件。执行 resin run/debug,会自动在你选择的浏览器中打开项目 index 页面。也可以在 IDEA 下方的 Application Servers 面板中进行 Resin 的启动,停止等操作。Resin 启动的打印信息也在此窗口显示。
在 Python 中调用系统命令可以使用两种方法,一种是 os 模块中的 system() 方法,一种是 subprocess 模块中的 call() 方法。
这个方法会接受一个字符串命令,然后在 subshell 中执行,通常是 linux/OSX 下的 bash ,或者 Windows 下面的 cmd.exe。根据官方的文档,os.system()
方法时使用标准 C 方法 system() 来调用实现的,所以存在和 C 方法中一样的限制。
os.system("python –version")
举例
import os
cmd = "git --version"
returned_value = os.system(cmd) # returns the exit code in unix
print('returned value:', returned_value)
默认情况下不使用 shell,他只是简单的执行传入的字符串
运行带参数的命令需要传 list
subprocess.call(["python", "–version"])
subprocess.call() 当使用 shell=True 时和 os.system() 一样使用 shell
subprocess.call(["python", "–version"], shell=True)
举例
import subprocess
cmd = "date"
# returns output as byte string
returned_output = subprocess.check_output(cmd)
# using decode() function to convert byte string to string
print('Current date is:', returned_output.decode("utf-8"))
subproecess 模块中如果想要实现更加复杂的命令,可以使用 Popen()。popen() 会创建一个管道,fork 一个子进程,然后该子进程执行命令。返回值在标准 IO 流中,该管道用于父子进程间通信。父进程要么从管道读信息,要么向管道写信息,至于是读还是写取决于父进程调用 popen 时传递的参数(w 或 r)。
import subprocess
child = subprocess.Popen(['ping','-c','4','douban.com'])
child.wait()
print 'main process'
默认情况下,开启子进程之后不会等待 child 执行完成,需要使用 wait() 来等待子进程完成。
父进程对子进程还有其他的操作
child.poll() # 检查子进程状态
child.kill() # 终止子进程
child.send_signal() # 向子进程发送信号
child.terminate() # 终止子进程
子进程的标准输入、标准输出和标准错误
child.stdin
child.stdout
child.stderr
举例
import subprocess
child1 = subprocess.Popen(["ls","-l"], stdout=subprocess.PIPE)
print child1.stdout.read()
使用 subprocess 创建子进程能够重定向 stdin stdout,总体来说要比 os.system() 更加强大。
最近因为好奇所以大致的看了一下 Telegram 的 bot,很早 就开始用 Telegram 但事实上,我大部分情况下就是将 Telegram 作为一个跨平台同步工具和备份工具来使用,直到最近因为 Ingress 的一些朋友和一个图书群我对 Telegram 才有了更多的使用,也在 Telegram 发现了每周会推送一本书的 Channel。
所以我想照着有一个叫做 to_kindle_bot 的号自己写了一个将图书发送到 kindle 邮箱的机器人,bot 要实现的功能很简单,就是将图书资源发送给 bot 之后将图书发送邮件推送到关联的 kindle 邮箱。将任务拆解开,主要的步骤也比较简单
再做的过程中,我又增加了一些功能
所以一一解决这些问题即可。
Telegram Bots 是一些特殊的账号,不需要设定电话号码。用户可以通过如下方式和 bot 交互:
@botname
并在后面输入请求,这样可以从 inline bots 直接发送内容到任何聊天,群组或频道这一部分其实也没有什么要多说的,botFather 会创建好 bot,然后我用 Python 的 python-telegram-api。基本上也都是非常成熟的文档。
因为早之前就已经做了一个图书相关的网站,所以理所当然的将用 telegram uid 和用户的 kindle 邮箱做了以下映射。
这部分其实之前也做过,所以直接使用 Flask mail 或者标准库里面的 smtp 去发就行了,邮件服务早之前 也做过调查,也是现成的用 mailgun 了。
这一部分确实花了我一些精力,使用 python 的 ebooklib 库来解析了 epub 格式的电子书,也因为 epub 格式鱼龙混杂,所以修了几个 bug,但是 epub 格式的图书解析确实解决了,但是 mobi 格式我是没有找到任何方式来解析,不过幸好,调研的过程中发现了 Calibre 自带的 ebook-meta 命令行工具,在测试之后发现非常好用,支持的格式也非常多,几乎能够支持 99% 的情况,那这样一来获取图书 meta 和封面的工作也 OK。
这部分一开始准备想要用 Amazon 提供的 kindlegen 命令行工具来实现的,但是发现他只能够将 其他的格式转为 mobi 格式,那这样局限就比较大了,虽然能够 cover 我现在的需求,但是如果以后有 mobi 格式转 epub 格式的需求,其实就有了问题,不够幸好还是 Calibre 这个强大的工具,提供了 ebook-convert 命令行工具,能够转化非常多的格式,所以理所当然的就采用了它。
至此就完成了 bot 90% 的工作,剩下就是组织一些文案和 bot 的零星的 bug 问题,所以可以试用下 @kindlepush_bot。
bot 现在为止一共有 start
help
email
settings
这几个功能,除开 settings 功能还未完成, email 命令用来设置 kindle 邮箱,start 和 help 命令用来显示帮助,settings 功能我设想可以让用户选择一个频率,bot 会自动在这个频率下发送一本书到 kindle 邮箱中。
另外如果大家喜欢电子书,我用 Flask 练手的电子书网站 https://book.einverne.info 也欢迎使用。
Jstack 用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息。
这里需要注意的是 Java 8 引入了 Java Mission Control,Java Flight Recorder,和 jcmd
等工具来帮助诊断 JVM 和 Java 应用相关的问题。推荐使用最新的工具以及 jcmd
来进行诊断。
Prints Java thread stack traces for a Java process, core file, or remote debug server. This command is experimental and unsupported.
应用有些时候会挂起或者突然变慢,定位根本原因可能不是简单的事情。线程 dump 提供了当前运行的 Java 进程的当前状态 SNAPSHOT。
jstack 命令能够:
如果 Java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外,jstack 工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息,如果运行的 java 程序呈现 hung 的状态,jstack 是非常有用的。
thread dump 就是将当前时刻正在运行的 JVM 的线程拷贝一份,可以用来分析程序执行情况。
JVM 使用线程来执行内部或外部的操作。
有很多的方法可以获取 Java Thread Dump 信息1,这里使用最常用的 jstack。
打印某个进程的堆栈信息
jstack [PID]
jstack -l [PID]
jstack -m [PID]
jstack -F [PID]
关于如何找到 PID,有很多方法,使用 jps -v
或者 ps -aux
或者 htop
等等方法都可以。
说明:
-l
选项会打印额外的信息,比如说锁信息, locks such as a list of owned java.util.concurrent ownable synchronizers,可以查看 AbstractOwnableSynchronizer
-F
Force a Stack Dump在执行 jstack -l [PID] > /tmp/output.txt
之后可以对 /tmp/output.txt
进行分析
jstack
输出开头是当前 dump 的时间和 JVM 基本信息(包括版本等):
2018-05-24 14:41:06
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):
下面这个部分是 Safe Memory Reclamation(SMR) 安全的内存分配信息:
Threads class SMR info:
_java_thread_list=0x00007f3cd4005870, length=30, elements={
0x00007f3d14011800, 0x00007f3d142dd800, 0x00007f3d142e1800, 0x00007f3d142f4000,
0x00007f3d142f6000, 0x00007f3d142f8000, 0x00007f3d142fa000, 0x00007f3d14333800,
0x00007f3d14340000, 0x00007f3d14bc6800, 0x00007f3c900a1000, 0x00007f3c90255000,
0x00007f3c9025e800, 0x00007f3c90264000, 0x00007f3d14bdf800, 0x00007f3c64008800,
0x00007f3c6400b000, 0x00007f3d14c1e800, 0x00007f3c54025800, 0x00007f3c54027000,
0x00007f3c54042800, 0x00007f3c54044800, 0x00007f3c24005800, 0x00007f3c0c008800,
0x00007f3c0c00a000, 0x00007f3c0c00b800, 0x00007f3c48027000, 0x00007f3c48010000,
0x00007f3c48011000, 0x00007f3cd4004800
}
接下来就是程序的线程信息(非 VM 线程,非 GC 线程):
"main" #1 prio=5 os_prio=0 cpu=1071286.79ms elapsed=509136.64s tid=0x00007f3d14011800 nid=0xad5 runnable [0x00007f3d1993a000]
java.lang.Thread.State: RUNNABLE
at org.eclipse.swt.internal.gtk.OS.Call(Native Method)
at org.eclipse.swt.widgets.Display.sleep(Display.java:5570)
at smartgit.Wx.d(SourceFile:305)
at com.syntevo.smartgit.n.a(SourceFile:398)
at com.syntevo.smartgit.n.a(SourceFile:247)
"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=94.43ms elapsed=509136.51s tid=0x00007f3d142dd800 nid=0xadc waiting on condition [0x00007f3cf10f4000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.3/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@11.0.3/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.3/Reference.java:213)
Locked ownable synchronizers:
- None
线程信息又可以划分成几个部分,每一个线程都包含了如下信息:
Section | Example | 解释 |
---|---|---|
线程名字 | main 和 Reference Handler | 可读的线程名字,这个名字可以通过 Thread 方法 setName 设定 |
线程 ID | #1 | 每一个 Thread 对象的唯一 ID,这个 ID 是自动生成的,从 1 开始,通过 getId 方法获得 |
是否守护线程 | daemon | 这个标签用来标记线程是否是守护线程,如果是会有标记,如果不是这没有 |
优先级 | prio=10 | Java 线程的优先级,可以通过 setPriority 方法设置 |
OS 线程的优先级 | os_prio | |
CPU 时间 | cpu=94.43ms | 线程获得 CPU 的时间 |
elapsed | elapsed=509136.51s | 线程启动后经过的 wall clock time |
Address | tid | Java 线程的地址,这个地址表示的是 JNI native Thread Object 的指针地址 |
OS 线程 ID | nid | The unique ID of the OS thread to which the Java Thread is mapped. |
线程状态 | wating on condition | 线程当前状态 线程状态下面就是线程的堆栈信息 |
Locked Ownable Synchronizer |
线程的运行状态:
New
: 线程对象创建,不可执行Runnable
: 调用 thread.start() 进入 runnable,获得 CPU 时间即可执行Running
: 执行Waiting
: thread.join()
或调用锁对象 wait()
进入该状态,当前线程会保持该状态直到其他线程发送通知到该对象Timed_Waiting
:执行 Thread.sleep(long)、thread.join(long) 或 obj.wait(long) 等就会进该状态,与 Waiting 的区别在于 Timed_Waiting 的等待有时间限制;Blocked
: 等待锁,进入同步方法,同步代码块,如果没有获取到锁会进入该状态。该线程尝试进入一个被其他线程占用的 synchronized 块,当前线程直到锁被释放之前一直都是 blocked 状态Dead
:执行结束,或者抛出了未捕获的异常之后Deadlock
: 死锁Waiting on condition
:等待某个资源或条件发生来唤醒自己Waiting on monitor entry
:在等待获取锁terminated
线程已经结束 run()
并且通知其他线程 joining以上内容来自 Oracle
通过 jstack 信息可以分析线程死锁,或者系统瓶颈,但是这篇文章比较粗浅,只介绍了大概,等以后熟悉了补上。
主要关注 RUNNABLE
或者 BLOCKED
线程,然后是 TIMED_WAITING
状态的线程。这些状态可以帮助我们定位:
异常的 CPU 使用率,通常需要我们关注 RUNNABLE 线程,可以和其他命令一起使用获取额外的信息,比如 top -H -p PID
,可以显示操作系统中特定 CPU 使用率高的线程。
另一方面,如果程序性能突然变慢,可以查看 BLOCKED 线程。这种情况下,单一一个 dump 可以不足以看出问题,我们需要在邻近时间的多个 dump,然后一次比较同一个线程在不同时间点。
一个比较推荐的做法是,每隔 10 秒获取 dump,连续获取 3 次。
主要注意的是任何在线的工具都有可能将 jstask 信息泄漏,上传文件之前请小心。
FastThread 是一个不错的在线分析工具。提供了友好的界面,包括线程的 CPU 使用率,stack 长度,以及其他信息。
唯一的缺点就是 FastThread 会将 jstack 信息存储在云端。
JStack Review 也是一个在线的分析工具,不过是 client-side only,数据不会发送出去。
Spotify Online Java Thread Dump Analyser 是使用 JavaScript 编写的开源分析工具。
除了在线的分析工具,还有一些不错的独立工具可以用来分析。
JProfiler 是一款比较出名的工具,有 10天的试用期。
IBM TMDA can be used to identify thread contention, deadlocks, and bottlenecks. It is freely distributed and maintained but it does not offer any guarantee or support from IBM
Irockel TDA is a standalone open-source tool licensed with LGPL v2.1. The last version (v2.4) was released in August 2020 so it is well maintained. It displays the thread dump as a tree providing also some statistics to ease the navigation
HandlerMethodArgumentResolver 是一个接口:
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
HandlerMethodArgumentResolver 的接口定义如下:
关于 HandlerMethodArgumentResolver 执行流程,大部分可以在类 InvocableHandlerMethod 中看到
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 获取 Controller 中函数的参数对象
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "' with arguments " + Arrays.toString(args));
}
Object returnValue = this.doInvoke(args);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Method [" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "] returned [" + returnValue + "]");
}
return returnValue;
}
private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 获取执行的具体函数的参数
MethodParameter[] parameters = this.getMethodParameters();
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = this.resolveProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
// 首先判断是否有参数解析器支持参数 parameter,采用职责链的设计模式
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
// 如果参数解析器支持解析参数 parameter,那么解析参数成 Controller 的函数需要的格式
args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var9) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(this.getArgumentResolutionErrorMessage("Failed to resolve", i), var9);
}
throw var9;
}
} else if (args[i] == null) {
throw new IllegalStateException("Could not resolve method parameter at index " + parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
}
}
}
return args;
}
AbstractNamedValueMethodArgumentResolver 抽象类主要用来解析方法参数 (resolving method arguments from a named value),请求参数,请求头,path 变量都是 named value 的例子。他下面有很多的子类
AbstractCookieValueMethodArgumentResolver
ExpressionValueMethodArgumentResolver
MatrixVariableMethodArgumentResolver
PathVariableMethodArgumentResolver
RequestAttributeMethodArgumentResolver
RequestHeaderMethodArgumentResolver
RequestParamMethodArgumentResolver
ServletCookieValueMethodArgumentResolver
SessionAttributeMethodArgumentResolver
PathVariableMethodArgumentResolver 用来解析注解 @PathVariable
的参数。
@PathVariable
用来解析 URI 中的 Path 变量,Path 变量是必须的,并且没有默认值。
用来将请求参数解析到注解 @RequestParam
修饰的方法参数中。
比如请求 https://localhost:8080/hello?uid=1234
@RequestMapping(value="/hello",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> test(@RequestParam(value = "uid", required = true, defaultValue = "1") String uid){
return m;
}
其中将请求的 uid 值解析到方法参数 uid 就是通过 RequestParamMethodArgumentResolver 来实现的。
AbstractMessageConverterMethodArgumentResolver 是一个抽象类,主要用来将请求 body 中的内容解析到方法参数中,其子类:
代码片段
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
protected final List<HttpMessageConverter<?>> messageConverters;
protected final List<MediaType> allSupportedMediaTypes;
}
AbstractMessageConverterMethodProcessor 的子类 RequestResponseBodyMethodProcessor:支持 @RequestBody
和 @ResponseBody
,使用举例:
@RequestMapping(value="/hello/test",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> test(@RequestBody Map<String,Object> m){
return m;
}
antigen 是 zsh 的插件管理工具,在他 GitHub 主页上的一句话非常形象的解释了他的功能。
Antigen is to zsh, what Vundle is to vim.
在过去几年的使用里面 antigen 并没有出现多大的问题,但是随着 antigen 以及 zsh 安装的插件过多,导致每一次打开一个新的终端都会变得很慢,所以我在今年早些的时候切换成了 zinit。
curl -L git.io/antigen > antigen.zsh
或者
apt-get install zsh-antigen
或者直接 git clone 该项目,然后指定 antigen.zsh 的位置。
如果使用过 Vim 的 Vundle 对 antigen 的配置应该不陌生。
source /path-to-antigen-clone/antigen.zsh
# Load the oh-my-zsh's library.
antigen use oh-my-zsh
# Bundles from the default repo (robbyrussell's oh-my-zsh).
antigen bundle git
antigen bundle heroku
antigen bundle pip
antigen bundle lein
antigen bundle command-not-found
# Syntax highlighting bundle.
antigen bundle zsh-users/zsh-syntax-highlighting
# Load the theme.
antigen theme robbyrussell
# Tell antigen that you're done.
antigen apply
使配置生效 source ~/.zshrc
可以从这个页面 查看更多的插件。
更加详细的配置可以参考我的配置
在安装 antigen 之后可以直接在命令行输入 antigen version
来查看版本。或者使用其他命令来直接安装插件,更新插件等等。
➜ antigen version
apply -- Load all bundle completions
bundle -- Install and load the given plugin
bundles -- Bulk define bundles
cache-gen -- Generate cache
cleanup -- Clean up the clones of repos which are not used by any bundles currently lo
help -- Show this message
init -- Load Antigen configuration from file
list -- List out the currently loaded bundles
purge -- Remove a cloned bundle from filesystem
reset -- Clears cache
restore -- Restore the bundles state as specified in the snapshot
revert -- Revert the state of all bundles to how they were before the last antigen up
selfupdate -- Update antigen itself
snapshot -- Create a snapshot of all the active clones
theme -- Switch the prompt theme
update -- Update all bundles
use -- Load any (supported) zsh pre-packaged framework
version -- Display Antigen version