写在前面的话
不知道是因为第一份工作的影响还是受在博客园上看到的那句“源代码里没有秘密”的影响,总之,近来对很多框架的源码都很感兴趣,拿到一个都想看看。其实自从学习Java以来也看过不少了,从刚开始接触的Tomcat,到Struts2,再到入职当前这份工作后看的Log4J,Commons Validator和SpringBatch,可惜除了Commons Validator的源码都看完了,其他的框架源码都半途而废了,并且Commons Validator看完后,也没有什么记录下来,所以基本上都可以忽略,其实当时也知道看完要有总结和记录才会有真正的收获,然而还是因为各种事情而没有做。所以这次看JUnit,发奋看完后一定要做总结,并记录。
对于JUnit,一直以为很简单,可以很快的看完,就当练练手,然而当我真正看完后,才发现JUnit其实并不简单,内部框架提供了很多特性,平时都没用过,而且也没见过,比如使用RunWith注解以指定特定的Runner、Rule、使用hamcrest框架的Assert.assertThat()方法等。其实JUnit提供的这些特性在一些特殊场合很有用处,然而对于单元测试,很多人却因为太忙或者因为嫌烦,维护麻烦等各种理由而尽量避免。我也是一样,虽然我一直承认单元测试非常重要,它不仅仅可以在前期影响设计、而且可以为后期的一些重构或者bug修复提供信心,但是要我真正的认真的为一个模块写单元测试,也感觉有点厌烦,再加上当前项目又是一个很少写单元测试的环境,对这方面坚持就松懈了。
后来,想系统的看一下项目的源码,可是代码量太多,也没有比较明确的模块分工,没有比较完善的单元测试,再加上当前项目主要是基于文件对数据的处理,没有看到数据流,只是看代码的话,感觉晕头转向的,所以就想着补一些单元测试以使自己可以更好的理解项目代码。问题是项目是跑在Linux Server上的,项目在开发的过程中没有完全考虑支持跨平台,同时有些操作内存消耗也很大,所以并不是所有的代码都可以在本地跑,等等,总之各种原因吧,我开始打算写一个可以在Linux下用跑测试代码的工具。然后悲剧的发现除了会使用eclipse中的JUnit,其实我对JUnit一无所知。所以有些时候工具虽然能提供我们方便,但是它也隐藏了内部细节,最后我们自以为已经很了解某些东西了,其实离开了工具,我们一无所知。
最后加个注释,这篇文章所有的代码是基于JUnit4.10的,今天我发现这个版本和之前的版本(JUnit4.4)的代码还是有比较大的差别的。本系列最后可能会涉及到一点和JUnit之前版本相关的信息,不过这个就要看有没有这个时间了。L
深入JUnit源码之Runner
Runner是JUnit的核心,它封装了一个测试类中所有的测试方法,如BlockJUnit4ClassRunner,或者是多个Runner,如Suite;在运行过程中,遍历并运行所有测试方法(可以通过Filter和Sorter控制是否要执行某些测试方法以及执行测试方法的顺序)。
在使用JUnit时,可能最常用的几个注解就是@BeforeClass、@AfterClass、@Before、@After、@Test、@Ignore了,这几个注解所表达的意思相信很多人都很熟悉了,并且从它们的名字中也可以略知一二。这几个注解,除了@Test标记了哪个方法为测试方法,该方法必须为public,无返回,不带参;@Ignore标明某个方法即使有@Test的注解,也会忽略不运行,如JUnit文档中解释,在某些情况下,我们可能想临时的不想执行某些测试方法,除了将该测试方法整个注释掉,JUnit为我们提供了@Ignore注解,此时即使某方法包含@Test注解,该方法也不会作为测试方法执行,@Ignore还可以注解在类上,当一个类存在@Ignore注解时,该类所有的方法都不会被认为是测试方法;而剩下的四个注解则是JUnit为在测试类运行时的不同切面提供了切入点,如@BeforeClass和@AfterClass注解分别在测试类运行时前后各提供了一个切入点,这两个注解必须使用在public,静态,无返回,不带参的方法中,可以为每种注解指定多个方法,多个方法的执行顺序依赖与Java的反射机制,因而对一种注解的多个方法,在实际中不应该存在顺序依赖,为一种注解写多个方法的情况应该很少;而@Before和@After注解则是在每个测试方法的运行前后各提供了一个切入点,这两个注解必须使用在public,无返回,不带参的方法中。同@BeforeClass和@AfterClass,同一种注解可以注释多个方法,他们的执行顺序也依赖于反射机制,因而不能对顺序有依赖。更直观的,如下图所示。
加点不完全相关的,这事实上是一种AOP思想的实现。自从知道AOP后,一直很喜欢这个想法,它事实上也是一种分层的思想。一个request从一个管道流进,经过层层处理后从另一个管道流出,在管道的流动过程中,每一层都对自己实现的功能做一些处理,如安全验证、运行时间记录、进入管道后打开某个链接出去之前关闭该链接、异常记录等等相对独立的功能都可以抽取出来到一个层中,从而在实际编码业务逻辑过程中可以专注于业务,而不用管这些不怎么相关但有必须有的逻辑,不仅是代码的模块分层更加清晰,减轻程序员的负担,还提高了程序的安全性,因为这样就可以部分避免有些事情必须要做容易忘了尴尬。AOP的思想最出名的应该是Spring中提供的支持了,但是我个人更喜欢Struts2通过Interceptor提供的AOP实现,这是题外话。
一个简单的测试例子
为了更加清晰的了解JUnit的一些行为,我们先来看一下如下的一个测试例子:
2 @BeforeClass
3 public static void beforeClass() {
4 System.out.println("beforeClass() method executed.");
5 System.out.println();
6 }
7 @BeforeClass
8 public static void beforeClass2() {
9 System.out.println("beforeClass2() method executed.");
10 System.out.println();
11 }
12 @AfterClass
13 public static void afterClass() {
14 System.out.println("afterClass() method executed.");
15 System.out.println();
16 }
17 @Before
18 public void before() {
19 System.out.println("before() method executed.");
20 }
21 @After
22 public void after() {
23 System.out.println("after() method executed");
24 }
25 @Test
26 public void testSucceeded() {
27 System.out.println("testSucceeded() method executed.");
28 }
29 @Test
30 @Ignore
31 public void testIgnore() {
32 System.out.println("testIgnore() method executed.");
33 }
34 @Test
35 public void testFailed() {
36 System.out.println("testFailed() method executed.");
37 throw new RuntimeException("Throw delibrately ");
38 }
39 @Test
40 public void testAssumptionFailed() {
41 System.out.println("testAssumptionFailed() method executed.");
42 Assume.assumeThat(0, Is.is(1));
43 }
44 @Test
45 public void testFilteredOut() {
46 System.out.println("testFilteredOut() method executed.");
47 }
48 }
2 private final Set<String> excludedMethods = new HashSet<String>();
3 public MethodNameFilter(String excludedMethods) {
4 for(String method : excludedMethods) {
5 this.excludedMethods.add(method);
6 }
7 }
8 @Override
9 public boolean shouldRun(Description description) {
10 String methodName = description.getMethodName();
11 if(excludedMethods.contains(methodName)) {
12 return false;
13 }
14 return true;
15 }
16 @Override
17 public String describe() {
18 return this.getClass().getSimpleName() + "-excluded methods: " +
19 excludedMethods;
20 }
21 }
22 public class AlphabetComparator implements Comparator<Description> {
23 @Override
24 public int compare(Description desc1, Description desc2) {
25 return desc1.getMethodName().compareTo(desc2.getMethodName());
26 }
27 }
2 public static void main(String[] args) {
3 RunNotifier notifier = new RunNotifier();
4 Result result = new Result();
5 notifier.addFirstListener(result.createListener());
6 notifier.addListener( new LogRunListener());
7
8 Runner runner = null ;
9 try {
10 runner = new BlockJUnit4ClassRunner(CoreJUnit4SampleTest. class );
11 try {
12 ((BlockJUnit4ClassRunner)runner).filter( new MethodNameFilter( " testFilteredOut " ));
13 } catch (NoTestsRemainException e) {
14 System.out.println( " All methods are been filtered out " );
15 return ;
16 }
17 ((BlockJUnit4ClassRunner)runner).sort( new Sorter( new AlphabetComparator()));
18 } catch (Throwable e) {
19 runner = new ErrorReportingRunner(CoreJUnit4SampleTest. class , e);
20 }
21 notifier.fireTestRunStarted(runner.getDescription());
22 runner.run(notifier);
23 notifier.fireTestRunFinished(result);
24 }
25 }
2 public void testRunStarted(Description description) throws Exception {
4 println("==>JUnit4 started with description: \n" + description);
5 println();
6 }
7 public void testRunFinished(Result result) throws Exception {
8 println("==>JUnit4 finished with result: \n" + describe(result));
9 }
10 public void testStarted(Description description) throws Exception{
11 println("==>Test method started with description: " + description);
13 }
14 public void testFinished(Description description) throws Exception {
16 println("==>Test method finished with description: " + description);
18 println();
19 }
20 public void testFailure(Failure failure) throws Exception {
21 println("==>Test method failed with failure: " + failure);
22 }
23 public void testAssumptionFailure(Failure failure) {
24 println("==>Test method assumption failed with failure: " + failure);
26 }
27 public void testIgnored(Description description) throws Exception {
28 println("==>Test method ignored with description: " + description);
30 println();
31 }
32 private String describe(Result result) {
33 StringBuilder builder = new StringBuilder();
34 builder.append("\tFailureCount: " + result.getFailureCount())
35 .append("\n");
36 builder.append("\tIgnoreCount: " + result.getIgnoreCount())
37 .append("\n");
38 builder.append("\tRunCount: " + result.getRunCount())
39 .append("\n");;
40 builder.append("\tRunTime: " + result.getRunTime())
41 .append("\n");
42 builder.append("\tFailures: " + result.getFailures())
43 .append("\n");;
44 return builder.toString();
45 }
46 private void println() {
47 System.out.println();
48 }
49 private void println(String content) {
50 System.out.println(content);
51 }
52 }
levin.blog.junit.sample.simple.CoreJUnit4SampleTest
beforeClass2() method executed.
beforeClass() method executed.
==>Test method started with description: testAssumptionFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
before() method executed.
testAssumptionFailed() method executed.
after() method executed
==>Test method assumption failed with failure: testAssumptionFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest): got: <0>, expected: is <1>
==>Test method finished with description: testAssumptionFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
==>Test method started with description: testFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
before() method executed.
testFailed() method executed.
after() method executed
==>Test method failed with failure: testFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest): Throw delibrately
==>Test method finished with description: testFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
==>Test method ignored with description: testIgnore(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
==>Test method started with description: testSucceeded(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
before() method executed.
testSucceeded() method executed.
after() method executed
==>Test method finished with description: testSucceeded(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
afterClass() method executed.
==>JUnit4 finished with result:
FailureCount: 1
IgnoreCount: 1
RunCount: 3
RunTime: 36
Failures: [testFailed(levin.blog.junit.sample.simple.CoreJUnit4SampleTest): Throw delibrately ]
深入Runner和其周边支持类的源码
从上面这个例子中,我们已经知道JUnit的核心功能以及不同注解方法执行的顺序问题,然而既然本文是关注内部源码的,因而接下来就要讨论如何实现上述的这些功能。
所谓面向对象中的类即是对某些事物或行为进行抽象和封装,从而实现某些功能,一个类可以看成是一个模块,它一般包含数据和行为,并提供给外界一定的接口,多个类之间通过各自的接口与外界交互,从而形成一个大的系统,有点类似分治的算法。在分治算法中最重要的是找到正确的方法以将问题划分成各个区间,并最后使用一定的规则将各个区间解决的问题连结在一起,以解决整个系统的问题。在面向对象中同样要找到一个正确的方法将系统的问题划分成各个小模块,用类来封装,并定义类的接口以使各个类之间可以交互连结以形成一个大的系统。在实现中,我们也经常遇到某些事物是不同的,但是他们有一些共同的属性或行为,在面向对象中对这个情况的处理是将相同的属性或行为抽象成一个父类,而由各自的子类继承父类提供各自不同的属性和行为。然而有些时候,某些事物他们都具有某种行为,但是这些行为的结果却是不同的,对这种情况,面向对象则采用多态的方式支持这种需求,即在父类中定义行为,在子类中重写行为。在面向对象设计中,如何设计系统的类结构,包括类的继承结构、类之间的交互接口等问题了是面向对象设计的核心问题。
看完类结构图,那么我们再来看一下源码吧。由于Description在JUnit中应用广泛,又是相对独立于功能的,因而将从Description开始:
如上文所说,Description是JUnit中对Runner和测试方法的描述,它包含三个字段:
2 private final String fDisplayName;
3 private final Annotation[] fAnnotations;
displayName对测试方法的格式为:<methodName>(<className>),如:
对Runner来说,displayName一般为Runner所封装的测试类,然而对没有根类的Suite,该值为”null”。annotations字段为测试方法或测试类上所具有的所有注解类。children对测试方法来说为空,对Runner来说,表达Runner内部所有的测试方法的Description或Runner的Description。作为JUnit的用户,除非自定义Runner,其他的,我们一般都是通过注册自己的RunListener来实现自己想要的统计和信息提示工作,而在Listener中并没有直接暴露给我们Runner或者是测试类的实例,它是通过提供Description实例的方式来获取我们需要的信息。Description提供以下的接口供我们使用:
2 public ArrayList < Description > getChildren();
3 public boolean isSuite();
4 public boolean isTest();
5 public int testCount();
6 public < T extends Annotation > T getAnnotation(Class < T > annotationType);
7 public Collection < Annotation > getAnnotations();
8 public Class <?> getTestClass();
9 public String getClassName();
10 public String getMethodName();
Runner实现了Disacribable接口,该接口只包含一个方法:
2 public abstract Description getDescription();
3 }
该方法在ParentRunner中创建一个Description,并遍历当前Runner下的Children,并将这些child的Description添加到Description中最后返回:
2 public Description getDescription() {
3 Description description = Description.createSuiteDescription(
4 getName(), getRunnerAnnotations());
5 for (T child : getFilteredChildren())
6 description.addChild(describeChild(child));
7 return description;
8 }
9 BlockJUnit4ClassRunner:
10 @Override
11 protected Description describeChild(FrameworkMethod method) {
12 return Description.createTestDescription(
13 getTestClass().getJavaClass(),
14 testName(method), method.getAnnotations());
15 }
16 Suite:
17 @Override
18 protected Description describeChild(Runner child) {
19 return child.getDescription();
20 }
21
RunNotifier类和RunListener类对测试方法运行事件的发布
RunListener是JUnit提供的自定义对测试运行方法统计的接口,JUnit用户可以继承RunListener类,在JUnit运行开始、结束以及每一个测试方法的运行开始、结束、测试失败以及假设出错等情况下加入一些自己的逻辑,如统计整个JUnit运行的时间(这个在Result中已经实现了)、每个运行方法的时间、运行最后有多少方法成功,多少失败等,如上例中的LogRunListener。RunListener定义了如下接口:
2 public void testRunStarted(Description description) throws Exception {
4 }
5 public void testRunFinished(Result result) throws Exception {
6 }
7 public void testStarted(Description description) throws Exception {
8 }
9 public void testFinished(Description description) throws Exception {
11 }
12 public void testFailure(Failure failure) throws Exception {
13 }
14 public void testAssumptionFailure(Failure failure) {
15 }
16 public void testIgnored(Description description) throws Exception {
17 }
18 }
这个类需要注意的是:1. testFinished()不管测试方法是成功还是失败,这个方法总是会被调用;2. 在测试方法中跑出AssumptionViolatedException并不认为是测试失败,一般在测试方法中调用Assume类中的方法而失败,会跑该异常,在JUnit的默认实现中,对这些方法只是简单的忽略,并发布testAssumptionFailure()事件,并不认为该方法测试失败,自定义的Runner可以改变这个行为;3. 当我们需要自己操作Runner实例是,Result的信息需要自己手动的注册Result中定义的Listener,不然Result中的信息并不会填写正确,如上例中的做法:
2 notifier.addFirstListener(result.createListener());
在实现时,所有事件响应函数提供给我们有三种信息:Description、Result和Failure。其中Description已经在上一小节中介绍过了它所具有的信息,这里不再重复。对于Result,前段提到过只有它提供的Listener后才会取到正确的信息,它包含的信息有:总共执行的测试方法数、忽略的测试方法数、以及所有在测试过程中抛出的异常列表、整个测试过程的执行时间等,Result实例在JUnit运行结束时传入testRunFinished()事件方法中,以帮助我们做一些测试方法执行结果的统计信息,事实上,我感觉这些信息很多时候还是不够的,需要我们在自己的RunListener中自己做一些信息记录。Failure类则记录了测试方法在测试失败时抛出的异常以及该测试方法对应的Description实例,当在构建Runner过程中出现异常,Failure也会用于描述测试类,如上例中,如果beforeClass()方法不是静态的话,在初始化Runner时就会出错,此时的运行结果如下:
levin.blog.junit.sample.simple.CoreJUnit4SampleTest
==> Test method started with description: initializationError(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
==> Test method failed with failure: initializationError(levin.blog.junit.sample.simple.CoreJUnit4SampleTest): Method beforeClass() should be static
==> Test method finished with description: initializationError(levin.blog.junit.sample.simple.CoreJUnit4SampleTest)
==> JUnit4 finished with result:
FailureCount: 1
IgnoreCount: 0
RunCount: 1
RunTime: 3
Failures: [initializationError(levin.blog.junit.sample.simple.CoreJUnit4SampleTest): Method beforeClass() should be static ]
Runner中的run()方法需要传入RunNotifier实例,它是对RunListener的封装,我们可以向其注册多个RunListener,当Runner需要发布某个事件时,就会通过它来代理,它则会遍历所有已注册的RunListener,并运行相应的事件。当运行某个RunListener抛异常时,它会首先将这个RunListener移除,并发布测试失败事件,在该事件中的Description为一个名为“Test mechanism”的Description,因为此时是注册的事件处理器失败,无法获知一个Description实例:
2 void run() {
3 synchronized (fListeners) {
4 for (Iterator < RunListener > all = fListeners.iterator();
5 all.hasNext();)
6 try {
7 notifyListener(all.next());
8 } catch (Exception e) {
9 all.remove(); // Remove the offending listener first to avoid an infinite loop
11 fireTestFailure( new Failure(
12 Description.TEST_MECHANISM, e));
13 }
14 }
15 }
16 abstract protected void notifyListener(RunListener each) throws Exception;
18 }
在RunNotifier类中还有一个pleaseStop()方法从而可以在测试中途停止整个测试过程,其实现时在发布testStarted事件时,如果发现pleaseStop字段已经为true,则抛出StoppedByUserException,当Runner接收到该异常后,将该异常直接抛出。
事实上,ParentRunner在发布事件时,并不直接和RunNotifier打交道,而是会用EachTestNotifier类对其进行封装,该类只是对MultipleFailureException做了处理,其他只是一个简单的代理。在遇到MultipleFailureException,它会遍历内部每个Exception,并对每个Exception发布testFailure事件。当在运行@After注解的方法时抛出多个异常类(比如测试方法已经抛出异常了,@After注解方法中又抛出异常或者存在多个@After注解方法,并多个@After注解方法抛出了多个异常),此时就会构造一个MultipleFailureException,这种设计可能是出于对防止测试方法中的Exception被@After注解方法中的Exception覆盖的问题引入的。
FrameworkMethod与FrameworkField类
FrameworkMethod是对Java反射中Method的封装,它提供了对方法的验证、调用以及处理子类方法隐藏父类方法问题,其主要提供的接口如下:
2 public Object invokeExplosively( final Object target, final Object params);
3 public String getName();
4 public void validatePublicVoidNoArg( boolean isStatic, List < Throwable > errors);
5 public void validatePublicVoid( boolean isStatic, List < Throwable > errors)
6 public void validateNoTypeParametersOnArgs(List < Throwable > errors);
7 @Override
8 public boolean isShadowedBy(FrameworkMethod other);
9 @Override
10 public Annotation[] getAnnotations();
11 public < T extends Annotation > T getAnnotation(Class < T > annotationType);
其中isShadowedBy()方法用于处理子类方法隐藏父类方法的问题,JUnit支持测试类存在继承关系,并且会遍历所有父类的测试方法,但是如果子类的方法隐藏了父类的方法,则父类的方法不会被执行。这里的隐藏是指当子类的方法名、所有参数类型相同时,不管是静态方法还是非静态方法,都会被隐藏。其实现如下:
2 if ( ! other.getName().equals(getName()))
3 return false ;
4 if (other.getParameterTypes().length != getParameterTypes().length)
5 return false ;
6 for ( int i = 0 ; i < other.getParameterTypes().length; i ++ )
7 if ( ! other.getParameterTypes()[i].equals( getParameterTypes()[i]))
9 return false ;
10 return true ;
11 }
类似FrameworkMethod,FrameworkField是对Java反射中的Field的封装,它提供了对字段取值、处理隐藏父类字段的问题,其主要提供的接口如下:
2 @Override
3 public Annotation[] getAnnotations();
4 public boolean isPublic();
5 @Override
6 public boolean isShadowedBy(FrameworkField otherMember) {
7 return otherMember.getName().equals(getName());
8 }
9 public boolean isStatic();
10 public Field getField();
11 public Class <?> getType();
12 public Object get(Object target)
13 throws IllegalArgumentException, IllegalAccessException;
在处理隐藏问题是,它只是判断如果父类中某个字段的名和子类中某个字段的名字相同,则父类中的字段不会被JUnit处理,即被隐藏。事实上,关于隐藏的问题,只是对JUnit识别的方法和字段有效,即有JUnit相关注解的方法字段,其他方法并不受影响(事实上也是不可能受到影响)。
TestClass类
TestClass 是对 Java 中 Class 类的封装,在 TestClass 构造过程中,它会收集所有传入 Class 实例中具有注释的字段和方法,它会搜索类的所有继承结构。因而 TestClass 的成员如下:2 private Map < Class <?> , List < FrameworkMethod >> fMethodsForAnnotations;
3 private Map < Class <?> , List < FrameworkField >> fFieldsForAnnotations;
2 members.add( 0 , member);
3 else
4 members.add(member);
在获得测试类中所有注解的方法和字段的Map后,TestClass提供了对Annotation对应的方法和字段的查询接口:
2 Class <? extends Annotation > annotationClass);
3 public List < FrameworkField > getAnnotatedFields(
4 Class <? extends Annotation > annotationClass);
5 public < T > List < T > getAnnotatedFieldValues(Object test,
6 Class <? extends Annotation > annotationClass,
7 Class < T > valueClass);
2 public String getName();
3 public Constructor <?> getOnlyConstructor();
4 public Annotation[] getAnnotations();
5 public boolean isANonStaticInnerClass();
Runner、ParentRunner与BlockJUnit4ClassRunner类
Runner是JUnit中对所有Runner的抽象,它只包括三个方法:
2 public abstract Description getDescription();
3 public abstract void run(RunNotifier notifier);
4 public int testCount() {
5 return getDescription().testCount();
6 }
7 }
其中getDescription()的实现已经在Description相关的小节中做过计算了,不在重复,testCount()方法很简单,也已经实现了,因而本节主要介绍run()方法的运行过程,而该方法的实现则是在ParentRunner中。
用ParentRunner命名这个Runner是想表达这是一个在测试方法树中具有子节点的节点,它的子节点可以是测试方法(BlockJUnit4ClassRunner)或者Runner(Suite)。ParentRunner以测试类Class实例作为构造函数的参数,在构造函数内部用TestClass对测试类进行封装,并对一些那些规定的方法做验证:
1. @BeforeClass和@AfterClass注解的方法必须是public,void,static,无参
2 validatePublicVoidNoArgMethods(AfterClass. class , true, errors);
2. 对有@ClassRule修饰的字段,必须是public,static,并且该字段的实例必须是实现了TestRule接口的。为了兼容性,也可以实现MethodRule接口。
在验证过程中,ParentRunner通过一个List<Throwable>收集所有的验证错误信息,如果存在错误信息,则验证不通过,ParentRunner将抛出一个InitializationError的Exception,该Exception中包含了所有的错误信息,一般在这种情况下,会创建一个ErrorReportingRunner返回以处理验证出错的问题,该Runner将在下面的小节中详细介绍。
在Runner构造完成后,就可以调用run()方法运行该Runner了:
2 public void run( final RunNotifier notifier) {
3 EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription());
5 try {
6 Statement statement = classBlock(notifier);
7 statement.evaluate();
8 } catch (AssumptionViolatedException e) {
9 testNotifier.fireTestIgnored();
10 } catch (StoppedByUserException e) {
11 throw e;
12 } catch (Throwable e) {
13 testNotifier.addFailure(e);
14 }
15 }
该方法的核心是构造一个Statement实例,然后调用该实例的evaluate()方法。在JUnit中,Statement是一个类链表,一个Runner的执行过程就是这个Statement类链表的执行过程。ParentRunner对Statement类链表的构造主要是对测试类级别的构造,如@BeforeClass、@AfterClass、@ClassRule等执行Statement:
2 Statement statement = childrenInvoker(notifier);
3 statement = withBeforeClasses(statement);
4 statement = withAfterClasses(statement);
5 statement = withClassRules(statement);
6 return statement;
7 }
8 protected Statement childrenInvoker( final RunNotifier notifier) {
9 return new Statement() {
10 @Override
11 public void evaluate() {
12 runChildren(notifier);
13 }
14 };
15 }
16 private void runChildren( final RunNotifier notifier) {
17 for ( final T each : getFilteredChildren())
18 fScheduler.schedule( new Runnable() {
19 public void run() {
20 ParentRunner. this .runChild(each, notifier);
21 }
22 });
23 fScheduler.finished();
24 }
25 private List < T > getFilteredChildren() {
26 if (fFilteredChildren == null )
27 fFilteredChildren = new ArrayList < T > (getChildren());
28 return fFilteredChildren;
29 }
30 protected abstract List < T > getChildren();
31 protected abstract void runChild(T child, RunNotifier notifier);
其中getChildren()、runChild()方法由子类实现。
2 List < FrameworkMethod > befores = fTestClass
3 .getAnnotatedMethods(BeforeClass. class );
4 return befores.isEmpty() ? statement :
5 new RunBefores(statement, befores, null );
6 }
7 protected Statement withAfterClasses(Statement statement) {
8 List < FrameworkMethod > afters = fTestClass
9 .getAnnotatedMethods(AfterClass. class );
10 return afters.isEmpty() ? statement :
11 new RunAfters(statement, afters, null );
12 }
13 private Statement withClassRules(Statement statement) {
14 List < TestRule > classRules = classRules();
15 return classRules.isEmpty() ? statement :
16 new RunRules(statement, classRules, getDescription());
17 }
18 protected List < TestRule > classRules() {
19 return fTestClass.getAnnotatedFieldValues( null ,
20 ClassRule. class , TestRule. class );
21 }
关于Statement将在下一节中详细讲,不过这里从Statement的构造过程可以看出Rule的执行要先于@BeforeClass注解方法或晚于@AfterClass注解方法。
在ParentRunner关于执行方法的实现中,还有一个对每个测试方法Statement链的执行框架的实现,其主要功能是加入事件发布和对异常的处理逻辑,从这里的Statement实例是一个测试方法的运行整体,它包括了@Before注解方法、@After注解方法以及@Rule注解字段的TestRule实例的运行过程,因而testStarted事件的发布是在@Before方法运行之前,而testFinished事件的发布是在@After方法运行之后:
3 EachTestNotifier eachNotifier = new EachTestNotifier( notifier, description);
5 eachNotifier.fireTestStarted();
6 try {
7 statement.evaluate();
8 } catch (AssumptionViolatedException e) {
9 eachNotifier.addFailedAssumption(e);
10 } catch (Throwable e) {
11 eachNotifier.addFailure(e);
12 } finally {
13 eachNotifier.fireTestFinished();
14 }
15 }
ParentRunner还实现Filterable接口和Sortable接口,不过这里很奇怪的实现时为什么sorter要保存成字段,感觉这个完全可以通过方法参数实现,而filteredChildren字段的实现方式在多线程环境中运行的话貌似也会出问题。Filter类主要提供一个shouldRun()接口以判断传入的Description是否可以运行,若否,则将该Description从ParentRunner中移除;否则,将该Filter应用到该child中,以处理child可能是Filterable实例(ParentRunner)的问题,从而以递归、先根遍历的方式遍历测试实例树上的所有节点,若一个父节点的所有子节点都被过滤了,则抛出NoTestRemainException:
2 for (Iterator < T > iter = getFilteredChildren().iterator();
3 iter.hasNext(); ) {
4 T each = iter.next();
5 if (shouldRun(filter, each))
6 try {
7 filter.apply(each);
8 } catch (NoTestsRemainException e) {
9 iter.remove();
10 }
11 else
12 iter.remove();
13 }
14 if (getFilteredChildren().isEmpty()) {
15 throw new NoTestsRemainException();
16 }
17 }
18 private boolean shouldRun(Filter filter, T each) {
19 return filter.shouldRun(describeChild(each));
20 }
JUnit中提供几种默认实现的Filter:1. ALL不做任何过滤;2. matchMethodDescription只保留某个指定的测试方法。
2 @Override
3 public boolean shouldRun(Description description) {
4 return true ;
5 }
6 @Override
7 public String describe() {
8 return " all tests " ;
9 }
10 @Override
11 public void apply(Object child) throws NoTestsRemainException {
12 // 因为不会做任何过滤行为,因而不需要应用到子节点中
13 }
14 @Override
15 public Filter intersect(Filter second) {
16 return second; // 因为本身没有任何过滤行为,所以可以直接返回传入的Filter
17 }
18 };
19 public static Filter matchMethodDescription( final Description desiredDescription) {
21 return new Filter() {
22 @Override
23 public boolean shouldRun(Description description) {
24 if (description.isTest())
25 return desiredDescription.equals(description);
26 // explicitly check if any children want to run
27 for (Description each : description.getChildren())
28 if (shouldRun(each))
29 return true ;
30 return false ;
31 }
32 @Override
33 public String describe() {
34 return String.format( " Method %s " , desiredDescription.getDisplayName());
36 }
37 };
38 }
Sorter类实现Comparator接口,ParentRunner通过其compare()方法构造和ParentRunner子节点相关的Comparator实例,并采用后根递归遍历的方式对测试方法树中的所有测试方法进行排序,因而这里只会排序同一个节点下的所有子节点,而节点之间的顺序不会受影响。
2 fSorter = sorter;
3 for (T each : getFilteredChildren())
4 sortChild(each);
5 Collections.sort(getFilteredChildren(), comparator());
6 }
7 private Comparator <? super T > comparator() {
8 return new Comparator < T > () {
9 public int compare(T o1, T o2) {
10 return fSorter.compare(describeChild(o1), describeChild(o2));
12 }
13 };
14 }
最后ParentRunner还提供了一个RunnerScheduler的接口字段,以控制JUnit测试方法的执行过程,我们可以注入一个使用多线程方式运行每个测试方法的RunnerScheduler,不过从代码上看,貌似JUnit对多线程的支持并不好,所以这个接口的扩展目前来看我还找不到什么用途:
2 void schedule(Runnable childStatement);
3 void finished();
4 }
BlockJUnit4ClassRunner节点下所有的子节点都是测试方法,它是JUnit中运行测试方法的核心Runner,它实现了ParentRunner中没有实现的几个方法:
2 protected List < FrameworkMethod > getChildren() {
3 return computeTestMethods();
4 }
5 protected List < FrameworkMethod > computeTestMethods() {
6 return getTestClass().getAnnotatedMethods(Test. class );
7 }
8 @Override
9 protected void runChild( final FrameworkMethod method, RunNotifier notifier) {
11 Description description = describeChild(method);
12 if (method.getAnnotation(Ignore. class ) != null ) {
13 notifier.fireTestIgnored(description);
14 } else {
15 runLeaf(methodBlock(method), description, notifier);
16 }
17 }
18 protected Statement methodBlock(FrameworkMethod method) {
19 Object test;
20 try {
21 test = new ReflectiveCallable() {
22 @Override
23 protected Object runReflectiveCall() throws Throwable {
24 return createTest();
25 }
26 }.run();
27 } catch (Throwable e) {
28 return new Fail(e);
29 }
30 Statement statement = methodInvoker(method, test);
31 statement = possiblyExpectingExceptions(method, test, statement);
32 statement = withPotentialTimeout(method, test, statement);
33 statement = withBefores(method, test, statement);
34 statement = withAfters(method, test, statement);
35 statement = withRules(method, test, statement);
36 return statement;
37 } // 这个方法的实现可以看出每个测试方法运行时都会重新创建一个新的测试类实例,这也可能是@BeforeClass、AfterClass、@ClassRule需要静态的原因吧,因为静态的话,每次类实例的重新创建对其结果都不会有影响。
38 另,从这里对Statement的构建顺序,JUnit对TestRule的运行也要在@Before注解方法之前或@After注解方法之后
39 protected Object createTest() throws Exception {
40 return getTestClass().getOnlyConstructor().newInstance();
41 }
42 protected Statement methodInvoker(FrameworkMethod method, Object test) {
43 return new InvokeMethod(method, test);
44 }
45 protected Statement possiblyExpectingExceptions(FrameworkMethod method, object test, Statement next) {
47 Test annotation = method.getAnnotation(Test. class );
48 return expectsException(annotation) ? new ExpectException(next,
49 getExpectedException(annotation)) : next;
50 }
51 private Class <? extends Throwable > getExpectedException(Test annotation) {
52 if (annotation == null || annotation.expected() == None. class )
53 return null ;
54 else
55 return annotation.expected();
56 }
57 private boolean expectsException(Test annotation) {
58 return getExpectedException(annotation) != null ;
59 }
60 protected Statement withPotentialTimeout(FrameworkMethod method,
61 Object test, Statement next) {
62 long timeout = getTimeout(method.getAnnotation(Test. class ));
63 return timeout > 0 ? new FailOnTimeout(next, timeout) : next;
64 }
65 private long getTimeout(Test annotation) {
66 if (annotation == null )
67 return 0 ;
68 return annotation.timeout();
69 }
70 protected Statement withBefores(FrameworkMethod method, Object target,
71 Statement statement) {
72 List < FrameworkMethod > befores = getTestClass().getAnnotatedMethods(
73 Before. class );
74 return befores.isEmpty() ? statement : new RunBefores(statement,
75 befores, target);
76 }
77 protected Statement withAfters(FrameworkMethod method, Object target,
78 Statement statement) {
79 List < FrameworkMethod > afters = getTestClass().getAnnotatedMethods(
80 After. class );
81 return afters.isEmpty() ? statement : new RunAfters(statement,
82 afters, target);
83 }
84 private Statement withRules(FrameworkMethod method, Object target,
85 Statement statement) {
86 Statement result = statement;
87 result = withMethodRules(method, target, result);
88 result = withTestRules(method, target, result);
89 return result;
90 }
91 private Statement withMethodRules(FrameworkMethod method, Object target,
92 Statement result) {
93 List < TestRule > testRules = getTestRules(target);
94 for (org.junit.rules.MethodRule each : getMethodRules(target))
95 if ( ! testRules.contains(each))
96 result = each.apply(result, method, target);
97 return result;
98 }
99 private List < org.junit.rules.MethodRule > getMethodRules(Object target) {
100 return rules(target);
101 }
102 protected List < org.junit.rules.MethodRule > rules(Object target) {
103 return getTestClass().getAnnotatedFieldValues(target, Rule. class ,
104 org.junit.rules.MethodRule. class );
105 }
106 private Statement withTestRules(FrameworkMethod method, Object target,
107 Statement statement) {
108 List < TestRule > testRules = getTestRules(target);
109 return testRules.isEmpty() ? statement :
110 new RunRules(statement, testRules, describeChild(method));
111 }
112 protected List < TestRule > getTestRules(Object target) {
113 return getTestClass().getAnnotatedFieldValues(target,
114 Rule. class , TestRule. class );
115 }
在验证方面,BlockJUnit4ClassRunner也加入了一些和自己相关的验证:
2 protected void collectInitializationErrors(List < Throwable > errors) {
3 super .collectInitializationErrors(errors);
4 validateNoNonStaticInnerClass(errors);
5 validateConstructor(errors);
6 validateInstanceMethods(errors);
7 validateFields(errors);
8 }
1. 如果测试类是一个类的内部类,那么该测试类必须是静态的:
2 if (getTestClass().isANonStaticInnerClass()) {
3 String gripe = " The inner class " + getTestClass().getName()
4 + " is not static. " ;
5 errors.add( new Exception(gripe));
6 }
7 }
2. 测试类的构造函数必须有且仅有一个无参的构造函数(事实上关于只有一个构造函数的验证在构造TestClass实例的时候已经做了,因而这里真正起作用的知识对无参的验证):
2 validateOnlyOneConstructor(errors);
3 validateZeroArgConstructor(errors);
4 }
5 protected void validateOnlyOneConstructor(List < Throwable > errors) {
6 if ( ! hasOneConstructor()) {
7 String gripe = " Test class should have exactly one public constructor " ;
8 errors.add( new Exception(gripe));
9 }
10 }
11 protected void validateZeroArgConstructor(List < Throwable > errors) {
12 if ( ! getTestClass().isANonStaticInnerClass()
13 && hasOneConstructor()
14 && (getTestClass().getOnlyConstructor(). getParameterTypes().length != 0 )) {
16 String gripe = " Test class should have exactly one public zero-argument constructor " ;
17 errors.add( new Exception(gripe));
18 }
19 }
20 private boolean hasOneConstructor() {
21 return getTestClass().getJavaClass().getConstructors().length == 1 ;
22 }
3. @Before、@After、@Test注解的方法必须是public,void,非静态,不带参数:
2 validatePublicVoidNoArgMethods(After. class , false , errors);
3 validatePublicVoidNoArgMethods(Before. class , false , errors);
4 validateTestMethods(errors);
5 if (computeTestMethods().size() == 0 )
6 errors.add( new Exception( " No runnable methods " ));
7 }
8 protected void validateTestMethods(List < Throwable > errors) {
9 validatePublicVoidNoArgMethods(Test. class , false , errors);
10 }
4. 带有@Rule注解的字段必须是public,非静态,实现了TestRule接口或MethodRule接口。
2 RULE_VALIDATOR.validate(getTestClass(), errors);
3 }
JUnit核心执行的序列图
写了好几天,终于把JUnit核心的执行所有代码分析完了,不过发现写的那么细,很多东西其实很难表达,因而贴了很多代码,感觉写的挺乱的,所以画一张序列图吧,感觉很多时候还是图的表达效果更好一些,要我看那么一大段的问题,也感觉挺烦的。
Suite与Parameterized
Suite是其子节点是Runner的Runner,其内部保存了一个Runner的List。Suite的功能实现很简单,因为它将大部分的方法代理给了其内部Runner实例:
2 @Override
3 protected List < Runner > getChildren() {
4 return fRunners;
5 }
6 @Override
7 protected Description describeChild(Runner child) {
8 return child.getDescription();
9 }
10 @Override
11 protected void runChild(Runner runner, final RunNotifier notifier) {
12 runner.run(notifier);
13 }
Suite重点在于如何构建一个Suite,Suite提供两个构造函数:
1. 提供一个带SuiteClasses注解的类,所有测试类由SuiteClasses指定,而klass类作为这个Suite的根类。此时一般所有的测试类是klass的内部类,因而可以通过@RunWith指定运行klass类的Runner是Suite,然后用SuiteClasses注解指定可以作为测试类的类。
3 this (builder, klass, getAnnotatedClasses(klass));
4 }
5 protected Suite(RunnerBuilder builder, Class <?> klass, Class <?> [] suiteClasses) throws InitializationError {
7 this (klass, builder.runners(klass, suiteClasses));
8 }
9 @Retention(RetentionPolicy.RUNTIME)
10 @Target(ElementType.TYPE)
11 @Inherited
12 public @ interface SuiteClasses {
13 public Class <?> [] value();
14 }
15 private static Class <?> [] getAnnotatedClasses(Class <?> klass) throws InitializationError {
17 SuiteClasses annotation = klass.getAnnotation(SuiteClasses. class );
18 if (annotation == null )
19 throw new InitializationError(String.format( " class '%s' must have a SuiteClasses annotation " , klass.getName()));
22 return annotation.value();
23 }
2. 在有些情况下,我们需要运行多个没有相关的测试类,此时这些测试类没有一个公共的根类的Suite,则需要使用一下的构造函数(此时Suite的getName()返回”null”,即其Description的displayName的值为”null”,关于RunnerBuilder将会在后面的文章中介绍):
3 this ( null , builder.runners( null , classes));
4 }
5 protected Suite(Class <?> klass, List < Runner > runners) throws InitializationError {
7 super (klass);
8 fRunners = runners;
9 }
Parameterized继承自Suite,它类似BlockJUnit4ClassRunner是对一个测试类的运行过程的封装,但是它支持在测试类中定义一个获得参数数组的一个列表,从而可以为构造该测试类提供不同的参数值以进行多次测试。因而Parameterized这个Runner其实就是根据参数数组列表的个数创建多个基于该测试类的Runner,并运行所有这些Runner中测试方法。
由于这个测试类的构造函数是带参的,而BlockJUnit4ClassRunner则限制其测试类必须是不带参的,因而Parameterized需要创建自己的Runner,它集成自BlockJUnit4ClassRunner,除了构造函数的差别,Parameterized的根节点是该测试类本身,它处理@BeforeClass、@AfterClass、@ClassRule的注解问题,因而其子节点的Runner不需要重新对其做处理了,因而在集成的Runner中应该避免这些方法、字段重复执行。
3 private final int fParameterSetNumber;
4 private final List < Object[] > fParameterList;
5 TestClassRunnerForParameters(Class <?> type, List < Object[] > parameterList, int i) throws InitializationError {
8 super (type);
9 fParameterList = parameterList;
10 fParameterSetNumber = i; // 参数数组列表中的位置
11 }
12 @Override
13 public Object createTest() throws Exception {
14 return getTestClass().getOnlyConstructor().newInstance(
15 computeParams()); // 带参创建测试类实例
16 }
17 private Object[] computeParams() throws Exception {
18 try {
19 return fParameterList.get(fParameterSetNumber);
20 } catch (ClassCastException e) {
21 throw new Exception(String.format(
22 " %s.%s() must return a Collection of arrays. " ,
23 getTestClass().getName(), getParametersMethod(
24 getTestClass()).getName()));
25 }
26 }
27 @Override
28 protected String getName() {
29 return String.format( " [%s] " , fParameterSetNumber);
30 }
31 @Override
32 protected String testName( final FrameworkMethod method) {
33 return String.format( " %s[%s] " , method.getName(),
34 fParameterSetNumber);
35 }
36 @Override
37 protected void validateConstructor(List < Throwable > errors) {
38 validateOnlyOneConstructor(errors); // 去除不带参构造函数验证
39 }
40 @Override
41 protected Statement classBlock(RunNotifier notifier) {
42 return childrenInvoker(notifier); // 不处理@BeforeClass、
43 // @AfterClass、@ClassRule等注解
44 }
45 @Override
46 protected Annotation[] getRunnerAnnotations() {
47 return new Annotation[ 0 ]; // 去除测试类的Annotation,这些应该在 Parameterized中处理
49 }
50 }
Parameterized对Suite的行为改变只是在创建Runner集合的过程,Parameterized通过类中查找@Parameters注解的静态方法获得参数数组列表,并更具这个参数数组列表创建TestClassRunnerForParameters的集合:
2 @Target(ElementType.METHOD)
3 public static @ interface Parameters {
4 }
5 public Parameterized(Class <?> klass) throws Throwable {
6 super (klass, Collections. < Runner > emptyList());
7 List < Object[] > parametersList = getParametersList(getTestClass());
8 for ( int i = 0 ; i < parametersList.size(); i ++ )
9 runners.add( new TestClassRunnerForParameters(getTestClass(). getJavaClass(), parametersList, i));
11 }
12 private List < Object[] > getParametersList(TestClass klass)
13 throws Throwable {
14 return (List < Object[] > ) getParametersMethod(klass). invokeExplosively( null );
16 }
17 private FrameworkMethod getParametersMethod(TestClass testClass)
18 throws Exception {
19 List < FrameworkMethod > methods = testClass .getAnnotatedMethods(Parameters. class );
21 for (FrameworkMethod each : methods) {
22 int modifiers = each.getMethod().getModifiers();
23 if (Modifier.isStatic(modifiers) &&
24 Modifier.isPublic(modifiers))
25 return each;
26 }
27 throw new Exception( " No public static parameters method on class " + testClass.getName());
29 }
一个简单的Parameterized测试类如下,不过如果在Eclipse中单独的运行testEqual()测试方法会出错,因为Eclipse在Filter传入的方法名为testEqual,而在Parameterized中这个方法名已经被改写成testEqual[i]了,因而在Filter过程中,所有的测试方法都被过滤掉了,此时会抛NoTestRemainException,而在Eclipse中出现的则是InitializationError的Exception,这里也是Eclipse中JUnit的插件没有做好,它并没有给出Failure类的详细信息:
2 @Parameters
3 public static List < Object[] > data() {
4 return Arrays.asList( new Object[][] {
5 { 0 , 0 }, { 1 , 1 }, { 2 , 1 }, { 3 , 2 },
6 { 4 , 3 }, { 5 , 5 }, { 6 , 8 }
7 });
8 }
9 @BeforeClass
10 public static void beforeClass() {
11 System.out.println( " .testing. " );
12 }
13 private final int input;
14 private final int expected;
15 public ParameterizedTest( int input, int expected) {
16 this .input = input;
17 this .expected = expected;
18 }
19 @Test
20 public void testEqual() {
21 Assert.assertEquals(expected, compute(input));
22 }
23 public int compute( int input) {
24 if (input == 0 || input == 1 ) {
25 return input;
26 }
27 if (input == 2 ) {
28 return 1 ;
29 }
30 return compute(input - 1 ) + compute(input - 2 );
31 }
32 }
ErrorReportingRunner和IgnoredClassRunner
关于JUnit4,还剩下最后两个Runner,分别是ErrorReportingRunner和IgnoredClassRunner,为了编程模型的统一(这是一个非常好的设计想法,将各种变化都封装在Runner中),JUnit中即使一个Runner实例创建失败或是该测试类有@Ignored的注解,在这两种情况中,JUnit分别通过ErrorReportingRunner和IgnoredClassRunner去表达。在ErrorReportingRunner中,为每个Exception发布测试失败的信息;IgnoredClassRunner则只是发布testIgnored事件:
2 private final List < Throwable > fCauses;
3 private final Class <?> fTestClass;
4 public ErrorReportingRunner(Class <?> testClass, Throwable cause) {
5 fTestClass = testClass;
6 fCauses = getCauses(cause);
7 }
8 @Override
9 public Description getDescription() {
10 Description description = Description.createSuiteDescription( fTestClass);
12 for (Throwable each : fCauses)
13 description.addChild(describeCause(each));
14 return description;
15 }
16 @Override
17 public void run(RunNotifier notifier) {
18 for (Throwable each : fCauses)
19 runCause(each, notifier);
20 }
21 @SuppressWarnings( " deprecation " )
22 private List < Throwable > getCauses(Throwable cause) {
23 if (cause instanceof InvocationTargetException)
24 return getCauses(cause.getCause());
25 if (cause instanceof InitializationError)
26 return ((InitializationError) cause).getCauses();
27 if (cause instanceof org.junit.internal.runners.InitializationError)
28 return ((org.junit.internal.runners.InitializationError) cause)
29 .getCauses();
30 return Arrays.asList(cause);
31 }
32 private Description describeCause(Throwable child) {
33 return Description.createTestDescription(fTestClass,
34 " initializationError " );
35 }
36 private void runCause(Throwable child, RunNotifier notifier) {
37 Description description = describeCause(child);
38 notifier.fireTestStarted(description);
39 notifier.fireTestFailure( new Failure(description, child));
40 notifier.fireTestFinished(description);
41 }
42 }
43 public class IgnoredClassRunner extends Runner {
44 private final Class <?> fTestClass;
45 public IgnoredClassRunner(Class <?> testClass) {
46 fTestClass = testClass;
47 }
48 @Override
49 public void run(RunNotifier notifier) {
50 notifier.fireTestIgnored(getDescription());
51 }
52 @Override
53 public Description getDescription() {
54 return Description.createSuiteDescription(fTestClass);
55 }
56 }
写在最后的话
写了一个周末了,这一篇终于是写完了,初次尝试将阅读框架源码以文字的形式表达出来,确实不好写,感觉自己写的太详细了,也贴了太多源码,因为有些时候感觉源码比文字更有表达能力,不过太多的源码就把很多实质的东西掩盖了,然后文章看起来也没有什么大的价值干,而且太长了,看到那么多的东西,要我自己看到也就有不想看的感觉,1万多字啊,呵呵。总之以后慢慢改进吧,这一篇就当是练习了,不想复工了,也没那么多的时间重写。。。。。。