概要
背景:960系统架构老旧,采用过时的JKD1.6,并且还是单服务架构,为了实现集群部署,提高系统的健壮性,必须要分离系统定时器(针对jdk1.6未找到可以使用的调度系统),特此自研一套定时任务调度系统,可以实现自动轮询、故障转移等功能。设计思路:960实现动态配置,自定义注解,通过暴露的接口参数反射到特定的定时任务上;建立调度系统,实现手动自动注册的方式,实现定时任务动态管理,可查询执行日志;使用http/https进行交互,减轻使用门槛及提高跨平台兼容性。
调度平台就只展示一下几个关键页面吧,后面再来写相关文档,本次重点介绍基于jdk1.6、springmvc3分离系统定时任务的实现:
整体架构
jdk1.6、 spingmvc、反射 系统启动后自动注册调度系统,通过在调度系统后台配置与@AsdJob("staticBuild")对应的名称,通过接口回调回来,再通过反射执行定时任务方法
技术名词解释
- 集群部署:系统冗余部署,主要好处如下:1、均衡系统压⼒,减少单个服务器因压⼒过于集成⽽超负荷运转的发⽣⼏率。2 加快⽤户的访问的速度。3 ⾃动备援,避免因单点故障⽽导致整个系统瘫痪的情况
技术细节
960系统改造部分
- 配置文件
asd-job.url=http://192.0.1.0:8034/Cate/register
asd-job.access_token=677c71c7b5xxxxxxxxxx9952b0
asd-job.heart_time=60000
asd-job.address=http://192.2.0.1:8011/asdJob/execute.jhtml
asd-job.name=960-job
- 配置类JobConfig
package net.shopxx.job.config;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
/**
* @author zhanqi
* @since 2023/3/24 16:07
*/
@Component
public class JobConfig {
private String asdJobUrl;
private String address;
private String name;
private String accessToken;
private Integer heartTime;
public String getAsdJobUrl() {
return asdJobUrl;
}
public void setAsdJobUrl(String asdJobUrl) {
this.asdJobUrl = asdJobUrl;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public Integer getHeartTime() {
return heartTime;
}
public void setHeartTime(Integer heartTime) {
this.heartTime = heartTime;
}
public JobConfig() {
}
public JobConfig(String asdJobUrl, String address, String name, String accessToken,Integer hearttime) {
this.asdJobUrl = asdJobUrl;
this.address = address;
this.name = name;
this.accessToken = accessToken;
this.heartTime = heartTime;
}
public void execute() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(doGet(String.format("%s?auth=%s&accessToken=%s&name=%s&address=%s", asdJobUrl, "xxxxxxxxx",accessToken, name, address)));
}
}, 0,heartTime);
}
public String doGet(String httpUrl) {
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
URL url = new URL(httpUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(15000);
connection.setReadTimeout(60000);
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 封装输入流is,并指定字符集
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
// 存放数据
StringBuilder sb = new StringBuilder();
String temp;
while ((temp = bufferedReader.readLine()) != null) {
sb.append(temp);
sb.append("\n");
}
result = sb.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != connection) {
connection.disconnect();
}
}
return result;
}
}
- applicationContext.xml 加入配置
<bean id="jobConfig" class="net.shopxx.job.config.JobConfig">
<property name="asdJobUrl" value="${asd-job.url}" />
<property name="address" value="${asd-job.address}" />
<property name="name" value="${asd-job.name}" />
<property name="accessToken" value="${asd-job.access_token}"/>
<property name="heartTime" value="${asd-job.heart_time}"/>
</bean>
- 定义一个自定义注解
package net.shopxx.job.annotation;
/**
* @author zhanqi
* @since 2023/3/24 16:07
*/
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AsdJob {
String value();
}
- 写一个BaseJob类方便扫描特定类
package net.shopxx.job.base;
/**
* @author zhanqi
* @since 2023/3/23 19:04
*/
public class BaseJob {
}
- 实现一个api接口来与调度平台交互,通过参数反射方法执行定时任务
/*
* Copyright 2005-2013 shopxx.net. All rights reserved.
* Support: http://www.shopxx.net
* License: http://www.shopxx.net/license
*/
package net.shopxx.job.server;
import com.alibaba.fastjson.JSONObject;
import net.shopxx.controller.mobile.BaseController;
import net.shopxx.job.annotation.AsdJob;
import net.shopxx.job.base.BaseJob;
import net.shopxx.util.SpringUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
@Controller("asdJob")
@RequestMapping("/asdJob")
public class AsdJobController extends BaseController {
Set<BeanDefinition> components =new ConcurrentSkipListSet<BeanDefinition>();
/**
* 执行定时器
*/
@RequestMapping(value = "/execute", method = RequestMethod.POST)
@ResponseBody
public Object execute(@RequestBody JSONObject json) {
Object obj = exeMethod(json);
return obj;
}
/**
* 心跳检查
*/
@RequestMapping(value = "/check", method = RequestMethod.POST)
@ResponseBody
public Integer check() {
return 200;
}
/**
* 反射执行方法
* @param json
* @return
*/
private Object exeMethod(JSONObject json){
if(components.size()<1){
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(BaseJob.class));
components = provider.findCandidateComponents("net.shopxx.job");
}
String params = json.getString("params");
String url = json.getString("url");
Iterator<BeanDefinition> it = components.iterator();
while (it.hasNext()) {
try {
Class<?> cls = Class.forName(it.next().getBeanClassName());
Object o= SpringUtils.getBean(cls.getName());
System.out.println(o);
Method[] methods=o.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
AsdJob asdJob= methods[i].getAnnotation(AsdJob.class);
if(asdJob!=null&&asdJob.value().equals(url)){
try {
if (StringUtils.isBlank(params)) {
return methods[i].invoke(o);
}
return methods[i].invoke(o,params);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return "找不到对应的任务名称";
}
}
- 定时任务改造示例
package net.shopxx.job;
import net.shopxx.job.annotation.AsdJob;
import net.shopxx.job.base.BaseJob;
import net.shopxx.service.StaticService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Job - 静态化
*
* @author SHOP++ Team
* @version 3.0
*/
@Component("staticJob")
@Lazy(false)
public class StaticJob extends BaseJob {
@Resource(name = "staticServiceImpl")
private StaticService staticService;
/**
* 生成静态
*/
// @Scheduled(cron = "${job.staticBuild.cron}")
@AsdJob("staticBuild")
public void build() {
staticService.buildAll();
}
}
- 实现类初始化赋值StartupListener,后期可以不用配置applicationContext.xml
package net.shopxx.listener;
import net.shopxx.job.config.JobConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Service;
/**
* @author zhanqi
* @since 2023/3/24 17:07
*/
/**
* 启动监听器
*
* @author Storezhang
*/
@Service("startupListener")
@Lazy
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private JobConfig jobConfig;
@Override
public void onApplicationEvent(ContextRefreshedEvent evt) {
jobConfig.execute();
}
}
小结
只要你想做这件事是正确的,任何妖魔鬼怪都阻止不了你