Spring+Quartz实现定时任务

时间:2022-11-13 07:50:27

Spring是一个很优秀的框架,它无缝的集成了Quartz,简单方便的让企业级应用更好的使用Quartz进行任务的调度。

Jar包依赖

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
需要注意一点,与Spring3.1以下版本整合必须使用Quartz 1.x.x。

在Spring中使用Quartz方式:

  • 任务类继承QuartzJobBean
  • 在配置文件里定义任务类和要执行的方法,类和方法仍然是普通类
  • 使用注解的方式

任务类Job

package com.chen.quatrz;
import org.springframework.stereotype.Service;

@Service(value="springD")
public class SpringD {

    public void one() {
        System.out.println("spring task D 定时任务");
    }
    
}

package com.chen.quatrz;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service(value="springC")
public class SpringC {

    /*
        字段   允许值   允许的特殊字符
        秒    0-59    , - * /
        分    0-59    , - * /
        小时    0-23    , - * /
        日期    1-31    , - * ? / L W C
        月份    1-12 或者 JAN-DEC    , - * /
        星期    1-7 或者 SUN-SAT    , - * ? / L C #
        年(可选)    留空, 1970-2099    , - * / 
        
        - 区间  
        * 通配符  
        ? 你不想设置那个字段
     */
    
    @Scheduled(cron="1/2 * * * * ?")
    public void one() {
        System.out.println("spring task C 定时任务");
    }
    
}

xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:task="http://www.springframework.org/schema/task"
    xmlns:tx="http://www.springframework.org/schema/tx"
   	xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-4.0.xsd
	http://www.springframework.org/schema/tx 
	http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
	http://www.springframework.org/schema/task 
	http://www.springframework.org/schema/task/spring-task-4.0.xsd"
	default-lazy-init="false">
	
	<context:annotation-config />
	
	<context:component-scan base-package="com.chen.quartz" />
	
	<!-- 定时任务注解扫描-->   
	<task:annotation-driven />  
	
	<bean id="springDJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
		 <property name="targetObject" ref="springD" />
		 <property name="targetMethod" value="one" />
		 <!-- 是否并发调度 -->
		 <!-- <property name="concurrent" value="true" /> -->
	</bean>
	
	<!-- 触发器 -->
	<bean id="springDJobCT" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
		 <property name="jobDetail" ref="springDJob" />
		 <property name="cronExpression" value="2/5 * * * * ?" />
	</bean>
	
	<!-- 线程池 -->
	<bean id="executor"
		class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
		<!-- 核心线程数 -->
		<property name="corePoolSize" value="5" />
		<!-- 最大线程数 -->
		<property name="maxPoolSize" value="10" />
		<!-- 队列最大长度 >=mainExecutor.maxSize -->
		<property name="queueCapacity" value="90" />
		<!-- 线程池维护线程所允许的空闲时间 -->
		<property name="keepAliveSeconds" value="3000" />
		<!-- 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.  -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
        </property>
	</bean>
    
    <!-- 调度工厂 -->
    <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean" >
		 <property name="triggers">
			  <list>
			  	 <ref bean="springDJobCT" />
			  </list>
		 </property>
		 <property name="taskExecutor" ref="executor" />
	</bean>
	
    <!-- Spring内部有一个task,是Spring自带的一个设定时间自动任务调度 -->
    <!-- ref是工作类
        method是工作类中要执行的方法
        
        initial-delay是任务第一次被调用前的延时,单位毫秒
        
        fixed-delay是上一个调用完成后再次调用的延时
        
        fixed-rate是上一个调用开始后再次调用的延时(不用等待上一次调用完成)
        
        cron是表达式,表示在什么时候进行任务调度 
    -->
	<!-- <task:scheduled-tasks>
		<task:scheduled ref="springD" method="one" cron="2/5 * * * * ?"/>
	</task:scheduled-tasks> -->
	
</beans>

测试

package com.chen.junit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/quartz-task.xml"})
public class SpringTest {
	
	@Test
	public void one() throws InterruptedException {
	    Thread.sleep(1000000);
	}
	
}

运行结果

Spring+Quartz实现定时任务
运行时可能会报异常:
DEBUG 2018-01-19 15:56:45,822 org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor: Could not find default TaskScheduler bean
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:371)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:331)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:183)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:162)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:85)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:151)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:128)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:331)
	at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:773)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:483)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:125)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:109)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:261)
	at org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:68)
	at org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:86)
	at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:72)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:117)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:212)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:200)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:259)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:261)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:219)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:83)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:68)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:163)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
DEBUG 2018-01-19 15:56:45,822 org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor: Could not find default ScheduledExecutorService bean
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:371)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:331)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:183)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:162)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:85)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:151)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:128)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:331)
	at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:773)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:483)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:125)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:109)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:261)
	at org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:68)
	at org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:86)
	at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:72)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:117)
	at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:212)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:200)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:259)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:261)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:219)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:83)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:68)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:163)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
DEBUG 2018-01-19 15:56:45,838 org.springframework.core.env.PropertySourcesPropertyResolver: Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
DEBUG 2018-01-19 15:56:45,838 org.springframework.core.env.PropertySourcesPropertyResolver: Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
DEBUG 2018-01-19 15:56:45,838 org.springframework.core.env.PropertySourcesPropertyResolver: Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
DEBUG 2018-01-19 15:56:45,838 org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate: Storing ApplicationContext in cache under key [[MergedContextConfiguration@5f9d02cb testClass = SpringTest, locations = '{classpath:spring/quartz-task.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]
DEBUG 2018-01-19 15:56:45,838 org.springframework.test.context.DefaultCacheAwareContextLoaderDelegate: Spring test ApplicationContext cache statistics: [ContextCache@5b799640 size = 1, hitCount = 0, missCount = 1, parentContextCount = 0]
DEBUG 2018-01-19 15:56:45,838 org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor: No @Scheduled annotations found on bean class: class com.chen.junit.SpringTest
DEBUG 2018-01-19 15:56:46,821 org.quartz.utils.UpdateChecker: Checking for available updated version of Quartz...
spring task C 定时任务
DEBUG 2018-01-19 15:56:47,009 org.quartz.core.QuartzSchedulerThread: batch acquisition of 1 triggers
DEBUG 2018-01-19 15:56:47,009 org.quartz.core.JobRunShell: Calling execute on job DEFAULT.springDJob
spring task D 定时任务
DEBUG 2018-01-19 15:56:47,321 org.quartz.utils.UpdateChecker: Quartz version update check failed: Server returned HTTP response code: 403 for URL: http://www.terracotta.org/kit/reflector?kitID=quartz&pageID=update.properties&id=-1062717439&os-name=Windows+7&jvm-name=Java+HotSpot%28TM%29+64-Bit+Server+VM&jvm-version=1.8.0_131&platform=amd64&tc-version=2.2.1&tc-product=Quartz&source=Quartz&uptime-secs=1&patch=UNKNOWN
spring task C 定时任务
spring task C 定时任务

Spring的定时任务调度器会尝试获取一个注册过的 task scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的scheduler bean,获取的步骤如下:

1.尝试从配置中找到一个TaskScheduler Bean

2.寻找ScheduledExecutorService Bean

3.使用默认的scheduler

前两步,如果找不到的话,就会以debug的方式抛出异常,分别是:

logger.debug("Could not find default TaskScheduler bean", ex);
logger.debug("Could not find default ScheduledExecutorService bean", ex);

所以,日志中打印出来的两个异常,根本不是什么错误信息,也不会影响定时器的使用,只不过是spring的自己打印的一些信息罢了,不过没搞明白,为什么非要用异常的方式打出来,估计是为了看这清晰点吧。也或者,这里面有一些重要的信息需要提示开发者。

Cron Expressions

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: 

1、Seconds Minutes Hours DayofMonth Month DayofWeek Year

2、Seconds Minutes Hours DayofMonth Month DayofWeek


例  "0 0 12 ? * WED" 在每星期三下午12:00 执行,

个别子表达式可以包含范围, 例如,在前面的例子里("WED")可以替换成 "MON-FRI", "MON, WED, FRI"甚至"MON-WED,SAT".

每一个字段都有一套可以指定有效值,如

Seconds (秒)         :可以用数字0-59 表示,

Minutes(分)          :可以用数字0-59 表示,

Hours(时)             :可以用数字0-23表示,

Day-of-Month(天) :可以用数字1-31 中的任一一个值,但要注意一些特别的月份

Month(月)            :可以用0-11 或用字符串  “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示

Day-of-Week(每周):可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示

●星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,*在分钟字段时,表示“每分钟”;

●问号(?):该字符只在日期和星期字段中使用,虽然我现在不知道它的值是多少,但是它的值是唯一的,通过日期可以推出星期,通过本周是周几也可以推出日期。

●减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;

●逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

●斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;

●L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;

●W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;

●LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;

●井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;

● C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。

1)Cron表达式的格式:秒 分 时 日 月 周 年(可选)。

               字段名                 允许的值                        允许的特殊字符  
               秒                         0-59                               , - * /  
               分                         0-59                               , - * /  
               小时                     0-23                               , - * /  
               日                         1-31                               , - * ? / L W C  
               月                         1-12 or JAN-DEC         , - * /  
               周几                     1-7 or SUN-SAT           , - * ? / L C #  
               年 (可选字段)     empty, 1970-2099      , - * / 

2)Cron表达式范例:

                 每隔5秒执行一次:*/5 * * * * ?

                 每隔1分钟执行一次:0 */1 * * * ?

                 每天23点执行一次:0 0 23 * * ?

                 每天凌晨1点执行一次:0 0 1 * * ?

                 每月1号凌晨1点执行一次:0 0 1 1 * ?

                 每月最后一天23点执行一次:0 0 23 L * ?

                 每周星期天凌晨1点实行一次:0 0 1 ? * L

                 在26分、29分、33分执行一次:0 26,29,33 * * * ?

                 每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?


参考:
http://blog.csdn.net/kollyqaq/article/details/51191047
http://blog.csdn.net/oarsman/article/details/52801877
http://blog.csdn.net/jxq0816/article/details/51620400