单元测试的目的是在不涉及依赖的情况下测试代码(隔离)。一个设计良好的系统需要遵循 SOLID 原则。
Unit test 目标是针对一个模块或者一段代码隔离测试,应该消除其他类,或者系统依赖带来的副作用。
测试替身(Test Doubles)用来消除这类副作用,test Doubles 可以分为:
通常情况下可以通过手工代码来 mock objects 或者使用 mock framework 来模拟类的行为。Mockito 是一个非常流行的 Mock framework,使用 Mockito 的三段论:
在 http://search.maven.org 中搜索 a:"mockito-core"
或者 g:"org.mockito"
比如说增加:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
mockito-all 是一个包含了所有依赖 (hamcrest 和 objenesis) 的单一 jar 包。
mockito-core 不包含 hamcrest 和 objenesis 依赖,可以自己来控制依赖的版本。
第一种方法,使用 JUnit 的 @RunWith
@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationTest {
...
}
或者用代码启用
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
最常用的注解就是 @Mock
注解,使用 @Mock
注解来创建和插入 mocked 实例,这样就省去了手动调用 Mockito.mock()
方法。
@Spy
注解可以 mock 真实对象的方法,让真实对象方法被调用时,就像 mock object 一样可以被配置。
参数捕获器,用于捕获 mock 方法参数,进行验证使用
@InjectMocks
注解会自动将 mock fields 注入到被测试的 object 中。
需要注意的是,在 JUnit 4 中必须使用 @RunWith(MockitoJUnitRunner.class)
或 MockitoAnnotations.initMocks(this)
来初始化 mocks 并注入。
在 JUnit 5 中必须使用 @ExtendWith(MockitoExtension.class)
@RunWith(MockitoJUnitRunner.class) // JUnit 4
// @ExtendWith(MockitoExtension.class) for JUnit 5
public class SomeManagerTest {
@InjectMocks
private SomeManager someManager;
@Mock
private SomeDependency someDependency; // this will be injected into someManager
// tests...
}
通过 when().thenReturn()
方法链可以用来指定一个方法调用预先定义好的返回值。这个方法也可以指定抛出一个异常
Properties properties = mock(Properties.class);
when(properties.get(”Anddroid”)).thenThrow(new IllegalArgumentException(...));
try {
properties.get(”Anddroid”);
fail(”Anddroid is misspelled”);
} catch (IllegalArgumentException ex) {
// good!
}
当 mock 方法是知道确定的返回值,那么可以使用 thenReturn
或者 doReturn
,方法会 mock 一个确定的返回值。
thenReturn(T value) Sets a return value to be returned when the method is called.
Answer
当需要根据不同条件来 mock 方法并且返回不同返回值时需要 Answer
,比如需要根据方法传入参数来返回对应的返回值的情况。
Mockito 暂时还不支持 Mock 静态方法,所以需要借助 jmockit 完成静态方法的 Mock:
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.30</version>
<scope>test</scope>
</dependency>
new MockUp<StaticClass>() {
@mockit.Mock
public boolean isXXX() {
return true;
}
};
《Mockito Cookbook》参考代码:https://github.com/marcingrzejszczak/mockito-cookbook
最近又一次使用 Clonezilla 来克隆系统,和以往不同的是,这一次我是备份了整块硬盘到镜像,然后从镜像恢复系统到另外一块硬盘,而不是以往是复制一个分区,所以又产生了一些问题,所以有了这篇文章,一方面来记录一下中间遇到的问题,另一方面也学习巩固一下关于 Linux 启动过程中的必要流程。
关于最基础的计算机启动过程也就不展开说,阮一峰,和网上大部分文章已经讲的非常清晰了,这里只简单的列举一些基础名词,必须要知道的概念。
全称是 Basic Input Output System,也就是一块被写入开机程序的只读内存,电脑通电之后第一时间会读取的芯片。
POST 不是 HTTP 请求方法的 POST,而是 Power On Self Test,开机自检,在通电之后 BIOS 加载后会自动执行。
[[MBR]] 是主引导分区,全称是 Master Boot Record,该分区决定该设备是否能够启动。如果设备能够启动,那么该设备第一个扇区,512 字节就需要表明该设备能够启动。
主引导记录告诉计算机去哪一块硬盘寻找操作系统,主引导记录由三部分组成
[[GRUB]] 是 Linux 下最流行的启动管理器(Boot Loader),全称是 GRand Unified Bootloader。计算机在读取 MBR 前 446 字节机器码之后,将运行事先安装的启动管理器 boot loader 也就是 GRUB。
GRUB 设计兼容 multiboot specification,为了使得 GRUB 能够引导启动各种版本的 Linux。GRUB 也能够让用户选择从不同的 kernels 启动。Grub 的配置文件地址在 /boot/grub/grub.conf
.
GRUB1 现在正在被 GRUB2 代替,Grub2 在 /boot/grub2/grub.cfg
配置。
所以总结来看,计算机通电之后,先通过 BIOS,到 MBR,通过 GRUB 引导操作系统启动。以上就是计算机 boot sequence 的过程。
通过 GRUB 引导启动操作系统,之后操作系统就会接管系统启动,操作系统启动过程也分为很多步骤。
BIOS 在 MBR 中搜索 boot record,因为主引导分区必须保证尽量小,所以通过 stage 1.0 找到 1.5 GRUB 引导,GRUB 引导必须在 boot record 和第一个分区之间,将 GRUB 加载到内存之后进入 stage 1.5.
上面提到 GRUB 必须在引导记录和磁盘第一个分区之间,这一块区域因为历史原因被预留,磁盘的第一个分区从 sector 63 开始,MBR 在 sector 0,所以留下了 62512 byte sectors,大概 31774 bytes,用来保存 core.img, core.img 只有 25389 字节,所以完全足够。
需要注意的是,/boot
目录需要存放在支持 GRUB 的文件系统上。stage 1.5 在找到 /boot
文件和加载必要的驱动之后执行。
GRUB stage 2 阶段会加载 Linux kernel 到内存中,kernel 和相关的文件在 /boot
目录中。kernel 文件都以 vmlinuz
开头。
kernel 都是压缩的格式以节省空间,当计算机解压并加载内核到内存之后,会加载 systemd,自此系统启动过程结束,内核和 systemd 都已经在运行。
systemd 是所有进程的创始者,他负责将系统启动至一个可工作的状态,他负责包括挂载系统文件,启动管理系统服务等等工作。
首先 systemd 会尝试使用 /etc/fstab
中定义的内容挂载文件系统,包括定义的根分区,swap 分区文件等等。
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
# / was on /dev/sda1 during installation
UUID=3d1b7e3e-c184-4664-9555-2b088997f2c8 / ext4 errors=remount-ro 0 1
# swap was on /dev/sda5 during installation
UUID=b99bf592-a25b-4ca0-b597-fc62e121aae1 none swap sw 0 0
关于 systemd 的启动顺序,可以参考阮一峰的这篇
默认情况下 GRUB 会有 10 秒时间来给用户选择系统,这个设置可以通过修改 /etc/default/grub
来修改。
GRUB_DEFAULT=0 # 默认启动的系统序号
GRUB_TIMEOUT=4 # 默认等待多久以预设系统开机
更新之后使用 sudo update-grub
来更新 GRUB。
GRUB Manual 手册中提供了完整的参数解释。
使用 Clonezilla 复制硬盘所有分区到另外一块硬盘,而我这边遇到的情况是复制结束之后硬盘没有 boot 分区,导致 BIOS 无法找到主引导分区。
解决办法是使用 Clonezilla 的专家(高级)模式,在高级模式中会自动修复 grub 的问题
这个问题表现形式可能是各种各样的,开机黑屏,或者在 grub 引导之后出现各种乱码命令。对于这个问题的解决方法可能需要是修改 /etc/fstab
,将其中硬盘的 UUID ,通过 sudo blkid 查看获取后保证 /etc/fstab
中启动的硬盘是一块。可以参考之前的文章
一直都知道 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([email protected]/Native Method)
at java.lang.ref.Reference.processPendingReferences([email protected]/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run([email protected]/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