Quartz-Spring定时任务器持久化,通过Service动态添加,删除,启动暂停任务

转载请注:http://blog.csdn.net/ljqwstc/article/details/78257091


最近接了一个活,需要在项目中添加定时任务。定时任务可以用jdk自带的timer,但是考虑到业务需求,这里可以用小巧灵活非常好用到的Quartz定时管理框架。

写这篇博客主要有两个原因。

第一:是希望自己日后如果有遗忘翻看的时候能想起来怎么操作,所以可能记录的东西不会很全,望谅解,若有写的不对的地方,欢迎拍砖留言。

第二:自己在开始做之前也看了非常多的文章,始终感觉模棱两可,不知怎么下手,在这,我希望有quartz业务需求的朋友看了我的文章后能知道怎么下手,完成业务需求。


废话不多说,下面就简单介绍了如何quartz如何整合spring框架,以及quartz的持久化还有动态添加删除、启动等操作。


首先添加maven的依赖:

        <!--quartz定时任务-->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>

spring配置文件中添加如下bean

    <!--
    自动装配类
    @Autowired
    private Scheduler scheduler;
    -->
    <!-- quartz持久化存储  -->
    <!--实现动态配置只需定义一下schedulerbean就可以了-->
    <bean name="schedulerFactoryBean"
          class="org.springframework.scheduling.quartz.SchedulerFactoryBean" lazy-init="true">
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
        <property name="applicationContextSchedulerContextKey" value="applicationContext"/>
        <property name="quartzProperties">
            <props>
                <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
                <!-- 线程池配置 -->
                <prop key="org.quartz.threadPool.class">${org.quartz.threadPool.class}</prop>
                <prop key="org.quartz.threadPool.threadCount">${org.quartz.threadPool.threadCount}</prop>
                <prop key="org.quartz.threadPool.threadPriority">${org.quartz.threadPool.threadPriority}</prop>
                <prop key="org.quartz.jobStore.misfireThreshold">${org.quartz.jobStore.misfireThreshold}</prop>
                <!-- JobStore 配置 -->
                <prop key="org.quartz.jobStore.class">${org.quartz.jobStore.class}</prop>
                <!-- 集群配置 -->
                <prop key="org.quartz.jobStore.isClustered">true</prop>
                <prop key="org.quartz.jobStore.clusterCheckinInterval">15000</prop>
                <prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">1</prop>
                <!-- 数据表设置 -->
                <prop key="org.quartz.jobStore.tablePrefix">${org.quartz.jobStore.tablePrefix}</prop>
                <prop key="org.quartz.jobStore.dataSource">${org.quartz.jobStore.dataSource}</prop>
            </props>
        </property>
    </bean>
解释:
1.一般公司项目中都已经定义好了dataSource,比如我们公司是用DruidDataSource,所以直接拿来用就可以了。如果没有的话,需要配置一个dataSource,这里就不阐述了。
2.因为是要持久化的,所以org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX  
这样设置以后就是持久化,会将任务的一些相关信息和配置存入到数据库中。具体需要用到什么数据表,下面会有。
3.Scheduler简单说相当于一个容器一样,里面放着jobDetail和Trriger
4.其他的一些配置可以根据自己的业务需求,到quartz的官网查看配置文档进行添加。
 
  

这个是quartz.properties文件

#============================================================================
# Configure Datasources
#============================================================================
#JDBC驱动
#org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
#org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8
#org.quartz.dataSource.myDS.user = root
#org.quartz.dataSource.myDS.password = root
#org.quartz.dataSource.myDS.maxConnections =5
#集群配置
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 60000

#============================================================================
# Configure JobStore
#============================================================================

#默认配置,数据保存到内存
#org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
#持久化配置
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties:true

#数据库表前缀
org.quartz.jobStore.tablePrefix:qrtz_
org.quartz.jobStore.dataSource:myDS

这个是quartz官方的表,运行就可以了,就建表成功了。

#
# Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar
#
# PLEASE consider using mysql with innodb tables to avoid locking issues
#
# In your Quartz properties file, you'll need to set 
# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#

DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;


CREATE TABLE QRTZ_JOB_DETAILS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
    IS_DURABLE VARCHAR(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT(13) NULL,
    PREV_FIRE_TIME BIGINT(13) NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT(13) NOT NULL,
    END_TIME BIGINT(13) NULL,
    CALENDAR_NAME VARCHAR(200) NULL,
    MISFIRE_INSTR SMALLINT(2) NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
        REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_SIMPLE_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    REPEAT_COUNT BIGINT(7) NOT NULL,
    REPEAT_INTERVAL BIGINT(12) NOT NULL,
    TIMES_TRIGGERED BIGINT(10) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_CRON_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    CRON_EXPRESSION VARCHAR(200) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_SIMPROP_TRIGGERS
  (          
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_BLOB_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    BLOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_CALENDARS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME  VARCHAR(200) NOT NULL,
    CALENDAR BLOB NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR(200) NOT NULL, 
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_FIRED_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    FIRED_TIME BIGINT(13) NOT NULL,
    SCHED_TIME BIGINT(13) NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(200) NULL,
    JOB_GROUP VARCHAR(200) NULL,
    IS_NONCONCURRENT VARCHAR(1) NULL,
    REQUESTS_RECOVERY VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_SCHEDULER_STATE
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
    CHECKIN_INTERVAL BIGINT(13) NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  

CREATE TABLE QRTZ_LOCKS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME  VARCHAR(40) NOT NULL, 
    PRIMARY KEY (SCHED_NAME,LOCK_NAME)
)ENGINE=MyISAM DEFAULT CHARSET=utf8;  


commit;

ok,到这里,我们需要的配置基本完成,接下来,就是怎么样去创建任务,实现任务的动态添加,修改。


首先,我们回顾一下,如果要执行一个任务,是不是要自定义一个类,比如SendEmailJob,然后这个类实现Job接口,再重写excute方法?然后再定义jobDetail,Trigger,然后再Scheduler.schdulerJob(jobDetail,trigger),安排任务?

那如果业务需求改了,现在需要通知用户的任务,抓取新闻的任务,这样的话就需要写非常非常多的job实现类,对吧?

所以,考虑到这一点,我们可以写一个QuartzJobFactory类实现Job接口,作为统一的Job入口,并且在job的上下文中加入一个我们POJO类(保存着一些job的相关信息),那么,在QuartzJobFactory类的excute方法中,我们可以根据我们传进去的POJO类对象获取当前job的相关信息,从而实现动态的去具体执行什么任务(方法)。

下面详细解释下实现方法:

这个是我自己定义的POJO类对象,用于保存当前任务的详细信息。
public class ScheduleJob implements Serializable {

    private static final long serialVersionUID = -5115028108119830917L;
    /**
     * 任务名称
     */
    private String jobName;
    /**
     * 任务分组
     */
    private String jobGroup;
    /**
     * 触发器名称(默认和任务名称相同)
     */
    private String triggerName;
    /**
     * 触发器分组(默认和任务分组相同)
     */
    private String triggerGroup;
    /**
     * 任务需要调用的是哪个类的类名
     */
    private String className;
    /**
     * 任务需要调用的是哪个类的方法名
     */
    private String methodName;
    /**
     * 方法所需参数数组
     */
    private ArrayList paramArray;
    /**
     * 任务运行时间表达式
     */
    private String cron;
    /**
     * 任务运行时间(特指只运行一次的任务)
     */
    private String runDate;
    /**
     * 任务描述
     */
    private String desc;
    getter and setter.....
}

这个POJO类主要的作用的,保存任务的一些相关信息。当我们在创建jobDetail的时候,可以获取他的dataMap,set进去。这样我们可以在QuartzJobFactory的excute方法中的参数中获取到dataMap,并从中拿到这个POJO类对象,从而获取了该job具体要怎么执行,执行哪个方法,方法参数是什么,就都有了!

接下来,我们继续看:

这个是我定义的QuartzJobFactory,Job的统一入口:
@PersistJobDataAfterExecution
public class QuartzJobFactory implements Job {

    /**
     * The constant logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(Constants.LOG_SYSTEM_RUN);

    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        ScheduleJob scheduleJob = (ScheduleJob) jobExecutionContext.getMergedJobDataMap().get("scheduleJob");
        //获取service层接口名和接口方法名
        String springBeanName = scheduleJob.getClassName();
        String methodName = scheduleJob.getMethodName();
        //获取参数数组和参数类型数组
        ArrayList paramArray = scheduleJob.getParamArray();
        Class[] paramType = new Class[paramArray.size()];
        for (int i = 0; i < paramArray.size(); i++) {
            paramType[i] = paramArray.get(i).getClass();
        }
        /**
         * 反射运行service层的方法
         */
        Object bean = SpringContextsUtil.getBean(springBeanName);
        Method method = ReflectionUtils.findMethod(bean.getClass(), methodName, paramType);
        ReflectionUtils.invokeMethod(method, bean, paramArray.toArray());

        logger.info("任务名称 = [" + scheduleJob.getJobName() + "]," +
                "任务调用的类=[" + scheduleJob.getClassName() + "]," +
                "任务调用的方法=[" + scheduleJob.getMethodName() + "]---->>成功启动运行");
    }
}
有状态的两个注解
支持并发
@PersistJobDataAfterExecution
如果不支持并发
@disallowconcurrentexecution

通过SpringContextsUtil工具类调用getBean去获取bean,这里的参数是String 类型的beanName,也就是我们配置文件配置bean是name那个属性。或者是我们在写Service层的时候,通过注@Service("beanName")   ,当然是推荐后者啦!简单方便。
获得了bean以后,在通过方法名获取Method,再利用反射就可以实现对Service层方法的调用了。

下面是我的SpringContextUtil工具类:
@Component
public class SpringContextsUtil implements ApplicationContextAware {


	private static ApplicationContext applicationContext;

	public static Object getBean(String beanName) throws BeansException {
		return applicationContext.getBean(beanName);
	}

	public static <T> T getBean(String beanName, Class<T> clazs) {
		return applicationContext.getBean(beanName, clazs);
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		SpringContextsUtil.applicationContext = applicationContext;
	}
}

ok,现在就只剩下对任务的添加,删除,启动,暂停等动态操作了。

为了方便管理,在我的quartz项目中,我的jobName和triggerName相同,jobGroup和triggerGroup相同。

既然是对任务的一些动态操作。我创了一个QuartzProducer接口,里面有对job的动态操作的一些方法。如下:
 
   
public interface QuartzProducer {

    /**
     * 添加简单任务,只运行一次的任务
     *
     * @param jobName
     * @param jobGroup
     * @param className
     * @param methodName
     * @param paramArray
     * @param runDateTime 格式:yyyyMMddHHmmss  
     * @throws SchedulerException
     * @throws ParseException
     */
    public void addSimpleJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String runDateTime) throws SchedulerException, ParseException;


    /**
     * 添加循环任务,特定时间循环运行,例如每个星期3,12点运行等
     *
     * @param jobName
     * @param jobGroup
     * @param className
     * @param methodName
     * @param paramArray
     * @param cron
     * @throws SchedulerException
     */
    public void addCronJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String cron) throws SchedulerException;

    /**
     * 修改简单任务,一般指修改运行时间
     *
     * @param jobName
     * @param jobGroup
     * @param runDateTime 格式:yyyyMMddHHmmss  
     */
    public void updateSimpleJob(String jobName, String jobGroup, String runDateTime) throws SchedulerException, ParseException;

    /**
     * 修改cron任务,一般指修改循环运行时间
     *
     * @param jobName
     * @param jobGroup
     * @param cron
     */
    public void updateCronJob(String jobName, String jobGroup, String cron) throws SchedulerException;

    /**
     * 移除任务
     *
     * @param jobName
     * @param jobGroup
     */
    public void deleteJob(String jobName, String jobGroup) throws SchedulerException;

    /**
     * 移除所有任务
     */
    public void deleteAll() throws SchedulerException;

    /**
     * 暂停任务
     *
     * @param jobName
     * @param jobGroup
     */
    public void pauseJob(String jobName, String jobGroup) throws SchedulerException;

    /**
     * 暂停所有任务
     */
    public void pauseAll() throws SchedulerException;

    /**
     * 恢复某个任务
     *
     * @param jobName
     * @param jobGroup
     */
    public void resumeJob(String jobName, String jobGroup) throws SchedulerException;

    /**
     * 恢复所有
     */
    public void resumeAll() throws SchedulerException;

    /**
     * 关闭任务调度器
     */
    public void shutDown() throws SchedulerException;

    /**
     * 开启任务调度器
     */
    public void startScheduler() throws SchedulerException;
}

实现类:
 
   
//定义该bean的name为"quartzProducer"
@Service("quartzProducer")
public class QuartzProducerImpl implements QuartzProducer {
	
	
	//虽然在Spring配置中配置的是SchedulerFactoryBean这个类,但是我们自动转配就写这样,一样是可以使用的
    @Autowired
    private Scheduler scheduler;

    @Override
    public void addSimpleJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String runDateTime) throws SchedulerException, ParseException {
        //判断是否已存在相同jobName,jobGroup,若存在则删除
        if (scheduler.getJobDetail(JobKey.jobKey(jobName, jobGroup)) != null) {
            deleteJob(jobName, jobGroup);
        }
        JobDetail jobDetail = JobBuilder.newJob(QuartzJobFactory.class).withIdentity(jobName, jobGroup).build();
        //任务具体执行的内容封装,返回给job统一入口
        ScheduleJob job = new ScheduleJob(jobName, jobGroup, jobName, jobGroup, className, methodName, paramArray, null, runDateTime, null);
        //将我自定义的POJO类存放到DataMap中
		jobDetail.getJobDataMap().put("scheduleJob", job);
        //创建SimpleTrigger,在特定时间仅运行一次
        Date runDate = DateUtils.parseDate(runDateTime, DateStyle.YYYYMMDDHHMMSS.getValue());
        SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).
                startAt(runDate).build();
        scheduler.scheduleJob(jobDetail, trigger);
    }

}

只贴了一个添加的方法,其他方法可以自行实现。注释应该比较清楚了,这里不过多赘述。

ok,到这里,我们已经完全了quartz跟spring的整合,以及如何调用service层的业务逻辑,任务的动态处理。


接下来就非常非常简单了,一般来说就是前端调用controller层,给它传参,比如要调用什么接口,接口的方法,jobname,jobgroup,方法参数,什么时候运行,或者是cron表达式。然后controller层再调用我们刚刚写的quartzProducer就可以了。


其他方法,比如定时任务修改,停用,删除,都是类似的,controller-->service

好了,大概的思路就是这样子,如果有什么不对的地方请拍砖,有什么不太清楚的地方也请拍砖!

转载请注:http://blog.csdn.net/ljqwstc/article/details/78257091
 
 


猜你喜欢

转载自blog.csdn.net/ljqwstc/article/details/78257091