image

近日在读阿兰•德波顿的「写给无神论者」,里面提到基督教的人性本恶的观点给人带来的好处,当你对一件事情的结果期望并不高的时候,那么当结果出现一点好处的时候,你就会感到很快乐;相反,当你对一件事情有着过高的期望,最后结果并不令人满意的时候,你就会感到失落,绝望。

德波顿这样捧高悲观,而贬低乐观有点太偏颇了。但是,在各种舆论阵地上,“悲观主义”都在被不公正地对待,各个社交网络上永远都是“正能量”,“乐观”,“积极向上”,每一个人似乎都非常排斥“悲观主义”,我想悲观的人大概也比较受人讨厌。

诚然,过分地悲观会无情地伤害你的心灵,太宰治(愿我们都不要成为他那样的人)笔下的叶藏就是个典型的例子,由于对人类普遍的失望,叶藏一直都是郁郁寡欢、懦弱怕事,不断地和遇到他的女子殉情,最终失去了为人的资格。但是适度地悲观是非常有善的。现实是现实,到了一个人的心中,就成了“心中的现实”。当一个人处于乐观的状态下的时候,往往会对高估了现实的状况,而轻微悲观的人“心中的现实”更加接近实际的现实,他们能够更加准确地估计到现实中可能存在的一些问题,并且做好相应的准备,在问题真正到来的时候不会惊慌失措,因而能够保持更加稳定,更加健康的心理状态。

而那些盲目的乐观主义者(我们的周围不缺乏这样的人),由于对现实有着过高的认识和期望,对问题认识不足,月盈而亏,当结果不尽如人意的时候,各种负面情绪就会出现。你以为你有光明的未来,以为有特殊的才能,但是,到最后却发现你不过是芸芸众生中平凡的一员,很多中年人的失落就是来源于此吧。

所以,与其做个乐观主义者,不如做个悲观的行动主义者,与其相信有那种可能性,不如自己去寻找那种可能性,并且为可能的失败做好准备(成功永远都是属于少数人的)。

有一次和 @imsoz 在聊天的时候,他给我介绍了一个叫做 Git flow 的分支管理模型,我听了很有兴趣,于是昨天花了一点时间去了解了一下。

Git flow 分支模型

Git flow 是一个分支模型,它提供了一个经过实践检验的分支管理的模型。整个分支模型的工作图大概是下面这样样子的:

image

它包含有两个长期分支和三个支持的分支:

  • master 分支:使用过 git 的同学应该都这个分支很熟悉,这个分支代表了可以用于生产环境的代码。
  • develop 分支:用于开发的分支,或者叫做“集成分支”,这里的代码可以用来做 nightly build。

上面两个是长期的分支,这两个分支在开发的过程中会一直存在下去。除了这两个长期分支,还有三个用于支持的分支:

  • feature 分支:一般上从 develop 分支拉出来,最后要 merge 回 develop 分支,或者废弃掉。feature 分支是用来开发一个在下一次的产品 release 中可能被加上去的特性的,一个 feature 分支在特性开发完毕后就结束了,所以 feature 分支只是短暂地存在。
  • release 分支:release 分支用来支持准备一个将要被 release 的产品版本,在这个分支上,我们可能会修改版本号,做一些 release 相关的事情,或者做一些 bug 的 hotfix,release 分支从 develop 分支拉出来,当你认为你的下一次的 release 所需要的特性都已经开发完毕的时候,你就可以从 develop 分支拉出 release 分支来,最后 release 分支需要 merge 回 develop 分支和 master 分支。
  • hotfix 分支:顾名思义,hotfix 分支就是当生产环境的代码出现了重大的 bug 的时候需要的分支,这个时候你可以从 master 分支中拉出一个 hotfix 分支,在修复完 bug 后,记得要把 hotfix 的分支合并回到 develop 和 master 分支上。

上面就是 Git flow 的整个分支模型,相对来说还是比较简单的,更加具体的说明可以看这篇关于 Git flow 分支模型最原始的 blog:http://nvie.com/posts/a-successful-git-branching-model/

Git flow 扩展

但是有一个问题就是上面的这些分支的创建,合并,删除等等操作,都是需要好几步的,比较费时间,费精力,Git flow 还提供了一个 git 的扩展,让我们可以非常方便地在项目中使用上述的分支模型,具体的使用方式作者都已经写在了 github 上,大家可以自己去看:https://github.com/nvie/gitflow。在 Mac 下可以用 brew install git-flow 来安装。

之前我也了解过 git 的一些使用,并且已经在一些项目中尝试去使用 git,但是在分支管理这一块一直比较头疼,而 Git flow 恰好给我提供了一个现成的经过实践的分支模型,刚好公司后面有一个项目可能也会用到 git,到时候我会尝试下在项目组中推广使用 Git flow,让实践去检验一把。

Velocity 对 Java Bean 中布尔类型的属性的获取问题

今天朋友遇到一个问题,是 Velocity 下面一个 Boolean 类型的变量在模板上没有办法输出,我大致简化一下这个问题,现在我们有一个简单的 Java Bean:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleBean {
    private Boolean hasKatong = false;

    public Boolean isHasKatong() {
        return hasKatong;
    }

    public void setHasKatong(Boolean hasKatong) {
        this.hasKatong = hasKatong;
    }
}

然后有一个简单的模板:

1
$simpleBean.hasKatong

模板合并的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
    Properties p = new Properties();
    p.put("resource.loader", "class");
    p.put("class.resource.loader.class",
        "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
    Velocity.init(p);
    VelocityContext context = new VelocityContext();
    context.put("simpleBean", new SimpleBean());
    Template template = Velocity.getTemplate("mytemplate.vm");
    StringWriter sw = new StringWriter();
    template.merge(context, sw);
    System.out.println(sw.toString());
}

大家猜测一下这段代码的输出会是什么?可能大多数人都会认为是 false,但是在 Velocity 1.5 下,这段代码不会输出任何东西,反而会有一个 Warning:

INFO: Null reference [template ‘mytemplate.vm’, line 1, column 1] : $simpleBean.hasKatong cannot be resolved.

而在 Velocity 1.7 下,输出就是如大家所预测的那样,是 false

具体的分析过程并不复杂,Velocity 1.5 和 1.7 在寻找 isXXXX 这样的方法的时候处理稍微有一点不一样,具体在 BooleanPropertyExecutor 这个类上,在找到方法,对方法的返回值的判断上有一点不一样,1.5 是这样的:

1
2
3
4
5
if (isAlive()) {
    if (getMethod().getReturnType() != Boolean.TYPE) {
        setMethod(null);
    }
}

而 1.7 的是这样的:

1
2
3
4
5
6
7
8
if (isAlive())
{
    if( getMethod().getReturnType() != Boolean.TYPE &&
        getMethod().getReturnType() != Boolean.class )
    {
        setMethod(null);
    }
}

可以看到,1.7 中增加了对返回值是 Boolean 的支持,而 1.5 只支持返回值是 boolean 的方法,那么既然知道了问题的根本原因,解决方法就显而易见了,要么将 hasKatong 这个属性的类型从 Boolean 改成 boolean,要么修改下 velocity 的模板,将属性获取直接改成方法调用:$simpleBean.isHasKatong()

Java Bean 规范对布尔类型属性的定义

当然,照理说像 velocity 这样的著名开源组件,不应该在这种问题上犯错误,然后我看了一下 Java Bean 的规范:

image

其实这段话已经说的很清楚了,只有原生类型的 boolean 的 Accessor 方法才能够用 is 前缀,其他的都用 get,其实在 JDK 的 Introspector 的实现中,也是这样处理的。

那么,这么看来,Velocity 1.5 的处理是正确的,那么 1.7 增加对 Boolean 的支持是为什么呢?

其实,Java Bean 的规范在 is 这种 Accessor 的规定上,是有点不怎么符合开发人员的直觉的,很多人都会在这个问题上纠结:Boolean 类型的属性的 Accessor 是不是应该用 is 开头?,我觉得大部分人的直觉对这个答案的回答应该都是,所以 Velocity 这样处理只不过是顺着大多数人的直觉的意思罢了,无可厚非。

我承认这次我又冲动了,竟然买了一本这样的一本书,本以为应该是一本不错的书,看完了却发现就是「读者」上面的那些小故事的档次啊。果然下决定看一本书之前还是得去豆瓣上稍微看一些书评,这不,一不小心又看了一本励志的、成功学的书,让人好郁闷不是。

当然,如果「致加西亚的信」真的是那种成功学或励志的书,也就罢了,虽然这些书大部分的时候毫无用处,但是至少在你低落的时候,还能够给你一点强行针的效果,但是「致加西亚的信」这本书却是一点吸引力都没有,情节没有任何跌倒起伏,人物也看不出有什么特点。一个军人,完成了总统交给的命令,都能够让作者联想到现在的企业的员工太矫情,上级交给的任务不能一声不吭的就直接完成,我想作者大概是被过度洗脑了吧,当然,我也不好去恶意揣测一个一百多年前的作者的想法,但是这本书对于现在来说,真的是一无是处啊。

「致加西亚的信」企图用军人上下级之间的关系来说明企业内部的员工的上下级关系,这样的行为显然是不对的,军队是一个要求高执行力的地方,但是企业不一样,企业不仅仅要看中一个员工的执行力,还需要看看员工的创造力,而对人性的压制必然会导致创造力的泯灭,每一个员工都有自己的特点,有自己的个性,你不能要求他像一个军人一样,接到任务就一股脑去做,做一件事情之前,了解这件事情相关的东西那都是必须的,期间必然会和上级之间有一个交流的过程,不可能拿到任务过几天就直接给上级一个结果。

另外,「致加西亚的信」里面还提到员工的忠诚度的问题,这个话题刚好之前和朋友聊天的时候谈到过,这位朋友的想法和我的比较接近,我们都认为个人和公司的最基本关系就在于那一张劳动合同,无所谓忠诚度。一段能够持久下去的关系(无论这是什么关系),肯定是双方相互付出的结果,一段只有单方面付出的关系是不会长久的。所以啊,当企业的管理者们在抱怨员工的忠诚度的时候,不妨也想想自己的公司能够给员工带来什么东西,是不是符合员工在这家公司所付出的东西。(「忠诚」这个词,和「感恩」是一样的,都只能是对自己的要求,而不能要求别人「忠于你」或者对你「感恩」

但是不可否认,当我们在一家公司呆久了以后,肯定会存在这一些感情,但是如果你仔细分析这些感情,你会发现,这些感情都是你和某些同事之间的感情(难保你会和某些人相处地比较好,另一些相处地并不好),多年以后,你怀念的可能是某个深夜和同事们一起加班为项目做冲刺,可能是一次让你们非常怀念的 outing,所以,所谓感情,那是指人和人之间的感情,作为一个集体的公司,我认为就是只有利益罢了,我付出我的劳动,公司给我应有的回报,这些回报包括成长机会,工资,股票,还有其他的福利等等。

当然,「致加西亚的信」是一本销量达到 4000 万的畅销书,这样的销量肯定有其存在的理由,或许是我们这个时代的人已经和过去的时代的人大不相同了~,这是一本适合于那个时代的书,并不符合我们的时代,应该永远都留在那个时代。

每个人都有自己理想的下午,有些人喜欢在家里宅着躺在床上,被子病了得有人照顾不是?有些人喜欢在一个秋季的阳光明媚的下午,约几个好友爬山登高,流一把汗,呼吸呼吸新鲜的空气。有些人喜欢把家里打扫的干干净净,然后美滋滋地看上一部喜欢的电影。

而我的理想的下午,不在家,那必须是个安静的地方,稍微有点人气,不至于感到寂寞,看一本自己喜欢的书,把自己沉浸在一个或美好,或悲伤的故事里,或者写一些代码,精炼自己的技艺,让自己对这个世界的理解更进一步。

理想的下午,约上几个志同道合的朋友是必须的,不必太多,一两个就成,有着一些共同的想法,追求着共同的理想,相互知道对方的一些秘密,相互之间有一些默契,需要的时候交流交流彼此的想法,从对方身上学一些东西,但也不必为各自的沉默而感到尴尬。

每个人都有自己状态最棒的时候,而我感觉最好的时候,便是在这样一个理想的下午,不必为生活或者工作上的事情烦心,放下心中的焦虑与不安,那是完完全全属于你自己的下午,在这样的下午,你只干自己喜欢的事情;在这样的下午,你真真切切感受到生活的温度;在这样的下午,做回真真的自己。

对了,就像一次长跑,完全沉浸在跑步的节奏里面,你无需知道终点到底在什么地方,需要做的只是找到自己的节奏,迈开脚步向前跑~,在这种状态下,你可以放下包袱自由地思考,思考生命和星空的意义,思考“从哪里来”、“到哪里去”这样的究级的问题,而不必担心别人说你不着边际。

嗯~,给自己多几个理想的下午,算是自私一点吧,在这些理想的下午,完完全全地为自己而活~

最近工作中涉及到了一个分布式事务的产品,这个产品是在 Spring 的事务上做的,我对其中涉及到的 Spring 的事务的传播特性不是很了解,所以今天花了一个下午的时间认真了解了一下,写了一堆的测试代码。

进入正题,Spring 的事务的传播特性分为以下的七种:

  • PROPAGATION_REQUIRED
  • PROPAGATION_SUPPORTS
  • PROPAGATION_MANDATORY
  • PROPAGATION_REQUIRES_NEW
  • PROPAGATION_NOT_SUPPORTED
  • PROPAGATION_NEVER
  • PROPAGATION_NESTED

下面一种种来解释:

PROPAGATION_REQUIRED

默认的事务传播特性,通常情况下我们用这个事务传播特性就可以了。如果当前事务上下文中没有事务,那么就会新创建一个事务。如果当前的事务上下文中已经有一个事务了,那么新开启的事务会开启一个新的逻辑事务范围,但是会和原有的事务共用一个物理事务,我们暂且不关心逻辑事务和物理事务的区别,先看看这样会导致怎样的代码行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void txRollbackInnerTxRollbackPropagationRequires() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务设置了 setRollbackOnly,
                    status.setRollbackOnly();
                }
            });
        }
    });
}

这段代码在一个嵌套的事务内部(内外层的事务都是 PROPAGATION_REQUIRED 的)设置了回滚,那么对于外部的事务来说,它会收到一个 UnexpectedRollbackException,因为内部的事务和外部的事务是共用一个物理事务的,所以显然内部的事务必然会导致外部事务的回滚,但是因为这个回滚并不是外部事务自己设置的,所以外层事务回滚的时候会需要抛出一个 UnexpectedRollbackException,让事务的调用方知道这个回滚并不是外部事务自己想要回滚,是始料未及的。

但是,如果内层的事务不是通过设置 setRollbackOnly() 来回滚,而是抛出了 RuntimeException 来回滚,那么外层的事务接收到了内层抛出的 RuntimeException 也会跟着回滚,这个是可以预料到的行为,所以不会有 UnexpectedRollbackException

PROPAGATION_SUPPORTS

PROPAGATION_SUPPORTS 的特性是如果事务上下文中已经存在一个事务,那么新的事务(传播特性为 PROPAGATION_SUPPORTS)就会和原来的事务共用一个物理事务,其行为和 PROPAGATION_REQUIRED 一样。但是,如果当前事务上下文中没有事务,那么 PROPAGATION_SUPPORTS 就按无事务的方式执行代码:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void txRollbackInnerTxRollbackPropagationSupports() {
    supportsTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            throw new CustomRuntimeException();
        }
    });
}

看上面这段代码,虽然我们在事务(PROPAGATION_SUPPORTS 的)中抛出了一个 RuntimeException,但是因为其事务上下文中没有事务存在,所以这段代码实际上是以无事务的方式执行的,因此代码中的 jdbcTemplate.update() 操作也不会被回滚。

PROPAGATION_MANDATORY

PROPAGATION_MANDATORY 要求事务上下文中必须存在事务,如果事务上下文中存在事务,那么其行为和 PROPAGATION_REQUIRED 一样。如果当前事务上下文中没有事务,那么就会抛出 IllegalTransactionStateException,比如下面这段代码就会这样:

1
2
3
4
5
6
7
8
9
10
@Override
public void txRollbackInnerTxRollbackPropagationMandatory() {
    mandatoryTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
        }
    });
}

PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRES_NEW 无论当前事务上下文中有没有事务,都会开启一个新的事务,并且和原来的事务完全是隔离的,外层事务的回滚不会影响到内层的事务,内层事务的回滚也不会影响到外层的事务(这个说法得稍微打点折扣:因为如果内层抛出 RuntimeException 的话,那么外层还是会收到这个异常并且触发回滚),我们分析下几段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });

            // 外部事务发生回滚,内部事务应该不受影响还是能够提交
            throw new RuntimeException();
        }
    });
}

这段代码外层的事务回滚了,但是不会影响到内层的事务的提交,内层事务不受外层的事务的影响。再看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew2() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            // Nested transaction committed.
            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务发生回滚,但是外部事务不应该发生回滚
                    status.setRollbackOnly();
                }
            });
        }
    });
}

这段代码在内层事务上设置了 setRollbackOnly,内层事务肯定会回滚,但是由于内层事务和外层事务是隔离的,所以外层事务不会被回滚。再看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew3() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");

            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务抛出 RuntimeException,外部事务接收到异常,依旧会发生回滚
                    throw new RuntimeException();
                }
            });
        }
    });
}

这段代码在内层事务抛出了一个 RuntimeException,虽然内层事务和外层事务在事务上是隔离,但是 RuntimeException 显然还会抛到外层去,所以外层事务也会发生回滚。

PROPAGATION_NOT_SUPPORTED

PROPAGATION_NOT_SUPPORTED 不管当前事务上下文中有没有事务,代码都会在按照无事务的方式执行,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void txRollbackInnerTxRollbackPropagationNotSupport() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            notSupportedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });
            // 外部事务回滚,不会把内部的也连着回滚 
            transactionStatus.setRollbackOnly();
        }
    });
}

上面这段代码中虽然外部事务发生了回滚,但是由于内部的事务是 PROPAGATION_NOT_SUPPORTED,根本不在外层的事务范围内,所以内层事务不会发生回滚。

PROPAGATION_NEVER

PROPAGATION_NEVER 如果当前事务上下文中存在事务,就会抛出 IllegalTransactionStateException 异常,自己也会按照非事务的方式执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void txRollbackInnerTxRollbackPropagationNever2() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            neverTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });
        }
    });
}

比如上面这段代码,在一个 PROPAGATION_REQUIRES 里面嵌入了一个 PROPAGATION_NEVER,内层就会抛出一个 IllegalTransactionStateException,导致外层事务被回滚。

PROPAGATION_NESTED

PROPAGATION_NESTED 只能应用于像 DataSource 这样的事务,可以通过在一个事务内部开启一个 PROPAGATION_NESTED 而达到一个事务内部有多个保存点的效果,一个内嵌的事务发生回滚,只会回滚到它自己的保存点,外层事务还会继续,比如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void txRollbackInnerTxRollbackPropagationNested() {
    nestedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");

            nestedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务设置了 rollbackOnly,外部事务应该不受影响,可以继续提交
                    status.setRollbackOnly();
                }
            });
        }
    });
}

内层的事务发生了回滚,只会回滚其内部的操作,不会影响到外层的事务。

总结

Spring 的事务传播特性种类繁多,大多数都来自于 EJB 的事务,大家可以自己写一些小的程序来测试 Spring 各个事务特性的行为,加深印象,我自己也写了一个工程,通过单元测试去测试各个事务传播特性的行为,大家有兴趣的话,可以下过来跑一下:https://github.com/khotyn/spring-tx-test

前几天在 HotCode 的用户群里面,有同学问起“如何将 JVM 中的 class dump 出来”,当时我下意识的回答就是“可以在 JVM 启动的时候挂一个 agent 上去,然后通过 Instrumentation API 在 class 加载的时候做拦截,把类 dump 出来。”,今天无聊在翻 R 大博客的时候,发现还可以通过 sa-jdi.jar 里面的一个类做 dump,这里就集中介绍一下这几个方法,然后介绍我在 sa-jdi.jar 基础上改的一个小工具。

采用 classLoader.getResourceAsStream()

将一个类从 JVM 中 dump 出来,最简单的方法当然就是直接从 jar 包中把对应的 class 文件找到,然后 dump 出来了,我们可以用 classLoadergetResourceAsStream 来做:

1
2
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream in = loader.getResourceAsStream("com/khotyn/Test.class");

拿到 InputStream 后,你就可以随便玩了。

这个方法简单是简单,但是缺点也很明显,在有些 Java 程序中,类不一定是从 Class 文件中过来,有些是在运行时生成的,有些则在载入到 JVM 之前被增强过,所以这个方法有些类是 dump 不出来的,有些类则 dump 出来不是你想要的。

采用 javaagent

另外一个方法是通过在 JVM 启动的时候挂在一个 javaagent,然后用 Instrucmentation API 在类被加载到虚拟机之前做拦截,参考代码如下:

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 com.khotyn.test;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import org.apache.commons.io.FileUtils;

/**
 * A demo to demonstrate how to use JVM ti to dump class file from JVM.
 * 
 * @author khotyn.huangt 13-8-3 PM2:21
 */
public class AgentMain {

    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {

            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer)
                                                                                              throws IllegalClassFormatException {
                try {
                    FileUtils.writeByteArrayToFile(new File("/tmp/" + className.replace('.', '/') + ".class"),
                                                   classfileBuffer);
                } catch (IOException e) {
                    // Quite
                }
                return null;
            }
        });
    }
}

将上面的代码打成一个 jar 包(注意把依赖的 apache commons.io 也打入,也可以直接下载我的 demo 工程:agentDumpClass.zip),然后在 jar 包的 META-INF/MANIFEST.MF 中填上如下的内容:

1
2
3
4
5
6
7
8
9
Manifest-Version: 1.0
Boot-Class-Path: agentDump.jar
Built-By: apple
Build-Jdk: 1.7.0_17
Class-Path: lib/commons-io-2.4.jar
Premain-Class: com.khotyn.test.AgentMain
Created-By: Apache Maven
Can-Redefine-Classes: true
Archiver-Version: Plexus Archiver

然后你就可以执行类似于下面的命令来进行 dump 了:

1
java -javaagent:/Users/apple/workspace/agentDumpClass/target/agentDump.jar Test

由于 premain 方法是在 java 程序的 main 方法执行之前执行的,所以这个方法几乎可以拦截到所有的类,另外,由于注册的 ClassFileTransformer 是 ClassLoader 加载 class 之后,JVM 定义 class 之前被执行的,所以无论是在运行时生成的类,还是经过增强后的类,这个方法都能够 dump 出来,比第一种方法要强很多。然而这个方法还是有一些缺点:

  • 需要在 JVM 启动时增加特别的参数。
  • 只能随 class 被加载进行 dump,不能随时进行 dump

采用 sa-jdi.jar 的 ClassDump 工具

这个方式是 R 大在博客中介绍的方法,可以说是最强大的方法,不像前面的两个方法,这个方法可以在 JVM 进程外执行,且像 javaagent 的那个方法一样,都可以将运行时和被增强过的类 dump 出来,非常方便,至于具体的用法大家就直接看 R 大的文章吧。

改进 ClassDump

ClassDump 工具虽然强大,但是命令略显繁琐,特别是当你只需要 dump 特定的类的时候,还需要专门写一个 ClassFilter 的实现类,这么好的工具,应该直接做成命令行工具才好,于是我修改了 ClassDump 的代码,让它可以支持正则表达式的方式来对需要 dump 的类进行过滤,改进后的 ClassDump 放在了我的 github 仓库上:https://github.com/khotyn/tools

大家可以直接用下面的方式来使用这个改进版:

1
sudo classDump 17118 'com.khotyn.*' dump

classDump 这个命令的第一个参数是目标 JVM 的 PID,第二个参数是一个正则表达式,表示你所要 Dump 出来的类,第三个参数可选,是 dump 的目录。

但是这个工具有一个缺点,就是目前只能在 Mac 下用(因为我用 Mac,呵呵,我把修改后的类直接打入到了 Mac 的 jdk 的 sa-jdi.jar 下面),不过要做其他的平台的也很简单啦,只要按照以下步骤来打包出自己的 sa-jdi.jar 就可以:

  • 下载我修改过的两个类:ClassDump.classRegexClassFilter.class
  • 从 jdk 目录下拷贝一份 sa-jdi.jar 出来
  • 用下面的命令将修改过的两个类打到 sa-jdi.jar 中去:jar uf sa-jdi.jar sun/jvm/hotspot/tools/jcore/ClassDump.class sun/jvm/hotspot/tools/jcore/RegexClassFilter.class
  • 然后配合仓库中的 classDump 脚本就可以用了。

上一篇文章中,我介绍了一下 sed 的基础,包括执行方式、地址选择器以及基本命令,在这一篇文章中,我们继续来了解一下 sed 的高级命令,之所以称它们为高级命令,是因为这些命令会改变 sed 的执行流,废话不说,我们来看看这些命令吧:

高级命令

N (Next)

这里要介绍的第一个命令是 N,它和我们前面介绍过的 n 命令很像,也是要读取下一行的内容,不同的是,N 读取下一行的内容并且将这些内容附加到 pattern space 当前的内容后面。这样,当你需要连着处理多行内容的时候,N 命令就会特别有用,比如,我们有下面一段文本:

1
2
3
4
one two three four
one two three 
four three two
three four

如果我们要把 two three four 替换成 2 3 4,注意例子中的 two three four 可能在不同的行中,那么我们就可以用 N 命令来处理:

1
2
3
N
s/\n/ /
s/two three four/2 3 4/

输出的内容为:

1
2
one 2 3 4 one two three
four three 2 3 4

这个结果不是我们想要的,不过的确是符合了上面的 sed 脚本的执行结果:

  1. 首先,sed 脚本读取了文本的第一行,这个时候 pattern space 中的内容为 one two three four
  2. 然后 sed 脚本执行 N 命令,将下一行读取并附加到当前的 pattern space 内容的后面,这个时候,pattern space 中的内容就变为 one two three four\none two three
  3. 下一个命令,将换行符 \n 替换成一个空格,pattern space 中的内容为 one two three four one two three
  4. 然后下一个命令,将 pattern space 中的 two three four 替换成 2 3 4,这个时候 pattern space 中的内容为 one 2 3 4 one two three
  5. 到达脚本的结尾,输出 pattern space 中的内容,也就是我们输出内容的第一行。
  6. 然后 sed 脚本读取文本的下一行,注意因为之前第二行已经被 N 命令读取了,所以 sed 脚本开始读取第三行,依旧按照前面的命令执行,最后就输出了输出内容中的第二行。

虽然这个结果不是我们想要的,不过算是了解了 N 的作用了。

D (Delete)

同样,前面我们介绍过 d 命令,它用来删除 pattern space 中的内容,并且读取下一行到 pattern space 中,sed 脚本也随之从头开始执行。D 命令和 d 命令稍微有点不同,D 命令会删除 pattern space 中的第一行的内容,它不会从文本中读取新的行进来,当然 sed 脚本还是会从头开始执行,如果我们有这么一个文本:

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


blank

blank


blank






blank

这个文本中的有些段落之间有多个空行,我们希望把多余的空行去掉,也就是如果段落之间有多个空行,就删掉只剩下一个,我们的 sed 脚本可以这么写:

1
2
3
4
/^$/{
	N
	/^\n$/D
}

这个脚本先匹配出空行,然后读取空行的下一行,如果两行都是空行的话,就把 pattern space 中的第一个空行删除掉,然后继续读取下一行到 pattern space 中,结果就是把多余的空行都删除掉,只剩下一个空行了。

P (Print)

P 命令和 p 命令也稍微不同,P 命令不像 p 命令那样会把 pattern space 中的所有内容打印出来,它只会将 pattern space 的第一行打印出来,这里就不做过多的介绍了。

h (hold), H (Hold), g (get), G (Get), x (exchange)

这里面有五个命令,之所以一起介绍是因为,这五个命令都是操作 hold space 的,之前我们已经知道了 pattern space 了,hold space 可以认为就是一个内容的临时存放点,你可以将 pattern space 中的内容放到 hold space 中,等到需要使用的时候再将 hold space 中的内容拿回到 pattern space 中,我们来看一下这五个命令的作用吧:

  • h:将 pattern space 中的内容拷贝到 hold space 中,hold space 中原来的内容会被覆盖。
  • H:将 pattern space 中的内容添加到 hold space 当前内容的后面。
  • g:将 hold space 中的内容拷贝到 pattern space 中,pattern space 中原来的内容将会被覆盖。
  • G:将 hold space 中的内容添加到 pattern space 中目前的内容后面。
  • x:交换 pattern space 和 hold space 中的内容。

下面我们来看一个简单的例子:

1
2
3
4
5
6
1

2

11

22

111

222

现在我们要将上面的 1 和 2 的位置调换,就是先出现 2 再出现 1,我们的脚本可以这么写:

1
2
3
4
5
6
7
/1/{
	h
	d
}
/2/{
	G
}

这段脚本先匹配 1 所在的行,然后放到 hold space 中,将 pattern space 中的内容清除掉,然后匹配到 2 所在的行,将 hold space 中的内容添加到 pattern space 后面,这样,pattern space 中就是先有 2,再有 1 了。

最后,我们得到的结果就是:

1
2
3
4
5
6
2
1
22
11
222
111

b

b 命令是一个跳转命令,它是无条件的,它的语法是这样的:

1
[address]b [label]

[label] 是要跳转到的标签,你可以在 sed 脚本中用 : 开头来表示一个标签,比如下面的:

1
2
3
:start
s/start/end/g
b start

如果 b 后面不带参数,那么就表示直接跳到脚本的末尾了。

t

除了 b 这样一个跳转命令以外,sed 还有一个 t 的条件跳转命令,如果在当前行有一个替换被成功执行了,那么 t 就会跳转到特定的标签上,它的语法 b 是类似的。

1
[address]t [label]

看下面这段代码:

1
2
3
:begin
s/start/end/
t begin

这条 t 命令只有在当前行的 start 成功被替换成 end 的时候才会跳转到 :begin 标签那里。

总结

sed 的高级命令相对于基本命令来说不怎么常用,但是在处理特定的问题的时候,这些命令还是很有用的。不过,不管怎么说,sed 都不是一门完备的语言,所以其适用的问题域也是比较有限的,sed 最大的优势在于逐行处理文本上,用适当的工具处理适当的问题,才能发挥出工具最大的威力。

之前写的一篇文章有提到采用 sed 来匹配不包含连续字符串的行,平时在做日志分析的时候也经常要用到 sed,但是仅仅用了 sed 的字符串替换的功能,没有系统地去学习过 sed 用法,这次找到一本叫「sed & awk」的书,便花时间对 sed 做了系统的学习。

sed 的执行方式

要了解 sed,必须了解 sed 的执行方式,sed 是一个行处理器,脱胎于 ed(ed 是一个行编辑器,awk 和 grep 也是基于 ed 的),简单地说,sed 的执行方式是这样的:sed 会从输入的文本中读取一行,放到 pattern space 中,然后用 sed 脚本去处理,处理完后继续读取下一行,继续处理。

假设我们有下面一段文本需要处理:

1
2
3
4
5
6
7
8
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

我们要把文本中的 CA 替换成 California,OK 替换成 Oklahoma,于是我们写了下面一段 sed 脚本:

1
2
s/CA/California/
s/OK/Oklahoma/

那么 sed 的执行方式是这样的:

  1. 先读取第一行 John Daggett, 341 King Road, Plymouth MA 到 pattern space
  2. 然后执行脚本的第一行命令,将其中的 CA 替换成 California。
  3. 然后执行脚本的第二行命令,将其中的 OK 替换成 Oklahoma。
  4. 文本的第一行处理完毕,继续读取文本的下一行。
  5. 继续第 2 步和第 3 步。

当然,这只是大部分情况下 sed 的执行方式,sed 的基本命令都是按照这种方式来执行的,一些高级命令可以改变 sed 的执行流。不过在了解这些 sed 命令之前,我们先了解下 sed 的地址选择器,它是很多命令的组成部分。

sed 的地址选择器

默认的情况,sed 脚本会对文本的每一行做处理,但是有时候我们只希望我们的命令作用于特定的几行,这个时候,我们就可以用 sed 的地址选择器,sed 的地址选择器可以是一个正则表达式(sed 的正则表达式总是放在两个 / 中间),行号,或者地址符号(这个是什么东西?我也不清楚),具体的使用方式如下:

  • 如果没有指定地址选择器,那么命令默认会应用在每一行上。
  • 如果只有一个地址选择器,那么命令会作用在每一个符合这个地址选择器的行上。
  • 如果是两个用 , 分割的地址,那么命令会先作用到第一个符合第一个地址选择器的行上,然后继续作用于后续的行,直到(包括)第一个符合第二个地址选择器的行为止。
  • 地址选择器后面可以跟上一个 !,表示反向选择。

另外在一个地址选择器中,你可以用一对 {} 将多个命令包含在其中,下面我们来看一个例子:

1
2
3
4
5
/^\.TS/,/^\.TE/{
     /^$/d
     s/^\.ps 10/.ps 8/
     s/^\.vs 12/.vs 10/
}

这个 sed 脚本的第一行就是一个地址选择器,由 , 分开的两个地址选择器组成,都是正则表达式形式的,表示后面 {} 中的命令会从第一个以 .TS 开头的行一直作用到第一个以 .TE 开头的行为止。

sed 的基本命令

了解完 sed 的地址选择器后,我们就可以继续了解 sed 的基本命令了。

替换(substitution)

sed 的文本替换命令是我最常用的命令,它的语法是这样的:

1
[address]s/pattern/replacement/flags

它由这几个部分组成:

  • 最前面是一个地址选择器,是可选的。
  • 然后后面是一个命令 s,表示是替换命令。
  • 后面紧跟一个正则表达式,表示要被替换的文本。
  • 再后面是希望替换成的文本。
  • 最后是标记位。

其中标记位可以是:

  • n:一个从 1 到 512 的数字,表示只替换第 n 个符合 pattern 子串。
  • g:默认情况下,替换命令只会替换一行中第一个符合 pattern 的子串,加上 g 以后会将行中所有符合 pattern 的子串都进行替换。
  • p:将 pattern space 中的内容打印出来。
  • w file :将 pattern space 中的内容写到文件中。

举一个例子,假设我们要将第一个例子中的最后一行的 MA 换成 Massachusetts,就可以这样写:

1
$s/MA/Massachusetts/

其中的 $ 是一个地址选择器,表示最后一行。

替换命令的替换文本基本上就是一个字符串,但是还是有一些特殊字符:\&\n,其中:

  • \:转义,转义特殊字符。
  • &:代表要被替换的文本,也就是符合 pattern 的子串。
  • \n:当前面的正则表达式中有捕获部分的时候(即,正则表达式的 () 语法),可以在替换文本中用这种反向引用的方式进行引用。

这些特殊字符在当你需要将匹配的文本中的某些部分放到替换文本中的时候会特别有用。

删除(delete)

删除命令很简单,它的语法是:

1
[address]d

前面可以带一个地址选择器,后面是一个 d,表示删除命令,举一个简单的例子,假设我们要筛选出不包含 abc 的行,可以这样写:

1
/abc/d

把包含 abc 的行全部都删除掉,这样就筛选出了不包含 abc 的行。

追加,插入和变化(append,insert,change)

这三个命令的作用分别是:insert 将提供的文本插入到 pattern space 的当前行前面,append 将提供的文本追加到 pattern space 的当前行后面,change 命令替换 pattern space 中的内容,之所以将这三个命令放到一起说,是因为这三个命令需要将提供的文本放在命令的第二行,它们的语法分别是这样的:

append

[line-address]a\
text

insert

[line-address]i\

text

changae

[address]c\

text

来一个例子,我们有下面一段文本:

1
2
3
4
5
6
7
8
9
I want to see @f1(what will happen) if we put the
font change commands @f1(on a set of lines).
If I understand things (correctly), the @f1(third) line causes problems. (No?).
Is this really the case, or is it (maybe) just something else?
Let's test having two on a line @f1(here) and @f1(there) as
well as one that begins on one line and ends @f1(somewhere
on another line).
What if @f1(it is here) on the line?
Another @f1(one).

假设为了阅读的美观,我们希望段落之间能够空出一行来,段落结束的标记我们暂且简单地假设以 . 结尾,我们要做的就是在每一个以 . 结尾的行后面再插入一行,除了最后一行之外,那么我们的 sed 脚本就可以这么写:

1
2
3
4
5
$!{
	/\.$/a\
	\

}

来说明一下这段脚本,第一行是一个地址选择器,表示选择除最后一行以外的行,因为我们不希望在最后一个段落后面也加上一个空行,然后里面的命令是对所有的以 . 结尾的行运用 a 命令去追加一个空行。

列出(list)

列出命令和打印命令其实很像,不同的是列出命令会将不可见字符给列出来,比如 windows 下的换行符,假设我们有下面一段文本(^M 可以用 Ctrl+V,然后 Ctrl+M 来输入):

1
2
3
^M^M
^M
^M^M^M

上面一段文本用 sed -n 'l' 输出的内容是:

1
2
3
\r\r$
\r$
\r\r\r$

而用 sed -n 'p' 输出的内容是三个空空的行。

列出命令将不可见字符打印出来了,而打印命令则没有。

转换(transform)

转换命令和替换命令听起来是一样,但是它们还是不同的,转换命令就像是多个 tr 命令用管道连在一起作用一样,它的语法是:

1
[address]y/abc/xyz/

转换命令的一个使用的场景就是大小写的转换:

1
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

上面这个命令会将文本中所有的小写字母转换成大写字母。

打印(print)

打印命令其实就是一个简单的 p,将 pattern space 中的内容打印出来,比如:

1
$!p

表示将除最后一行以外的内容全部打印出来。需要注意的是,默认情况下 sed 会将 pattern space 中的内容都打印出来,要关闭这个功能,可以加上一个 -n 参数,就像我在介绍列出命令的时候做的那样。

打印行号

打印行号也就是一个简单的 = 号,大家可以去试一下,这里不再多讲了。

下一个(next)

next 命令是一个 n,它的作用是将 pattern space 中的内容立即输出,然后将下一行读入到 pattern space 中,然后继续执行接下来的命令,比如:

/H1/{

n

/^$/d

}

就是先匹配到含有 H1 的行,然后将这一行打印出来,接着读取下一行到 pattern space,如果是空的话,就删除掉。

读取和写入文件(read,write)

sed 的读取文件的功能可以将文件中的内容追加到 pattern space 后面,前面那个在段落后面添加空行的例子我们可以这么做:首先创建一个只包含一个空行的文件叫做 temp,接着我们就可以用下面的命令来达到我们的目的了:

1
2
3
$!{
    /\.$/r temp
}

sed 的写入文件的功能和读取文件的功能类似,语法是:

1
[address]w file

表示将 pattern space 中的内容写入到文件。

退出(quit)

sed 的退出命令是让 sed 停止读取新的行,也停止输出,基本上就是让 sed 退出了,它的命令的语法是:

1
[line-address]q

它只能作用在单行的地址选择器上。

总结

sed 的基本命令相对来说还是比较简单的,最主要的还是要用好地址选择器,在下一篇中,我会介绍一些 sed 的高级命令。