0%

实施nginx和keepalived的规划、安装、配置等步骤。

前面的《统一web访问层方案》中就目的、目标和整体方案进行了讨论,本文讨论具体的实施。简单来说就是在两台服务器上分别部署NginX,并通过keepalived实现高可用。

规划和准备

需要统一访问的应用系统:

应用系统 域名/虚拟目录应用服务器及URL
svn dev.mycompany.com/svn http://50.1.1.21/svn
svn web管理 dev.mycompany.com/submin http://50.1.1.21/submin
网站 www.mycompany.com http://50.1.1.10; http://50.1.1.11; http://50.1.1.12
OA oa.mycompany.com http://50.1.1.13:8080; http://50.1.1.14:8080

web访问服务器

用两台接入服务器50.1.1.3/4分别作为主、备(MASTER、BACKUP)服务器,使用RHEL5.6x64,配置了yum 私服。

两台接入服务器公用一个虚拟IP(VIP):50.1.1.2

安装

两台接入服务器分别安装NginX和keepalived:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

#准备依赖包:
yum -y install gcc pcre-devel zlib-devel openssl-devel

#下载
wget http://nginx.org/download/nginx-1.2.4.tar.gz
wget http://www.keepalived.org/software/keepalived-1.2.7.tar.gz

#安装NginX
tar zxvf nginx-1.2.4.tar.gz
cd nginx-1.2.4
./configure
make && make install

#安装keepalived
tar zxvf keepalived-1.2.7.tar.gz
cd keepalived-1.2.7
./configure
make
make install

cp /usr/local/etc/rc.d/init.d/keepalived /etc/rc.d/init.d/
cp /usr/local/etc/sysconfig/keepalived /etc/sysconfig/
mkdir /etc/keepalived
cp /usr/local/etc/keepalived/keepalived.conf /etc/keepalived/
cp /usr/local/sbin/keepalived /usr/sbin/

#加入启动
echo "/usr/local/nginx/sbin/nginx" >> /etc/rc.local
echo "/etc/init.d/keepalived start" >> /etc/rc.local

配置

配置NginX

两台接入服务器的NginX的配置完全一样,主要是配置/usr/local/nginx/conf/nginx.conf的http。其中多域名指向是通过虚拟主机(配置http下面的server)实现;同一域名的不同虚拟目录通过每个server下面的不同location实现;到后端的服务器在http下面配置upstream,然后在server或location中通过proxypass引用。要实现前面规划的接入方式,http的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
http {
include mime.types;
default_type application/octet-stream;

sendfile on;

upstream dev.hysec.com {
server 50.1.1.21:80;
}


upstream www.hysec.com {
ip_hash;
server 50.1.1.10:80;
server 50.1.1.11:80;
server 50.1.1.12:80;
}

upstream oa.hysec.com {
ip_hash;
server 50.1.1.13:8080;
server 50.1.1.14:8080;


server {
listen 80;
server_name dev.hysec.com;
location /svn {
proxy_pass http://dev.hysec.com;
}

location /submin {
proxy_pass http://dev.hysec.com;
}
}

server {
listen 80;
server_name www.hysec.com;
location / {
proxy_pass http://www.hysec.com;
}
server {
listen 80;
server_name oa.hysec.com;
location / {
proxy_pass http://oa.hysec.com;
}
}

验证方法:

首先用IP访问前表中各个应用服务器的url,再用域名和路径访问前表中各个应用系统的域名/虚拟路径

配置keepalived

按照上面的安装方法,keepalived的配置文件在/etc/keepalived/keepalived.conf。主、从服务器的配置相关联但有所不同。如下:

  • Master配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

! Configuration File for keepalived

global_defs {
notification_email {
wanghaikuo@hysec.com
wanghaikuo@gmail.com
}

notification_email_from wanghaikuo@hysec.com
smtp_server smtp.hysec.com
smtp_connect_timeout 30
router_id nginx_master

}

vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 101
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
50.1.1.2
}
}
  • Backup配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
! Configuration File for keepalived

global_defs {
notification_email {
wanghaikuo@hysec.com
wanghaikuo@gmail.com
}

notification_email_from wanghaikuo@hysec.com
smtp_server smtp.hysec.com
smtp_connect_timeout 30
router_id nginx_backup

}

vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 99
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
50.1.1.2
}
}

验证:

  1. 先后在主、从服务器上启动keepalived:

    /etc/init.d/keepalived start

  2. 在主服务器上查看是否已经绑定了虚拟IP:

    ip addr

  3. 停止主服务器上的keepalived:

    /etc/init.d/keepalived stop

  4. 然后在从服务器上查看是否已经绑定了虚拟IP

  5. 启动主服务器上的keepalived,看看主服务器能否重新接管虚拟IP

3.3 让keepalived监控NginX的状态

经过前面的配置,如果主服务器的keepalived停止服务,从服务器会自动接管VIP对外服务;一旦主服务器的keepalived恢复,会重新接管VIP。 但这并不是我们需要的,我们需要的是当NginX停止服务的时候能够自动切换。

keepalived支持配置监控脚本,我们可以通过脚本监控NginX的状态,如果状态不正常则进行一系列的操作,最终仍不能恢复NginX则杀掉keepalived,使得从服务器能够接管服务。

如何监控NginX的状态
最简单的做法是监控NginX进程,更靠谱的做法是检查NginX端口,最靠谱的做法是检查多个url能否获取到页面。

如何尝试恢复服务
如果发现NginX不正常,重启之。等待3秒再次校验,仍然失败则不再尝试。

根据上述策略很容易写出监控脚本。这里使用nmap检查nginx端口来判断nginx的状态,记得要首先安装nmap。监控脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#!/bin/sh
# check nginx server status
NGINX=/usr/local/nginx/sbin/nginx
PORT=80

nmap localhost -p$PORT | grep "$PORT/tcp open"
#echo$?
if [$? -ne 0 ];then
$NGINX -s stop
$NGINX
sleep 3
nmap localhost -p$PORT | grep "$PORT/tcp open"
[$? -ne 0 ] && /etc/init.d/keepalived stop
fi

不要忘了设置脚本的执行权限,否则不起作用。

假设上述脚本放在/opt/chk_nginx.sh,则keepalived.conf中增加如下配置:

1
2
3
4
5
6
7
8
9
10

vrrp_script chk_http_port {
script "/opt/chk_nginx.sh"
interval 2
weight 2
}

track_script {
chk_http_port
}

更进一步,为了避免启动keepalived之前没有启动nginx , 可以在/etc/init.d/keepalived的start中首先启动nginx:

1
2
3
4
5
6
7
8
9
10

start() {
/usr/local/nginx/sbin/nginx
sleep 3
echo -n$"Starting$prog: "
daemon keepalived${KEEPALIVED_OPTIONS}
RETVAL=$?
echo
[$RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
}

4 还可以做什么

对于简单重复性劳动,人总是容易犯错,这种事情最好交给机器去做。 比如,在这个案例中,作为统一接入服务器,可能经常要修改nginx的配置、nginx下面的html文件等。而且,一定要保证集群中的每台服务器的配置相同。 最好的做法是由配置管理服务器来管理,如果没有,也可以使用简单的linux文件同步来解决。

5 支持https

需要安装openSSL:

yum install openssl-devel

在nginx/conf下生成秘钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#生成RSA密钥
openssl dsaparam -rand -genkey -out myRSA.key 1024

#生成CA密钥:(要输入一个自己记得的密码)
openssl gendsa -des3 -out cert.key myRSA.key

#用这个CA密钥来创建证书,需要上一步创建的密码
openssl req -new -x509 -days 365 -key cert.key -out cert.pem

#把证书设置为root专用
chmod 700 cert.*

#生成免密码文件
openssl rsa -in cert.key -out cert.key.unsecure

如果要启用SSL,首先在安装nginx是要增加配置参数:–with-http_ssl_module ,
然后在nginx中进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 这里是SSL的相关配置
server {
listen 443;
server_name www.example.com; # 你自己的域名
root /home/www;
ssl on;
ssl_certificate cert.perm;
#使用.unsecure文件可以在nginx启动时不输入密码
ssl_certificate_key cert.key.unsecure;
location / {
#...
}
}

公共证书的申请过程:

  1. 生成RSA(私钥)文件:

    openssl genrsa -des3 -out myRSA.key 2048

  2. 生成csr文件:

    openssl req -new -key myRSA.key -out my.csr

  3. 将csr提交给证书机构,比如GlobalSign。

  4. 证书机构会返回私有证书(crt)和中级证书(crt)

  5. 到机构网站下载根证书(root_CA.cer), 将根证书拼接到私有证书之后

  6. 在nginx中配置证书:

1
2
3
ssl_certificate /etc/ssl/my.crt;
ssl_certificate_key /etc/ssl/myRSA.key;
ssl_client_certificate /etc/ssl/root_CA.cer;

6 支持webservice

通过chunkin-nginx-module模块支持webservice。

否则会报错:411:http 头中缺少 Conten-Length 参数

步骤:

1
2
3
4
5
6
git clone https://github.com/agentzh/chunkin-nginx-module.git

#重新编译nginx
cd PATH/TO/NGINX/SOURCE
./configure xxx --add-module=/PATH/TO/chunkin-nginx-module
make && make install

在nginx的server{}节点中增加配置:

1
2
3
4
5
6
7
8

chunkin on;

error_page 411 = @my_411_error;

location @my_411_error {
chunkin_resume;
}

7 状态监控

编译时需要增加--with-http_stub_status_module参数。

查看编译参数:使用命令/usr/local/nginx/sbin/nginx -V

安装好之后增加配置:

1
2
3
4
5
6
7

location /nginx_status {
stub_status on;
access_log off;
# deny all;
allow all;
}

重新加载配置后,会看到一些文本:

Active connections: 1 (对后端发起的活动连接数)

server accepts handled requests

5 5 5 (处理连接个数,成功握手次数,处理请求数)

Reading: 0 Writing: 1 Waiting: 0 (读取客户端header数,返回客户端header数,等待数即active-reading-writing)

早在2012年8月,就通过这篇文章知道了Jekyll, 但是一直没有去尝试。

直到最近静下心来,才发现使用Jekyll 搭建博客非常简单。当然,上手简单,想用好并不容易。

本文记录在使用Jekyll搭建博客过程中的一些过程和经验,并持续完善和改进。

本文分成3个部分:

  • 基础篇:最简单、最快速的使用Jekyll
  • 进阶篇:一些个性化定制的选项
  • 推广篇:博客推广的一些手段和方法

基础篇

关于Jekyll

已经有太多的文章介绍了Jekyll(/‘dʒiːk əl/)。
简单的说,Jekyll是用ruby语言实现的一个静态网站生成器,可以将Markdown (或者Textile)编辑的文档生成html。
当然也可以用来生成博客。
我使用Markdown标记语言,其语法可以参考这里

Jekyll支持Liquid模板语言,写文档时的感觉很像是在写Django模板。Jekyll定义了一些内置的变量,包括全局变量、页面变量等。
在文档中可以设置页面变量的值

与RoR类似,Jekyll也可以通过插件来增加额外的功能。

关于github Pages

github是程序员的facebook。github Pages是github提供的静态网页托管。可以为用户或者项目创建站点。
有意思的是,github Pages对于上传的静态文件会通过Jekyll进行处理后再发布出来。

于是,一些”不务正业“的程序员就开始使用github Pages建立博客,现在这股风潮已经愈演愈烈,一些程序员聚集的博客站点可能要小心应对了。

使用github Pages写博客的好处

为什么说”一些程序员聚集的博客站点可能要小心应对了“呢? 因为github Pages简直是为程序员量身定制的博客系统。
(当然,估计也只有程序员会愿意折腾这些事情)。

对我来说,使用github Pages写博客的好处主要体现在以下方面:

  1. 自由,随意定制
  2. 方便,在github上托管
  3. 可控,有版本管理
  4. 直接,只需提交,不需要先导出再提交,让人愿意持续更新文章
  5. 高效,使用markdown语言能提高写作的效率(但是个人觉得不如org-mode)
  6. 免费,无限流量,无限空间

关于Jekyll Bootstrap

jekyll-bootstrap是用Jekyll建立博客的一套模板,提供了主题(themes)、评论、。。等功能,

对于Jekyll的初学者能提供很大的帮助,其网站上号称“基于GitHub Pages建博客的最快方式”,可以“用3分钟就建立一个博客”。

3分钟建立博客

让我们看看上述工具的组合如何用3分钟建立博客。假设你已经有git的基础,在github上托管过项目。并且使用的不是windows。

# 检查ruby版本
ruby -v
#更换更快的gem源,可选
gem sources --remove http://rubygems.org/
gem sources -a http://ruby.taobao.org/
gem sources -l

#如果不是1.9.3+,需要升级到1.9.3
bash < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer )
source ~/.bashrc

rvm install 1.9.3
# 安装jekyll, 并使用rdiscount作为markdown解析器
sudo gem install jekyll
gem install rdiscount

# 使用Jekyll-Bootstrap,其实就是一个复制的过程。下面的USERNAME代表你在github上的用户名
git clone https://github.com/plusjade/jekyll-bootstrap.git USERNAME.github.com


# 使用GitHub Pages的账户主页建立博客,必须使用如下形式的项目名称并使用主分支
# 如果使用项目主页,必须使用项目的gh-pages分支
cd USERNAME.github.com
git remote set-url origin git@github.com:USERNAME/USERNAME.github.com.git
git push origin master

好了,等上几分钟,你的主页就发布在了https://USERNAME.github.com。

其他操作

jekyll命令

安装jekyll会产生一个命令行工具:jekyll,提供以下功能:

build                Build your site
doctor               Search site and print specific deprecation warnings
help                 Display global or [command] help documentation.
import               Import your old blog to Jekyll
new                  Creates a new Jekyll site scaffold in PATH
serve                Serve your site locally

Rakefile

Jekyll-Bootstrap提供了一个Rakefile(ruby的makefile),包含一些博客相关的任务(task),包括:

$rake -T
rake page # Create a new page.
rake post # Begin a new post in ./_posts
rake preview # Launch preview environment
rake theme:install # Install theme
rake theme:package # Package theme
rake theme:switch # Switch between Jekyll-bootstrap themes.

进阶篇

放弃Jekyll bootstrap

Jekyll bootstrap确实能带来一些变量,但是和RoR一样,充满了各种puzzle。
更加让中国人不爽的是,作者将其缩写定义为“JB”。

经过初步的尝试后,我决定放弃JB,也不想尝试Octopress。我的选择是用原生的Jekyll来构建博客,让一切都在掌控之中。

Jekyll的目录结构

使用Jekyll创建一个干净的站点:

$jekyll new clearly
$tree clearly/
clearly/
├── _config.yml
├── _layouts
│ ├── default.html
│ └── post.html
├── _posts
│ └── 2013-05-29-welcome-to-jekyll.markdown
├── css
│ ├── main.css
│ └── syntax.css
└── index.html

其中,博客文章放在_posts目录中,可以使用子目录。
博客文章必须使用
YEAR-MONTH-DAY-title.MARKUP
的形式命名,比如:
2011-12-31-new-years-eve-is-awesome.md

_layouts目录存放页面模板,其他还可以使用html、css、image等静态资源。

Jekyll会把任何不以下划线开头的文件和目录都复制/生成到网站(在本地是生成到_site/目录)。

设计模板

jekyll把_layouts目录中的文档看做是模板,如果某个文档中的头部变量声明中指定了layout:

---
layout: post
...
---

则Jekyll在生成页面时会使用该模板进行渲染,用文档的内容替换模板中的

早在2012年8月,就通过这篇文章知道了Jekyll, 但是一直没有去尝试。

直到最近静下心来,才发现使用Jekyll 搭建博客非常简单。当然,上手简单,想用好并不容易。

本文记录在使用Jekyll搭建博客过程中的一些过程和经验,并持续完善和改进。

本文分成3个部分:

  • 基础篇:最简单、最快速的使用Jekyll
  • 进阶篇:一些个性化定制的选项
  • 推广篇:博客推广的一些手段和方法

基础篇

关于Jekyll

{% asset_img jekyll.jpg Jekyll %}

已经有太多的文章介绍了Jekyll(/‘dʒiːk əl/)。
简单的说,Jekyll是用ruby语言实现的一个静态网站生成器,可以将Markdown (或者Textile)编辑的文档生成html。
当然也可以用来生成博客。
我使用Markdown标记语言,其语法可以参考这里

Jekyll支持Liquid模板语言,写文档时的感觉很像是在写Django模板。Jekyll定义了一些内置的变量,包括全局变量、页面变量等。
在文档中可以设置页面变量的值

与RoR类似,Jekyll也可以通过插件来增加额外的功能。

关于github Pages

github是程序员的facebook。github Pages是github提供的静态网页托管。可以为用户或者项目创建站点。
有意思的是,github Pages对于上传的静态文件会通过Jekyll进行处理后再发布出来。

于是,一些”不务正业“的程序员就开始使用github Pages建立博客,现在这股风潮已经愈演愈烈,一些程序员聚集的博客站点可能要小心应对了。

使用github Pages写博客的好处

为什么说”一些程序员聚集的博客站点可能要小心应对了“呢? 因为github Pages简直是为程序员量身定制的博客系统。
(当然,估计也只有程序员会愿意折腾这些事情)。

对我来说,使用github Pages写博客的好处主要体现在以下方面:

  1. 自由,随意定制
  2. 方便,在github上托管
  3. 可控,有版本管理
  4. 直接,只需提交,不需要先导出再提交,让人愿意持续更新文章
  5. 高效,使用markdown语言能提高写作的效率(但是个人觉得不如org-mode)
  6. 免费,无限流量,无限空间

关于Jekyll Bootstrap

jekyll-bootstrap是用Jekyll建立博客的一套模板,提供了主题(themes)、评论、。。等功能,

对于Jekyll的初学者能提供很大的帮助,其网站上号称“基于GitHub Pages建博客的最快方式”,可以“用3分钟就建立一个博客”。

3分钟建立博客

让我们看看上述工具的组合如何用3分钟建立博客。假设你已经有git的基础,在github上托管过项目。并且使用的不是windows。

# 检查ruby版本
ruby -v
#更换更快的gem源,可选
gem sources --remove http://rubygems.org/
gem sources -a http://ruby.taobao.org/
gem sources -l

#如果不是1.9.3+,需要升级到1.9.3
bash < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer )
source ~/.bashrc

rvm install 1.9.3
# 安装jekyll, 并使用rdiscount作为markdown解析器
sudo gem install jekyll
gem install rdiscount

# 使用Jekyll-Bootstrap,其实就是一个复制的过程。下面的USERNAME代表你在github上的用户名
git clone https://github.com/plusjade/jekyll-bootstrap.git USERNAME.github.com


# 使用GitHub Pages的账户主页建立博客,必须使用如下形式的项目名称并使用主分支
# 如果使用项目主页,必须使用项目的gh-pages分支
cd USERNAME.github.com
git remote set-url origin git@github.com:USERNAME/USERNAME.github.com.git
git push origin master

好了,等上几分钟,你的主页就发布在了https://USERNAME.github.com。

其他操作

jekyll命令

安装jekyll会产生一个命令行工具:jekyll,提供以下功能:

build                Build your site
doctor               Search site and print specific deprecation warnings
help                 Display global or [command] help documentation.
import               Import your old blog to Jekyll
new                  Creates a new Jekyll site scaffold in PATH
serve                Serve your site locally

Rakefile

Jekyll-Bootstrap提供了一个Rakefile(ruby的makefile),包含一些博客相关的任务(task),包括:

$rake -T
rake page # Create a new page.
rake post # Begin a new post in ./_posts
rake preview # Launch preview environment
rake theme:install # Install theme
rake theme:package # Package theme
rake theme:switch # Switch between Jekyll-bootstrap themes.

进阶篇

放弃Jekyll bootstrap

Jekyll bootstrap确实能带来一些变量,但是和RoR一样,充满了各种puzzle。
更加让中国人不爽的是,作者将其缩写定义为“JB”。

经过初步的尝试后,我决定放弃JB,也不想尝试Octopress。我的选择是用原生的Jekyll来构建博客,让一切都在掌控之中。

Jekyll的目录结构

使用Jekyll创建一个干净的站点:

$jekyll new clearly
$tree clearly/
clearly/
├── _config.yml
├── _layouts
│ ├── default.html
│ └── post.html
├── _posts
│ └── 2013-05-29-welcome-to-jekyll.markdown
├── css
│ ├── main.css
│ └── syntax.css
└── index.html

其中,博客文章放在_posts目录中,可以使用子目录。
博客文章必须使用
YEAR-MONTH-DAY-title.MARKUP
的形式命名,比如:
2011-12-31-new-years-eve-is-awesome.md

_layouts目录存放页面模板,其他还可以使用html、css、image等静态资源。

Jekyll会把任何不以下划线开头的文件和目录都复制/生成到网站(在本地是生成到_site/目录)。

设计模板

jekyll把_layouts目录中的文档看做是模板,如果某个文档中的头部变量声明中指定了layout:

---
layout: post
...
---

则Jekyll在生成页面时会使用该模板进行渲染,用文档的内容替换模板中的{{ content }}部分。

模板本身也是文档,所以一个模板也可以用layout变量指定使用一个模板作为布局,这就是模板的继承。

此外,在设计模板时,利用好Liquid语言的include语法能够带来很大的变量。被包含的页面部件需要放在_includes文件夹中。

因为Jekyll生成的是静态站点,可能会需要大量的js以增加动态特性,在设计模板时要遵循Unobtrusive JavaScript原则

灵活的导航

使用静态的导航菜单会带来两个问题:

  1. 文档过长
  2. “当前项”的高亮不好处理

可以在_config.yml中设置一个导航菜单的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
menuitems:
- name: 首页
url: /index.html
- name: 分类
url: /categories.html
- name: 标签
url: /tags.html
- name: 归档
url: /archive.html
- name: 读书
url: /reading.html
- name: 工作
url: /working.html
- name: 关于
url: /about.html

然后在模板的导航部分可以这样写:

1
2
3
4
5
6
7
8
9
10
11
<ul class="nav">
&#123;% for item in site.menuitems %&#125;
&#123;% if item.url == page.url %&#125;
<li class="active">
&#123;% else %&#125;
<li>
&#123;% endif %&#125;
<a href="{{item.url}}">{{item.name}}</a>
</li>
&#123;% endfor %&#125;
</ul>

分类、标签、归档和RSS

这些都是博客站点必须有的元素。分类、标签和归档可以安装不同的方式检索博客文章;RSS可以订阅博客。

用Jekyll的变量和模板很容易实现这些元素。

注意:不管文件的扩展名是md、html还是xml、txt,只要文件的头部包含变量声明,Jekyll的模板引擎就会对其进行处理。
其中md和html文件都会处理为html,其他类型会保持扩展名。

但如果不是写文档,最好不要使用md,否则会按照markdown语法进行渲染,带来一些意想不到的麻烦。

具体的例子可以参考JB中的代码。

你可能需要对每个分类、每个标签建立单独的索引页面,这个活手工做比较麻烦,可以使用Jekyll插件或者自己写脚本生成文件,
但这不符合“KISS”原则,这里不进行探讨。

对于标签云(tag cloud),在不使用插件的情况下,可以使用js实现,参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="tag-cloud">
{/% for tag in site.tags %/}
<a href="/pages/tags.html#{/{ tag[0] }/}-ref" id="{/{ forloop.index }/}" class="__tag" style="margin: 5px">{/{ tag[0] }/}</a>
{/% endfor %/}
</div>

<script type="text/javascript">
$(function() {
var minFont = 15.0,
maxFont = 40.0,
diffFont = maxFont - minFont,
size = 0;

&#123;% assign max = 1.0 %/}
&#123;% for tag in site.tags %/}
&#123;% if tag[1].size > max %/}
&#123;% assign max = tag[1].size %/}
&#123;% endif %/}
&#123;% endfor %/}

&#123;% for tag in site.tags %/}
size = (Math.log(&#123;&#123; tag[1].size &#125;&#125;) / Math.log({{ max }})) * diffFont + minFont;
$("#{{ forloop.index }}").css("font-size", size + "px");
{/% endfor %/}
});
</script>

关于分类和标签的设计,可以参考这篇文章

分页

TODO: ajax分页
TODO: 浮动标题 on paginator

语法高亮

对于程序员,博客中难免会包含一些代码。实现代码高亮可以有几种方法:

  • 利用外部资源,比如GitHub Gist

    简单,但是需要使用外部链接或通过js嵌入到页面,不利于文档和代码的统一维护

  • 使用js在前端渲染,比如google-code-prettify

    简单高效,对语言的支持不够多

  • 使用Jekyll插件,比如调用pygments

    推荐方式。支持100多种语言

1
2

~~~Jekyll在编译markdown时,会将符合“代码格式”的内容放到一个< pre>< code></ code>< /pre>标签中。
pre><code>区块进行处理,甚至会自动判断使用的语言。~~~
1
2

~~~在post模板的合适位置中增加以下内容:
1
2
3
4
5
6
7
<link href="/js/google-code-prettify/prettify.css" rel="stylesheet">
<script src="/js/google-code-prettify/prettify.js"></script>
<script>
$(document).ready(function(){
prettyPrint();
});
</script>

如果要更改配色方案,只需要修改css文件。

本站采用pygments的方式:

  • 安装pygments
1
pip install pygments
  • 在_config.yml中设置
1
pygments:       true
  • 在代码的前后增加过滤器:
1
2
3
4
5
{/% highlight ruby linenos %/}
def foo
puts 'foo'
end
{/% endhighlight %/}
  • 更改样式

    这里获取css样例,并自行更改。

文档目录(TODO)

如果写比较长的文章,提供一个类似于developerworks上的文档目录进行导航可以方便阅读。

使用公式

MathJax is an open source JavaScript display engine for mathematics that works in all modern browsers.

使用maruku来解析markdown文件,可以把LaTeX解析成图片,

优点是网页加载速度快。但是在windows下安装复杂,且需要安装有LaTeX
Mathjax http://www.mathjax.org/,缺点是动态加载,速度慢。

参考:http://chen.yanping.me/cn/blog/2012/03/10/octopress-with-latex/

处理图片

设置一个IMAGE_ROOT变量,可以可以在post中设置,也可以在post的模板中通过指定的规则capture(或者assign)。

则引可以使用/images/xxx.png的形式插入图片,便于以后的调整和管理。

处理表格(TODO)

博客搬家(TODO)

用Jekyll写博客的,通常不会是新博主,会存在博客搬家的需求。

Jekyll提供了一个import的子命令(需要插件jekyll-import),可以将旧的博客导入到Jekyll。

exitwp是一个用python开发的工具,号称是将wordpress的博客导出并转换成markdown,但实际上
任何能导出rss/atom的博客都可以用这个工具进行转换。

git clone https://github.com/thomasf/exitwp
sudo pip install --upgrade  -r pip_requirements.txt
cd exitwp/wordpress-xml/
wget http://your/atom/file/xml
cd ..
python exitwp.py

推广篇

使用域名

Github Pages会为站点分配类似”USERNAME.github.com”的二级域名。你也可以使用自己的域名。

  1. 申请域名

    免费的域名(.tk)可以在DotTK申请。

    .tk是南太平洋岛国托克劳的国家域名,支持域名转发(可隐藏原URL)、电邮转发、A记录解析、CNAME别名记录、MX邮件记录、设置DNS服务器等服务。

    收费的域名到处有,是否使用国内的域名商随你。

  2. 域名解析服务

    一般来说域名提供商会提供简单的解析服务,也支持将解析服务指向到其他的提供者。

    国外的如Godaddy,可能被墙。

    国内的如DNSPod,有免费版。

  3. 配置

    • 在jekyll站点中增加CNAME文件,记录使用的域名
    • 如果使用顶级域名,在域名解析服务提供商那里将A记录指向204.232.175.78
    • 如果使用二级域名,在域名解析服务提供商那里增加CNAME记录,指向204.232.175.78

社会化网络

Jekyll生成的是静态网站,诸如评论、推荐、关注之类的功能就无法实现了。

不过好在现在有很多社会化网络应用,通过混搭(marshup) 可以把各种各样第三方的功能部件(widgets)加到你的博客中。

与博客相关的Widget主要有几类:

  1. 社会化评论

    专门提供评论功能的网站,可以为博客增加评论功能。也可能附带着关注、相关文章、推荐等功能。

    国外的有disqus,国内的有友言多说

  2. 社会化推荐

    自动推荐跨站的相关文章。包括自动推相关文章。

    国内的有友荐无觅

    Jekyll本身也可以实现站内文章推荐的功能。

  3. 社会化分享

    将自己喜欢的网址分享给别人,通常附带推荐功能。

    国内的有加网百度分享等。其中加网提供了划词分享功能。

  4. 社交网站

    可以发布简短的动态。比如Twitter, Facebook, Google Plus, 新浪微博等网站。与博客的联动可以是自己发布博客动态,
    也可以是由别人推荐(这种方式即为社会化推荐)。

    如果是自己发布动态,需要让别人能够方便的“关注/Follow”你,最好提供“一键关注的按钮”,或者提供连接能够让别人在这些网上方便的找到你。

  5. 社会化登录

    没懂,感觉也就是OpenID或OAuth的集合。暂时不予考虑。

由于存在着伟大的墙,我只好尽量选择国内的社会化网络资源。对于更喜欢的国外的资源,尽量考虑如何不拖慢墙内用户的访问速度。

我对社会化网络资源的利用方式如下:

  • 评论功能

    只选一个,我选择了友言

  • 推荐功能

    可以有多个,那么先加上友荐和无觅,Jekyll自带的相关文章功能也在测试中。

  • 分享功能

    只选一个,还是选择百度分享吧,与百度统计可以勾搭在一起,而且据说有利于百度的SEO。

流量分析和统计

第三方的流量分析和统计工具可以说是最古老的marshup,尽管没有社会化网络的功能。

可以选择的有国外的Google AnalysisSiteMeter和国内的百度统计
量子恒道统计等。

出于种种无奈,还是选择了百度。

部分。

模板本身也是文档,所以一个模板也可以用layout变量指定使用一个模板作为布局,这就是模板的继承。

此外,在设计模板时,利用好Liquid语言的include语法能够带来很大的变量。被包含的页面部件需要放在_includes文件夹中。

因为Jekyll生成的是静态站点,可能会需要大量的js以增加动态特性,在设计模板时要遵循Unobtrusive JavaScript原则

灵活的导航

使用静态的导航菜单会带来两个问题:

  1. 文档过长
  2. “当前项”的高亮不好处理

可以在_config.yml中设置一个导航菜单的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
menuitems:
- name: 首页
url: /index.html
- name: 分类
url: /categories.html
- name: 标签
url: /tags.html
- name: 归档
url: /archive.html
- name: 读书
url: /reading.html
- name: 工作
url: /working.html
- name: 关于
url: /about.html

然后在模板的导航部分可以这样写:

1
2
3
4
5
6
7
8
9
10
11
<ul class="nav">
&#123;% for item in site.menuitems %&#125;
&#123;% if item.url == page.url %&#125;
<li class="active">
&#123;% else %&#125;
<li>
&#123;% endif %&#125;
<a href="{{item.url}}">{{item.name}}</a>
</li>
&#123;% endfor %&#125;
</ul>

分类、标签、归档和RSS

这些都是博客站点必须有的元素。分类、标签和归档可以安装不同的方式检索博客文章;RSS可以订阅博客。

用Jekyll的变量和模板很容易实现这些元素。

注意:不管文件的扩展名是md、html还是xml、txt,只要文件的头部包含变量声明,Jekyll的模板引擎就会对其进行处理。
其中md和html文件都会处理为html,其他类型会保持扩展名。

但如果不是写文档,最好不要使用md,否则会按照markdown语法进行渲染,带来一些意想不到的麻烦。

具体的例子可以参考JB中的代码。

你可能需要对每个分类、每个标签建立单独的索引页面,这个活手工做比较麻烦,可以使用Jekyll插件或者自己写脚本生成文件,
但这不符合“KISS”原则,这里不进行探讨。

对于标签云(tag cloud),在不使用插件的情况下,可以使用js实现,参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="tag-cloud">
{/% for tag in site.tags %/}
<a href="/pages/tags.html#{/{ tag[0] }/}-ref" id="{/{ forloop.index }/}" class="__tag" style="margin: 5px">{/{ tag[0] }/}</a>
{/% endfor %/}
</div>

<script type="text/javascript">
$(function() {
var minFont = 15.0,
maxFont = 40.0,
diffFont = maxFont - minFont,
size = 0;

&#123;% assign max = 1.0 %/}
&#123;% for tag in site.tags %/}
&#123;% if tag[1].size > max %/}
&#123;% assign max = tag[1].size %/}
&#123;% endif %/}
&#123;% endfor %/}

&#123;% for tag in site.tags %/}
size = (Math.log(&#123;&#123; tag[1].size &#125;&#125;) / Math.log({{ max }})) * diffFont + minFont;
$("#{{ forloop.index }}").css("font-size", size + "px");
{/% endfor %/}
});
</script>

关于分类和标签的设计,可以参考这篇文章

分页

TODO: ajax分页
TODO: 浮动标题 on paginator

语法高亮

对于程序员,博客中难免会包含一些代码。实现代码高亮可以有几种方法:

  • 利用外部资源,比如GitHub Gist

    简单,但是需要使用外部链接或通过js嵌入到页面,不利于文档和代码的统一维护

  • 使用js在前端渲染,比如google-code-prettify

    简单高效,对语言的支持不够多

  • 使用Jekyll插件,比如调用pygments

    推荐方式。支持100多种语言

1
2

~~~Jekyll在编译markdown时,会将符合“代码格式”的内容放到一个< pre>< code></ code>< /pre>标签中。
pre><code>区块进行处理,甚至会自动判断使用的语言。~~~
1
2

~~~在post模板的合适位置中增加以下内容:
1
2
3
4
5
6
7
<link href="/js/google-code-prettify/prettify.css" rel="stylesheet">
<script src="/js/google-code-prettify/prettify.js"></script>
<script>
$(document).ready(function(){
prettyPrint();
});
</script>

如果要更改配色方案,只需要修改css文件。

本站采用pygments的方式:

  • 安装pygments
1
pip install pygments
  • 在_config.yml中设置
1
pygments:       true
  • 在代码的前后增加过滤器:
1
2
3
4
5
{/% highlight ruby linenos %/}
def foo
puts 'foo'
end
{/% endhighlight %/}
  • 更改样式

    这里获取css样例,并自行更改。

文档目录(TODO)

如果写比较长的文章,提供一个类似于developerworks上的文档目录进行导航可以方便阅读。

使用公式

MathJax is an open source JavaScript display engine for mathematics that works in all modern browsers.

使用maruku来解析markdown文件,可以把LaTeX解析成图片,

优点是网页加载速度快。但是在windows下安装复杂,且需要安装有LaTeX
Mathjax http://www.mathjax.org/,缺点是动态加载,速度慢。

参考:http://chen.yanping.me/cn/blog/2012/03/10/octopress-with-latex/

处理图片

设置一个IMAGE_ROOT变量,可以可以在post中设置,也可以在post的模板中通过指定的规则capture(或者assign)。

则引可以使用/images/xxx.png的形式插入图片,便于以后的调整和管理。

处理表格(TODO)

博客搬家(TODO)

用Jekyll写博客的,通常不会是新博主,会存在博客搬家的需求。

Jekyll提供了一个import的子命令(需要插件jekyll-import),可以将旧的博客导入到Jekyll。

exitwp是一个用python开发的工具,号称是将wordpress的博客导出并转换成markdown,但实际上
任何能导出rss/atom的博客都可以用这个工具进行转换。

git clone https://github.com/thomasf/exitwp
sudo pip install --upgrade  -r pip_requirements.txt
cd exitwp/wordpress-xml/
wget http://your/atom/file/xml
cd ..
python exitwp.py

推广篇

使用域名

Github Pages会为站点分配类似”USERNAME.github.com”的二级域名。你也可以使用自己的域名。

  1. 申请域名

    免费的域名(.tk)可以在DotTK申请。

    .tk是南太平洋岛国托克劳的国家域名,支持域名转发(可隐藏原URL)、电邮转发、A记录解析、CNAME别名记录、MX邮件记录、设置DNS服务器等服务。

    收费的域名到处有,是否使用国内的域名商随你。

  2. 域名解析服务

    一般来说域名提供商会提供简单的解析服务,也支持将解析服务指向到其他的提供者。

    国外的如Godaddy,可能被墙。

    国内的如DNSPod,有免费版。

  3. 配置

    • 在jekyll站点中增加CNAME文件,记录使用的域名
    • 如果使用顶级域名,在域名解析服务提供商那里将A记录指向204.232.175.78
    • 如果使用二级域名,在域名解析服务提供商那里增加CNAME记录,指向204.232.175.78

社会化网络

Jekyll生成的是静态网站,诸如评论、推荐、关注之类的功能就无法实现了。

不过好在现在有很多社会化网络应用,通过混搭(marshup) 可以把各种各样第三方的功能部件(widgets)加到你的博客中。

与博客相关的Widget主要有几类:

  1. 社会化评论

    专门提供评论功能的网站,可以为博客增加评论功能。也可能附带着关注、相关文章、推荐等功能。

    国外的有disqus,国内的有友言多说

  2. 社会化推荐

    自动推荐跨站的相关文章。包括自动推相关文章。

    国内的有友荐无觅

    Jekyll本身也可以实现站内文章推荐的功能。

  3. 社会化分享

    将自己喜欢的网址分享给别人,通常附带推荐功能。

    国内的有加网百度分享等。其中加网提供了划词分享功能。

  4. 社交网站

    可以发布简短的动态。比如Twitter, Facebook, Google Plus, 新浪微博等网站。与博客的联动可以是自己发布博客动态,
    也可以是由别人推荐(这种方式即为社会化推荐)。

    如果是自己发布动态,需要让别人能够方便的“关注/Follow”你,最好提供“一键关注的按钮”,或者提供连接能够让别人在这些网上方便的找到你。

  5. 社会化登录

    没懂,感觉也就是OpenID或OAuth的集合。暂时不予考虑。

由于存在着伟大的墙,我只好尽量选择国内的社会化网络资源。对于更喜欢的国外的资源,尽量考虑如何不拖慢墙内用户的访问速度。

我对社会化网络资源的利用方式如下:

  • 评论功能

    只选一个,我选择了友言

  • 推荐功能

    可以有多个,那么先加上友荐和无觅,Jekyll自带的相关文章功能也在测试中。

  • 分享功能

    只选一个,还是选择百度分享吧,与百度统计可以勾搭在一起,而且据说有利于百度的SEO。

流量分析和统计

第三方的流量分析和统计工具可以说是最古老的marshup,尽管没有社会化网络的功能。

可以选择的有国外的Google AnalysisSiteMeter和国内的百度统计
量子恒道统计等。

出于种种无奈,还是选择了百度。

从emacs切换回vim

以前在博客园时,用emacs org-mode 写博客,并且写了一系列《emacs 学习笔记》
emacs 和 org-mode 的强大毋庸置疑,但是经过1年多的使用,还是有些不适应:

  1. 小手指很受伤。

  2. 过于依赖配置。

    由于我的工作要经常登录到linux服务器进行操作,这就带来了一个问题:
    服务器上的emacs在不配置的情况下几乎无法使用,但是在服务器上使用vim,又不符合手指中记忆的快捷键。

  3. emacs有点重,比如不得不使用的ecb,cedet,jdee等等,都是大块头。

  4. 我还没有做好准备去掌握Erlang语言。但是对于vim,我可以使用我喜欢的python去写插件。

经过艰难的取舍,还是决定在个人工作领域也回到vim。保护手指,保护大脑。

插件管理器(Vundle)

重新关注vim后,首先发现了一系列插件管理器。主要有:

  1. Vundle
  2. vim-addon-manager
  3. pathogen.vim
  4. vvundle
  5. vvimana

经过简单的比较,我选择了Vundle。这里不想对上述插件管理器做一个完整的对比,只是简单说一个我看中的Vundle的特点:

  1. 只需要维护需要的插件列表就可以统一安装,同样,复制环境时也只需要复制一个文件(.vimrc)
  2. 支持git更新
  3. 支持插件搜索功能
  4. 自动管理插件依赖关系

安装Vundle

安装Vundle只需要一条命令:

$git clone https://github.com/gmarik/vundle.git ~/.vim/bundle/vundle

如果你使用git管理vim配置,还可以使用git submodule:

git submodule add https://github.com/gmarik/vundle.git vim/bundle/vundle

会在.gitmodule中增加如下配置:

[submodule "vim/bundle/vundle"]
    path = vim/bundle/vundle
    url = https://github.com/gmarik/vundle.git

之后运行git命令:

git submodule init
git submodule update

即可。

配置插件

在.vimrc中配置需要的插件,作者给出了一个例子:

set nocompatible               " be iMproved
filetype off                   " required!
set rtp+=~/.vim/bundle/vundle/
call vundle#rc()


" let Vundle manage Vundle
" required!
Bundle 'gmarik/vundle'


" My Bundles here:
"
" original repos on github
Bundle 'tpope/vim-fugitive'
Bundle 'Lokaltog/vim-easymotion'
Bundle 'rstacruz/sparkup', {'rtp': 'vim/'}
Bundle 'tpope/vim-rails.git'
" vim-scripts repos
Bundle 'L9'
Bundle 'FuzzyFinder'
" non github repos
Bundle 'git://git.wincent.com/command-t.git'
" ...

注意:

  1. 对于重名的Vim插件,需要在插件后面加上作者的姓氏, 比如 Bundle ‘Javascript-Indentation’
  2. 对于插件名称中包含空格和斜杠的情况, 需要将空格和斜杠替换为 -

安装插件

只需要在启动vim后,运行命令:

:BundleInstall

Vbundle就会自动安装或更新前面配置好的插件

其他操作

使用帮助:

:h vundle

查看插件清单:

:BundleList

搜索插件:

:BundleSearch markdown

清理不用的插件:

:BundleClean
#或者
:BundleClean markdown

必备插件(TODO)

下面是我使用的一些vim插件,直接在.vimrc中配置。可以在 github 查看。

编辑器增强

  • NERDTree(Bundle ‘The-NERD-tree’)可以在窗口左侧打开文件浏览器

  • Bundle ‘vim-orgmode’

  • Bundle ‘winmanager’

  • Bundle ‘SuperTab’

语法高亮

  • Markdown(Bundle ‘Markdown’) markdown文件的语法高亮

vim基本操作

以前整理过一个 vim 常用命令备忘, 如下:

vim_cheet_sheet.xlsx

别人的一个更详细的版本:

vi-vim-cheat-sheet-list

如果已经有一定的基础,还可以使用vim cheat sheet。下面的图分别可以用于打印版或者桌面背景。

统计学:从数据到结论,ISBN:9787503749964,作者:吴喜之 @豆瓣

##第2章 数据的收集

2013-05-19: 1st

  • 获取数据

    一手数据vs二手数据——自己调研的or别人公布的

    观测数据 vs 试验数据——是否可控

  • 个体、总体和样本

    单个对象:个体element, individual, unit

    所有要研究的个体的集合:总体(有限总体)population

    被研究的个体的集合:该总体的一个样本sample

    抽样方法:

    • 简单随机抽样:每个个体有同等机会被选到样本——随机样本random sample

    • 样本量sample size

    • 随机样本的产生:使用随机数(random number)

    • 方便样本:比如。。。

  • 收集数据时的误差

    样本的特征(比如男女比例)不一定和总体完全一样,这叫抽样误差(sampling error)

    调查问卷不一定被回答,这叫未响应误差(nonresponse error)

    回答不一定真实,这叫响应误差(response error)

    • 抽样误差不可避免
    • 未响应误差和响应误差应该尽量避免
  • 抽样调查和一些常用的方法

    抽样调查的设计目的是确保样本对总体的代表性,以保证后续推断的可靠性

    抽样方法可以分为概率抽样方法和非概率抽样方法

    概率抽样假定每个个体出现在样本中的概率是已知的,从而能够对数据进行合理的统计推断;

    非概率抽样方法比较省时省力,但是推断时要慎重:依赖于抽样方案的设计和实施,

    这种推断无法根据漂亮的统计理论来进行,也很难客观建立抽样误差的范围。

简单随机样本(全部随机抽样)很难实施,通常会采用局部随机抽样。下面是一些抽样方法:

  1. 概率抽样方法

    • 系统抽样:

      根据样本量确定距离n;

      为每个单元编号;

      随机选取一个开始点;

      安装n等距抽样。

      如果编号是随机的,则等价于简单随机抽样。

    • 分层抽样

      对样本进行分类;

      在各类中简单随机抽样;

      对结果进行汇总。

      可以按照比例,也可以不安装比例,也可以加权(加权系数的和为1)

    • 整群抽样

      把总体划分成若干群cluster, 群中的个体不相似;

      随机抽取几个群;

      单级整群抽样:调查抽取的整个群;两级整群抽样:对随机抽取的群再进行简单随机抽样

      适用于各群差异不大的情况,主要用于区域抽样。

    • 多级抽样

      多层次分群,在最后一级进行调查

      每级的抽样方法可能不同,整个抽样计划可能比较复杂

  2. 非概率抽样方法

    • 目的抽样

      主观选择对象,样本的多少依赖于预先就有的知识

    • 方便抽样

      随意选取(不具有随机性),用于初期的评估或探索性研究

    • 判断抽样

      主观评判选择样本,是方便抽样的延伸

    • 定额抽样

      非概率的分层抽样。先确定分类和比例,然后对各类进行方便抽样或判断抽样

    • 雪球抽样

      对于样本稀少的情况,依赖于一个目标推荐另一个目标,偏差较大

    • 自我选择

      让个体自愿参加调查

  3. 实际的抽样方法可能是各种抽样方法的组合。考虑精确度的同时,考虑方便性、可行性、经济性


计算机中常用的数据形式:

  • 原始数据

  • 汇总表格(不能还原成原始数据)


小结

数据总是从一个总体中抽取出来的,是总体的一个代表,称为样本

数据是否可控分为观测数据和试验数据

数据来源可以分为一手数据和二手数据

数据科恩能够有抽样误差(不可避免),对于响应/非响应误差要尽量避免

样本的抽取有多种方法

抽取样本、收集数据是为了从样本中得到总体的信息,关系到后续分析和推断的结果是否合理

100个股民可能有101种分析手段,但通常都是采用技术分析(Technical Analysis,TA)。本文视图整理技术分析理论的总体理论框架,以便对于研究内容有一个定位和界定,并作为今后学习的索引。

Read more »

一篇旧的博文,原文发表在博客园

快年底了,假如你们公司的美国总部给每个人发了一笔201212.21美元的特别奖金,作为程序员的你, 该如何把这笔钱收入囊中?

1.美元?美元!

你可能觉得,这根本不是问题。在自己的账户中直接加上一笔“转入”就行了。但是首先就遇到了币种的问题。

一般来说,银行账户都是单币种的。你可能会说不对啊,我的一卡通就能存入不同的币种啊?但那是一个“账号(Account Number)”对应的多个“账户(Account)”。 通常财务记账的时候,一个“账户(Account)”都使用同一币种。

账户(Account)记录了资金的往来,包含很多条目(Entry)。账户会记录结余,结余等于所有条目中金额的总和。

我们不可能为每个币种设计一种条目,所以需要抽象出一个货币类——Money,适用于各种不同的币种:

Money类至少要记录金额和币种:

  • 对于金额,由于货币存在最小面额,所以金额的类型可以采用定点小数或者整型。考虑到会对金额进行一些运算,用整数处理应该更方便。如果用java语言实现,可以使用long类型。

  • 对于币种,java提供了java.util.Currency类,专门用于表示货币,符合ISO 4217货币代码标准。Currency使用Singleton模式,需要用getInstance方法获得实例。

    主要的方法包括:

    • String getCurrencyCode() 获取货币的ISO 4217货币代码
    • int getDefaultFractionDigits() 获取与此货币一起使用的默认小数位数
    • static Currency getInstance(Locale locale) 返回给定语言环境的国家/地区的 Currency 实例
    • static Currency getInstance(String currencyCode) 返回给定货币代码的 Currency 实例。
    • String getSymbol() 获取默认语言环境的货币符号
    • String getSymbol(Locale locale) 获取指定语言环境的货币符号
    • String toString() 返回此货币的 ISO 4217 货币代码

    通过Currency类的帮助,我们的Money类看起来大概是这个样子(为了方便,提供多种构造函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Money {
private long amount;
private Currency currency;

public double getAmount() {
return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits()).doubleValue();

}

public Currency getCurrency() {
return currency;
}

public Money(double amount, Currency currency) {
this.currency = currency;
this.amount = Math.round(amount * centFactor());
}

public Money(long amount, Currency currency) {
this.currency = currency;
this.amount = amount * centFactor();
}

private static final int[] cents = new int[] { 1, 10, 100, 1000,10000 };

private int centFactor() {
return cents[currency.getDefaultFractionDigits()];
}
}

用Money类表示我们的$201212.21奖金,就是:

1
Money myMoney = new Money(201212.21,Currency.getInstance(Locale.US));

2.存入账户

终于解决了币种的问题,可以把钱存入账户了。存入的逻辑是:在条目中记录一笔账目,并计算账户的余额。

不同币种之间相加或相减是没有意义的,为了避免人为错误,在Money的代码中就要禁止这种操作。我们可以采用抛出异常的方式。 为了简单起见,这里不再定义一个单独的”MoneyException”,而是直接使用java.lang.Exception:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Money add(Money money) throws Exception{
if(!money.getCurrency().equals(this.currency)){
throw(new Exception("different currency can't be add"));
}
BigDecimal value = this.getAmount().add(money.getAmount());
Money result = new Money(value.doubleValue(),this.getCurrency());
return result;
}

public Money minus(Money money) throws Exception{
if(!money.getCurrency().equals(this.currency)){
throw(new Exception("different currency can't be minus"));
}

BigDecimal value =this.getAmount().add(money.getAmount().negate());
Money result = new Money(value.doubleValue(),this.getCurrency());
return result;

}

3.收税

先不要高兴得太早,这笔钱属于“一次性所得”,需要交20%的个人所得税。税后所得应该是多少?

你可能说:是80%。只要为Money加上一个multiply(double factor)方法就可以进行计算了。

但是牵扯到了舍入的问题。由于货币存在最小单位,在做乘/除法运算的时候就要考虑到舍入的问题了。最好是能够控制舍入的行为。假如税务部门对于 舍入的计算有明确规定,我们也可以做一个遵纪守法的好公民。

在java.math.BigDecimal中定义了7种舍入模式:

  • ROUND_UP:等于远离0的数。
  • ROUND_DOWN:等于靠近0的数。
  • ROUND_CEILING:等于靠近正无穷的数。
  • ROUND_FLOOR:等于靠近负无穷的数。
  • ROUND_HALFUP:等于靠近的数,若舍入位为5,应用ROUNDUP。
  • ROUND_HALFDOWN:等于靠近的数,若舍入位为5,应用ROUNDDOWN。
  • ROUND_HALFEVEN:舍入位前一位为奇数,应用ROUNDHALFUP;舍入位前一位为偶数,应用ROUNDHALFDOWN。

我们可以借用这些模式作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static final int ROUND_UP = BigDecimal.ROUND_UP;
public static final int ROUND_DOWN = BigDecimal.ROUND_DOWN;
public static final int ROUND_CEILING = BigDecimal.ROUND_CEILING;
public static final int ROUND_FLOOR = BigDecimal.ROUND_FLOOR;
public static final int ROUND_HALF_UP = BigDecimal.ROUND_HALF_UP;
public static final int ROUND_HALF_DOWN = BigDecimal.ROUND_HALF_DOWN;
public static final int ROUND_HALF_EVEN = BigDecimal.ROUND_HALF_EVEN;
public static final int ROUND_UNNECESSARY = BigDecimal.ROUND_UNNECESSARY;


public Money multiply(double multiplicand, int roundingMode) {
BigDecimal amount = this.getAmount().multiply(new BigDecimal(multiplicand));
amount = amount.divide(BigDecimal.ONE,roundingMode);
return new Money(amount.doubleValue(),this.getCurrency());
}

public Money divide(double divisor, int roundingMode) {
BigDecimal amount = this.getAmount().divide(new BigDecimal(divisor),
roundingMode);
Money result = new Money(amount.doubleValue(), this.getCurrency());
return result;
}

4.转成人民币

尽管各领域的国际化提了十几年,但是在国内想直接用美元消费还是有一定困难。所以你决定将这笔钱换成人民币。

对于账户来说,就是在美元账户和人民币账户分别做一笔转出和转入。 转入和转出的amount值是不同的,因为涉及到币种转换的问题。 显然,账户对象不应该知道如何进行汇率转换,责任又落在了Money类上。

最直观的做法是在Money类上增加一个convertTo(Currency currency)的方法。 但汇率实在是一个复杂的问题:

  • 汇率是经常变化的;
  • 汇率转换时的舍入处理会有相关的约定;

这些复杂的问题处理如果直接放在Money类上会显得十分笨重,单独设计一个MoneyConverter类会比较好:

1
2
3
4
5
import java.util.Currency;

public interface MoneyConverter {
Money convertTo(Money money,Currency currency) throws Exception;
}

我们实现一个最简单的转化器,使用固定的汇率值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Locale;

public class SimpleMoneyConverter implements MoneyConverter {

private static final BigDecimal DOLLAR_TO_CNY = new BigDecimal(6.2365);
private static final Currency DOLLAR = Currency.getInstance(Locale.US);
private static final Currency CNY = Currency.getInstance(Locale.CHINA);
@Override
public Money convertTo(Money money,Currency target) throws Exception{
if(!known(money.getCurrency()) || !known(target)){
throw (new Exception("unknown currency"));
}

BigDecimal factorSource =BigDecimal.ONE, factorTarget = BigDecimal.ONE;
if(money.getCurrency().equals(DOLLAR))
factorSource = DOLLAR_TO_CNY;
if(target.equals(DOLLAR))
factorTarget = DOLLAR_TO_CNY;
BigDecimal value = money.getAmount().multiply(factorSource).divide(factorTarget);

return new Money(value.doubleValue(),target);
}

private boolean known(Currency currency){
return(currency.equals(DOLLAR) || currency.equals(CNY) );
}

}

可以看到,即使是最简单的转换器,处理起来也比较麻烦。所以千万不要在Money类中做这件事情。

通过转换器可以很容易得到转成人民币后的值。

5.分钱

有好处不能独享。这笔钱你决定和老婆三七开。当然,你三!

这又是一个新的舍入问题:即使你指定各自的舍入计算方法,也不能保证各部分舍入后的值加总后仍等于原值。

前面的“可定制乘除法”似乎不能很好的解决这个问题,所以我们需要一个新的方法: Money[] allocate(double[] ratioes)

传入分配比例的数组,返回分配结果的数组。

为了保证分配的公平,可以使用伪随机数来处理误差。

该方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public Money[] allocate(double[] ratioes) throws Exception{
if(ratioes.length==0){
throw (new Exception("there is no ratio"));
}

double ratioTotal = 0;
for(double ratio:ratioes){
ratioTotal += ratio;
}

if(0==ratioTotal){
throw(new Exception("total of ratioes is zero"));
}


double total = this.getAmount().doubleValue();
double delta = total;
Money[] results = new Money[ratioes.length];

for(int i=0;i<ratioes.length;i++){
double amount = total*ratioes[i]/ratioTotal;
results[i] = new Money(amount,this.getCurrency());
delta -= results[i].getAmount().doubleValue();
}

int i = (int)(Math.random() * ratioes.length);
results[i] = results[i].minus(new Money(delta,this.getCurrency()));
return results;
}

6.记账

将一切重要的数据保存到数据库是很通常的做法。但是将Money保存到数据库的时候,你要小心了!

Money不能作为单独的实体。如果把Money当做实体来处理,就会产生一些问题:

会有很多实体关联到Money,比如本文中的Account,Entry等。
需要非常小心处理对Money对象的引用,避免多个实体引用到同一个Money对象。在第一点的前提下,这会变得很困难。
所以应该把Money嵌入到需要的实体中,而不是把Money作为单独的实体。这样,Money仅仅是实体对象(比如Entry)的一个属性,只不过其具有多个内置的属性值。

在JPA中,可以使用@Embeddable来标注Money类。

更复杂的情况是,由于一个Account中的所有Entry都应该具有相同的Currency,将Currency保存到Account中会更简洁,Entry中只记录ammount。

可以为Money的currency属性增加@Transient标注,在Entry类的getMoney中进行组装。

7.来点高级的

在DDD(领域驱动设计)中,Money是典型的值对象(Value Object)。值对象与实体的根本区别是:值对象不需要进行标识(ID)。

这会带来一些处理上的不同:

  • 实体对象根据ID判断是否相等,值对象只根据内部属性值判断是否相等
  • 值对象通常小而且简单,创建的代价较小
  • 值对象只传递值,不传递对象引用,不用判断值对象是否指向同一个物理对象
  • 通常将值对象设计为通过构造函数进行属性设置,一旦创建就无法改变其属性值
  • 由于值对象根据内部属性值判等,我们要为Money类覆盖equals方法: public boolean equals(Object other)

8.其他未尽事宜

我们还可以为Money类增加互相比较的方法(略)

可以在构造函数中进行格式校验(略)

可以增加一些帮助显式的方法 使用currency的getSymbol(Locale locale)方法、和NumberFormat的format方法,比如:

1
2
3
NumberFormat nf=NumberFormat.getCurrencyInstance(Locale.CHINA);

String s=nf.format(73084.803984);// result:¥73,084.80

9.小结

本文探讨如何在应用中处理货币类型,包括币种转换、各种计算、如何持久化等内容。

货币类型是典型的值对象,本文也介绍了一点值对象的特点。更多的内容可以参考DDD。

JPA定义了Java ORM及实体操作API的标准。本文摘录了JPA的一些关键信息以备查阅。如果有hibernate的基础,通过本文也可以快速掌握JPA的基本概念及使用。

Read more »

在规则引擎中,通常会使用某种表述性的语言(而不是编程语言)来描述规则。
所以规则描述语言也是规则引擎的一个重要组成部分。

目前在规则描述语言方面,并没有一个通用的标准获得规则引擎厂商的广泛支持,大部分规则描述语言都是厂商私有的。
大体来说,规则语言可以分为结构化的(Structured)和基于标记的(Markup,通常为xml)。

常见的规则描述语言包括:

  • srl(Structured Rule Language) : Fair Isaac(以前是Blaze Software)定义的结构化规则描述语言
  • drl(Drools Rule Language) : Jboss(以前是drools.org)定义的结构化规则描述语言
  • RuleML(Rule Markup Language): www.ruleml.org定义的xml规则描述语言
  • SRML(Simple Rule Markup Language): xml规则描述语言
  • BRML(Business Rules Markup Language):xml规则描述语言
  • SWRL(A Semantic Web Rule Language):www.daml.org定义的xml规则描述语言

不管是哪种规则描述语言,都需要描述一组条件以及在此条件下执行的操作(即规则)。

概览

下面是一个drl的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.sample

import com.sample.DroolsTest.Message;

rule "Hello World"
when
m : Message( status == Message.HELLO, myMessage : message )
then
System.out.println( myMessage );
m.setMessage( "Goodbye cruel world" );
m.setStatus( Message.GOODBYE );
update( m );
end

rule "GoodBye"
when
Message( status == Message.GOODBYE, myMessage : message )
then
System.out.println( myMessage );
end

完整的drl文件会包含以下几个部分:

  • package 声明
  • imports
  • declares
  • globals
  • functions
  • queries
  • rules

package和import声明

与Java语言类似,drl的头部需要有package和import的声明。

规则文件当中必须要有一个 package 声明,同时 package 声明必须要放在规则文件的第一行。

规则定义

drl中,一个规则的标准结构如下:

1
2
3
4
5
6
7
rule "name"
attributes
when
LHS
then
RHS
end

一个规则通常包括三个部分:属性部分(attribute)、条件 部分(LHS)和结果部分(RHS)。

条件部分(LHS)

在 LHS 当中,可以包含多个条件,如果 LHS 部分没空的话,那么引擎会自动添加一 个 eval(true)的条件。

多个条件之间之间用可以使用 andor 来进行连接。默认是 and 关系。

每个条件的语法为:

[绑定变量名:]Object([field 约束])

“绑定变量名”是可选的。绑定的变量可以在该LHS后续的条件中引用。

“field 约束”是指当前对象里相关字段的条件限制, 多个约束之间可以用“&&”(and)、“||”(or)和“,”(and)来连接。

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
when
$customer:Customer()
then
……


when
$customer:Customer(age>20,gender==􏰃male􏰃)
Order(customer==$customer,price>1000)
then
……


when
Customer(age>20 || (gender=='male' && city==‘sh'))
then
……

约束中可以使用的比较操作符包括:

1
>、>=、<、<=、= =、!=、contains、not contains、 memberof、not memberof、matches、not matches

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
when
$order:Order();
$customer:Customer(age >20, orders contains$order);
then
……


when
$order:Order(name memberOf orderNames); # orderNames为数组或集合类型
then
……

when
$customer:Customer(name matches "李.*"); # 正则表达式匹配
then
……

结果部分(RHS)

RHS 部分定义了当LHS满足是要进行的操作。

RHS中可以编写代码,可以使用 LHS 部分当中定义的绑定变量名以及drl头部定义的全局变量。

在 RHS 当中虽然可以直接编写 Java 代码,但不建议在代码当中有条件判断,如果需要条件判断,那么请重新考虑将其放在 LHS 当中,否则就违背了使用规则的初衷。

使用宏函数

RHS中可以使用宏函数对工作空间(Working Memory)进行操作。当调用宏函数后,所有未设置“no-loop”属性的规则都会被重新匹配,符合条件的重新触发。

宏函数包括:

  • insert:将一个 Fact 对象插入到当前的 Working Memory
  • update:对当前 Working Memory 中的 Fact 进行更新
  • retract :从 Working Memory 中删除某个 Fact 对象

这些宏函数都是StatefulSession中定义的方法。

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
when
……
then
Customer cus=new Customer();
cus.setName("张三");
insert(cus);


when
$customer:Customer(name=="张三",age<10);
then
$customer.setAge($customer.getAge()+1);
update($customer);


when
$customer:Customer(name=="张三",age<10);
then
Customer customer=new Customer();
customer.setName("张三");
customer.setAge($customer.getAge()+1);

# 用新对象替换Working Memory中的旧对象
update(drools.getWorkingMemory().getFactHandleByIdentity($customer),customer);


when
$customer:Customer(name=="张三");
then
retract($customer);

modify代码块

modify代码块用于快速修改并更新(update)某个 Fact 对象的多个属性。语法及例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
modify(fact-expression){
<修改 Fact 属性的表达式>[,<修改 Fact 属性的表达式>*]
}


when
$customer:Customer(name=="张三",age==20);
then
modify($customer){
setId("super man"),
setAge(30)
}

drools宏对象

通过使用 drools 宏对象可以实现在规则文件里直接访问 Working Memory, 从而获取对当前的 Working Memory 的更多控制。

drools 宏对象的常用方法包括:

  • drools.getWorkingMemory():获取当前的 WorkingMemory 对象
  • drools.halt():在当前规则执行完成后,不再执行 其它未执行的规则。
  • drools.getRule():得到当前的规则对象
  • drools.insert(new Object):向当前的 WorkingMemory 当中插入 指定的对象,功能与宏函数 insert 相同。
  • drools.update(new Object):更新当前的 WorkingMemory 中指定 的对象,功能与宏函数 update 相同。
  • drools.update(FactHandle Object):更新当前的 WorkingMemory 中指定 的对象,功能与宏函数 update 相同。
  • drools.retract(new Object):从当前的 WorkingMemory 中删除指 定的对象,功能与宏函数 retract 相 同。

例如:

1
2
3
4
5
6
when
eval(true);
then
Customer cus=new Customer();
cus.setName("张三");
drools.insert(cus)

kcontext宏对象

kcontext 也是 Drools 提供的一个宏对象,它的作用主要是用来得到当前的 KnowledgeRuntime 对象,KnowledgeRuntime 对象可以实现与引擎的各种交互。

规则属性

主要的规则属性如下:

13 个: auto-focus、、lock-on-active、no-loop、 ruleflow-group、、when。

  • salience

    设置规则执行的优先级,数字越大越先执行,数字相同的使用随机顺序。默认值为0,可以设置为负数。

  • no-loop

    默认为false。设置为true时,表示该规则只会被引擎检查一次。引擎内部对Fact更新时,忽略本规则的再次检查。

  • date-effective

    设置规则的开始生效日期。默认接受“dd-MMM-yyyy”格式的字符串。可以用代码修改日期格式,如:

    System.setProperty("drools.dateformat","yyyy-MM-dd")

  • date-expires

    设置规则的过期日期。格式与date-effective相同。

  • enabled

    默认为true。设置规则是否可用。

  • dialect

    设置规则中使用的编程语言。默认为java,还可以设置为mvel。通过drools.getRule().getDialect()可以获取该属性的设置。

  • duration

    延迟指定的时间后,在 另一个线程中 触发规则。单位为毫秒。

  • activation-group

    为规则划指定一个活动组(组名为字符串)。同一个活动组中的规则只执行一个,根据优先级(salience)来决定执行哪一个规则。

  • agenda-group 和 auto-focus

    为规则指定一个议程(agenda)组。指定了议程组的规则只有在该议程组得到焦点时才被触发。但如果规则同时指定了auto-focus属性为true,则该规则自动得到焦点。

    指定议程组焦点可以通过回话(session):

    1
    2
    session.getAgenda().getAgendaGroup("GROUP_NAME").setFocus();
    session.fireAllRules();

    也可以实现org.drools.runtime.rule.AgendaFilter 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test;
import org.drools.runtime.rule.Activation;
import org.drools.runtime.rule.AgendaFilter;

public class TestAgendaFilter implements AgendaFilter {
private String startName;
public TestAgendaFilter(String startName){
this.startName=startName;
}
public boolean accept(Activation activation) {
String ruleName=activation.getRule().getName();
if(ruleName.startsWith(this.startName)){
return true;
}else{
return false;
}
}
}
  • ruleflow-group

    指定规则流组。

  • lock-on-active

    当在规则上使用 ruleflow-group 属性或 agenda-group 属性的时候,将 lock-on-action 属性 的值设置为 true,可能避免因某些 Fact 对象被修改而使已经执行过的规则再次被激活执行。lock-on-active 是 no-loop 的增强版属性,在规则流中很有用。

举例如下:

1
2
3
4
rule "rule1"
salience 1
when
……

注释

Drools 当中注释的写法与编写 Java 类的注 释的写法完全相同,注释的写法分两种:单行注释与多行注释。

单行注释可以采用“#”或者“//”来进行标记, 多行注释以“/”开始,以“/”结束。

例如:

1
2
3
4
5
6
7
8
9
/* 规则rule1的注释
这是一个测试用规则
*/
rule "rule1"
when
eval(true) #没有条件判断
then
System.out.println("rule1 execute"); //仅仅是输出
end

类型声明

可以在规则文件中定义Fact类型,而不需要编写Java类。比如:

1
2
3
4
5
6
7
8
9
10
11
declare Address
city : String
addressName : String
end

declare Person
name:String
birthday:Date
address:Address //使用declare声明的对象作为address属性类型
order:Order //使用名为Order的JavaBean作为order属性类型
end

在KnowledgeBase中可以获取规则文件中定义的Fact类型,比如:

1
2
3
4
5
//获取规则文件当中定义的Address对象并对其进行实例化
FactType addressType=knowledgeBase.getFactType("test","Address");
Object add=addressType.newInstance();
addressType.set(add, "city","Beijing");
addressType.set(add, "addressName","Capital");

在声明中还可以定义元数据。可以为 Fact 对象、Fact对象的属性或者是规则来定义元数据,元数据定义采用的是“@”符号开头。

比如:

1
2
3
4
5
6
declare User
@createTime(2009-10-25)
username : String @maxLenth(30)
userid : String @key
birthday : java.util.Date
end

元数据的获取?(TODO)

全局变量(TODO)

函数和import function

函数的定义和使用

函数是定义在规则文件当中一代码块,作用是将在规则文件当中若干个规则都会用到的业务操作封装起来,实现业务代码的复用,减少规则编写的工作量。
函数的可见范围是函数所在的规则文件。

函数以function定义,可以是void,也可以有返回值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package test
import java.util.List;
import java.util.ArrayList;
/*
一个测试函数
用来向Customer对象当中添加指定数量的Order对象的函数
*/

function void setOrder(Customer customer,int orderSize) {
List ls=new ArrayList();
for(int i=0;i< orderSize;i++){
Order order=new Order();
ls.add(order);
}
customer.setOrders(ls);
}

/*
测试规则
*/
rule "rule1" when
$customer :Customer();
then
setOrder($customer,5);
System.out.println("rule 1 customer has order size:"+$customer.getOrders().size());
end

/*
测试规则
*/
rule "rule2" when
$customer :Customer();
then
setOrder($customer,10);
System.out.println("rule 2 customer has order size:"+$customer.getOrders().size());
end

引入静态方法

实际应用当中,可以考虑使用在 Java 类当中定义静态方法的办法来替代在规则文件当 中定义函数。

Drools 提供了一个特殊的 import 语句:import function,通过该 import 语句,可以实现将一个 Java 类中静态方法引入到一个规则文件当中,使得该文件当中的规 则可以像使用普通的 Drools 函数一样来使用 Java 类中某个静态方法。

比如:

1
2
3
4
5
6
package test;
public class RuleTools {
public static void printInfo(String name){
System.out.println("your name is :"+name);
}
}
1
2
3
4
5
6
7
8
9
10
11
package test
import function test.RuleTools.printInfo;
/*
测试规则
*/
rule "rule1"
when
eval(true);
then
printInfo("test import function");
end

查询定义

查询用于根据条件在当前的 WorkingMemory 当中查找 Fact。Drools 当中查询可分为两种:一种是不需要外部传入参数;一种是需要外部传入参 数。

如:

1
2
3
4
5
6
7
query "testQuery"
customer:Customer(age>30,orders.size >10)
end

query "testQuery2"(int$age,String$gender)
customer:Customer(age>$age,gender==$gender)
end

通过session可以在外部调用规则文件中的查询,比如:

1
2
3
4
5
6
7
8
9
10
……
QueryResults queryResults=statefulSession.getQueryResults("testQuery");
for(QueryResultsRow qr:queryResults){
Customer cus=(Customer)qr.get("customer"); //打印查询结果
System.out.println("customer name :"+cus.getName());
}

QueryResults queryResults2=statefulSession.getQueryResults("testQuery2", new Object[]{new Integer(20),"F"});

……