Spring+Quartz实现动态添加定时任务

发布时间:2018-12-03
 
技术:spring4.0.2+quartz2.2.1
 

概述

在最近工作中,由于涉及到定时任务特别多,而这些工作又是由下属去完成的,在生成环境中经常会出现业务逻辑错误,分析下来多数是定时任务运行问题,所以就希望把定时任务优化一下,主要实现2个方面 1.定时任务动态配置及持久化 2.可视化的管理界面,可以非常清晰的管理自己的所有定时任务 源码是我重新梳理后的(陆陆续续花了我好几天晚上),整个框架去除了多余的内容,仅保留quartz及springAop,能非常好的解决业务中的定时任务,而且还能所见即所得的知道目前有哪些任务在跑,对任务具体执行的情况进行日志分析;框架采用ssm搭建,结合自己工作中框架的结构问题做了优化,如果对这个单体架构感兴趣,底层可以在platform_parent中扩展,应用层可在cloud_parent中进行扩展

详细

一、准备工作

1.java环境搭建,具体参考包中的webapp/resources/doc/平台开发环境安装Guide_V1.0.docx文档

image.png

2.使用源码中的webapp/resources/doc/init.sql初始化表结构及数据

t_timetask 任务表

t_timetask_log 任务运行日志

3.数据库连接配置在cloud_parent中的pom.xml中,数据库名称ffxl_cloud,默认账号root,密码123456,同样可在pom.xml中修改

4.运行quartz项目,此处注意,使用的端口号需要与platform_parent下pom.xml中的quartz.job.url的一致,程序中用的是8080端口,具体使用哪个配置,请参考maven中profiles的使用

5.运行admin项目,注意,此处端口要与quartz不同,程序中用的是80端口,浏览器中输入http://localhost/admin 运行结果如图:

image.png

二、代码引入

1、文件引入顺序:lib_parent → platform_parent → cloud_parent

2、代码结构

image.png

三、程序实现

quartz项目部分代码

1.quartz项目启动时,初始化数据库中的定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package com.ffxl.quartz.init;
 
import java.util.ArrayList;
import java.util.List;
 
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 
import com.ffxl.cloud.model.STimetask;
import com.ffxl.cloud.model.STimetaskExample;
import com.ffxl.cloud.model.base.BaseSTimetaskExample.Criteria;
import com.ffxl.cloud.model.warpper.ScheduleJob;
import com.ffxl.cloud.service.STimetaskService;
import com.ffxl.quartz.task.util.QuartzJobFactory;
import com.ffxl.quartz.task.util.QuartzJobFactoryDisallowConcurrentExecution;
 
 
/**
 * 根据上下文获取spring类
 *
 * @author
 */
public class InitQuartzJob implements ApplicationContextAware{
  private static final Logger logger = LoggerFactory.getLogger(InitQuartzJob.class);
   
  private static ApplicationContext appCtx;
  public static SchedulerFactoryBean schedulerFactoryBean = null;
 
 
  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    if (this.appCtx == null) {
      this.appCtx = applicationContext;
    }
  }
   
  public static void init() {
    schedulerFactoryBean = (SchedulerFactoryBean) appCtx.getBean(SchedulerFactoryBean.class);
    Scheduler scheduler = schedulerFactoryBean.getScheduler();
    try {
      logger.info(scheduler.getSchedulerName());
    } catch (SchedulerException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
    }
    // 这里从数据库中获取任务信息数据
    STimetaskService sTimetaskService = (STimetaskService) appCtx.getBean(STimetaskService.class);
    STimetaskExample example = new STimetaskExample();
    Criteria c = example.createCriteria();
    c.andJobStatusEqualTo("1"); // 已发布的定时任务
    List<STimetask> list = sTimetaskService.selectByExample(example);
    List<ScheduleJob> jobList = new ArrayList<ScheduleJob>();
    for (STimetask sTimetask : list) {
      ScheduleJob job1 = new ScheduleJob();
      job1.setJobId(sTimetask.getId());
      job1.setJobGroup(sTimetask.getGroupName()); // 任务组
      job1.setJobName(sTimetask.getName());// 任务名称
      job1.setJobStatus(sTimetask.getJobStatus()); // 任务发布状态
      job1.setIsConcurrent(sTimetask.getConcurrent() ? "1" : "0"); // 运行状态
      job1.setCronExpression(sTimetask.getCron());
      job1.setBeanClass(sTimetask.getBeanName());// 一个以所给名字注册的bean的实例
      job1.setMethodName(sTimetask.getMethodName());
      job1.setJobData(sTimetask.getJobData()); // 参数
      jobList.add(job1);
    }
 
    for (ScheduleJob job : jobList) {
      try {
        addJob(job);
      } catch (SchedulerException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }
 
  /**
   * 添加任务
   *
   * @param scheduleJob
   * @throws SchedulerException
   */
  public static void addJob(ScheduleJob job) throws SchedulerException {
    if (job == null || !ScheduleJob.STATUS_RUNNING.equals(job.getJobStatus())) {
      return;
    }
 
    Scheduler scheduler = schedulerFactoryBean.getScheduler();
    logger.debug(scheduler + "...........................................add");
    TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
 
    CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
 
    // 不存在,创建一个
    if (null == trigger) {
      Class clazz = ScheduleJob.CONCURRENT_IS.equals(job.getIsConcurrent()) ? QuartzJobFactory.class
                                                                           : QuartzJobFactoryDisallowConcurrentExecution.class;
 
      JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(job.getJobName(), job.getJobGroup()).usingJobData("data", job.getJobData()).build();
 
      jobDetail.getJobDataMap().put("scheduleJob", job);
 
      CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
 
      trigger = TriggerBuilder.newTrigger().withDescription(job.getJobId().toString()).withIdentity(job.getJobName(), job.getJobGroup())
          .withSchedule(scheduleBuilder).build();
 
      scheduler.scheduleJob(jobDetail, trigger);
    } else {
      // Trigger已存在,那么更新相应的定时设置
      CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
 
      // 按新的cronExpression表达式重新构建trigger
      trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).usingJobData("data", job.getJobData()).withSchedule(scheduleBuilder).build();
 
      // 按新的trigger重新设置job执行
      scheduler.rescheduleJob(triggerKey, trigger);
    }
  }
 
}

 

2.提供job对应的操作服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
package com.ffxl.quartz.task;
 
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import net.sf.json.JSONArray;
 
import org.apache.log4j.Logger;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.alibaba.fastjson.JSONObject;
import com.ffxl.cloud.annotation.ControllerLogAnnotation;
import com.ffxl.cloud.model.STimetask;
import com.ffxl.cloud.model.warpper.ScheduleJob;
import com.ffxl.platform.util.JsonResult;
import com.ffxl.platform.util.StringUtil;
import com.ffxl.quartz.init.InitQuartzJob;
 
@Component
@RequestMapping(value = "/opt")
public class JobSerlvet {
  public final Logger log = Logger.getLogger(this.getClass());
 
  @Autowired
  private SchedulerFactoryBean schedulerFactoryBean;
 
  /**
   * 获取所有计划中的任务列表
   *
   * @return
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/getAllJob")
  @ResponseBody
  @ControllerLogAnnotation(description = "获取所有计划中的任务列表")
  public void getAllJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    Scheduler scheduler = schedulerFactoryBean.getScheduler();
    GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
    Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
    List<ScheduleJob> jobList = new ArrayList<ScheduleJob>();
    for (JobKey jobKey : jobKeys) {
      List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
      for (Trigger trigger : triggers) {
        ScheduleJob job = new ScheduleJob();
        job.setJobId(trigger.getDescription());//description 放的是job的id
        job.setJobName(jobKey.getName());
        job.setJobGroup(jobKey.getGroup());
        job.setDescription("触发器:" + trigger.getKey());
        Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
        job.setJobStatus(triggerState.name());
        if (trigger instanceof CronTrigger) {
          CronTrigger cronTrigger = (CronTrigger) trigger;
          String cronExpression = cronTrigger.getCronExpression();
          job.setCronExpression(cronExpression);
        }
        jobList.add(job);
      }
    }
  //输出
    if(jobList.size() >0){
        JSONArray listArray=JSONArray.fromObject(jobList);
        Map<String,Object> m =new HashMap<String, Object>();
        m.put("job", listArray);
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\",\"data\":"+m+"}");
        out.close();
      }else{
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"没有计划任务\"}");
        out.close();
      }
     
  }
 
  /**
   * 所有正在运行的job
   *
   * @return
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/getRunningJob")
  @ResponseBody
  public void getRunningJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    Scheduler scheduler = schedulerFactoryBean.getScheduler();
    List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
    List<ScheduleJob> jobList = new ArrayList<ScheduleJob>(executingJobs.size());
    for (JobExecutionContext executingJob : executingJobs) {
      ScheduleJob job = new ScheduleJob();
      JobDetail jobDetail = executingJob.getJobDetail();
      JobKey jobKey = jobDetail.getKey();
      Trigger trigger = executingJob.getTrigger();
      job.setJobName(jobKey.getName());
      job.setJobGroup(jobKey.getGroup());
      job.setDescription("触发器:" + trigger.getKey());
      Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
      job.setJobStatus(triggerState.name());
      if (trigger instanceof CronTrigger) {
        CronTrigger cronTrigger = (CronTrigger) trigger;
        String cronExpression = cronTrigger.getCronExpression();
        job.setCronExpression(cronExpression);
      }
      jobList.add(job);
    }
    //输出
    if(jobList.size() >0){
      JSONArray listArray=JSONArray.fromObject(jobList);
      Map<String,Object> m =new HashMap<String, Object>();
      m.put("job", listArray);
      response.setHeader("Content-type", "text/html;charset=UTF-8");
      response.setCharacterEncoding("UTF-8");
      PrintWriter out = response.getWriter();
      out.write("{\"code\":\"2000\",\"message\":\"成功\",\"data\":"+m+"}");
      out.close();
    }else{
      response.setHeader("Content-type", "text/html;charset=UTF-8");
      response.setCharacterEncoding("UTF-8");
      PrintWriter out = response.getWriter();
      out.write("{\"code\":\"5000\",\"message\":\"没有正在执行的任务\"}");
      out.close();
    }
  }
   
  /**
   * 添加任务
   *
   * @param
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/addJob")
  @ResponseBody
  public void addJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in = request.getInputStream(); 
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    ScheduleJob job = new ScheduleJob();
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        job.setJobId(sTimetask.getId());
        job.setJobGroup(sTimetask.getGroupName()); //任务组
        job.setJobName(sTimetask.getName());// 任务名称
        job.setJobStatus(sTimetask.getJobStatus());  // 任务发布状态
        job.setIsConcurrent(sTimetask.getConcurrent()?"1":"0"); // 运行状态
        job.setCronExpression(sTimetask.getCron());
        job.setBeanClass(sTimetask.getBeanName());// 一个以所给名字注册的bean的实例
        job.setMethodName(sTimetask.getMethodName());
        job.setJobData(sTimetask.getJobData()); //参数
      }
    
    InitQuartzJob.addJob(job);
    //输入
    response.setHeader("Content-type", "text/html;charset=UTF-8");
    response.setCharacterEncoding("UTF-8");
    PrintWriter out = response.getWriter();
    out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
    out.close();
  }
 
  /**
   * 暂停一个job
   *
   * @param scheduleJob
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/pauseJob")
  @ResponseBody
  public void pauseJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in = request.getInputStream(); 
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey = JobKey.jobKey(sTimetask.getName(), sTimetask.getGroupName());
        scheduler.pauseJob(jobKey);
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
        out.close();
      }else{
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"任务不存在\"}");
        out.close();
      }
    }
     
  }
 
  /**
   * 恢复一个job
   *
   * @param scheduleJob
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/resumeJob")
  @ResponseBody
  public void resumeJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in = request.getInputStream(); 
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey = JobKey.jobKey(sTimetask.getName(), sTimetask.getGroupName());
        scheduler.resumeJob(jobKey);
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
        out.close();
      }else{
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"任务不存在\"}");
        out.close();
      }
    }
  }
 
  /**
   * 删除一个job
   *
   * @param scheduleJob
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/deleteJob")
  @ResponseBody
  public void deleteJob(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in = request.getInputStream(); 
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey = JobKey.jobKey(sTimetask.getName(), sTimetask.getGroupName());
        scheduler.deleteJob(jobKey);
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
        out.close();
      }else{
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"任务不存在\"}");
        out.close();
      }
    }
  }
 
  /**
   * 立即执行job
   *
   * @param scheduleJob
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/runAJobNow")
  @ResponseBody
  public void runAJobNow(HttpServletRequest request,HttpServletResponse response) throws SchedulerException, IOException {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in = request.getInputStream(); 
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey = JobKey.jobKey(sTimetask.getName(), sTimetask.getGroupName());
        scheduler.triggerJob(jobKey);
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
        out.close();
      }else{
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"任务不存在\"}");
        out.close();
      }
    }
     
     
     
  }
 
  /**
   * 更新job时间表达式
   *
   * @param scheduleJob
   * @throws SchedulerException
   * @throws IOException
   */
  @RequestMapping(value="/updateJobCron")
  @ResponseBody
  public void updateJobCron(HttpServletRequest request,HttpServletResponse response) throws SchedulerException{
    try {
    StringBuffer info=new StringBuffer(); 
    ServletInputStream in;
  
      in = request.getInputStream();
    
    BufferedInputStream buf = new BufferedInputStream(in); 
    byte[] buffer=new byte[1024];  
    int iRead; 
    while((iRead=buf.read(buffer))!=-1){ 
        info.append(new String(buffer,0,iRead,"UTF-8")); 
    }
    // 释放资源
    buf.close();
    in.close();
    
    if(info!=null&&!StringUtil.isEmpty(info.toString())){ 
      JSONObject json = JSONObject.parseObject(info.toString());
      STimetask sTimetask = JSONObject.toJavaObject(json, STimetask.class);
      if(sTimetask !=null){
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey = JobKey.jobKey(sTimetask.getName(), sTimetask.getGroupName());
 
        TriggerKey triggerKey = TriggerKey.triggerKey(sTimetask.getName(), sTimetask.getGroupName());
 
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
 
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(sTimetask.getCron());
 
        trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
 
        scheduler.rescheduleJob(triggerKey, trigger);
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"2000\",\"message\":\"成功\"}");
        out.close();
      }else{
        //输出
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":\"5000\",\"message\":\"任务不存在\"}");
        out.close();
      }
    }
  }catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
  
  }
}

3.编写 定时清除timeTaskLog 7天之前的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class TimeTaskLogDispatchController {
    private static final Logger LOGGER = Logger.getLogger(TimeTaskLogDispatchController.class);
    /**
     * 定时清除timeTaskLog 7天之前的记录
     */
    public void deleteTimeTaskLog(String data) {
        LOGGER.info("【定时清除timeTaskLog 7天之前的记录】>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>开始执行");
         
        STimetaskLogService bActiveService = (STimetaskLogService) ApplicationContextUtils.getBean(STimetaskLogService.class);
        Date currentDate = new Date();
        int day = -7;
        Date deleteDate = DateUtil.getAfterNumDay(currentDate, day);
        int ret = bActiveService.deleteLog(deleteDate);
        if(ret >0){
            LOGGER.info("【定时清除timeTaskLog 7天之前的记录】>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>执行成功");
        }else{
            LOGGER.info("【定时清除timeTaskLog 7天之前的记录】>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>执行失败");
        }
      }
}

4.dispatcher-servlet.xml中添加如下配置

1
2
3
4
5
6
<!-- 初始化springUtils -->
    <bean id="springUtils" class="com.ffxl.quartz.task.util.SpringUtils" />
    <!-- 初始化Scheduler -->
    <bean id="schedulerFactoryBean"  class="org.springframework.scheduling.quartz.SchedulerFactoryBean" />
    <!-- 初始化job  -->
    <bean id="initQuartzJob" class="com.ffxl.quartz.init.InitQuartzJob"  init-method="init"  lazy-init="false" />

admin项目部分代码
1.可视化项目的controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package com.ffxl.admin.controller.task;
 
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import javax.servlet.http.HttpSession;
 
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
 
import com.ffxl.admin.controller.BaseController;
import com.ffxl.admin.util.DataTablesUtil;
import com.ffxl.cloud.model.STimetask;
import com.ffxl.cloud.model.warpper.ScheduleJob;
import com.ffxl.cloud.service.STimetaskService;
import com.ffxl.platform.constant.Const;
import com.ffxl.platform.core.Page;
import com.ffxl.platform.exception.BusinessException;
import com.ffxl.platform.util.CronUtil;
import com.ffxl.platform.util.DateUtil;
import com.ffxl.platform.util.HttpConnectUtil;
import com.ffxl.platform.util.JsonDateValueProcessor;
import com.ffxl.platform.util.JsonResult;
import com.ffxl.platform.util.Message;
import com.ffxl.platform.util.StringUtil;
 
/**
 * 定时任务
 * @author wison
 *
 */
@Controller
@RequestMapping(value = "/task")
public class TimeTaskController extends BaseController {
    private  static String JOB_URL =  Const.QUARTZ_JOB_URL;
    private  static String ALL_JOB = JOB_URL+"/opt/getAllJob"; //所有计划中的任务列表
    private  static String RUNNING_JOB = JOB_URL+"/opt/getRunningJob";//所有正在运行的job
    private  static String ADD_JOB = JOB_URL+"/opt/addJob";//添加任务
    private  static String PAUSE_JOB =JOB_URL+ "/opt/pauseJob";//暂停一个job
    private  static String RESUME_JOB = JOB_URL+"/opt/resumeJob";//恢复一个job
    private  static String DELETE_JOB = JOB_URL+"/opt/deleteJob";//删除一个job
    private  static String RUNA_JOB =JOB_URL+ "/opt/runAJobNow";//立即执行job
    private  static String UPDATE_JOB = JOB_URL+"/opt/updateJobCron";//更新job时间表达式
     
     
    private static final Logger logger = LoggerFactory.getLogger(TimeTaskController.class);
    @Autowired
    private STimetaskService stimetaskService;
 
     
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        DateFormat fmt = new SimpleDateFormat(DateUtil.STANDARD_DATE_FORMAT_STR);
        CustomDateEditor dateEditor = new CustomDateEditor(fmt, true);
        binder.registerCustomEditor(Date.class, dateEditor);
    }
 
    /**
     * 列表页面跳转
     * @return
     */
    @RequestMapping(value="/list")
    public ModelAndView userList(STimetask task){
        ModelAndView mv = this.getModelAndView();
        mv.setViewName("system/timeTaskList");
        return mv;
    }
     
    /**
     * 列表
     * @return
     */
    @RequestMapping(value="/task_list")
    @ResponseBody
    public JsonResult taskList(DataTablesUtil dataTables, STimetask task, Page page, HttpSession session){
         List<STimetask> list = stimetaskService.selectByPage(task, page);
         // 查询task的运行状态
         
         String result = HttpConnectUtil.httpRequest(RUNNING_JOB, Const.REQUEST_METHOD_POST, null);
         if(result!=null){
         JSONObject jsonResult = JSONObject.fromObject(result);
         Map<String, ScheduleJob> map = new HashMap<String, ScheduleJob>();
         if( jsonResult.get("code").equals("2000")){
           JSONObject js = (JSONObject) jsonResult.get("data");
           JSONArray dataArray = (JSONArray) js.get("job");
           if(dataArray.size() > 0){
             List<ScheduleJob> jobList =  JSONArray.toList(dataArray,ScheduleJob.class);
               for(ScheduleJob job: jobList){
                 map.put(job.getJobId().toString(), job);
               }
           }
         }
         for(STimetask st: list){
               if(map.containsKey(st.getId())){
                 st.setConcurrent(true);
               }
             }
         }
         // 查询task的计划状态
         String planResult = HttpConnectUtil.httpRequest(ALL_JOB, Const.REQUEST_METHOD_POST, null);
         if(planResult!=null){
         JSONObject jsonPlanResult = JSONObject.fromObject(planResult);
         Map<String, ScheduleJob> planMap = new HashMap<String, ScheduleJob>();
         if(jsonPlanResult.get("code").equals("2000")){
           JSONObject js = (JSONObject) jsonPlanResult.get("data");
           JSONArray dataArray = (JSONArray) js.get("job");
           if(dataArray.size() > 0){
             List<ScheduleJob> jobList =  JSONArray.toList(dataArray,ScheduleJob.class);
             for(ScheduleJob job: jobList){
               planMap.put(job.getJobId().toString(), job);
             }
           }
         }
         for(STimetask st: list){
               if(planMap.containsKey(st.getId())){
                 String status = planMap.get(st.getId()).getJobStatus();
                 st.setPlanStatus(status);
               }
             }
           }
          //返回dataTable所需数据
         dataTables = this.getDataTables(page, dataTables, list);
        return new JsonResult("2000", dataTables);
    }
     
    /**
     * 立即执行一次job
     * 用于测试任务是否正确
     * @param id
     * @return
     */
    @RequestMapping(value="/run_task2job")
    @ResponseBody
    public JsonResult run_task2job(String id){
      //查询task
      STimetask stimetask = stimetaskService.selectByPrimaryKey(id);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      JSONObject jsonArray = JSONObject.fromObject(stimetask,jsonConfig);
      String result = HttpConnectUtil.httpRequest(RUNA_JOB, Const.REQUEST_METHOD_POST, jsonArray.toString());
      logger.info(result);
      if(result ==null){
        return new JsonResult("5000", "定时项目未启动",null);
      }else{
        return new JsonResult("2000", null);
      }
    }
     
    /**
     * 添加job到计划列表
     * @param id
     * @return
     */
    @RequestMapping(value="/add_task2job")
    @ResponseBody
    public JsonResult add_task2job(String id){
      //查询task
      STimetask stimetask = stimetaskService.selectByPrimaryKey(id);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      JSONObject jsonArray = JSONObject.fromObject(stimetask,jsonConfig);
      String result = HttpConnectUtil.httpRequest(ADD_JOB, Const.REQUEST_METHOD_POST, jsonArray.toString());
      logger.info(result);
      if(result ==null){
        return new JsonResult("5000", "定时项目未启动",null);
      }else{
        return new JsonResult("2000", null);
      }
      
    }
     
    /**
     * 从计划列表中暂停job
     * @param id
     * @return
     */
    @RequestMapping(value="/stop_task2job")
    @ResponseBody
    public JsonResult stop_task2job(String id){
      //查询task
      STimetask stimetask = stimetaskService.selectByPrimaryKey(id);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      JSONObject jsonArray = JSONObject.fromObject(stimetask,jsonConfig);
      String result = HttpConnectUtil.httpRequest(PAUSE_JOB, Const.REQUEST_METHOD_POST, jsonArray.toString());
      logger.info(result);
      if(result ==null){
        return new JsonResult("5000", "定时项目未启动",null);
      }else{
        return new JsonResult("2000", null);
      }
    }
    /**
     * 从计划列表中移除job
     * @param id
     * @return
     */
    @RequestMapping(value="/remove_task2job")
    @ResponseBody
    public JsonResult remove_task2job(String id){
      //查询task
      STimetask stimetask = stimetaskService.selectByPrimaryKey(id);
      JsonConfig jsonConfig = new JsonConfig();
      jsonConfig.registerJsonValueProcessor(Date.class, new JsonDateValueProcessor());
      JSONObject jsonArray = JSONObject.fromObject(stimetask,jsonConfig);
      String result = HttpConnectUtil.httpRequest(DELETE_JOB, Const.REQUEST_METHOD_POST, jsonArray.toString());
      logger.info(result);
      if(result ==null){
        return new JsonResult("5000", "定时项目未启动",null);
      }else{
        return new JsonResult("2000", null);
      }
    }
     
    /**
     * 变更job状态
     * @param id
     * @return
     */
    @RequestMapping(value="/update_task")
    @ResponseBody
    public JsonResult update_task(String ids,String type){
      //查询task
      String[] idArray = ids.split(",");
      Map<String,String> selectedIdMap =  new HashMap<String,String>();
      List<String> idList = new ArrayList<String>();
      for (int i = 0; i < idArray.length; i++) {
          idList.add(idArray[i]);
      }
      int ret = stimetaskService.updatebyOperate(idList,type);
      if(ret >0){
          return new JsonResult(true);
      }else{
          return new JsonResult(false);
      }
    }
     
    /**
     * 删除job
     * @param id
     * @return
     */
    @RequestMapping(value="/delete_task")
    @ResponseBody
    public JsonResult delete_task(String ids){
      //查询task
      String[] idArray = ids.split(",");
      Map<String,String> selectedIdMap =  new HashMap<String,String>();
      List<String> idList = new ArrayList<String>();
      for (int i = 0; i < idArray.length; i++) {
          idList.add(idArray[i]);
      }
      int ret = stimetaskService.deleteByIds(idList);
      if(ret >0){
          return new JsonResult(true);
      }else{
          return new JsonResult(false);
      }
    }
     
     
     
     
     
    /**
     * 详情页面
     * @return
     */
    @RequestMapping(value="/task_detail")
    public ModelAndView detail(String id){
        ModelAndView mv = this.getModelAndView();
        STimetask model = new STimetask();
        model = stimetaskService.selectByPrimaryKey(id);
        mv.addObject("model", model);
        mv.setViewName("system/timeTaskDetail");
        return mv;
    }
    /**
     * 解析cron
     * @return
     */
    @RequestMapping(value="/analysis_cron")
    @ResponseBody
    public JsonResult analysisCron(String cron){
        try {
          Date date = new Date();
          String dateStr = DateUtil.formatStandardDatetime(date);
          List<String> dateList = CronUtil.cronAlgBuNums(cron, dateStr, 5);
          return new JsonResult("2000", dateList);
        } catch (Exception e) {
          e.printStackTrace();
          return new JsonResult("5000", null);
        }
    }
     
    /**
     * 验证名称是否存在
     * @param id
     * @param groupName
     * @param name
     * @return
     *
     */
    @RequestMapping(value="/check_name")
    @ResponseBody
    public Boolean check_name(String id, String groupName, String name){
      if(StringUtil.isEmpty(groupName,name)){
        throw new BusinessException(Message.M4003);
      }
      STimetask task = new STimetask();
      task.setId(id);
      task.setGroupName(groupName);
      task.setName(name);
      STimetask queryTask = stimetaskService.checkName(task);
      if(queryTask !=null){
        logger.debug("组.任务名 exists,return false");
        return false;
      }else{
        logger.debug("组.任务名 not exists,return true");
        return true;
      }
    }
     
    /**
     * 保存
     * @return
     */
    @RequestMapping(value="/task_save")
    @ResponseBody
    public JsonResult userSave(STimetask task,  HttpSession session){
        //获取系统操作人员
        String longName = "admin";
        task.setModifyUserId(longName);
        try{
            int ret= stimetaskService.insertOrUpdateByUser(task,longName);
            if(ret > 0){
                return new JsonResult("2000",task);
            }else{
                return new JsonResult("5000");
            }
        }catch(BusinessException e){
            return new JsonResult("5001",e.getMessage(),null);
        }   
    }
     
}

2.前端页面

1.png

可以看到在这里我把定时任务的状态分为两大类,任务状态跟业务有关,分为已发布和未发布;计划状态跟定时任务的运行有关,分为None,正常运行,已暂停,任务执行中,线程阻塞,未计划,错误

2.1计划状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function formatPlan(value, rowData, rowIndex){
            if(value=="None"){
                return "<span class='text-danger'>None</span>"
            }
            if(value=="NORMAL"){
                return "<span class='text-success'>正常运行</span>"
            }
            if(value=="PAUSED"){
                return "<span class='text-yellow'>已暂停</span>"
            }
            if(value=="COMPLETE"){
                return "<span class='text-success'>任务执行中</span>"
            }
            if(value=="BLOCKED"){
                return "<span class='text-danger'>线程阻塞</span>"
            }
            if(value=="ERROR"){
                return "<span class='text-danger'>错误</span>"
            }else{
                return "<span class='text-danger'>未计划</span>"
            }  
        }

 

2.2操作逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function formatOpt(value, rowData, rowIndex) {
            var msg = "";
            msg+="<a href='#' class='btnstyle'  onclick='showDetail(\""
                + rowData.id + "\")'>编辑</a>";
            //已发布,
            if(rowData.jobStatus=='1'){
                var value = rowData.planStatus;
                if(value=="None"|| value==null){
                    msg +='<a  href="#"class="btnstyle"  onclick="addJob(\''+rowData.id+'\')" '
                        +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                        +' data-placement="top" title="定时任务按照计划开始执行" >计划</a>';
                }
                if(value=="NORMAL"){
                    msg +=  '<a  href="#"class="btnstyle"  onclick="runJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="紧执行一次" >立即执行</a>'
                    +   '<a  href="#"class="btnstyle"  onclick="stopJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="定时任务暂时停止执行" >暂停</a>'
                    +'<a  href="#"class="btnstyle"  onclick="removeJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="定时任务从计划列表中移除" >移除</a>';
                }
                if(value=="PAUSED"){
                    msg +=  '<a  href="#"class="btnstyle"  onclick="runJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="紧执行一次" >立即执行</a>'
                     
                         '<a  href="#"class="btnstyle"  onclick="addJob(\''+rowData.id+'\')" '
                        +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                        +' data-placement="top" title="定时任务按照计划开始执行" >计划</a>'
                        +'<a  href="#"class="btnstyle"  onclick="removeJob(\''+rowData.id+'\')" '
                        +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                        +' data-placement="top" title="定时任务从计划列表中移除" >移除</a>';
                }
                if(value=="COMPLETE"){
                    msg +=  '<a  href="#"class="btnstyle"  onclick="runJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="紧执行一次" >立即执行</a>'
                }
                if(value=="BLOCKED"){
                    msg +=  '<a  href="#"class="btnstyle"  onclick="runJob(\''+rowData.id+'\')" '
                    +'onMouseOver="popTip(this,\' '+rowData+' \' )" class="btn btn-info btn-xs" data-toggle="tooltip"'
                    +' data-placement="top" title="紧执行一次" >立即执行</a>'
                }
                if(value=="ERROR"){
                     
                }
            }
            return  msg;
        }

简单概述为:已发布的定时任务出现【计划】按钮;执行【计划】后,定时任务正常运行,且出现【立即执行】【暂停】【移除】三个按钮,【立即执行】用于测试当前定时任务的业务逻辑是否正确,【暂停】很容易理解,就是把当前任务暂停,及时到达cron定义的时间,定时任务也不会执行,【移除】仅仅是把计划列表中相应的任务暂时清除掉,你可以理解为清除缓存中的定时任务,并不是物理删除

3.定时任务详情

2.png

3.点击规则弹出页面如下,可自行修改界面,这里我是从网上随便找了个插件,然后按照自己想要的修改下源码

3.png

 

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

posted on   demo例子集  阅读(10496)  评论(0编辑  收藏  举报

(评论功能已被禁用)
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示