Scala与PowerMock

这个题目对搜索引擎应该是比较友好的,虽然土了些。

PowerMock是个好东西,在某种程度上拯救了架构设计,使得我们不用过分考虑如何用复杂的架构设计来达到可以单元测试的目的。

Java里面如何使用PowerMock就不用多说了,随便google一下满屏幕都是,如果是同事还可以跟我要codekata的材料。但是这事儿换成Scala,情况就完全不同了。

首先要解决的问题是Scala用什么测试框架。比较流行的有ScalaTestSpecs,二者都支持BDD、ATDD,也就是这个BDD、ATDD把我折磨了一个晚上。

撇开ATDD不谈,我非常想认真尝试一下BDD,但是ScalaTest的BDD需要使用它自己的Runner,Specs的虽然支持JUnit,但是也需要用一个特殊的Runner,这对于PowerMock来说几乎是致命的。PowerMock+Junit,必须使用PowerMockRunner才能mockStatic,mockNew,PowerMock+TestNG倒是不需要Runner,但是ScalaTest不支持用TestNG来做BDD,而Specs根本就不支持TestNG。

有点儿混乱,总之一句话,现阶段,Scala+BDD+PowerMock这条路完全走不通。如果我的判断有问题,望指正。

那么就退一步,还是JUnit。从一个例子来说明我遭受到的痛苦以及整个解决过程。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyStatic {
    public static String hello() {
        // connect to twitter for example
        return tweet;
    }
}
 
public class HelloWorld {
    public String helloWorld() {
        return MyStatic.hello();
    }
}

上面一段代码,Java+PowerMock很容易处理:

1
2
3
4
5
6
7
8
9
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyStatic.class)
public HelloWorldTest {
    @Test
    public testHelloWorld() {
        mockStatic(MyStatic.class);
        ....
    }
}

如果是Scala,问题就接踵而至了。先看功能相似的Scala代码片段:

1
2
3
4
5
6
7
8
9
10
object MyStatic {
  def hello = {
    // connect to twitter for example
    tweet
  }
}
 
class HelloWorld {
  def helloWorld() = MyStatic.hello
}

@RunWith没有问题:

1
@RunWith(classOf[PowerMockRunner])

@PrepareForTest就开始出状况了。

首先貌似Scala对annotation的解释有些不太好,不能简单地传一个东西给@PrepareForTest,因为这玩意儿有两个参数,每个都是数组

1
2
3
4
5
6
7
8
9
@Target( { ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PrepareForTest {
    Class[] value() default IndicateReloadClass.class;
 
    String[] fullyQualifiedNames() default "";
}

所以不能:

1
@PrepareForTest(classOf[SomeClass])

必须要:

1
@PrepareForTest(Array(classOf[SomeClass]))

这个还算是比较容易解决,但是对于Scala里的object,情况又有变化。object不支持classOf这个运算符,classOf[MyStatic]是非法的;MyStatic.getClass倒是可以拿到类,但这个类是另一个,名字是MyStatic$(后面会详细解释这两个类以及它们之间的关系)。但即使getClass得到的类是正确的,也没法直接传给annotation,因为annotation要求参数必须是一个constant,不能是一个方法调用,因此@PrepareForTest(Array(MyStatic.getClass))也是不行的。

怎么办呢?还好@PrepareForTest还有第二个参数,我们可以这样:

1
@PrepareForTest(fullyQualifiedNames = Array("com.honnix.test.MyStatic"))

虽然比较恶心,而且不利于重构,但是聊胜于无吧。

然后就是mockStatic的问题。怎么写呢?

1
mockStatic(MyStatic.getClass)

这肯定不行,因为之前说了getClass得到的类是MyStatic$。那么怎么样才能mock MyStatic呢?答案就是,反射。

1
mockStatic(Class.forName("com.honnix.test.MyStatic"))

没有其他任何办法得到一个object的class信息,这里有一个很古老的bug

好吧,两个问题都“解决”了,跑跑看。结果却是我想mock的东西根本就没有被mock。要命了。

事情还得从头说起。Scala对于一个object编译的结果是生成两个类,一个MyStatic,一个MyStatic$:

1
2
3
4
5
6
7
public final class MyStatic
{
  public static final String hello()
  {
    return MyStatic$.MODULE$.hello();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class MyStatic$
  implements ScalaObject
{
  public static final  MODULE$;
 
  static
  {
    new ();
  }
 
  public String hello()
  {
    return "hello";
  }
 
  private MyStatic$()
  {
    MODULE$ = this;
  }
}

这是反汇编的结果,稍微有点乱,凑合着看吧。不用多解释,代码已经很明白了。

问题是,我对MyStatic进行mock,理论上说也应该可以的,也就是mock之后就无视MyStatic$了。为什么不行呢?还得来看看调用地方的反汇编代码:

1
2
3
4
5
public testHelloWorld {
    ...
    MyStatic$.MODULE$.hello();
    ...
}

这里面压根儿就没有MyStatic的事儿了!MyStatic被华丽的忽视了!

到此为止终于真相大白了。咱们得这样来mock。

1
2
3
4
5
6
7
8
9
10
@RunWith(classOf[PowerMockRunner])
@PrepareForTest(fullyQualifiedNames = Array("com.honnix.test.MyStatic$"))
class HelloWorldTest {
  @Test
  def testStartAndStop() {
    val mockMyStatic = mock(MyStatic.getClass)
    Whitebox.setInternalState(MyStatic.getClass, mockMyStatic)
    ...
  }
}

想法很简单,把MyStatic$里面的MODULE$给换喽。

代码很丑陋,所以需要强调一下PowerMock应该被用在那些确实不需要复杂的依赖注入的地方来帮助简化架构设计,对于需要做依赖注入的地方,我们还是绝对不能松懈的,不能因为PowerMock啥都能mock就乱写代码。

顺便说一句,Specs对于Mockito已经有很好的支持了,加上Scala那种类似DSL的表达式,用起来很爽。希望PowerMock的作者能考虑支持Scala。

Java调用Scala的一个真实案例

Scala调用Java,完全没有问题;但是反过来总有那么一点不爽,或者必须要修改。这里是项目中一个真实的案例,拿出来分享一下。

1. 类型转换

1
2
3
4
5
object AsnLoader {
  ...
  def batchLoadFile(files: List[String]): Map[String, AsnModule] = files.foldLeft(Map[String, AsnModule]())((x, y) => x ++ load(y))
  ...
}

这里的List是Scala里的类,从Java里面调这个方法是不行的,所以需要弄一个wrapper方法:

1
2
3
import collection.JavaConversions._
...
def batchLoadFile(files: java.util.List[String]): java.util.Map[String, AsnModule] = batchLoadFile(asScalaBuffer(files).toList)

虽然asScalaBuffer本身就是个implicit,但是这里仍需要显示调用一下,否则编译器会认为batchLoadFile(file: java.util.List[String])更符合调用匹配规则,结果就变成了无限递归……

Java里面这样调用:

1
AsnLoader.batchLoadFile(list);

这样看起来解决问题了,但是Eclipse的Scala插件会报一个莫名其妙的函数签名错误,其实根本就没错,直接maven或者用Intellij都是对的,Eclipse插件太烂或者太老。无奈只好把函数名也一起改了:batchLoadFileJ,这样就可以了。如果还不是很明白,去看看JavaConversions里面的implicit。

把Java的Map转换成Scala的immutale map稍微曲折了些,先转换成mutable map,再调用toMap方法转换成immutable map。

2. 构造方法

1
2
3
4
5
class DefaultTransformer(private val asnModule: AsnModule, private val dependencies: Map[String, AsnModule] = Map.empty,
                         private val repository: Map[String, AsnModule] = Map.empty,
                         private val messageHandler: MessageHandler = ConsoleMessageHandler(false)) {
  ...
}

这里面的Map都是Scala的Map,所以从Java里不能直接调用,而Scala的构造方法又比较特别,重载的话只能减少参数个数而不能改变类型。这时候就需要companion object上场了。

1
2
3
4
5
6
object DefaultTransformer {
  def apply(asnModule: AsnModule,
                 dependencies: java.util.Map[String, AsnModule],
                 repository: java.util.Map[String, AsnModule],
                 messageHandler: MessageHandler) = new DefaultTransformer(asnModule, dependencies, repository, messageHandler)
}

Java这样调用:

1
Transformer transformer = DefaultTransformer.apply(...);

3. Scala的bug

嗯,XML在什么语言里都是让人恶心的东西,即使Scala已经尽力做好了:https://issues.scala-lang.org/browse/SI-4865

单元测试拯救了架构设计?

经常听到这样一种说法:单元测试迫使你不得不进行仔细的架构设计。

这说法没错,如果没有好的架构设计,代码几乎是不能做单元测试的:到处都在new,静态方法随手调用,等等等等,如此这般。

但通常这样做的结果是引入了不必要的过度设计。本来new一个对象就用了,现在要做依赖注入,无论是用spring之类的基于xml的配置注入,还是guice那样的annotation注入,总之一但有了这些,代码几乎就变得不可读了,因为你从代码逻辑里你根本看不出某个对象是什么,是怎么构造出来的;为了弄明白一段代码究竟是怎么工作的,你需要找到做依赖注入的地方:xml或者annotation,或者随便什么。总之如果没有一个好的IDE帮你在各种不同的文件中穿梭,没有一个详细的设计文档,你需要相当长的时间来整明白一个前辈设计的优秀系统是如何被粘在一起工作的。

也许你同样有优于常人的头脑、敏锐的洞察力,可以在很短的时间内把东西看懂,然后继续开发,但并不是所有人都能做到这一点。这样问题就来了,一个所谓优秀的架构设计能维持多久?也许换了几拨人之后,最初的设计思想就完全没人能理解了,然后新人会在代码里恶狠狠地写下shit之类的注释,或者把看不懂的代码拷贝一份出来修修补补,或者经过一番痛苦的思想斗争之后完全重写,替换成他所熟悉的依赖注入框架。天呐,这是在做优秀的架构设计吗?不觉得是在自掘坟墓吗?

先别准备开口反驳,或者破口大骂,我一点儿反对依赖注入的意思都没有。我非常赞成在系统关键部位使用依赖注入,特别是那些模块接口的地方。这就像中医的针灸,针总是要扎在关键的位置,而不是插满全身,前者治病救人,后者纯粹就是解恨。

而选择依赖注入的方式,能用annotation就别用xml。推崇xml的人最喜欢说的一句话就是:用xml配置一下,可以修改系统行为而不用重新编译,特别是对于Java这样的静态语言。不错,xml是可以做到这一点,但是仔细回想一下,究竟有多少次我们这样干过?究竟有多少是真正的需求,而不是开发人员或者系统设计师一厢情愿的天真想法?注意,我们是在谈依赖注入的配置,而不是单纯的参数配置。必须弄明白的是,用作依赖注入的xml也是代码的一部分,也就是说,如果脱离了xml只看代码,你压根儿就读不懂。而且xml这玩意儿信息冗余得很,你得练就一双法眼,透过tag看本质。顺便说一句,Java里面的xml实现是我见过的最恶心的设计,简直可以作为Java语言使用的优秀典范,因为它可能用到了所有的Java语言特性。

同样的过度设计问题在AOP里也存在,不过这可能完全是我的个人喜好,可以任意批判。我根本无法理解为什么为了省掉一些异常处理的代码,而引入一堆一堆屎一样的xml配置。你可以说这是为了提炼出系统的本质,让人很容易看懂代码逻辑是什么而不用纠缠于异常情况,但是哥们儿,代码里50%以上都是在处理异常,异常根本就是逻辑的一部分,作为一个开发或者维护人员,我需要知道异常时候究竟干了什么,而不是忽略。如果需要业务人员也能很容易明白代码做了什么,我们需要的是DSL,而不是AOP。搞Java的人就喜欢弄出一个个看起来很酷的缩写,AOP,SOA,JEE,JSE,EJB,JCA,JAXP,JAXB,JMX,JMS……我已经晕了,不过也没什么,也就是一帮人躲在屋子里折腾出来的一些规范而已。

在需要依赖注入的地方用annotation的方式注入,在该简洁的地方毫不犹豫地直接调用,允许一定的代码冗余,把架构设计做得让中等水平的人可以很快看懂并且继续开发,让水平次点儿的人也能明白是怎么回事儿,这样的系统也许会具有更强的生命力。

但是一些耦合度很强的类怎样做单元测试呢?其实在Java世界里这个问题早就解决了,有一票儿的单元测试mock工具:JMock,Mockito,Powermock,等等。别再纠结于“做个依赖注入吧,否则怎么做单元测试”这种问题了,依赖注入不是为了解决如何做单元测试的,不能为了单元测试刻意做一些不必要的复杂设计,因为单元测试可能也就是芝麻而已。

JRuby和Java的互操作

我试图让新的BL框架支持所有流行的JVM语言。Scala很简单,天衣无缝,但是Ruby和Python就比较周折了。不谈Python,先看看Ruby,确切地说是JRuby。

jrubyc可以把一个Ruby的class编译成Java的,看看下面的例子:

Bar.java

1
2
3
4
5
package com.honnix.foo;
 
interface Bar {
    int getValue();
}

BarImpl.rb

1
2
3
4
5
6
7
8
9
10
11
java_package 'com.honnix.foo'
require 'java'
 
class BarImpl
  include Java::com.honnix.foo.Bar
 
  java_signature 'int getValue()'
    def get()
      1
    end
end

> javac Bar.java
> jrubyc --javac -cp com.honnix.foo BarImpl.rb

这样基本上就差不多了。

不过看看生产的Java代码,就知道这样玩儿的效率恐怕成问题。其实就是先build出来一个Ruby代码的巨大字符串,然后给交给Ruby引擎去跑,最后返回结果。

不管怎么说,只是这是一个方案,接下来就是要考虑怎么把Java的Document转成Ruby喜欢的格式,再转回来。

Java的范型方法

Java的范型一直让我头疼,也一直没仔细研究,总之感觉不好用。这两天折腾了一下,把范型方法整明白了。

一个简单的例子:

1
2
3
4
private static <T> T test(T a) {
    Person<T> person = new Person<T>(a);
    return person.getName();
}

调用的时候这样:

1
test(1);

是不是认为编译器编译的时候会自动将T转换成Integer?我们来看看javap的结果:

1
invokestatic	#22; //Method test:(Ljava/lang/Object;)Ljava/lang/Object;

看到什么了?没有Integer,只有Object!

如果这样设计test():

1
2
3
4
private static <T extends Number> T test(T a) {
    Person<T> person = new Person<T>(a);
    return person.getName();
}

结果就是:

1
invokestatic	#23; //Method test:(Ljava/lang/Number;)Ljava/lang/Number;

所以是不是可以得出结论,javac根本就不看调用的时候传入什么类型的东西,只看方法定义?

你完全可以这样写:

1
2
3
4
private static Object test(Object a) {
    Person<Object> person = new Person<Object>(a);
    return person.getName();
}

只是前者看上去比较牛逼罢了,其实效果完全相同。

maven-assembly-plugin处理依赖的时候感觉不大对头

按照maven推荐的方式组装binary:

  1. 建一个子module
  2. packaging类型为pom
  3. 依赖需要被组装的子module
  4. <useAllReactorProjects>true</useAllReactorProjects>

但是在写assemble文件的时候,有个诡异的地方:似乎moduleSet的dependencySet并不是选择的某个module所依赖的。

给自己建一个Nexus

我不是做CM的,但是家里有两台工作机器、一台二奶机,所以弄个Nexus还是有点意义的。

过程相当容易。

1. 这里下:http://nexus.sonatype.org/

2. 解包

3. bin/jsw/<arch>/nexus start(Windows底下不是这个,我不想写,自己研究吧)

4. http://<server>:8081/nexus

5. 在Repositories里面加自己需要的repository,注意三种类型:hosted、proxy、virtual;鼠标移动到问号上有提示,这里就不多说了

6. 修改$HOME/.m2/settings.xml

1
2
3
4
5
6
    <mirror>
      <id>id</id>
      <mirrorOf>*</mirrorOf>
      <name>xxx</name>
      <url>http://<server>:8081/nexus/content/groups/public/</url>
    </mirror>

这里把所有的repository都mirror到Nexus去。

7. 还有复杂的权限控制,我暂时用不到,以后需要再说吧。

Mac里面的JDK的源码和javadoc

在Apple还没完全转到OpenJDK之前,他自家的东西还是得用着。不过默认JDK源码和javadoc是不会装的。

到这里下:https://connect.apple.com/,右边找Dowloads->Java。

需要ADC,没有的话自己注册一个,也不花钱。

10.6 update 2之前是叫Developer Document,update 3之后就叫Developer Package了。找需要的下。装好之后在这里:/Library/Java/JavaVirtualMachines/<version>/Contents/Home。同样是src.jar和docs.jar,另外还有苹果自己的一个appledocs.jar。

How to Make Embedded Tomcat Support Hot Deploy

If you search “embedded tomcat” in google, you will get a bunch of instructions how to do this, but non of them mentioned how to enable hot deploy.

1
2
3
4
5
6
Embedded embedded = new Embedded();
 
// create engine, host, connector, context
...
 
embedded.start();

The above code will not enable hot deploy by default. So what we need to do is adding a LifeCycleListener to the host we create.

1
2
3
4
5
6
7
8
9
10
11
12
Embedded embedded = new Embedded();
 
...
 
StandardHost host = (StandardHost) embedded.createHost("localhost",
        getHome() + "/webapps");
host.setAutoDeploy(true);
host.setDeployOnStartup(true);
host.addLifecycleListener(new HostConfig());
 
...
embedded.start();

And the following things are the same as the stand alone Tomcat: define “.xml” and put war files or directory to “/webapps”.

One more thing about how to deploy the default web app:

  • use ROOT.xml, and ROOT.war
  • or use ROOT.xml and define “docBase” pointing to the war file or directory which should not be under “appBase”, and you can name the war or directory anything you like; if symbolic link is used and the original war file or directory get modified, your web app will also be redeployed

Java HotSpot JVM Memory Leak

频繁地调用AttachCurrentThread和DetachCurrentThread,在并发的情况下会有比较严重的Memory Leak,在一个CPU的机器上几率很小,但是多个或者多核的机器上很容易跑出来。

问题出在ParkEvent链表上(其实有问题的还有Park链表),对它的读写并没有全局加锁。具体可以参考源码:thread.cpp。

解决方法也很简单,调用者自己加把锁就行了。