博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
springboot-quartz 实现动态添加,修改,删除,暂停,恢复等功能
阅读量:5343 次
发布时间:2019-06-15

本文共 23439 字,大约阅读时间需要 78 分钟。

任务相关信息:

 

一、任务实体类

package cloud.app.prod.home.quartz;import java.io.Serializable;import java.util.Date;/** * Author : YongBo Xie 
* File Name: ScheduleJob.java 任务job实体类
* Created Date: 2018年4月3日 上午11:33:47
* Modified Date: 2018年4月3日 上午11:33:47
* Version: 1.0
*/public class ScheduleJobEntity implements Serializable { private static final long serialVersionUID = 6009097439379146309L; /** 任务ID */ private String jobId; /** 任务名称 */ private String jobName; /** 任务分组 */ private String jobGroup; /** 任务运行时间表达式 */ private String cronExpression; /** 任务类(必须是Spring中定义的Bean) */ private String targetObject; /** 任务方法 */ private String targetMethod; /** 是否并发 */ private boolean concurrent; /** 触发器开始执行任务时间 */ private Date startDate; /** 触发器结束执行任务时间 */ private Date endDate; public String getJobId() { return jobId; } public void setJobId(String jobId) { this.jobId = jobId; } public String getJobName() { return jobName; } public void setJobName(String jobName) { this.jobName = jobName; } public String getJobGroup() { return jobGroup; } public void setJobGroup(String jobGroup) { this.jobGroup = jobGroup; } public String getCronExpression() { return cronExpression; } public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; } public String getTargetObject() { return targetObject; } public void setTargetObject(String targetObject) { this.targetObject = targetObject; } public String getTargetMethod() { return targetMethod; } public void setTargetMethod(String targetMethod) { this.targetMethod = targetMethod; } public boolean isConcurrent() { return concurrent; } public void setConcurrent(boolean concurrent) { this.concurrent = concurrent; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } }
View Code

二、Spring应用上下文环境

package cloud.app.prod.home.quartz;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.stereotype.Component;/** * Author : YongBo Xie 
* File Name: SpringContextUtil.java
* Created Date: 2018年4月3日 上午11:27:55
* Modified Date: 2018年4月3日 上午11:27:55
* Version: 1.0
*/@Componentpublic class SpringContextUtil implements ApplicationContextAware { // Spring应用上下文环境 private static ApplicationContext applicationContext; /** * 实现ApplicationContextAware接口的回调方法,设置上下文环境 */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtil.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 根据指定bean名称,获取对应的实例对象 * @param name bean的名称 * @return * @throws BeansException */ public static Object getBean(String name) throws BeansException { if (applicationContext == null) { throw new RuntimeException("spring 上下文对象未初始化,无法完成bean的查找!"); } return applicationContext.getBean(name); } /** * 根据指定Bean类型,获取对应的实例对象 * @param requiredType bean的class类型 * @return * @throws BeansException */ public static
T getBean(Class
requiredType) throws BeansException { if (applicationContext == null) { throw new RuntimeException("spring 上下文对象未初始化,无法完成bean的查找!"); } return applicationContext.getBean(requiredType); }}
View Code

三、自定义MyJobFactory类,实现自动注入

package cloud.app.prod.home.quartz;import org.quartz.spi.TriggerFiredBundle;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.config.AutowireCapableBeanFactory;import org.springframework.scheduling.quartz.AdaptableJobFactory;import org.springframework.stereotype.Component;/** * Author : YongBo Xie 
* File Name: MyJobFactory.java
* Created Date: 2018年4月2日 下午3:27:30
* Modified Date: 2018年4月2日 下午3:27:30
* Version: 1.0
*/@Componentpublic class MyJobFactory extends AdaptableJobFactory { @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 调用父类的方法 Object jobInstance = super.createJobInstance(bundle); // 进行注入 capableBeanFactory.autowireBean(jobInstance); return jobInstance; }}
View Code

四、配置SchedulerFactoryBean

package cloud.app.prod.home.quartz;import java.io.IOException;import java.util.Properties;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.config.PropertiesFactoryBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.scheduling.annotation.EnableScheduling;import org.springframework.scheduling.quartz.SchedulerFactoryBean;import cloud.app.prod.home.quartz.MyJobFactory;/** * Author : YongBo Xie 
* File Name: QuartzConfigration.java
* Created Date: 2018年3月31日 下午3:42:04
* Modified Date: 2018年3月31日 下午3:42:04
* Version: 1.0
*/@Configuration@EnableSchedulingpublic class QuartzConfigration { @Autowired private MyJobFactory myJobFactory; @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); // 用于quartz集群,QuartzScheduler 启动时更新己存在的Job factory.setOverwriteExistingJobs(true); // 延时启动,应用启动1秒后 // factory.setStartupDelay(1); // 加载quartz数据源配置// factory.setQuartzProperties(quartzProperties()); // 自定义Job Factory,用于Spring注入 factory.setJobFactory(myJobFactory); return factory; } /** * 加载quartz数据源配置 * * @return * @throws IOException */ @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); }}
View Code

五、定时任务公共操作类

package cloud.app.prod.home.quartz;import java.util.ArrayList;import java.util.List;import java.util.Map;import org.apache.log4j.Logger;import org.quartz.CronScheduleBuilder;import org.quartz.CronTrigger;import org.quartz.Job;import org.quartz.JobBuilder;import org.quartz.JobDetail;import org.quartz.Scheduler;import org.quartz.TriggerBuilder;import org.quartz.TriggerKey;import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;import cloud.app.prod.home.common.FailException;import cloud.app.prod.home.utils.DSHUtils;/** * Author : YongBo Xie 
* File Name: QuartzUtil.java
* Created Date: 2018年4月3日 上午10:58:32
* Modified Date: 2018年4月3日 上午10:58:32
* Version: 1.0
*/public class QuartzUtil { private static Logger logger = Logger.getLogger(QuartzUtil.class); public static List
jobNames = new ArrayList
(); public static String getScheduleJobName(String jobName) { if (jobNames.contains(jobName)) { return jobName; } return null; } /** * 创建一个定时任务,并做安排(用于继承job类的执行方法) * * @param scheduler * @param scheduleJob * @param paramsMap * @param jobClass * @throws FailException */ public static void createSheduler(Scheduler scheduler, ScheduleJobEntity scheduleJob, Map
paramsMap, Class
jobClass) throws FailException { try { logger.info("----- scheduling job --------"); // 创建一项作业 JobDetail jobDetail = JobBuilder.newJob(jobClass) .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .build(); // 设置参数 for (Map.Entry
entry : paramsMap.entrySet()) { jobDetail.getJobDataMap().put(entry.getKey(), entry.getValue()); } // 作业的执行时间(当前时间的下一分钟)// Date runTime = DateBuilder.evenMinuteDate(new Date()); // 创建一个触发器 CronTrigger trigger = null; if (null != scheduleJob.getStartDate() && null != scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .startAt(scheduleJob.getStartDate()) // 该触发器开始执行作业时间 .endAt(scheduleJob.getEndDate()) // 该触发器结束作业时间 .withSchedule(CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())) // 具体执行时间 .build(); } else if (null != scheduleJob.getStartDate() && null == scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .startAt(scheduleJob.getStartDate()) // 该触发器开始执行作业时间 .withSchedule(CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())) // 具体执行时间 .build(); } else if (null == scheduleJob.getStartDate() && null != scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .endAt(scheduleJob.getEndDate()) // 该触发器结束作业时间 .withSchedule(CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())) // 具体执行时间 .build(); } else { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .withSchedule(CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())) // 具体执行时间 .build(); } // 告诉调度器使用该触发器来安排作业 scheduler.scheduleJob(jobDetail, trigger); // 启动调度器 scheduler.start(); logger.info("------ started scheduler -------"); logger.info("------ waiting 2 minutes ------"); Thread.sleep(2 * 60 * 1000);// logger.info("------- shutting down ------");// // 关闭调度器// scheduler.shutdown(true);// logger.info("------- shutdown complete -------"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 创建一个定时任务,并做安排(用于自定义执行方法) * * @param scheduler * @param scheduleJob * @param paramsMap * @throws FailException */ public static void createSheduler(Scheduler scheduler, ScheduleJobEntity scheduleJob, Map
paramsMap) throws FailException { try { // 新建一个基于Spring的管理Job类 MethodInvokingJobDetailFactoryBean methodInvJobDetailFB = new MethodInvokingJobDetailFactoryBean(); // 设置Job组名称 methodInvJobDetailFB.setGroup(scheduleJob.getJobGroup()); // 设置Job名称 methodInvJobDetailFB.setName(scheduleJob.getJobName()); // 设置任务类 methodInvJobDetailFB.setTargetObject(SpringContextUtil.getApplicationContext().getBean(scheduleJob.getTargetObject())); // 设置任务方法 methodInvJobDetailFB.setTargetMethod(scheduleJob.getTargetMethod()); // 将管理Job类提交到计划管理类 methodInvJobDetailFB.afterPropertiesSet(); // 并发设置 methodInvJobDetailFB.setConcurrent(scheduleJob.isConcurrent()); JobDetail jobDetail = methodInvJobDetailFB.getObject();// 动态 // 设置参数 for (Map.Entry
entry : paramsMap.entrySet()) { jobDetail.getJobDataMap().put(entry.getKey(), entry.getValue()); } // jobName存入到队列 每隔一段时间就会扫描所以需要时检测 if (!QuartzUtil.jobNames.contains(scheduleJob.getJobName())) { QuartzUtil.jobNames.add(scheduleJob.getJobName()); } // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression()); // 按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = null; if (null != scheduleJob.getStartDate() && null != scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .startAt(scheduleJob.getStartDate()) // 该触发器开始执行作业时间 .endAt(scheduleJob.getEndDate()) // 该触发器结束作业时间 .withSchedule(scheduleBuilder) // 具体执行时间 .build(); } else if (null != scheduleJob.getStartDate() && null == scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .startAt(scheduleJob.getStartDate()) // 该触发器开始执行作业时间 .withSchedule(scheduleBuilder) // 具体执行时间 .build(); } else if (null == scheduleJob.getStartDate() && null != scheduleJob.getEndDate()) { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .endAt(scheduleJob.getEndDate()) // 该触发器结束作业时间 .withSchedule(scheduleBuilder) // 具体执行时间 .build(); } else { trigger = (CronTrigger) TriggerBuilder.newTrigger() .withIdentity(scheduleJob.getJobName(), scheduleJob.getJobGroup()) .withSchedule(scheduleBuilder) // 具体执行时间 .build(); } // 暂时停止 任务都安排完之后统一启动 解决耗时任务按照顺序部署后执行紊乱的问题// scheduler.standby(); // 注入到管理类 scheduler.scheduleJob(jobDetail, trigger); logger.info(scheduleJob.getJobGroup() + "." + scheduleJob.getJobName() + "创建完毕"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 修改定时任务 * @param scheduler * @param scheduleJob * @param triggerKey * @param trigger * @throws FailException */ public static void updateScheduler(Scheduler scheduler, ScheduleJobEntity scheduleJob, TriggerKey triggerKey, CronTrigger trigger) throws FailException { try { if (!trigger.getCronExpression().equalsIgnoreCase(scheduleJob.getCronExpression())) { // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression()); // 按新的cronExpression表达式重新构建trigger trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); // 按新的trigger重新设置job执行 scheduler.rescheduleJob(triggerKey, trigger); logger.info(scheduleJob.getJobGroup() + "." + scheduleJob.getJobName() + " 更新完毕,目前cron表达式为:" + scheduleJob.getCronExpression() + " concurrent: " + scheduleJob.isConcurrent()); } } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 删除定时任务 * @param scheduler * @param triggerKey * @param trigger * @throws FailException */ public static void deleteScheduler(Scheduler scheduler, TriggerKey triggerKey, CronTrigger trigger) throws FailException { try { scheduler.pauseTrigger(triggerKey);// 停止触发器 scheduler.unscheduleJob(triggerKey);// 移除触发器 scheduler.deleteJob(trigger.getJobKey());// 删除任务 logger.info(triggerKey.getGroup() + "." + triggerKey.getName() + "删除完毕"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 暂停定时任务 * @param scheduler * @param trigger * @throws FailException */ public static void pauseScheduler(Scheduler scheduler, CronTrigger trigger) throws FailException { try { scheduler.pauseJob(trigger.getJobKey());// 暂停任务 logger.info(trigger.getJobKey().getGroup() + "." + trigger.getJobKey().getName() + "暂停完毕"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 恢复定时任务 * @param scheduler * @param trigger * @throws FailException */ public static void resumeScheduler(Scheduler scheduler, CronTrigger trigger) throws FailException { try { scheduler.resumeJob(trigger.getJobKey());// 恢复任务 logger.info(trigger.getJobKey().getGroup() + "." + trigger.getJobKey().getName() + "恢复完毕"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } /** * 立即执行定时任务 * @param scheduler * @param trigger * @throws FailException */ public static void triggerScheduler(Scheduler scheduler, CronTrigger trigger) throws FailException { try { scheduler.triggerJob(trigger.getJobKey());// 立即执行任务 logger.info(trigger.getJobKey().getGroup() + "." + trigger.getJobKey().getName() + "立即执行完毕"); } catch (Exception e) { logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } }}
View Code

六、具体任务操作类

package cloud.app.prod.home.quartz.mem;import java.util.HashMap;import java.util.Map;import org.apache.log4j.Logger;import org.quartz.CronTrigger;import org.quartz.DisallowConcurrentExecution;import org.quartz.Scheduler;import org.quartz.TriggerKey;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.scheduling.quartz.SchedulerFactoryBean;import org.springframework.stereotype.Component;import cloud.app.prod.home.common.FailException;import cloud.app.prod.home.mem.vo.MarketingActivitiesVO;import cloud.app.prod.home.quartz.QuartzUtil;import cloud.app.prod.home.quartz.ScheduleJobEntity;import cloud.app.prod.home.utils.DSHUtils;/** * Author : YongBo Xie 
* File Name: ScheduleRefreshDatabase.java
* Created Date: 2018年3月31日 下午3:58:08
* Modified Date: 2018年3月31日 下午3:58:08
* Version: 1.0
*/@Component@DisallowConcurrentExecution // 任务同步public class MarketingActivityScheduleHandle { private static Logger logger = Logger.getLogger(MarketingActivityScheduleHandle.class); @Autowired private SchedulerFactoryBean schedulerFactoryBean; // 公用Scheduler,同一个对象// public static Scheduler scheduler = (Scheduler) SpringContextUtil.getApplicationContext().getBean("schedulerFactoryBean"); @Scheduled(fixedRate = 5000) // 每隔5s查库// @Scheduled(cron = "0 0 1 * * ?") public void scheduleRefreshCron() throws FailException { try{ logger.info("----- scheduling job --------"); String searchCron = "*/5 * * * * ?";// 从数据库查询出来的 // 获取一个调度器 // SchedulerFactory factory = new StdSchedulerFactory(); // Scheduler scheduler = factory.getScheduler(); Scheduler scheduler = schedulerFactoryBean.getScheduler(); // 设置参数 MarketingActivitiesVO marketingActivitiesVO = new MarketingActivitiesVO(); marketingActivitiesVO.setId(DSHUtils.generateUUID()); ScheduleJobEntity scheduleJob = new ScheduleJobEntity(); scheduleJob.setJobId(marketingActivitiesVO.getId()); scheduleJob.setJobName("marketingActivity"); scheduleJob.setJobGroup("marketingActivityGroup"); scheduleJob.setCronExpression(searchCron); scheduleJob.setTargetObject("marketingActivityScheduleTask"); scheduleJob.setTargetMethod("executeInternal"); scheduleJob.setConcurrent(true); Map
paramsMap = new HashMap<>(); paramsMap.put("marketingActivitiesVO", marketingActivitiesVO); // 获取triggerKey TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()); // 获取trigger CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); // 不存在,创建一个 if (null == trigger) { // 创建一个触发器 QuartzUtil.createSheduler(scheduler, scheduleJob, paramsMap, MarketingActivityScheduleTask.class);// QuartzUtil.createSheduler(scheduler, scheduleJob, paramsMap); } else {
// Trigger已存在,那么更新相应的定时设置 QuartzUtil.updateScheduler(scheduler, scheduleJob, triggerKey, trigger); // 删除 QuartzUtil.deleteScheduler(scheduler, triggerKey, trigger); } }catch(Exception e){ logger.error(DSHUtils.formatException(e)); throw new FailException(e.getMessage()); } } }
View Code

七:1)、具体任务执行类(继承Job)

package cloud.app.prod.home.quartz.mem;import org.apache.log4j.Logger;import org.quartz.JobDataMap;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.EnableScheduling;import org.springframework.scheduling.quartz.QuartzJobBean;import org.springframework.stereotype.Component;import cloud.app.prod.home.common.FailException;import cloud.app.prod.home.mem.vo.MarketingActivitiesVO;import cloud.app.prod.home.rabbitmq.mem.MarketingActivitieRabbitMqSender;/** * Author : YongBo Xie 
* File Name: ScheduleTask.java
* Created Date: 2018年3月31日 下午3:37:43
* Modified Date: 2018年3月31日 下午3:37:43
* Version: 1.0
*/@Component // 此注解必加 @EnableScheduling // 此注解必加public class MarketingActivityScheduleTask extends QuartzJobBean { private static Logger logger = Logger.getLogger(MarketingActivityScheduleTask.class); @Autowired private MarketingActivitieRabbitMqSender marketingActivitieRabbitMqSender; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info("execute activity"); JobDataMap dataMap = context.getJobDetail().getJobDataMap(); MarketingActivitiesVO marketingActivitiesVO = (MarketingActivitiesVO) dataMap.get("marketingActivitiesVO"); logger.info("marketingActivitiesVO.id: " + marketingActivitiesVO.getId()); try { logger.info("marketingActivitieRabbitMqSender: " + marketingActivitieRabbitMqSender); marketingActivitieRabbitMqSender.sendRabbitmqDirect(marketingActivitiesVO); } catch (FailException e) { e.printStackTrace(); } }}
View Code
  2)、具体任务执行类(自定义执行方法)
package cloud.app.prod.home.quartz.mem;import org.apache.log4j.Logger;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableScheduling;import org.springframework.stereotype.Component;import cloud.app.prod.home.common.FailException;/** * Author : YongBo Xie 
* File Name: ScheduleTask.java
* Created Date: 2018年3月31日 下午3:37:43
* Modified Date: 2018年3月31日 下午3:37:43
* Version: 1.0
*/@Configuration @Component // 此注解必加 @EnableScheduling // 此注解必加public class ScheduleTask { private static Logger logger = Logger.getLogger(ScheduleTask.class); public void marketingActivity() throws FailException { logger.info("execute activity"); }}
View Code

 

 

转载于:https://www.cnblogs.com/BobXie85/p/8709390.html

你可能感兴趣的文章
【VS开发】ATL辅助COM组件开发
查看>>
python 时间字符串与日期转化
查看>>
FlatBuffers In Android
查看>>
《演说之禅》I &amp; II 读书笔记
查看>>
js的alert乱码问题
查看>>
客户端服务端web问题
查看>>
thinkphp3.2接入支付宝支付接口(PC端)
查看>>
response和request
查看>>
新手笔记-tftp与yum
查看>>
Google API v3 设置Icon问题处理
查看>>
【转】在Eclipse中安装和使用TFS插件
查看>>
spring-cloud-Zuul学习(四)【中级】--自定义zuul Filter详解【重新定义spring cloud实践】...
查看>>
Bootstrap 面板(Panels)
查看>>
30个php操作redis常用方法代码例子
查看>>
SUSE 安装 iServer、iDesktop启动异常问题
查看>>
#20. 暗之连锁——Yucai OJ第16次测试
查看>>
回到顶部浮窗设计
查看>>
C#中Monitor和Lock以及区别
查看>>
sql server不能删除数据库,显示错误:正在使用
查看>>
【NOIP2017】奶酪
查看>>