mockito 使用

单元测试的目的是在不涉及依赖的情况下测试代码(隔离)。一个设计良好的系统需要遵循 SOLID 原则。

  • (S) Single responsibility principle 单一职责
  • (O) Open/closed principle 开闭原则,对修改关闭,对扩展开放
  • (L) Liskov substitution principle 里氏替换原则,子类和父类表现一致
  • (I) Interface segregation principle 接口隔离原则,不要依赖不要使用的方法,或者在设计时尽量将大接口拆开
  • (D) Dependency inversion 依赖反转,将类和其他类隔离开,尽量依赖抽象,而不依赖具体实现,比如当修改了外部外部依赖具体实现,而不需要大规模的修改原始代码

Target and challenge of unit testing

Unit test 目标是针对一个模块或者一段代码隔离测试,应该消除其他类,或者系统依赖带来的副作用。

测试替身(Test Doubles)用来消除这类副作用,test Doubles 可以分为:

  • dummy object 传入不使用
  • fake object 有基本实现,但是非常简单,比如内存数据库
  • stub class 带着特殊目的的接口或者类的部分实现
  • mock object 接口或者类的虚假实现,可以用来定义固定的输出,mock object 在测试中用来执行特定的行为

通常情况下可以通过手工代码来 mock objects 或者使用 mock framework 来模拟类的行为。Mockito 是一个非常流行的 Mock framework,使用 Mockito 的三段论:

  • Mock 外部依赖,插入 mock 代码
  • 执行 unit test
  • 验证输出

Maven

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

mockito-all 是一个包含了所有依赖 (hamcrest 和 objenesis) 的单一 jar 包。

mockito-core

mockito-core 不包含 hamcrest 和 objenesis 依赖,可以自己来控制依赖的版本。

启用注解

第一种方法,使用 JUnit 的 @RunWith

@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationTest {
    ...
}

或者用代码启用

@Before
public void init() {
    MockitoAnnotations.initMocks(this);
}

@Mock 注解

最常用的注解就是 @Mock 注解,使用 @Mock 注解来创建和插入 mocked 实例,这样就省去了手动调用 Mockito.mock() 方法。

@Spy 注解

@Spy 注解可以 mock 真实对象的方法,让真实对象方法被调用时,就像 mock object 一样可以被配置。

@Captor 注解

参数捕获器,用于捕获 mock 方法参数,进行验证使用

@InjectMocks 注解

@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

通过 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!
}

thenReturn vs thenAnswer 区别

当 mock 方法是知道确定的返回值,那么可以使用 thenReturn 或者 doReturn,方法会 mock 一个确定的返回值。

thenReturn(T value) Sets a return value to be returned when the method is called.

Answer 当需要根据不同条件来 mock 方法并且返回不同返回值时需要 Answer,比如需要根据方法传入参数来返回对应的返回值的情况。

Mock 静态方法

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;
  }
};

Other

《Mockito Cookbook》参考代码:https://github.com/marcingrzejszczak/mockito-cookbook

reference


2017-09-27 mockito , unit-test , java , mock

从 Clonezilla 恢复系统学习 Linux 启动过程

最近又一次使用 Clonezilla 来克隆系统,和以往不同的是,这一次我是备份了整块硬盘到镜像,然后从镜像恢复系统到另外一块硬盘,而不是以往是复制一个分区,所以又产生了一些问题,所以有了这篇文章,一方面来记录一下中间遇到的问题,另一方面也学习巩固一下关于 Linux 启动过程中的必要流程。

基础知识

关于最基础的计算机启动过程也就不展开说,阮一峰,和网上大部分文章已经讲的非常清晰了,这里只简单的列举一些基础名词,必须要知道的概念。

BIOS

全称是 Basic Input Output System,也就是一块被写入开机程序的只读内存,电脑通电之后第一时间会读取的芯片。

POST

POST 不是 HTTP 请求方法的 POST,而是 Power On Self Test,开机自检,在通电之后 BIOS 加载后会自动执行。

MBR

[[MBR]] 是主引导分区,全称是 Master Boot Record,该分区决定该设备是否能够启动。如果设备能够启动,那么该设备第一个扇区,512 字节就需要表明该设备能够启动。

主引导记录告诉计算机去哪一块硬盘寻找操作系统,主引导记录由三部分组成

  • 1-446 字节,调用系统机器码
  • 447-510 字节,分区表
  • 511-512 字节,主引导记录签名 (0x55,0xAA)。

mbr

GRUB

[[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 的过程。

System startup

通过 GRUB 引导启动操作系统,之后操作系统就会接管系统启动,操作系统启动过程也分为很多步骤。

Stage 1.0

BIOS 在 MBR 中搜索 boot record,因为主引导分区必须保证尽量小,所以通过 stage 1.0 找到 1.5 GRUB 引导,GRUB 引导必须在 boot record 和第一个分区之间,将 GRUB 加载到内存之后进入 stage 1.5.

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 文件和加载必要的驱动之后执行。

Stage 2

GRUB stage 2 阶段会加载 Linux kernel 到内存中,kernel 和相关的文件在 /boot 目录中。kernel 文件都以 vmlinuz 开头。

kernel 都是压缩的格式以节省空间,当计算机解压并加载内核到内存之后,会加载 systemd,自此系统启动过程结束,内核和 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

默认情况下 GRUB 会有 10 秒时间来给用户选择系统,这个设置可以通过修改 /etc/default/grub 来修改。

GRUB_DEFAULT=0          # 默认启动的系统序号
GRUB_TIMEOUT=4          # 默认等待多久以预设系统开机

更新之后使用 sudo update-grub 来更新 GRUB。

GRUB Manual 手册中提供了完整的参数解释。

遇到的问题

No bootable device

使用 Clonezilla 复制硬盘所有分区到另外一块硬盘,而我这边遇到的情况是复制结束之后硬盘没有 boot 分区,导致 BIOS 无法找到主引导分区。

解决办法是使用 Clonezilla 的专家(高级)模式,在高级模式中会自动修复 grub 的问题

无法启动 Linux Mint 桌面

这个问题表现形式可能是各种各样的,开机黑屏,或者在 grub 引导之后出现各种乱码命令。对于这个问题的解决方法可能需要是修改 /etc/fstab ,将其中硬盘的 UUID ,通过 sudo blkid 查看获取后保证 /etc/fstab 中启动的硬盘是一块。可以参考之前的文章

reference


2017-09-25 linux , boot , mbr , uefi , bios , clonezilla

每天学习一个命令:使用 grep 查找文件内字符串

一直都知道 grep 很强大,但是一直都没有办法来定义它,直到看到 man 介绍第一行,非常简洁精炼 “print lines matching a pattern”,一下子就知道了 grep 的作用。

grep 全称是 Global Regular Expression Print。grep 的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。

grep 可用于 shell 脚本,因为 grep 通过返回一个状态值来说明搜索的状态,如果模板搜索成功,则返回 0,如果搜索不成功,则返回 1,如果搜索的文件不存在,则返回 2。我们利用这些返回值就可进行一些自动化的文本处理工作。

grep 名字的由来

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 但是不想搜索 everyoneeverybodyeverywhere 可以使用

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

reference


2017-09-23 grep , linux , egrep , command , regular

jinja2 笔记

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 支持的验证函数

验证函数 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值;常用于要求输入两次密码进行确认的情况
IPAddress 验证 IPv4 网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证 URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选值列表中

在模板中使用

<form method="POST">
\{\{ form.hidden_tag() \}\}
\{\{ form.name.label \}\} \{\{ form.name(id='my-text-field') \}\}
\{\{ form.submit() \}\}
</form>

2017-09-22 python , flask , jinja2 , template , web

Python 笔记之赋值语句和表达式

赋值语句比较简单,Learning Python 这本书中对赋值语句介绍比较详细,分类也讲述的比较细,这篇文章就只简单的记录一些容易混乱的知识点,并不记录所有赋值语句需要注意的点。

Augmented assignment and shared references

在之前的文章中就交代过共享引用是需要特别注意的,在 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])

Expression Statements and In-Place Changes

表达式同样也分为很多种

>>> L = [1,2]
>>> L.append(3)
>>> L
[1, 2, 3]

不过需要注意的是,比如 append() 方法并没有返回值,所有返回的 L 会是 None

>>> L = L.append(4)
>>> L
None

2017-09-20 python , notes , assignment , expression

Maven 介绍

Maven 是一个项目管理工具,主要用于项目构建,依赖管理,项目信息管理。自动化构建过程,从清理、编译、测试和生成报告、再到打包和部署。Maven 通过一小段描述信息来管理项目。

Maven 安装

安装之前先把 JDK 环境配置好。

Debian/Ubuntu/Linux Mint 下

sudo apt install maven

如果要手动安装则按照下面步骤,选择一个合适的版本

  • 下载 Maven 最新安装包,地址 http://maven.apache.org/download.cgi 比如 apache-Maven-3.3.9-bin.tar.gz
  • tar zxvf apache-Maven-3.3.9-bin.tar.gz
  • 将 apache-Maven-3.3.9 目录移动到 /opt/ 目录 命令: 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
    

    保存并关闭。

  • 使配置生效必须重启机器或者在命令行输入: source ~/.bashrc
  • 查看 Maven 是否安装成功: 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 作用

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 有以下几种依赖范围:

  • compile 编译依赖,在编译、测试、运行时都有效
  • test 测试依赖,只对于测试 classpath 有效, JUnit 典型
  • provided 已提供依赖,只在编译和测试有效,运行时无效,servlet-api 编译和测试项目时需要该依赖,但是在运行项目时,由于容器已经提供,不需要 Maven 重复引入
  • runtime 运行时依赖,对于测试和运行有效,编译主代码无效, JDBC 驱动实现,项目主代码编译只需要 JDK 提供的 JDBC 接口,只有执行测试或者运行项目才需要实现上述接口的具体 JDBC 驱动
  • system 系统依赖范围,和 provided 范围依赖一致,但是使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径。
  • import 导入依赖,一般不用

依赖范围和 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 通过仓库来统一管理这些依赖。

Maven 项目的每一个构件对应着仓库的路径为: groupId/artifactId/version/artifactId-version.packageing

Maven 的仓库分为远程仓库和本地仓库,当第一次运行 Maven 命令时,需要网络连接,从远程仓库下载可用的依赖和插件。而当以后在运行 Maven 命令的时候,Maven 会自动先检查本地 ~/.m2/repository 目录下的依赖,如果本地有缓存优先从本地获取,在找不到的情况下去远程仓库寻找。

常用的在线 maven 依赖查看网站

Maven 核心概念 生命周期和插件

Maven 的生命周期是抽象的,实际行为都有插件完成。

clean 生命周期

清理项目,包含三个阶段

  • pre-clean 执行清理前需要完成的工作
  • clean 清理上一次构建生成的文件
  • post-clean 执行一次清理后需要完成的工作

default 生命周期

定义了真正构建时所需要执行的步骤

  • validate 验证工程是否正确,所有需要的资源是否可用。
  • initialize
  • generate-sources
  • process-sources 主要资源文件
  • generate-resources
  • process-resources
  • compile 编译主源码
  • process-classed
  • generate-test-sources
  • process-test-sources 测试资源
  • generate-test-resources
  • process-test-resources
  • test-compile 编译项目测试代码
  • process-test-classes 已发布的格式,如 jar,将已编译的源代码打包
  • prepare-package
  • pre-integration-test 在集成测试可以运行的环境中处理和发布包
  • post-integration-test
  • verify 运行任何检查,验证包是否有效且达到质量标准。
  • install 将包安装到 Maven 本地仓库,供本地 Maven 项目使用
  • deploy 复制到远程仓库,供其他人使用

site 生命周期

建立和发布项目站点

  • pre-site 生成项目站点之前需要完成的工作
  • site 生成站点文档
  • post-site 生成之后
  • site-deploy 生成的站点发布到服务器上

具体的流程可以参考下图

Maven 生命周期

Maven 项目文件结构

下面是一个标准的 Maven 工程

src/main/java - 存放项目.java 文件;
src/main/resources - 存放项目资源文件;
src/test/java - 存放测试类.java 文件;
src/test/resources - 存放测试资源文件;
target - 项目输出目录;
pom.xml - Maven 核心文件(Project Object Model);

Maven 常用命令

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

reference


2017-09-20 maven , java , build , management

IntelliJ IDEA 中使用 Resin 调试

平时开发环境使用的是 jetty,而 Java Web 有一个更好更快的服务器 Resin,这篇文章就来说一下什么是 Resin,以及在 Debug 中如何使用。

什么是 Resin

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 安装

在 Resin 的官方 quick start 教程中有各大平台详细的安装指导。我在使用 apt 安装时没有成功,这里就记录下手工安装的过程。

http://caucho.com/download 网址下载, Resin 有两个版本, Pro 版和 GPL 开源版,个人使用开源基础版本已经足够。安装过程如下:

  1. 安装 JDK 6 或者以上版本,将 java 可执行程序配置到环境变量 JAVA_HOME
  2. tar -vzxf resin-4.0.x.tar.gz 根据下载的最新版本解压
  3. cd resin-
  4. ./configure 更多参数参考 configure options
  5. make
  6. sudo make install
  7. 执行 sudo resinctl start, 或者运行 java -jar lib/resin.jar start
  8. 浏览 http://localhost:8080

可以使用 sudo resinctl stop 来停止运行,更多内容可以参考官方指南

IntelliJ IEDA 中使用 Resin 调试

第一步,添加 Resin Local 选项,在 IDEA 中 Run/Debug Configuration 中添加 Resin Local 选项

resin-local

点击 configure 按钮,在弹出窗 Application Servers 中选择部分一中安装的 Resin 目录路径和目录下 Resin 的配置文件路径。

resin-configure

Run/Debug Configurations 中 Server 页面配置,基本都是默认。

Run/Debug Configurations 中 Deployment 页面配置,注意红色方框部分选择。选择 resin.xml 而不是 JMX 否则项目的 index 路径是 localhost:8080/appname/ 而不是 localhost:8080/

resin-deplyment

在 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 启动的打印信息也在此窗口显示。

run-resin


2017-09-19 Resin , IntelliJ , Java

Python 中 subprocess.call() vs os.system() 区别

在 Python 中调用系统命令可以使用两种方法,一种是 os 模块中的 system() 方法,一种是 subprocess 模块中的 call() 方法。

os.system()

这个方法会接受一个字符串命令,然后在 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)

subprocess.call()

默认情况下不使用 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()

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() 更加强大。

reference


2017-09-18 python , subprocess , system , subshell

写了一个推送图书到 Kindle 的 bot

最近因为好奇所以大致的看了一下 Telegram 的 bot,很早 就开始用 Telegram 但事实上,我大部分情况下就是将 Telegram 作为一个跨平台同步工具和备份工具来使用,直到最近因为 Ingress 的一些朋友和一个图书群我对 Telegram 才有了更多的使用,也在 Telegram 发现了每周会推送一本书的 Channel。

所以我想照着有一个叫做 to_kindle_bot 的号自己写了一个将图书发送到 kindle 邮箱的机器人,bot 要实现的功能很简单,就是将图书资源发送给 bot 之后将图书发送邮件推送到关联的 kindle 邮箱。将任务拆解开,主要的步骤也比较简单

  • 首先要了解的就是 Telegram bot 的 API
  • 再次存储用户的 kindle 邮箱
  • 再次就是将文件获取之后发送到对应的邮箱中

再做的过程中,我又增加了一些功能

  • 比如,在 bot 获取到图书之后会自动解析这本图书的 meta 信息和封面信息
  • 比如,kindle 有些格式不支持,所以要提前进行转码

所以一一解决这些问题即可。

How do bots works?

Telegram Bots 是一些特殊的账号,不需要设定电话号码。用户可以通过如下方式和 bot 交互:

  • 通过直接或将 bot 添加到群组并发送信息或命令给 bot
  • 直接在输入框中 @botname 并在后面输入请求,这样可以从 inline bots 直接发送内容到任何聊天,群组或频道

Telegram Bot API

这一部分其实也没有什么要多说的,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 主要命令功能

bot 现在为止一共有 start help email settings 这几个功能,除开 settings 功能还未完成, email 命令用来设置 kindle 邮箱,start 和 help 命令用来显示帮助,settings 功能我设想可以让用户选择一个频率,bot 会自动在这个频率下发送一本书到 kindle 邮箱中。

另外如果大家喜欢电子书,我用 Flask 练手的电子书网站 https://book.einverne.info 也欢迎使用。

reference


2017-09-17 kindle , telegram , bot , python-telegram-api

每天学习一个命令:jstack 打印 Java 进程堆栈信息

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.

jstack 命令能够:

  • Troubleshoot with jstack Utility
  • Force a Stack Dump
  • Stack Trace from a Core Dump
  • Mixed Stack

如果 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外,jstack 工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息,如果运行的 java 程序呈现 hung 的状态,jstack 是非常有用的。

thread dump 就是将当前时刻正在运行的 JVM 的线程拷贝一份,可以用来分析程序执行情况。

用法

打印某个进程的堆栈信息

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 输出

在执行 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 信息可以分析线程死锁,或者系统瓶颈,但是这篇文章比较粗浅,只介绍了大概,等以后熟悉了补上。

reference


2017-09-14 jstack , java , debug , linux , thread-dump

电子书

最近文章

  • Ubuntu 20.04 使用 MergerFS [[so-you-start So you Start]] 的独服有4块 2T 的硬盘,本来想配置一个 Soft RAID-10,但折腾了一个礼拜,重装了无数遍系统,配置了很多次,从 Ubuntu,Proxmox VE,Debian 都尝试了一遍,最终放弃了,想着充分利用其空间,使用 Proxmox VE,备份好数据,不用 RAID 了,毕竟如果使用默认的 RAID-1,我只能利用8T空间中的 2T 不到,而使用 RAID-10 也只能利用不到 4T 左右空间。至于使用单盘,所有的数据备份,和数据安全性的工作就完全依靠自己的备份去完成了。但是好处是可利用的空间大了。
  • So you Start 独服 Proxmox VE 虚拟机配置 Failover IP 最近买了一台 [[so-you-start]] 的独立服务器,开始的时候安装了 Ubuntu 没有充分利用独立服务器的优势,所以这两天把系统重新安装成了 Proxmox VE,然后在上面又安装了 Ubuntu 20.04,So you Start 提供了额外可以购买的 16 个 [[Failover IPs]],Failover IP 本来是为了可以将服务器迁移到另外的服务器而提供的机制,但在 Proxmox VE 虚拟化技术系统下,可以给虚拟机也分配不同的 IP,这样就实现了从一台服务器虚拟化多个 VPS 的操作。
  • 使用 Remark42 替换博客的 Disqus 评论系统 前两天用隐身窗口打开自己的博客的时候突然发现 Disqus 评论框上一大片广告,没想到现在 Disqus 已经这样了,并且之前还暴露出过隐私问题。所以就想着什么时候替换掉它。
  • club 域名宕机近 3 小时故障回顾 刚开始的时候收到了报警,说网站挂了,我的第一反应是 VPS 出问题了,赶紧 SSH 登录上去看,好像都正常。难道是 VPS 网络问题,于是看了看同一台机器上的其他服务,一切都没问题。
  • Rime 输入法中的快捷键 今天在整理 Rime 插件使用的时候,想起来整理一下 Rime 输入法的快捷键。