coolEx

Today will be better

slides库reveal.js简介

简介

reveal.js是一个功能强大的在线演示文稿库。尝试它的主要原因有:

  • 文稿内容可以使用html标签或者markdown,可以使用任何编辑器
  • 支持浏览器直接打开
  • 支持使用node作为服务提供在线分享
  • 支持通过chrome的打印成pdf文档

编辑

编写文稿比较方便,直接下载release版本的包,或者clone工程目录即可。
其中release版本可以在https://github.com/hakimel/reveal.js/releases
页面中选择最新版本下载即可。

基本的功能使用非常简单,可以直接编辑代码中的index.html文件,按照里面的格式编辑即可。
html文件中的section的标签对应slides的一页,其中按照标准的html标签编写。例如官方示例的第一页:

<section>
    <h1>Reveal.js</h1>
    <h3>The HTML Presentation Framework</h3>
    <p>
        <small>Created by <a href="http://hakim.se">Hakim El Hattab</a> / <a href="http://twitter.com/hakimel">@hakimel</a></small>
    </p>
</section>

当然,对我来说,reveal.js最方便的地方在于可以直接在源码中使用markdown语法。markdown使用详细介绍文档
使用时需要将markdown代码放在script标签中,并将type设置为text/template

<section data-markdown>
    <script type="text/template">
        ## Markdown support

        Write content using inline or external Markdown.
        Instructions and more info available in the [readme](https://github.com/hakimel/reveal.js#markdown).
    </script>
</section>

这样就可以容易的将markdown文档和slides结合起来了。

演示和分享

slides最大的作用当然是演示了,使用reveal.js标准结构的slides演示最方便的方式,就是直接在浏览器中打开index.html。这样可以方便的在没有网络的情况下,
单机进行演示。如果需要在线分享,需要安装nodejs,然后通过node安装grunt,最后通过npm install安装reveal.js依赖。

npm install
grunt serve

默认启动端口为8000。

最后,一般演讲完之后,还需要分享内容。这时可以通过chrome的打印功能,直接打印成pdf文档。
打印时有两个地方需要特别注意:

  1. 必须在访问的url上增加print-pdf参数。例如原url为/index.html,改成/index.html?print-pdf
  2. 打印页面中“布局”需要设置为“横向”,并且选项中勾选“背景图形”。

然后保存为pdf文件即可。

empty:expect的替代

之前为了能够处理命令交互,编译了静态链接的expect,结果遇到了大问题导致在没有tcl的机器上还是无法运行。找了半天,发现empty是一个不错的替代。

优势

首先,empty是一个纯C实现的,不依赖任何其他库的工具。这对于需要在不同机器上分发执行非常关键,之前expect就是因为无法确保机器上都有tcl或者其初始化脚本,导致无法使用。

其次,empty使用非常简单,可以方便的嵌入到shell脚本中,而无须像expect那样使用单独的脚本。这能够大大降低学习成本和后续维护成本。

使用

empty和一般应用安装方式类似,直接make即可。整个源码包中核心只有一个C文件,结构非常简单。下载地址在:http://sourceforge.net/project/showfiles.php?group_id=136798 这里。安装方式可以参考项目首页命令。

编译完成之后,就可以直接使用empty这个可执行程序了。它的help输出不是很友好,比较简单,大致参数有这几个:

  • -f:应该是fork的意思吧,这个参数表示会执行后面的命令,同时这个命令可以通过-i和-o指定命令的输入和输出管道。如果不指定。empty会以当前进程id和子进程id作为标识生成对应的文件。
  • -w:应该是watch的意思吧,该参数可以执行监听的标准输入管道(一般为-f的标准输出管道)和写入的标准输出管道(一般为-f的标准输入管道),然后后续以键值对的方式追加多个监听的key,和输出的value,即发现标准输入包含key时,向标准输出写入value。这个键值对可以有多个。
  • -i:标准输入对应的管道文件。对于-f参数指定的是执行命令的标准输入,后续可以通过这个标准输入管道,向执行命令发送标准输入内容。特别注意,对于-w参数这里表示监听输入的来源,需要设置为-f的标准输出管道。
  • -o:标准输出对应的管道文件。对于-f参数指定的是执行命令的标准输出,后续可以通过读取这个管道文件,监听命令的输出。对于-w参数,这里表示输出写入的管道,需要设置为-f的标准输入管道。
  • -s:应该是send的缩写吧,可以直接指定向标准输出管道(-o)写入字符
  • -t:设置watch的超时时间,如果超时时间内没有输入,则退出empty命令。默认值为0,表示不超时。

因此,大部分场景下,先通过-f启动命令,然后通过-w命令监视输出,并且写入结果。例如代码中的示例,如果要模拟ssh登录,可以这样:

ssh="ssh"                               # (/full/path/to/)ssh
target="localhost"                      # target host
login="luser"                           # username (Change it!)
password="TopSecret"                    # password (Change it!)

fifo_in="/tmp/empty.in"                 # input fifo
fifo_out="/tmp/empty.out"               # output

# -----------------------------------------------------------------------------
cmd="$ssh $login@$target"
tmp="/tmp/empty.tmp"                    # tempfile to store results

echo "Starting empty"
empty -f -L $tmp -i $fifo_in -o $fifo_out $cmd
if [ $? = 0 ]; then
        if [ -w $fifo_in -a -r $fifo_out ]; then
                echo "Sending Password"
                empty -w -v -i $fifo_out -o $fifo_in -t 5 assword: "$passwordn"
                echo "Sending tests"
                empty -s -o $fifo_in "echo "-- EMPTY TEST BEGIN --"n"
                empty -s -o $fifo_in "uname -an"
                empty -s -o $fifo_in "uptimen"
                empty -s -o $fifo_in "who am in"
                empty -s -o $fifo_in "echo "-- EMPTY TEST END --"n"
                echo "Sending exit"
                empty -s -o $fifo_in 'exitn'
                echo "Check results:"
                sleep 1
                cat $tmp
                rm -f $tmp
        else
                echo "Error: Can't find I/O fifos!"
                return 1
        fi
else
        echo "Error: Can't start empty in daemon mode"
        return 1
fi

echo "Done"

这么一大堆脚本,其实就三行和empty使用有关,一个是empty -f -L $tmp -i $fifo_in -o $fifo_out $cmd,该命令执行ssh命令,并且指定输入、输出和日志文件。empty -w -v -i $fifo_out -o $fifo_in -t 5 assword: "$password\n"该命令通过监听ssh命令的输出,如果遇到“assword:”字样,则向ssh进程发送密码。注意,这里的-i和-o参数和之前相反,因为需要监听ssh命令的输出,向ssh命令的输入发送字符。最后就是empty -s -o $fifo_in "echo \"-- EMPTY TEST BEGIN --\"\n"这时ssh已经认证成功了,因此就直接向远程写入通过ssh执行的命令即可。

除了这种预先知道命令每一步的输出之外,还有一些场景是命令可能在有些场景下会进行交互,有些场景下不会。我们遇到的场景就是webx提供的autoconfig命令。该命令默认会在缺少配置项的时候进行交互,提示用户是否使用默认值,并且写入到antx.properties文件。

这时,就不能简单的通过每一步等待特定的字符,然后输出了。这里我们利用了empty的另一个特性,即自动维护管道文件,命令开始执行时,empty会创建对应的标准输入和标准输出两个管道文件,命令结束时删除,这样就不用用户来维护命令的输入输出,也不会产生垃圾文件。因此,我们可以通过循环的方式,监听是否有我们需要的关键字,在每次监听前,增加判断管道文件是否存在,以防止命令已经正常退出。大致的脚本如下:

empty -f -i input -o output -L $HOME/autoconfig.log autoconfig -u $HOME/antx.properties $f
sleep 1
while true;
do
    if [ -w input -a -r output ]; then
        empty -w -i output -o input -t 5 ".*Yes.*" "yesn" ".*[Quit.*" "quitn"
    else
        break;
    fi
done

while循环里面,首先需要判断的是input和output两个文件是否存在。对于input文件,通过-w检查文件是否存在的同时,检查文件是否可写;对于output文件,通过-r检查文件是否存在的同时,检查文件是否可读。如果两个文件都存在,那么运行empty命令,等待输出并且设置超时时间。

这样的实现有两个好处,首先是可以兼容命令正常运行的流程,即无须进行交互。其次是不需要知道命令当前的状态,我们在一个empty命令里面已经兼容了命令所有可能的输出对应的用户输入值。

静态编译expect以及大坑

expect是脚本处理有交互命令的利器,但是这货依赖tcl,并且默认没有安装。为了方便分发到测试服务器上,尝试了下静态编译。结果以失败告终。下面是整个编译的过程。

编译tcl静态库

由于expect依赖tcl,必须先编译一个静态版本的tcl库。tcl的源码可以从阿里云镜像下载:

wget http://mirrors.aliyun.com/gentoo/distfiles/tcl-core8.6.4-src.tar.gz

编译的方式和编译普通应用类似,唯一需要注意的是,我们只需要编译出静态库即可。

tar vxf tcl-core8.6.4-src.tar.gz 
cd tcl8.6.4/unix/
./configure --disable-shared --prefix=$HOME/source/target
make
make install

这里我们直接安装到指定的路径,并且关闭动态链接库的编译。这样在$HOME/source/target里面,就会tcl的静态库。

编译expect

首先还是从镜像下载expect源码。

wget http://mirrors.aliyun.com/gentoo/distfiles/expect5.45.tar.gz

然后运行configure的时候,需要指定tcl的路径,并且关闭动态链接。

tar vxf expect5.45.tar.gz 
./configure --with-tcl=$HOME/source/target --disable-shared
make

这里遇到了很多链接的问题。首先是

expect.c:(.text+0xbc):对‘tclStubsPtr’未定义的引用
exp_main_sub.c:(.text+0x6fb):对‘tclIntStubsPtr’未定义的引用
tclUnixThrd.c:(.text+0x31):对‘pthread_key_delete’未定义的引用

这些符号无法找到,无法进行链接。向上查找这些错误对应的编译命令,发现已经在链接最终的expect命令了。此时的编译命令为:

gcc 
     -pipe -O2 -fomit-frame-pointer -Wall  
     -Wl,--export-dynamic  
    -o expect exp_main_exp.o 
    -L/alidata1/6080/source/expect5.45 -lexpect5.45 
    -L/alidata1/6080/source/target/lib -ltcl8.6 
    -ldl  -lieee -lm 
    -Wl,-rpath,/alidata1/6080/source/target/lib 
    -Wl,-rpath,/alidata1/6080/source/target/lib/expect5.45

查阅了tcl的faq,前面遇到无法链接的符号在tclstub库中,同时还发现少了pthread库的一些符号。加上这些之后,编译命令变成:

gcc 
     -pipe -O2 -fomit-frame-pointer -Wall  
     -Wl,--export-dynamic  
    -o expect exp_main_exp.o 
    -L/alidata1/6080/source/expect5.45 -lexpect5.45 
    -L/alidata1/6080/source/target/lib -ltcl8.6 
    -ldl  -lieee -lm 
    -Wl,-rpath,/alidata1/6080/source/target/lib 
    -Wl,-rpath,/alidata1/6080/source/target/lib/expect5.45 -ltclstub8.6 -lpthread

此时tcl和一些基础库的链接失败消失,但是出现了

pty_termios.c:(.text+0x116):对‘openpty’未定义的引用

这个错误。同样需要去查找这个函数实现的库,最终定位到了libutil。

最终将编译命令修改成:

gcc 
     -pipe -O2 -fomit-frame-pointer -Wall  
     -Wl,--export-dynamic  
    -o expect exp_main_exp.o 
    -L/alidata1/6080/source/expect5.45 -lexpect5.45 
    -L/alidata1/6080/source/target/lib -ltcl8.6 
    -ldl  -lieee -lm 
    -Wl,-rpath,/alidata1/6080/source/target/lib 
    -Wl,-rpath,/alidata1/6080/source/target/lib/expect5.45 -ltclstub8.6 -lpthread -lutil

终于能够正常编译出静态编译的expect命令了。

就在以为完事大吉的时候,发现这个静态编译的expect命令,虽然已经静态链接了tcl库,但是在其他没有tcl的机器上运行的时候,会提示:

Tcl_Init failed: Can't find a usable init.tcl in the following directories

原来为了支持扩展,tcl会要求初始化的时候执行这个初始化脚本。查阅了tcl的文档,要编译一个完全不依赖机器上tcl库的程序比较麻烦。最终,找到了一个简单的命令empty来处理命令的交互。

R表格处理(dplyr)

R表格处理(dplyr)

源文件是一个excel表格,主要的需求是按照日期分组,然后再按照另一列分组,同时求和和计算均值。大致的内容类似:

日期,用户,收入
2015-12-01 8:00:00,u1,2
2015-12-01 8:00:00,u1,4
2015-12-01 10:00:00,u2,5
2015-12-02 9:00:00,u1,10
2015-12-02 10:00:00,u2,4

统计内容是每天用户的数量、收入总数和平均每个用户的收入。例如:

日期,用户数,总收入,平均收入
2015-12-01,2,11,5.5

R中应该有多种方式能够实现这种数据聚合。网上很多都推荐使用dplyr包来进行运算。

首先是安装,和R其他包安装方式相同:

install.packages("dplyr")

然后是引入dplyr包,并加载入源输入(元数据是从excel导出的csv数据)。

library("dplyr")
data <- read.csv('input.csv')

这时的data变量内容为:

> data
                 日期 用户 收入
1  2015-12-01 8:00:00   u1    2
2  2015-12-01 8:00:00   u1    4
3 2015-12-01 10:00:00   u2    5
4  2015-12-02 9:00:00   u1   10
5 2015-12-02 10:00:00   u2    4

由于输出是按照日统计的,所以将第一列中的时间转成日期,以方便后续的聚合。

data$日期 = as.Date(data$日期)

修改之后会变成:

> data
        日期 用户 收入
1 2015-12-01   u1    2
2 2015-12-01   u1    4
3 2015-12-01   u2    5
4 2015-12-02   u1   10
5 2015-12-02   u2    4

然后开始聚合,这里的聚合有两个未读,首先是日期,然后是用户。

d_u_group <- group_by(data, 日期, 用户)

通过summarise函数,可以计算聚合之后的值:

d_u <- summarise(d_u_group, 'c' = n(), sum(收入))

由于这时的聚合是两个纬度的,所以还不是最终的数据:

> d_u
Source: local data frame [4 x 4]
Groups: 日期 [?]

        日期   用户     c sum(收入)
      (date) (fctr) (int)     (int)
1 2015-12-01     u1     2         6
2 2015-12-01     u2     1         5
3 2015-12-02     u1     1        10
4 2015-12-02     u2     1         4

最后再按照日期做聚合:

d_group <- group_by(d_u, 日期)
d <- summarise(d_group, '用户数' = n(), '总收入' = sum(收入), '平均收入' = 总收入/用户数)

最终的数据结果为:

> d
Source: local data frame [2 x 4]

        日期 用户数 总收入 平均收入
      (date)  (int)  (int)    (dbl)
1 2015-12-01      2     11      5.5
2 2015-12-02      2     14      7.0

容器化dns服务

容器化dns服务

之前将一个小应用使用docker compose部署之后,遇到一个问题,修改域名解析的IP。之前在虚拟机上可以直接修改hosts文件,
在docker容器中修改就比较麻烦,修改主机hosts文件也没有效果。

为了解决这个问题,引入了dnsmasq作为dns服务器。由于主机上本身已经有docker环境,因此也打算把dnsmasq放到容器中去运行。
首先是选择容器,在docker hub上下载量最多的是 andyshinn/dnsmasq 镜像。这个镜像的最大优势就是简单,整个镜像只有6M多。
对于无法访问外网的docker主机来说,部署比较方便。当然,这个镜像最大的劣势也是简单,没有提供配置文件,也无法可视化配置,使用时需要对dnsmasq有一定的了解。本文后面会介绍到刚开始使用遇到的不方便的地方。

安装

由于真正部署这个镜像的服务器无法访问外网,因此不能直接拉去docker镜像,暂时在一台可以访问外网的机器上拉取,然后导出,最后在机器上重新加载的方式来进行安装。也就是

  1. docker pull andyshinn/dnsmasq
  2. docker save andyshinn/dnsmasq > dnsmasq.tar
  3. scp dnsmasq.tar admin@xxx:.
  4. docker load < dnsmasq.tar

这样镜像就算是安装完成了。

启动

启动镜像前,参考了该镜像文档中的启动命令:

docker run -p 53:53/tcp -p 53:53/udp --cap-add=NET_ADMIN andyshinn/dnsmasq -S /consul/10.17.0.2

同时,看了下这个镜像的Dockerfile:

FROM alpine:3.2
RUN apk -U add dnsmasq
EXPOSE 53 53/udp
ENTRYPOINT ["dnsmasq", "-k"]

可以发现,这个镜像真是简单至极。默认就是运行了dnsmasq -k,即强制让dnsmasq运行在前端。然后dnsmasq没有任何特殊的配置,都需要启动过的时候手工指定。

而示例中的启动命令,有这几个含义:

  • 将容器的53端口(TCP和UDP)映射到主机上,这是dns的标准端口
  • 增加NET_ADMIN参数,使得容器中的进程可以修改网络配置
  • 给dnsmasq传递参数,针对.consul后缀的域名,使用10.17.0.2这个上游dns服务器进行解析

这样的启动方式,当然是无法满足需求的。dnsmasq命令的参数,可以在这个文档中进行查询。如果仅仅是解析一个域名,可以通过–address参数指定,域名和IP格式和上面类似:

docker run -p 53:53/tcp -p 53:53/udp --cap-add=NET_ADMIN andyshinn/dnsmasq --address /api.test.com/127.0.0.1

这样容器启动之后,就可以解析api.test.com这个域名到127.0.0.1。

应用

启动容器还是比较方便,然后就是将这个dns服务提供给其他docker容器使用。docker在启动参数中支持通过–dns指定dns服务器地址(指定后,会修改启动容器的/etc/resolv.conf文件)。如果在docker-compose中使用,可以在yml文件中增加dns项。

这样,容器启动之后,就可以使用自己搭建的dns服务了。然而,并没有~

NAT模式下dns服务的失效

首先遇到的问题,是容器中的服务仍然无法解析域名。使用docker exec命令进入容器,ping了域名还是提示无法解析。排查dnsmasq的日志没有输出。

由于dnsmasq解析日志不太好排查,查询了手册之后发现,可以通过–log-facility参数指定日志输出,将容器启动参数改成:

docker run -p 53:53/tcp -p 53:53/udp --cap-add=NET_ADMIN andyshinn/dnsmasq --address /api.test.com/127.0.0.1 -q --log-facility=-

这样可以将dnsmasq的日志输出到标准错误,也就能够在当前命令行中查看到。(如果在docker run的时候使用了-d参数放到了后台执行,可以直接通过docker logs来查看输出)

再次到应用容器中ping域名,会发现dnsmasq实际上已经完成了域名解析,但是域名解析的来源IP都是docker0设备的IP。如果将应用容器的dns服务器地址,修改成dnsmasq容器的内网地址,则解析正常。因此判断这个解析错误,可能和两个容器都运行在NAT模式中有关。

将dns容器改成host模式:

docker run --net=host --cap-add=NET_ADMIN andyshinn/dnsmasq --address /api.test.com/127.0.0.1 -q --log-facility=-

在将应用容器的dns服务器地址修改成主机的IP,可以发现能够正常解析域名。

dns解析自动更新

按照上面方式启动dns容器,可以正常工作。但是如果要增加解析记录,需要重新启动这个容器。文档里面有提到dnsmasq可以通过向进程发送SIGHUP信号,重新加载文件:

When it receives a SIGHUP, dnsmasq clears its cache and then re-loads /etc/hosts and /etc/ethers and any file given by –dhcp-hostsfile, –dhcp-hostsdir, –dhcp-optsfile, –dhcp-optsdir, –addn-hosts or –hostsdir. The dhcp lease change script is called for all existing DHCP leases. If –no-poll is set SIGHUP also re-reads /etc/resolv.conf. SIGHUP does NOT re-read the configuration file.

也就是说,如果通过–addn-hosts或者–hostsdir参数指定的文件(夹),可以通过信号来触发dnsmasq重新加载。为了方便区分,最终采用了后者,既–hostsdir指定hosts文件夹。

首先,在主机上创建一个文件夹和一个文件,写入需要解析的域名:

mkdir hosts
echo '127.0.0.1 a.test.com' > hosts/common

然后启动容器的时候,设置挂载卷,将这个目录共享给容器,同时指定这个目录未hostsdir:

docker run --net=host --cap-add=NET_ADMIN -v /home/admin/hosts:/media andyshinn/dnsmasq --addn-hosts=/media/common -q --log-facility=-

这样,如果需要新增解析记录,需要两步:

  1. 在主机hosts目录中,新增一条记录:echo 'b.test.com' >> hosts/common
  2. 向dns容器发送信号,使其重新加载文件:docker kill -s HUP 2beb1d9f3554

这样虽然还需要手工操作,但是无须再重新运行dns容器。

spring boot profile试用

spring boot profile试用

项目中需要在不同环境切换,之前类似的需求,主要在于一些配置的不同,如数据库配置等。这样的场景,通过maven的profile,结合resource filter即可。但是这次需要针对不同的场景,使用不同的实现类,以此来对接外部不同的系统。

本文基本上参考了spring博客中的介绍,通过@profile注解,来区分不同的使用场景。

自定义场景注解

@Profile注解需要接受一个字符串,作为场景名。这样每个地方都需要记住这个字符串。Spring的@Profile注解支持定义在其他注解之上,以创建自定义场景注解。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Profile("cloud")
public @interface Cloud {
}

这样就创建了一个@Cloud注解,该注解可以标识bean使用于cloud这个场景。后续就不再需要使用@Profile("cloud")的方式。这样即可以简化代码,同时可以利用IDE的自动补全:)

配置类场景控制

有了上面定义的注解,可以直接在有@Configure注解的配置类上的增加,以控制该类创建的Bean,只在指定场景下激活。例如:

@Configuration
@Cloud
public class CloudWebFilterConfig {

    private static final String FROM = "aliyun";

    @Bean
    public Filter ssoFilter() {
        return new SSOFilter();
    }

    @Bean
    public FilterRegistrationBean ssoFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(ssoFilter());
        filterRegistrationBean.setName("ssoFilter");
        filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));

        filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

        return filterRegistrationBean;
    }
}

上面的配置类,创建了两个bean,其中一个是springboot使用的filter,使用我们针对云上单点登录的实现;另一个是这个filter的配置,配置了这个filter的路径和名称,还有顺序。这里的配置和直接在web.xml中配置filter类似。唯一不同的是,FilterRegistrationBean支持设置filter的优先级。

基于service实现的场景控制

除了上述通过配置类的方式控制之外,@Profile和自定义的(@Cloud)注解还可以直接应用的@Service@Component等注解创建的服务实现上。同样,带上场景注解之后,这些服务实现只会在场景激活之后才会实例化。

例如,我们可以定义一个用户查询接口:

public interface UserService {
    List<AegisUser> fuzzyQuery(String query);
}

在不同场景下,我们需要有不同的实现。

@Service
@Cloud
public class UserServiceAImpl implements UserService {
    public List<AegisUser> fuzzyQuery(String query) {
        ...
    }
}
@Service
@Cloud
public class UserServiceBImpl implements UserService {
    public List<AegisUser> fuzzyQuery(String query) {
        ...
    }
}

上面创建了两个UserService的实现,一个标记为内部使用,一个标记为云上使用。

这样,对于上层平台,只需要直接@Autowired注解注入即可,无须再通过@Qualifier注解指定具体的实现了。运行时注入,会通过场景指定具体的实现类。

场景切换配置

spring boot在启动时需要指定当前激活的场景,主要方式有两个:

  1. 命令行参数指定:启动时,直接在命令行参数中增加:spring.profiles.active=cloud参数
  2. 在properties文件中指定,在应用依赖的properties文件中增加spring.profiles.active=cloud等配置,即可切换场景

为了能够在打包的时候就确定启动场景,最终还是采用了后者,并且和maven的resource filter结合。即在properties文件中使用占位符,在maven的profile中通过filter,通过maven profile来编译时替换。

<profiles>
    <profile>
        <id>internal</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <build.profile.id>internal</build.profile.id>
        </properties>
    </profile>
    <profile>
        <id>cloud</id>
        <properties>
            <build.profile.id>cloud</build.profile.id>
        </properties>
    </profile>
</profiles>

<build>
        <filters>
            <filter>profiles/${build.profile.id}/config.properties</filter>
        </filters>

        <resources>
            <resource>
                <filtering>true</filtering>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        ...
</build>    

这样只要在maven编译时通过-P参数指定maven profile即可。

gentoo prefix重生(llvm/clang)

gentoo prefix重生(llvm/clang)

前天手贱,更新了mac上gentoo prefix的perl,然后发现git svn无法使用。更新perl其他组件的时候,遇到了一堆问题。
特别是subversion1.8,在gcc-apple下已经无法编译了。

gentoo prefix portage镜像

之前gentoo prefix的portage rsync服务器挂掉过,然后改了其他一台,但是速度都非常慢。看见阿里云有gentoo prefix portage
的镜像,尝试修改成了这个镜像,但是后面更新的软件,包括portage都会存在问题。

刚开始都准备放弃重新做gentoo prefix了,下载了新的bootstrap-prefix.sh,发现了两个地方:
1. 新的脚本中portage树从rsync.prefix.bitzolder.nl下载
2. 新的脚本已经直接开始使用llvm作为prefix的构建编译器了

因此,还是先将portage的同步地址改了。由于新的portage已经去掉了make.conf中的SYNC变量,取而代之的是多了一个repos.conf目录
来存放仓库配置。因此在$EPREFIX/etc/portage/repos.conf下创建gentoo_prefix.conf文件,内容是:

[DEFAULT]
main-repo = gentoo_prefix

[gentoo_prefix]
location = /Users/babydragon/Gentoo/usr/portage
sync-type = rsync
sync-uri = rsync://rsync.prefix.bitzolder.nl/gentoo-portage-prefix

更新llvm

刚开始用了阿里云的镜像,emerge了llvm之后没有问题,但是加上了clang的USE之后,怎么都emerge不了。切换成官方镜像之后,又遇到llvm3.5.2循环依赖。
搜索了下发现bootstrap-prefix脚本中是bootstrap先emerge了llvm 3.4。按照这个方式和漫长的等待,llvm 3.4 emerge完成之后,再升级到3.5,
所有依赖的cxx包都能够正常安装了。

然后就是更新系统了,因为之前是perl损坏,直接使用:

perl-cleaner --all

来更新所有perl的依赖,发现编译的时候还有问题,看上去没有使用新的clang。继续查看脚本,发现stage2的时候,脚本会向make.conf文件中追加:

CC=clang
CXX=clang++
BUILD_CC=clang
BUILD_CXX=clang++

这几个变量,强制将编译器改成clang/clang++。

改完之后,git, svn和其他Perl的依赖,都能够正常的emerge了,git svn终于可以继续使用了。由于使用了llvm新版本,以后emerge boost等的时候,应该不会再有问题了吧。

springboot和mybatis结合

springboot和mybatis结合

依赖和数据源配置

springboot依赖了spring4,需要依赖mybatis-spring,最新版本是1.2.2。
数据源相关的依赖:

<!-- datasource -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP-java6</artifactId>
    <version>${HikariCP.version}</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${mybatis-spring.version}</version>
</dependency>

前两个是数据源的依赖,包括HikariCP和mysql驱动。后面两个是mybatis依赖,包括mybatis本身和mybatis-spring模块。

有了这些依赖之后,就可以通过spring4的配置类,对mybatis数据源等进行配置。

@Configuration
@PropertySource("classpath:datasource.properties")
@MapperScan(basePackages="xxx.repository", sqlSessionFactoryRef = "sqlSessionFactory")
public class DatasourceConfig {
    @Autowired
    private Environment env;

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.jdbc.Driver");
        config.setAutoCommit(false);
        config.setJdbcUrl(env.getProperty("xxx.db.url"));
        config.setUsername(env.getProperty("xxx.db.username"));
        config.setPassword(env.getProperty("xxx.db.password"));

        return new HikariDataSource(config);
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage("xxx.mybatis");
        return sessionFactory.getObject();
    }

}

首先引入配置文件,并且注入到env对象中。env类似System的properties对象,封装了配置文件中的key value。
然后通过MapperScan注解定义mapper接口包路径。这里同时定义了sqlSessionFactoryRef,是因为需要用到多数据源,
防止spring无法注入,后面会提到。

之后代码就可以开始定义输出的bean。一个是datasource,直接初始化一个Hikari的数据源,springboot提供了builder类,
但是查看源码和api之后,DataSourceBuilder无法配置autocommit属性。

再下面是事务管理,需要通过构造函数注入dataSource。最后一个是mybatis的sqlSessionFactory,主要也是注入一个数据源。

mapper(DAO)实现

dao实现和原先的ibatis差不多,但是mybatis可以通过注解的形式直接生成动态sql。既然springboot用了代码来取代xml,mybatis
中也同样去掉了xml。

插入

插入操作需要注意两个地方,一个是如何返回插入之后的主键(mysql),一个是如何使用数据类型的handler。
首先看代码:

@Insert("INSERT INTO aegis_cron_timer " +
            "(id, gmt_create, gmt_modified, name, expression, event_class_name, description, last_trigger_time, status, parameter) " +
            "VALUES (NULL, now(), now(), #{name:VARCHAR}, #{expression:VARCHAR}, " +
            "#{eventClassName:VARCHAR}, #{description:VARCHAR}, now(), #{status:VARCHAR}, " +
            "#{parameter,typeHandler=com.alibaba.aegis.seawater.cron.service.dao.mybatis.MapToJsonTypeHandler})")
@SelectKey(before = false, statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", resultType = java.lang.Long.class)
public Long insertCronTimer(CronTimer cronTimer);

针对mysql,可以通过SelectKey这个注解,设置插入后主键的返回。由于mysql是自增主键,所以设置为插入后执行,定义返回的类型为long
(数据库中定义了bigint)。

另外,这里有个字段需要从map序列化成json字符串,作为varchar类型存放到数据库中。在插入的sql中,可以直接在变量后面定义typeHandler,
值是对应handler的完整类名。

更新

更新操作比较简单,直接使用Update注解即可。和插入类似,如果需要指定type handler,直接在字段后面增加参数即可。更新函数可以返回一个int值,
表示本次更新的行数。

查询

查询通过Select注解完成,mybatis可以直接通过字段名字和查询结果的java bean之间做自动关联。如果名字不匹配,有两种方式,一种是通过sql中
增加AS关键字转成java bean中的字段名,一种是通过@Result注解指定二者的映射关系。

@Select("SELECT name, expression, event_class_name AS eventClassName, description, status, parameter " +
            "FROM aegis_cron_timer " +
            "WHERE status = 'ENABLE'")
@Results({
        @Result(column = "parameter", jdbcType = JdbcType.VARCHAR, property = "parameter", typeHandler = MapToJsonTypeHandler.class)
})
public List<CronTimer> listAllAvailableCronTimer();

这里通过Result注解配置了type handler,特别注意Result注解必须在Results注解中,不然不会生效。

自定义type handler

前文已经提到了如何在插入、更新、查询语句中使用type handler,type handler实现也比较简单。mybatis自带的type handler都是通过extends
BaseTypeHandler来实现的,但例子中直接实现了TypeHandler接口:

@MappedTypes(Map.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class MapToJsonTypeHandler implements TypeHandler<Map<String, Object>> {

    @Override
    public void setParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return jsonToMap(value);
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return jsonToMap(value);
    }

    @Override
    public Map<String, Object> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return jsonToMap(value);
    }

    private Map<String,Object> jsonToMap(String value) {
        if (StringUtils.isBlank(value)) {
            return Collections.emptyMap();
        } else {
            return JSON.parseObject(value, new TypeReference<Map<String, Object>>() {
            });
        }
    }
}

实现比较简单,序列化的时候直接通过fastjson将map对象转成json string,放到PreparedStatement中。反序列化的时候返回来转成Map即可。

多数据源实现

由于项目需要从老的数据库迁移到新的数据库,所以需要两个数据源,在设置多数据源的时候也踩了很多坑。

另一个数据源配置类:

@Configuration
@PropertySource("classpath:amon-datasource.properties")
@MapperScan(basePackages="com.alibaba.aegis.seawater.cron.migrate.repository",
        sqlSessionFactoryRef = "amonSqlSessionFactory", sqlSessionTemplateRef = "amonSqlSessionTemplate")
public class AmonDataSourceConfig {
    @Autowired
    private Environment env;

    @Bean(name = "amonDataSource")
    public DataSource amonDataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.jdbc.Driver");
        config.setAutoCommit(true);
        config.setJdbcUrl(env.getProperty("amon.db.url"));
        config.setUsername(env.getProperty("amon.db.username"));
        config.setPassword(env.getProperty("amon.db.password"));

        return new HikariDataSource(config);
    }

    @Bean(name = "amonTransactionManager")
    public DataSourceTransactionManager amonTransactionManager(@Qualifier("amonDataSource")DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "amonSqlSessionFactory")
    public SqlSessionFactory amonSqlSessionFactory(@Qualifier("amonDataSource")DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    }

    @Bean(name = "amonSqlSessionTemplate")
    public SqlSessionTemplate amonSqlSessionTemplate(@Qualifier("amonSqlSessionFactory")SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

这里也定义了一个配置文件,需要注意的是不要和前面一个的key相同,不然会覆盖的。定义bean的时候需要设置下name,或者函数名字改了也行。
需要定义的bean和之前的一样,特别注意MapperScan注解需要修改sqlSessionFactoryRef或者sqlSessionTemplateRef。这里两个都改了但是启动的时候会
提示:

Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.

这边定义了bean之后,直接使用就没有问题了。唯一需要特别注意的是@Transactional注解,由于定义了两个transactionManager,
无法通过类型来注入事务管理器了,需要注解中特别指定。比如使用前面定义的数据源的事物管理器,需要改成:

@Transactional("transactionManager")

这样spring可以通过名字注入bean。

DAO测试

为了方便测试,对应测试类中,重新覆盖了dataSource,采用h2这种内存数据库,解决单元测试数据干扰。

@Configuration
@MapperScan(basePackages="com.alibaba.aegis.seawater.cron.repository")
public class TestDatasourceConfig extends DatasourceConfig {
    @Autowired
    private Environment env;

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setName("cron")
                .addScript("h2.sql")
                .build();
    }

}

这里直接通过springboot提供的EmbeddedDatabaseBuilder来创建一个h2的数据库,并添加初始化数据库schema的sql文件。
这里需要注意的是,如果这个sql文件直接叫schema.sql,之前mysql数据源在执行的时候也会去执行,因此这里没有使用默认的名字。

其他坑

在springboot注入properties文件中配置的时候,还遇到一个恶心的问题,除了PropertySource注解指定的properties文件之外,
spring还会默认带上jvm变量、系统环境变量。刚开始直接把数据库用户名字段的key写成了username,结果由于测试服务器上使用了sudo
命令,sudo在切换用户的同时设置了USERNAME这个环境变量标识原始执行用户,导致springboot一直在注入这个值,调试了很久。

svn co时排除目录

某些原因想在svn co的时候排除某些目录,可以绕个圈子,分三步来完成:

co外层目录:

svn checkout --depth empty $URL [$LOCATION]

完成之后,会有一个只包含空目录的根目录

设置忽略目录:

cd $LOCATION
svn up --set-depth exclude <$DIR_TO_EXCLUDE>

这样svn会提示被忽略的目录标记为D,然后文件系统上也看不见了

更新剩余文件

svn up --set-depth infinity *

这样其他目录会被重新递归的更新,被忽略的目录不会更新,即使后面有人执行svn up,也无法更新被忽略的文件。

nanomsg实验——survey

nanomsg实验——survey

survey模式是由server发出询问,client针对请求回复响应的一种模式。这种模式在分布式系统中非常有用,
可以用来做服务发现、分布式事物等分布式询问。

客户端

客户端实现比较方便,除了基础调用(创建socket、连接url)之外,就是先接收服务端询问
(例子中比较简单,服务端询问是固定的,所以没有对内容进行检查)针对询问发送响应
(例子中是发送服务端当前时间)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <nanomsg/nn.h>
#include <nanomsg/survey.h>

using namespace std;

int main(int argc, const char **argv) {
    if(argc != 3) {
        fprintf(stderr, "usage: %s NAME URLn", argv[0]);
        exit(-1);
    }
    const char *name = argv[1];
    const char *url = argv[2];

    int sock = nn_socket(AF_SP, NN_RESPONDENT);
    if(sock < 0){
        fprintf(stderr, "nn_socket fail: %sn", nn_strerror(errno));
        exit(-1);
    }
    if(nn_connect(sock, url) < 0) {
        fprintf(stderr, "nn_connect fail: %sn", nn_strerror(errno));
        exit(-1);
    }

    while(1){
        char *buf = NULL;
        int bytes = nn_recv (sock, &buf, NN_MSG, 0);

        if(bytes > 0) {
            printf ("CLIENT (%s): RECEIVED "%s" SURVEY REQUESTn", name, buf);
            nn_freemsg (buf);

            char sendBuffer[128];
            time_t rawtime;
            struct tm * timeinfo;

            time (&rawtime);
            timeinfo = localtime (&rawtime);
            char *timeText = asctime (timeinfo);
            int textLen = strlen(timeText);
            timeText[textLen - 1] = '';
            sprintf(sendBuffer, "[ %s ] %s", name, timeText);
            int sendSize = strlen(sendBuffer) + 1;
            int actualSendSize = nn_send(sock, sendBuffer, sendSize, 0);

            if(actualSendSize != sendSize) {
                fprintf(stderr, "nn_send fail, expect length %d, actual length %dn", sendSize, actualSendSize);
                continue;
            }
        }
    }

    nn_shutdown(sock, 0);

    return 0;
}

这里收到消息后,就简单的打印,然后将响应数据写会给服务端。

服务端

服务端有个问题,之前搜索了几个例子都不太正常。经过尝试和简单查看代码之后发现,通过nanomsg基础api,
无法获取当前有多少客户端。但是,如果当前所有连接的客户端的响应都已经收到,再次调用nn_recv之后,
会直接返回-1,表示读取失败,同时errno(通过errno函数获取)被设置为EFSM,表示当前状态机状态不正确。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <nanomsg/nn.h>
#include <nanomsg/survey.h>

using namespace std;

const char *SURVEY_TYPE = "DATE";

int main(int argc, char** argv)
{

    if ( argc != 2 ) {
        fprintf(stderr, "usage: %s URLn", argv[0]);
        exit(-1);
    }
    const char *url = argv[1];
    int sock = nn_socket(AF_SP, NN_SURVEYOR);
    if(sock < 0) {
        fprintf (stderr, "nn_socket failed: %sn", nn_strerror (errno));
        exit(-1);
    }

    if(nn_bind(sock, url) < 0) {
        fprintf(stderr, "nn_bind fail: %sn", nn_strerror(errno));
        exit(-1);
    }

    while(1) {
        int sendSize = strlen(SURVEY_TYPE) + 1;
        int actualSendSize;
        printf ("SERVER: SENDING DATE SURVEY REQUESTn");
        if ((actualSendSize = nn_send(sock, SURVEY_TYPE, sendSize, 0)) != sendSize) {
            fprintf(stderr, "nn_send fail, expect length %d, actual length %dn", sendSize, actualSendSize);
            continue;
        }

        int count = 0;
        while(1) {
            char *buf = NULL;
            int bytes = nn_recv (sock, &buf, NN_MSG, 0);
            if (bytes < 0 && nn_errno() == ETIMEDOUT) break;
            if (bytes >= 0) {
                printf ("SERVER: RECEIVED "%s" SURVEY RESPONSEn", buf);
                ++count;
                nn_freemsg (buf);
            } else {
                fprintf(stderr, "nn_recv fail: %sn", nn_strerror(errno));
                break;
            }
        }
        printf("SERVER: current receive %d survey response.n", count);
        sleep(1);
    }

    nn_shutdown(sock, 0);

    return 0;

}

这里用了两个死循环,外层循环不停尝试向客户端发起询问。完成询问后,通过另外一个死循环读取所有的客户端响应,
当读取失败时退出循环。

之前找到的源码是直接判断错误是否ETIMEDOUT,经过打印会发现每次都没有超时,而是状态机错误:

/*  If no survey is going on return EFSM error. */
if (nn_slow (!nn_surveyor_inprogress (surveyor)))
    return -EFSM;

测试

测试和前文差不多,先启动一个server,然后再一个个启动client:

#!/bin/bash

BASE="$( cd "$( dirname "$0" )" && pwd )"
SERVER=$BASE/surveyserver
CLIENT=$BASE/surveyclient

URL="tcp://127.0.0.1:1234"

echo "start surveyserver to bind tcp: $URL"
$SERVER tcp://127.0.0.1:1234 &

echo "start to start surveyclient"
for((i = 0; i < 10; i++))
do
    echo "start client$i"
    $CLIENT client$i $URL &
    sleep 1
done

sleep 20
echo "kill all process and exit"

for pid in `jobs -p`
do
    echo "kill $pid"
    kill $pid
done

wait

输出为:

start surveyserver to bind tcp: tcp://127.0.0.1:1234
start to start surveyclient
start client0
SERVER: SENDING DATE SURVEY REQUEST
start client1
nn_recv fail: Operation cannot be performed in this state
SERVER: current receive 0 survey response.
start client2
SERVER: SENDING DATE SURVEY REQUEST
CLIENT (client0): RECEIVED "DATE" SURVEY REQUEST
SERVER: RECEIVED "[ client0 ] Tue Feb 17 23:32:43 2015" SURVEY RESPONSE
CLIENT (client1): RECEIVED "DATE" SURVEY REQUEST
SERVER: RECEIVED "[ client1 ] Tue Feb 17 23:32:43 2015" SURVEY RESPONSE
nn_recv fail: Operation cannot be performed in this state
SERVER: current receive 2 survey response.
start client3
SERVER: SENDING DATE SURVEY REQUEST
CLIENT (client0): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client1): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client2): RECEIVED "DATE" SURVEY REQUEST
...
SERVER: SENDING DATE SURVEY REQUEST
CLIENT (client0): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client1): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client2): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client3): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client4): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client5): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client6): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client7): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client9): RECEIVED "DATE" SURVEY REQUEST
CLIENT (client8): RECEIVED "DATE" SURVEY REQUEST
SERVER: RECEIVED "[ client0 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client1 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client2 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client3 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client4 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client5 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client6 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client7 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client9 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
SERVER: RECEIVED "[ client8 ] Tue Feb 17 23:33:09 2015" SURVEY RESPONSE
nn_recv fail: Operation cannot be performed in this state
SERVER: current receive 10 survey response.

从输出可以看见,每次最后一个接收完成之后,都会有一个“Operation cannot be performed in this state”
错误,也就是EFSM错误。