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 有些格式不支持,所以要提前进行转码

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

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

Spring 中 HandlerMethodArgumentResolver 使用

HandlerMethodArgumentResolver 是一个接口:

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter var1);

    @Nullable
    Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}

HandlerMethodArgumentResolver 的接口定义如下:

  • supportsParameter() 用于判断是否支持对某种参数的解析
  • resolveArgument() 当判断支持后将请求中的参数值进行相应的转换

关于 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

AbstractNamedValueMethodArgumentResolver 抽象类主要用来解析方法参数 (resolving method arguments from a named value),请求参数,请求头,path 变量都是 named value 的例子。他下面有很多的子类

AbstractCookieValueMethodArgumentResolver
ExpressionValueMethodArgumentResolver
MatrixVariableMethodArgumentResolver
PathVariableMethodArgumentResolver
RequestAttributeMethodArgumentResolver
RequestHeaderMethodArgumentResolver
RequestParamMethodArgumentResolver
ServletCookieValueMethodArgumentResolver
SessionAttributeMethodArgumentResolver

PathVariableMethodArgumentResolver

PathVariableMethodArgumentResolver 用来解析注解 @PathVariable 的参数。

@PathVariable 用来解析 URI 中的 Path 变量,Path 变量是必须的,并且没有默认值。

RequestParamMethodArgumentResolver

用来将请求参数解析到注解 @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

AbstractMessageConverterMethodArgumentResolver 是一个抽象类,主要用来将请求 body 中的内容解析到方法参数中,其子类:

  • AbstractMessageConverterMethodProcessor
  • HttpEntityMethodProcessor
  • RequestPartMethodArgumentResolver
  • RequestResponseBodyMethodProcessor

代码片段

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

reference

  • Spring doc

2017-09-10 spring , resolver , spring-boot

使用 antigen 来管理 zsh 插件

antigen 是 zsh 的插件管理工具,在他 GitHub 主页上的一句话非常形象的解释了他的功能。

Antigen is to zsh, what Vundle is to vim.

安装

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 之后可以直接在命令行输入 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

reference


2017-09-07 antigen , zsh , bash , linux , vim , tmux

爬虫相关技术整理

部分内容从 Python 3 网络爬虫开发实战 中整理。

Python 模块

主要依赖 Python 模块

数据库

  • MySQL
  • Redis

抓包

工具依赖

  • Selenium 自动化测试框架
  • Appium 移动端自动化测试框架

爬虫框架


2017-09-05 crawler , spider , python , mitm , linux

使用 Shell 命令来对 Unix 时间戳和日期进行转换 date 命令

在程序中经常要使用到 Unix timestamp 和日期的转换,通常情况下都是 Google 一个时间戳转换的网页在进行转换,其实 Linux 命令中就有能够快速实现转换的命令。主要都是集中在 date 这个命令。date 命令主要用于显示或设定系统时间和日期。

修改系统的时区

Linux 用来修正系统的时区

sudo dpkg-reconfigure tzdata

选择 Asia > Shanghai

date 常用命令

获取当前的 unix timestamp

date +%s    # 返回 10 位时间戳,%s 表示从 1970-01-01 0 点 (epoch 开始的秒数)
date +%s%3N # 返回 13 位时间戳,毫秒
date +%s%N  # 返回 10 + 9 位纳秒

将时间戳转换成日期

$ date +%s
1504516338
$ date -d @1504516338
Mon Sep  4 17:12:18 CST 2017

将 string 日期转成日期

使用 -d 参数可以用来将输入 String 转成特定格式日期,如果不指定具体时间,date 会使用 00:00:00

$ date -d "06/04/1989"
Sun Jun  4 00:00:00 CDT 1989 1559192456
$ date -d "04 June 1989"
Sun Jun  4 00:00:00 CDT 1989
$ date -d "June 04 1989"
Sun Jun  4 00:00:00 CDT 1989
$ date -d "June 04 1989 12:01:01"
Sun Jun  4 12:01:01 CDT 1989

-d 选项也有一些其他很强大的功能,比如

$ date -d '5 minutes ago' # 5 分钟前的时间
Mon Sep  4 17:22:58 CST 2017
$ date -d '100 days'      # 100 天以后的日期
Wed Dec 13 17:29:14 CST 2017
$ date -d '-100 days'     # 100 天以前的日子
Sat May 27 17:30:01 CST 2017
$ date -d '100 days ago'  # 同上
Sat May 27 17:31:10 CST 2017
$ date -d 'next monday'
Mon Sep 11 00:00:00 CST 2017

或者 -d 选项还可以有这样的语法

date -d@1559192456

格式化参数

可以使用 + 来输出不同格式

date +%<format options>

比如

$ date '+%Y-%m-%d %H:%M:%S'
2017-09-04 17:38:46
Format options Purpose of Option Output
date +%a 缩略方式显示星期 (like Mon, Tue, Wed) Thu
date +%A 全称显示星期 (like Monday, Tuesday) Thursday
date +%b Displays Month name in short (like Jan, Feb, Mar ) Feb
date +%B Displays Month name in full short (like January, February) February
date +%d Displays Day of month (e.g., 01) 07
date +%D Displays Current Date; shown in MM/DD/YY 02/07/13
date +%F Displays Date; shown in YYYY-MM-DD 2013-02-07
date +%H Displays hour in (00..23) format 23
date +%I Displays hour (01..12) format 11
date +%j Displays day of year (001..366) 038
date +%m Displays month (01..12) 02
date +%M Displays minute (00..59) 44
date +%S Displays second (00..60) 17
date +%N Displays nanoseconds (000000000..999999999) 573587606
date +%T Displays time; shown as HH:MM:SS Note: Hours in 24 Format 23:44:17
date +%u Displays day of week (1..7); 1 is Monday 4
date +%U Displays week number of year, with Sunday as first day of week (00..53) 05
date +%Y Displays full year i.e. YYYY 2013
date +%Z alphabetic time zone abbreviation (e.g., EDT) IS

reference


2017-09-04 shell , linux

Mastering the Vim

我已经用了很长一段时间 Vim 了,但是 Vim 最可贵之处便在于你永远达不到 Vim 的天花板,在使用的过程中我永远会发现操作 Vim 的其他便捷方法。最近看了一个关于 Vim 的讲座 ,革新我对 Vim 命令的认识。可以说掌握这样的一个世界观可以对 Vim 的操作上到另外一个层次。下面就总结一下这个视频中的精髓内容。

Text Objects and motions

@ChrisToomey 定义了一种 Vim Language,Vim 的语法由数词 + 动词 + 名词 组成,比如:

d 删除
w 单词
将两个字母组合起来就是 删除单词

这个经常使用的命令非常容易记住。如果想要删除三个单词,则是 3dw,所以可以总结出

{number}{command}{text-object}

这样的形式

  • number 是数量
  • command 是动作
  • text-object 是对象

重复和撤销,相信使用过一段时间 Vim 的人应该会知道 . 表示重复上一次命令, u 表示撤销上一次操作。而重复和撤销是针对命令而不是针对输入的,因此每使用一次 . 或者 u 都是命令级别。因此这就给予了 . 操作非常强大的功能。

Verbs: 常用的动作举例

d Delete
c Change  delete and enter insert mode
y yank
>  intend 缩进
v 选择

Nouns: 常见的动作 Motion

w 移动到下一个 word 开始
e 移动到下一个 word 的结尾
b 移动到上一个 word 的开始 back
2j   向下移动 2 lines

Vim 中定义了很多移动操作

基于内容 Nouns: Text Objects

w => words 表示移动一个单词
s => sentences 移动一个句子
p => paragraphs 向下移动一个段落
t => tags  (html xml)

动作 motions

a => all
i => in
t => 'till
f => find forward
F => find backward

iw => inner word
it => inner tag
i" => inner quotes
ip => inner paragraph
as => a sentence

命令 commands

d => delete(also cut)
c => change(delete, then into insert mode)
y => yank (copy)
v => visual select

组合举例

diw  delete in word,即使光标在 word 中也能够快速删除 word
yi)  yank all text inside the parentheses,光标在 `()` 中复制括号中的所有内容

上面的 Text Object

{command}{text object or motion}

在单词中间,diw 删除光标下的单词,dit 删除光标下 tag 中的内容

Nouns: Parameterized Text Objects

f,F => find the next character
t,T => find till
/,?   => search

比如有一行文本

the program print ("Hello, World!")

如果想要删除 Hello 前面的内容,可以在行首使用 dtH , 解读这个命令可以理解为 d => delete unti H 从这里删除直到遇到 H。典型的 verb + noun 组合。

记住动作加移动,能够很快的记忆很多命令。

比如使用 https://github.com/tpope/vim-surround 这个插件,可以轻松的实现,使用命令 cs"' 来将

"hello world"

变成

'hello world'

命令的语义解释就是 change surroundding double quote to single quote 将包围的双引号变成单引号

使用 cs'<p> 将单引号变成 <p>

<p>hello world</p>

使用 cst" 将 surrounding 变成双引号

"hello world"

或者可以使用 ds" 来将 surrounding 双引号删除 delete surrounding “

hello world

DOT command

Vim 中的 “.” 命令,以命令为单位,重复上一个命令。

Sublime , IntelliJ IDEA 中经常被人提及的 multiple cursor 的功能,能够在编辑器中提供多个光标一起编辑,其实 Vim 不需要多光标就能够做到,结合强大的 .n . 可以快速的编辑大量重复的内容。

比如在 Vim 中的 workflow 就是

  • 使用 /pattern 来所有需要编辑的内容
  • 使用可编辑的 edit,比如 cw 当前单词 ESC 退出,一个完成的动作
  • 使用 n 来找到下一个匹配的地点
  • 使用 . 来重复编辑操作,可以直接将单词替换为上一个动作编辑
  • 然后重复上面两个步骤,整个文件即可替换完成

macro

Vim 允许记录一个宏来完成一组命令

qa                  # 将命令记录到寄存器 a 中
q                   # 再次 q 结束记录
@a                  # 使用寄存器
@@                  # 使用上一次寄存器

refernence


2017-09-03 vim , linux , editor

电子书

最近文章

  • 配置 Rime 在 Vim 下退出编辑模式时自动切换成英文输入法 半年以前在 Obisidian 的文章下面有人曾经问过我一个问题,如何在 Vim 或者其他使用 Vim 模式的编辑器,比如 IntelliJ,或者 Obisidian 开启 Vim 模式后方便地切换中英文输入法,因为在编辑模式和普通模式下,需要经常切换输入法,使得体验变得非常槽糕。
  • Asus RT-AC86U 设置 前些天给家里买手机正好凑单了一个 Asus RT-AC86U,正好可以代替出了两次故障的小米 3G。
  • 扩展 Proxmox 系统分区以及 Proxmox 文件系统初识 昨天想要扩展一下之前安装的 Proxmox 容量,对系统进行了一次关机,然而关机之后就悲剧的发现在 U 盘中的系统启动不了了,将 U 盘拔下检测之后发现 U 盘可能挂了,一个全新的 U 盘,在连续 192 天运行之后挂掉了。无奈之下只能想办法先恢复一下 Proxmox 系统以及安装在系统之上的 OpenMediaVault 了。
  • 『译』我最喜欢的命令行工具 偶然间看到一篇介绍 cli 的文章,感觉写得不错,正好借此机会也整理一下我之前使用过,以及觉得非常值得推荐的 CLI 工具。
  • 使用 Clonezilla 将硬盘中系统恢复到虚拟机中 今年陆陆续续将工作的环境迁移到了 macOS,虽然已经把日常的资料迁移到了 macOS,但是之前的 Linux 上还有一些配置,以及可以的一些测试还需要用到 Linux 虚拟机,所以我就想能不能用 Clonezilla 将磁盘中的系统备份然后恢复到虚拟机里面。因为我发现 macOS 下的 Fusion 还是很强大的。