一、完整介绍地址:官方介绍

https://www.xuxueli.com/xxl-job/#/?id=%E4%B8%80%E3%80%81%E7%AE%80%E4%BB%8B

二、最新版本架构图:

三、介绍

目前我们在项目中可能接触到定时任务框架quartz,应用也是比较广泛的,其也是支持分布式任务调度的,通过数据库竞争锁来实现,当然会有很多的局限性(可能这也是xxl-job出现的原因),quartz支持多种数据库(quartz/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore at master · quartz-scheduler/quartz · GitHub),xxl-job其实也是在quartz的基础上实现的,但是修改了任务调度的模式,并且任务调度采用注册和RPC调用方式来实现。

管理后台:

四、技术栈

mysql、SSM,内置jetty作为RPC服务调用、quartz

五、xxl-job支持Postgresql数据库

目前由于xxl-job只支持mysql数据库,目前在github上拉了一个分支支持Postgresql 地址GitHub地址

六、 xxl-job 底层依赖 quartz 实现,架构如下

--------------------------------------------------------------------------------------------------------------------------------------------------

1、xxl-job添加执行器到任务调度中心有两种方式

(1)客户端执行器自动将名称和机器地址注册到任务调度中心

(2)可以在任务调度中心手动录入执行器名称和相关的机器地址(多个机器地址用逗号隔开)

2、自动注册流程

(1)在执行器客户端配置执行器名称和任务调度中心地址:

### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
##任务调度中心地址
xxl.job.admin.addresses=http://127.0.0.1:8080

### xxl-job executor address
##任务调度器名称和机器信息
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.ip=127.0.0.1
xxl.job.executor.port=9998
(2)相关注册执行代码:

在执行器启动时会读取配置,当存在任务调度中心地址会依次向任务调度中心注册其地址

XxlJobExecutor类在进行初始化时会进行如下操作。

//当存在多个任务调度中心时,创建代理类并注册,在NetComClientProxy
private static void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
if (adminAddresses!=null && adminAddresses.trim().length()>0) {
for (String address: adminAddresses.trim().split(",")) {
if (address!=null && address.trim().length()>0) {
String addressUrl = address.concat(AdminBiz.MAPPING);
AdminBiz adminBiz = (AdminBiz) new NetComClientProxy(AdminBiz.class, addressUrl, accessToken).getObject();
if (adminBizList == null) {
adminBizList = new ArrayList<AdminBiz>();
}
adminBizList.add(adminBiz);
}
}
}
}

在XxlJobExecutor被调用时执行getObject方法,完成向任务调度中心发送请求进行服务注册操作。

@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[] { iface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// filter method like "Object.toString()"
if (Object.class.getName().equals(method.getDeclaringClass().getName())) {
logger.error(">>>>>>>>>>> xxl-rpc proxy class-method not support [{}.{}]", method.getDeclaringClass().getName(), method.getName());
throw new RuntimeException("xxl-rpc proxy class-method not support");
}

// request
RpcRequest request = new RpcRequest();
request.setServerAddress(serverAddress);
request.setCreateMillisTime(System.currentTimeMillis());
request.setAccessToken(accessToken);
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);

// send
//向任务调度中心发送请求进行服务注册
RpcResponse response = client.send(request);

// valid response
if (response == null) {
throw new Exception("Network request fail, response not found.");
}
if (response.isError()) {
throw new RuntimeException(response.getError());
} else {
return response.getResult();
}

}
});

在JettyClient中调用send方法完成服务注册操作

public RpcResponse send(RpcRequest request) throws Exception {
try {
// serialize request
byte[] requestBytes = HessianSerializer.serialize(request);

// reqURL
String reqURL = request.getServerAddress();
if (reqURL!=null && reqURL.toLowerCase().indexOf("http")==-1) {
reqURL = "http://" + request.getServerAddress() + "/"; // IP:PORT, need parse to url
}
//发送post请求进行服务注册,简单注册一下IP和端口信息等
// remote invoke
byte[] responseBytes = HttpClientUtil.postRequest(reqURL, requestBytes);
if (responseBytes == null || responseBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Network request fail, RpcResponse byte[] is null");
return rpcResponse;
}

// deserialize response
RpcResponse rpcResponse = (RpcResponse) HessianSerializer.deserialize(responseBytes, RpcResponse.class);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);

RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Network request error: " + e.getMessage());
return rpcResponse;
}
}

服务注册地址:http://127.0.0.1:8080/api

相关注册信息如下:

总结:其实执行器注册到任务调度的信息非常简单,可以就简单的认为为应用的一些基本信息,IP、端口和应用名称等等,并不用将具体的任务类等信息注册到任务调度中心,所以任务调度中心无法感知执行器一些具体信息,只能需要靠运维和技术人员在任务调度中心进行选择配置,否则可能会无法正常执行任务。

————————————————

一、注册地址

地址:http://127.0.0.1:8080/api

任务调度中心对外提供注册地址/api用来接受任务执行器注册的相关服务器信息

1、xxl-job admin通过JobApiController来对外提供/api接口

@Controller
public class JobApiController {
private static Logger logger = LoggerFactory.getLogger(JobApiController.class);

private RpcResponse doInvoke(HttpServletRequest request) {
try {
// deserialize request
byte[] requestBytes = HttpClientUtil.readBytes(request);
if (requestBytes == null || requestBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("RpcRequest byte[] is null");
return rpcResponse;
}
//反序列化数据
RpcRequest rpcRequest = (RpcRequest) HessianSerializer.deserialize(requestBytes, RpcRequest.class);

// invoke
//调用服务注册方法
RpcResponse rpcResponse = NetComServerFactory.invokeService(rpcRequest, null);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);

RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Server-error:" + e.getMessage());
return rpcResponse;
}
}

//对外提供api接口
@RequestMapping(AdminBiz.MAPPING)
@PermessionLimit(limit=false)
public void api(HttpServletRequest request, HttpServletResponse response) throws IOException {

// invoke
RpcResponse rpcResponse = doInvoke(request);

// serialize response
byte[] responseBytes = HessianSerializer.serialize(rpcResponse);

response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
//baseRequest.setHandled(true);

OutputStream out = response.getOutputStream();
out.write(responseBytes);
out.flush();
}

}

在NetComServerFactory类中调用invokeService方法,根据反射调用AdminBiz接口的实现类AdminBizImpl的register方法完成服务注册操作

public static RpcResponse invokeService(RpcRequest request, Object serviceBean) {
if (serviceBean==null) {
serviceBean = serviceMap.get(request.getClassName());
}
if (serviceBean == null) {
// TODO
}

RpcResponse response = new RpcResponse();

if (System.currentTimeMillis() - request.getCreateMillisTime() > 180000) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The timestamp difference between admin and executor exceeds the limit."));
return response;
}
if (accessToken!=null && accessToken.trim().length()>0 && !accessToken.trim().equals(request.getAccessToken())) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The access token[" + request.getAccessToken() + "] is wrong."));
return response;
}

try {
//接口AdminBiz的实现类AdminBizImpl
Class<?> serviceClass = serviceBean.getClass();
//AdminBiz的register方法
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();

FastClass serviceFastClass = FastClass.create(serviceClass);
//调用AdminBizImpl的register方法
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);

Object result = serviceFastMethod.invoke(serviceBean, parameters);

response.setResult(result);
} catch (Throwable t) {
t.printStackTrace();
response.setError(t.getMessage());
}

return response;
}

调用的类:

参数:基本的服务器信息

在AdminBiz的实现类AdminBizImpl中调用dao完成注册信息入库操作:

@Override
public ReturnT<String> registry(RegistryParam registryParam) {
//在xxl_job_qrtz_trigger_registry表中添加或更新数据
int ret = xxlJobRegistryDao.registryUpdate(registryParam.getRegistGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
if (ret < 1) {
xxlJobRegistryDao.registrySave(registryParam.getRegistGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
}
return ReturnT.SUCCESS;
}
表中数据:

在任务调度中心会列表展示数据:

总结:简单来说执行器将其服务器信息通过http请求发送到任务调度中心,调度中心将相关注册信息插入或更新到xxl_job_qrtz_trigger_registry表中,在任务调度中心通过列表展示,接下来相关任务执行时会根据服务器信息通过http发送调用请求。
————————————————

在任务调度中心可以进行新建任务,新建任务之后可以在任务列表中查看相关任务,任务可以根据我们配置的cron表达式进行任务调度,或者也可以在任务列表中执行、暂停、删除和查看相关运行日志等操作。

一、任务调度中心管理

1、新建任务

2、任务列表任务操作

二、任务创建与操作

我们了解到xxl-job是基于quartz来实现定时任务的(其实任务调度中心任务执行基于quartz,任务执行器就是一个被调用执行的服务)

1、任务创建

调用JobInfoController的add方法添加任务

@RequestMapping("/add")
@ResponseBody
public ReturnT<String> add(XxlJobInfo jobInfo) {
//添加定时器
return xxlJobService.add(jobInfo);
}
调用XxlJobService的add方法,新建quartz任务

@Override
public ReturnT<String> add(XxlJobInfo jobInfo) {
// valid
//获取任务分组
XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup());

//省略部分任务相关信息检查的代码

// add in db
//将任务持久化到数据库的xxl_job_qrtz_trigger_info
xxlJobInfoDao.save(jobInfo);
if (jobInfo.getId() < 1) {
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) );
}

// add in quartz
String qz_group = String.valueOf(jobInfo.getJobGroup());
String qz_name = String.valueOf(jobInfo.getId());
try {
//添加任务到quartz中
XxlJobDynamicScheduler.addJob(qz_name, qz_group, jobInfo.getJobCron());
//XxlJobDynamicScheduler.pauseJob(qz_name, qz_group);
return ReturnT.SUCCESS;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
try {
xxlJobInfoDao.delete(jobInfo.getId());
XxlJobDynamicScheduler.removeJob(qz_name, qz_group);
} catch (SchedulerException e1) {
logger.error(e.getMessage(), e1);
}
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail"))+":" + e.getMessage());
}
}

数据库持久化数据:

在XxlJobDynamicScheduler中调用quartz的创建任务方法构建定时任务,定时任务执行类为QuartzJobBean的子类RemoteHttpJobBean,在定时任务执行时RemoteHttpJobBean会调用任务执行器接口,执行相关任务

public static boolean addJob(String jobName, String jobGroup, String cronExpression) throws SchedulerException {
// TriggerKey : name + group
//创建定时器别名
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
JobKey jobKey = new JobKey(jobName, jobGroup);

// TriggerKey valid if_exists
if (checkExists(jobName, jobGroup)) {
logger.info(">>>>>>>>> addJob fail, job already exist, jobGroup:{}, jobName:{}", jobGroup, jobName);
return false;
}

// CronTrigger : TriggerKey + cronExpression // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
//创建cron定时器
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();

// JobDetail : jobClass
//定时执行类
Class<? extends Job> jobClass_ = RemoteHttpJobBean.class; // Class.forName(jobInfo.getJobClass());

//创建任务
JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build();
/*if (jobInfo.getJobData()!=null) {
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));
// JobExecutionContext context.getMergedJobDataMap().get("mailGuid");
}*/

// schedule : jobDetail + cronTrigger
//添加定时任务到quartz
Date date = scheduler.scheduleJob(jobDetail, cronTrigger);

logger.info(">>>>>>>>>>> addJob success, jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date);
return true;
}

2、删除任务

删除任务简单来说就是将任务从quartz中移除,不再执行就好

JobInfoController中提供了删除任务接口

@RequestMapping("/remove")
@ResponseBody
public ReturnT<String> remove(int id) {
return xxlJobService.remove(id);
}
在XxlJobService中根据逻辑id删除任务,并且删除数据库中相关持久化数据

@Override
public ReturnT<String> remove(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());

try {
//从quartz中移除任务,并将数据库中相关数据删除
XxlJobDynamicScheduler.removeJob(name, group);
xxlJobInfoDao.delete(id);
xxlJobLogDao.delete(id);
xxlJobLogGlueDao.deleteByJobId(id);
return ReturnT.SUCCESS;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
}
return ReturnT.FAIL;
}

在XxlJobDynamicScheduler中调用removeJob方法删除相关任务

public static boolean removeJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
boolean result = false;
if (checkExists(jobName, jobGroup)) {
//删除任务
result = scheduler.unscheduleJob(triggerKey);
logger.info(">>>>>>>>>>> removeJob, triggerKey:{}, result [{}]", triggerKey, result);
}
return true;
}
3、暂停任务

暂停任务同意调用quartz的接口进行任务暂停处理

JobInfoController中提供接口处理

@RequestMapping("/pause")
@ResponseBody
public ReturnT<String> pause(int id) {
return xxlJobService.pause(id);
}
调用XxlJobService的paus进行任务停止

@Override
public ReturnT<String> pause(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());

try {
boolean ret = XxlJobDynamicScheduler.pauseJob(name, group); // jobStatus do not store
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
return ReturnT.FAIL;
}
}
调用XxlJobDynamicScheduler的pauseJob进行任务停止,最终还是执行quartz 的接口完成任务暂停

public static boolean pauseJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);

boolean result = false;
if (checkExists(jobName, jobGroup)) {
//任务暂停
scheduler.pauseTrigger(triggerKey);
result = true;
logger.info(">>>>>>>>>>> pauseJob success, triggerKey:{}", triggerKey);
} else {
logger.info(">>>>>>>>>>> pauseJob fail, triggerKey:{}", triggerKey);
}
return result;
}

4、恢复任务

恢复任务就是将暂停的任务继续重新执行

JobInfoController中提供接口重新执行任务

@RequestMapping("/resume")
@ResponseBody
public ReturnT<String> resume(int id) {
return xxlJobService.resume(id);
}
调用XxlJobService的resume接口继续任务

@Override
public ReturnT<String> resume(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());

try {
//继续任务
boolean ret = XxlJobDynamicScheduler.resumeJob(name, group);
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
return ReturnT.FAIL;
}
}

在XllJobDynamicScheduler中调用quartz的接口重新开启任务

public static boolean resumeJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);

boolean result = false;
if (checkExists(jobName, jobGroup)) {
//继续任务
scheduler.resumeTrigger(triggerKey);
result = true;
logger.info(">>>>>>>>>>> resumeJob success, triggerKey:{}", triggerKey);
} else {
logger.info(">>>>>>>>>>> resumeJob fail, triggerKey:{}", triggerKey);
}
return result;
}

5、更新任务

任务更新操作就是更新一下任务相关参数

XxlJobController中提供update接口,完成任务更新操作

@RequestMapping("/update")
@ResponseBody
public ReturnT<String> update(XxlJobInfo jobInfo) {
return xxlJobService.update(jobInfo);
}
在XxlJobService中调用update更新任务,并更新相关数据库中的数据

@Override
public ReturnT<String> update(XxlJobInfo jobInfo) {

//省略部分判断代码
// stage job info
XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId());
if (exists_jobInfo == null) {
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) );
}
//String old_cron = exists_jobInfo.getJobCron();

exists_jobInfo.setJobCron(jobInfo.getJobCron());
exists_jobInfo.setJobDesc(jobInfo.getJobDesc());
exists_jobInfo.setAuthor(jobInfo.getAuthor());
exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail());
exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy());
exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler());
exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam());
exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
exists_jobInfo.setExecutorFailStrategy(jobInfo.getExecutorFailStrategy());
exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout());
exists_jobInfo.setChildJobId(jobInfo.getChildJobId());
xxlJobInfoDao.update(exists_jobInfo);

// fresh quartz
String qz_group = String.valueOf(exists_jobInfo.getJobGroup());
String qz_name = String.valueOf(exists_jobInfo.getId());
try {
//重新配置定时任务
boolean ret = XxlJobDynamicScheduler.rescheduleJob(qz_group, qz_name, exists_jobInfo.getJobCron());
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
}

return ReturnT.FAIL;
}

在XxlJobDynamicScheduler中调用quartz相关方法重新配置定时任务

public static boolean rescheduleJob(String jobGroup, String jobName, String cronExpression) throws SchedulerException {

// TriggerKey valid if_exists
if (!checkExists(jobName, jobGroup)) {
logger.info(">>>>>>>>>>> rescheduleJob fail, job not exists, JobGroup:{}, JobName:{}", jobGroup, jobName);
return false;
}

// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//任务存在直接更新一下
if (oldTrigger != null) {
// avoid repeat
String oldCron = oldTrigger.getCronExpression();
if (oldCron.equals(cronExpression)){
return true;
}

// CronTrigger : TriggerKey + cronExpression
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();

// rescheduleJob
scheduler.rescheduleJob(triggerKey, oldTrigger);
} else {
//不存在和直接创建新的任务类似
// CronTrigger : TriggerKey + cronExpression
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();

// JobDetail-JobDataMap fresh
JobKey jobKey = new JobKey(jobName, jobGroup);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
/*JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.clear();
jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));*/

// Trigger fresh
HashSet<Trigger> triggerSet = new HashSet<Trigger>();
triggerSet.add(cronTrigger);

scheduler.scheduleJob(jobDetail, triggerSet, true);
}

logger.info(">>>>>>>>>>> resumeJob success, JobGroup:{}, JobName:{}", jobGroup, jobName);
return true;
}

6、立即执行任务

在任务调度中心可以立即进行任务调用

在JobInfoController中提供接口直接进行任务调用

@RequestMapping("/trigger")
@ResponseBody
public ReturnT<String> triggerJob(int id) {
return xxlJobService.triggerJob(id);
}
在XxlJobService中调用接口triggerJob可以直接进行任务调度

@Override
public ReturnT<String> triggerJob(int id) {

JobTriggerPoolHelper.trigger(id);
return ReturnT.SUCCESS;
在JobTriggerPoolHelper的trigger方法会将任务id添加多线程中,最终的实现同样是调用RemoteHttpJobBean的executeInternal进行任务调度,不是依赖quartz实现,接下来我们会用一篇博客详细讲解一下。
————————————————

一、任务调度中心发送任务执行请求

任务发送执行的操作有两种:

(1)根据配置的cron表达式周期性执行相关任务

(2)在任务调度中心主动执行任务

在注册quartz定时任务时已经注册执行类为RemoteHttpJobBean,所以周期性执行定时任务会调用RemoteHttpJobBean的executeInternal方法,在executeInternal中会调用JobTriggerPoolHelper.trigger(jobId),通过任务调度中心主动执行任务时也是会调用JobTriggerPoolHelper.trigger(jobId)方法,所以接下来我们要看的是JobTriggerPoolHelper.trigger(jobId)中做的逻辑处理就好。

public class RemoteHttpJobBean extends QuartzJobBean {
private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class);

@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {

// load jobId
JobKey jobKey = context.getTrigger().getJobKey();
Integer jobId = Integer.valueOf(jobKey.getName());

// trigger
//XxlJobTrigger.trigger(jobId);
JobTriggerPoolHelper.trigger(jobId);
}

}

JobTriggerPoolHelper.trigger所做的操作是将任务提交给一个线程池(任务调度中心默认开启50个线程),在线程池中调用XxlJobTrigger.trigger。

public void addTrigger(final int jobId){
triggerPool.execute(new Runnable() {
@Override
public void run() {
XxlJobTrigger.trigger(jobId);
}
});
}
在XxlJobTrigger.trigger中会根据jobId获取任务的基本配置信息(阻塞策略、路由策略、失败重试测试、分组服务器列表等等),然后根据路由策略选择是广播还是单播等,接下来就是组装消息体调用runExecutor方法发送http请求到任务执行器。

public static void trigger(int jobId) {

//获取任务信息
// load data
XxlJobInfo jobInfo = XxlJobDynamicScheduler.xxlJobInfoDao.loadById(jobId); // job info
if (jobInfo == null) {
logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
return;
}
//根据任务的分组信息找到分组,分组中存在服务器的IP和端口地址等
XxlJobGroup group = XxlJobDynamicScheduler.xxlJobGroupDao.load(jobInfo.getJobGroup()); // group info

//阻塞策略
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy
//失败策略
ExecutorFailStrategyEnum failStrategy = ExecutorFailStrategyEnum.match(jobInfo.getExecutorFailStrategy(), ExecutorFailStrategyEnum.NULL); // fail strategy
//执行路由测试
ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy
//服务器地址
ArrayList<String> addressList = (ArrayList<String>) group.getRegistryList();

//广播模式
// broadcast
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum && CollectionUtils.isNotEmpty(addressList)) {
//依次调用所有的服务器
for (int i = 0; i < addressList.size(); i++) {
String address = addressList.get(i);

// 1、save log-id
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
XxlJobDynamicScheduler.xxlJobLogDao.save(jobLog);
logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());

// 2、prepare trigger-info
//jobLog.setExecutorAddress(executorAddress);
jobLog.setGlueType(jobInfo.getGlueType());
jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
jobLog.setExecutorParam(jobInfo.getExecutorParam());
jobLog.setTriggerTime(new Date());

ReturnT<String> triggerResult = new ReturnT<String>(null);
StringBuffer triggerMsgSb = new StringBuffer();
triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
.append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()).append("("+i+"/"+addressList.size()+")"); // update01
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailStrategy")).append(":").append(failStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());

// 3、trigger-valid
if (triggerResult.getCode()==ReturnT.SUCCESS_CODE && CollectionUtils.isEmpty(addressList)) {
triggerResult.setCode(ReturnT.FAIL_CODE);
triggerMsgSb.append("<br>----------------------<br>").append(I18nUtil.getString("jobconf_trigger_address_empty"));
}

if (triggerResult.getCode() == ReturnT.SUCCESS_CODE) {
// 4.1、trigger-param
TriggerParam triggerParam = new TriggerParam();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
triggerParam.setLogId(jobLog.getId());
triggerParam.setLogDateTim(jobLog.getTriggerTime().getTime());
triggerParam.setGlueType(jobInfo.getGlueType());
triggerParam.setGlueSource(jobInfo.getGlueSource());
triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
triggerParam.setBroadcastIndex(i);
triggerParam.setBroadcastTotal(addressList.size()); // update02

// 4.2、trigger-run (route run / trigger remote executor)
//远程调用服务接口,执行任务
triggerResult = runExecutor(triggerParam, address); // update03
triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());

// 4.3、trigger (fail retry)
if (triggerResult.getCode()!=ReturnT.SUCCESS_CODE && failStrategy == ExecutorFailStrategyEnum.FAIL_TRIGGER_RETRY) {
triggerResult = runExecutor(triggerParam, address); // update04
triggerMsgSb.append("<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_fail_trigger_retry") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
}
}

// 5、save trigger-info
jobLog.setExecutorAddress(triggerResult.getContent());
jobLog.setTriggerCode(triggerResult.getCode());
jobLog.setTriggerMsg(triggerMsgSb.toString());
XxlJobDynamicScheduler.xxlJobLogDao.updateTriggerInfo(jobLog);

// 6、monitor trigger
JobFailMonitorHelper.monitor(jobLog.getId());
logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());

}
} else {
//单播模式
// 1、save log-id
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
XxlJobDynamicScheduler.xxlJobLogDao.save(jobLog);
logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());

// 2、prepare trigger-info
//jobLog.setExecutorAddress(executorAddress);
jobLog.setGlueType(jobInfo.getGlueType());
jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
jobLog.setExecutorParam(jobInfo.getExecutorParam());
jobLog.setTriggerTime(new Date());

ReturnT<String> triggerResult = new ReturnT<String>(null);
StringBuffer triggerMsgSb = new StringBuffer();
triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
.append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailStrategy")).append(":").append(failStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());

// 3、trigger-valid
if (triggerResult.getCode()==ReturnT.SUCCESS_CODE && CollectionUtils.isEmpty(addressList)) {
triggerResult.setCode(ReturnT.FAIL_CODE);
triggerMsgSb.append("<br>----------------------<br>").append(I18nUtil.getString("jobconf_trigger_address_empty"));
}

if (triggerResult.getCode() == ReturnT.SUCCESS_CODE) {
// 4.1、trigger-param
TriggerParam triggerParam = new TriggerParam();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
triggerParam.setLogId(jobLog.getId());
triggerParam.setLogDateTim(jobLog.getTriggerTime().getTime());
triggerParam.setGlueType(jobInfo.getGlueType());
triggerParam.setGlueSource(jobInfo.getGlueSource());
triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
triggerParam.setBroadcastIndex(0);
triggerParam.setBroadcastTotal(1);

// 4.2、trigger-run (route run / trigger remote executor)
//路由后远程调用服务接口,执行任务
triggerResult = executorRouteStrategyEnum.getRouter().routeRun(triggerParam, addressList);
triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());

// 4.3、trigger (fail retry)
if (triggerResult.getCode()!=ReturnT.SUCCESS_CODE && failStrategy == ExecutorFailStrategyEnum.FAIL_TRIGGER_RETRY) {
triggerResult = executorRouteStrategyEnum.getRouter().routeRun(triggerParam, addressList);
triggerMsgSb.append("<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_fail_trigger_retry") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
}
}

// 5、save trigger-info
jobLog.setExecutorAddress(triggerResult.getContent());
jobLog.setTriggerCode(triggerResult.getCode());
jobLog.setTriggerMsg(triggerMsgSb.toString());
XxlJobDynamicScheduler.xxlJobLogDao.updateTriggerInfo(jobLog);

// 6、monitor trigger
JobFailMonitorHelper.monitor(jobLog.getId());
logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
}

}

runExecutor方法中根据address服务器地址,XxlJobDynamicScheduler.getExecutorBiz中会获取代理类最终调用JettyClient的send方法。

public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
ReturnT<String> runResult = null;
try {
//获取代理对象
ExecutorBiz executorBiz = XxlJobDynamicScheduler.getExecutorBiz(address);
//最终调用执行
runResult = executorBiz.run(triggerParam);
} catch (Exception e) {
logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
}

StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
runResultSB.append("<br>address:").append(address);
runResultSB.append("<br>code:").append(runResult.getCode());
runResultSB.append("<br>msg:").append(runResult.getMsg());

runResult.setMsg(runResultSB.toString());
runResult.setContent(address);
return runResult;
}

在XxlJobDynamicScheduler的getExecutorBiz中会通过NetComClientProxy生成代理对象,在执行时会调用其方法。

public static ExecutorBiz getExecutorBiz(String address) throws Exception {
// valid
if (address==null || address.trim().length()==0) {
return null;
}

// load-cache
address = address.trim();
ExecutorBiz executorBiz = executorBizRepository.get(address);
if (executorBiz != null) {
return executorBiz;
}

// set-cache
executorBiz = (ExecutorBiz) new NetComClientProxy(ExecutorBiz.class, address, accessToken).getObject();
executorBizRepository.put(address, executorBiz);
return executorBiz;
}

getObject中生成代理对象,执行会执行JettyClient的send方法。

@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[] { iface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// filter method like "Object.toString()"
if (Object.class.getName().equals(method.getDeclaringClass().getName())) {
logger.error(">>>>>>>>>>> xxl-rpc proxy class-method not support [{}.{}]", method.getDeclaringClass().getName(), method.getName());
throw new RuntimeException("xxl-rpc proxy class-method not support");
}

// request
RpcRequest request = new RpcRequest();
request.setServerAddress(serverAddress);
request.setCreateMillisTime(System.currentTimeMillis());
request.setAccessToken(accessToken);
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);

//发起http调用,执行任务
// send
RpcResponse response = client.send(request);

// valid response
if (response == null) {
throw new Exception("Network request fail, response not found.");
}
if (response.isError()) {
throw new RuntimeException(response.getError());
} else {
return response.getResult();
}

}
});
}

任务调度中心向任务执行器发送的任务请求数据如下。

二、任务执行器接收任务执行

在任务执行器会根据内置的jetty提供web服务,提供请求处理器JettyServerHandler接收处理任务调度中心发送过来的任务

public class JettyServerHandler extends AbstractHandler {
private static Logger logger = LoggerFactory.getLogger(JettyServerHandler.class);

//接收请求,处理任务
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

// invoke
//调用任务执行
RpcResponse rpcResponse = doInvoke(request);

// serialize response
byte[] responseBytes = HessianSerializer.serialize(rpcResponse);

response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);

OutputStream out = response.getOutputStream();
out.write(responseBytes);
out.flush();

}

private RpcResponse doInvoke(HttpServletRequest request) {
try {
// deserialize request
byte[] requestBytes = HttpClientUtil.readBytes(request);
if (requestBytes == null || requestBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("RpcRequest byte[] is null");
return rpcResponse;
}
//反序列化数据
RpcRequest rpcRequest = (RpcRequest) HessianSerializer.deserialize(requestBytes, RpcRequest.class);

// invoke
//通过反射调用任务
RpcResponse rpcResponse = NetComServerFactory.invokeService(rpcRequest, null);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);

RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Server-error:" + e.getMessage());
return rpcResponse;
}
}

}

在invokeService根据发送过来的类名com.xxl.job.core.biz.ExecutorBiz和方法run,通过反射机制调用

public static RpcResponse invokeService(RpcRequest request, Object serviceBean) {
if (serviceBean==null) {
serviceBean = serviceMap.get(request.getClassName());
}
if (serviceBean == null) {
// TODO
}

RpcResponse response = new RpcResponse();

if (System.currentTimeMillis() - request.getCreateMillisTime() > 180000) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The timestamp difference between admin and executor exceeds the limit."));
return response;
}
if (accessToken!=null && accessToken.trim().length()>0 && !accessToken.trim().equals(request.getAccessToken())) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The access token[" + request.getAccessToken() + "] is wrong."));
return response;
}

try {
Class<?> serviceClass = serviceBean.getClass();
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();

FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);

Object result = serviceFastMethod.invoke(serviceBean, parameters);

response.setResult(result);
} catch (Throwable t) {
t.printStackTrace();
response.setError(t.getMessage());
}

return response;
}

我们看看com.xxl.job.core.biz.ExecutorBiz的run方法中做了什么处理操作。

在ExecutorBiz中根据发送过来的消息,根据demoJobHandler找到接口的实现类,接下来就可以新起线程去执行实现类DemoJobHandler了。

@Override
public ReturnT<String> run(TriggerParam triggerParam) {
// load old:jobHandler + jobThread
//创建执行线程
JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
//如果存在则直接使用老的线程
IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
String removeOldReason = null;

// valid:jobHandler + jobThread
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
if (GlueTypeEnum.BEAN == glueTypeEnum) {

// new jobhandler
//根据类名找到任务执行类
IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

// valid old jobThread
if (jobThread!=null && jobHandler != newJobHandler) {
// change handler, need kill old thread
removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";

jobThread = null;
jobHandler = null;
}

// valid handler
if (jobHandler == null) {
jobHandler = newJobHandler;
if (jobHandler == null) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
}
}

} else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {

// valid old jobThread
if (jobThread != null &&
!(jobThread.getHandler() instanceof GlueJobHandler
&& ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
// change handler or gluesource updated, need kill old thread
removeOldReason = "change job source or glue type, and terminate the old job thread.";

jobThread = null;
jobHandler = null;
}

// valid handler
if (jobHandler == null) {
try {
IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
}
}
} else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {

// valid old jobThread
if (jobThread != null &&
!(jobThread.getHandler() instanceof ScriptJobHandler
&& ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
// change script or gluesource updated, need kill old thread
removeOldReason = "change job source or glue type, and terminate the old job thread.";

jobThread = null;
jobHandler = null;
}

// valid handler
if (jobHandler == null) {
jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
}
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
}

// executor block strategy
if (jobThread != null) {
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
// discard when running
if (jobThread.isRunningOrHasQueue()) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
}
} else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
// kill running jobThread
if (jobThread.isRunningOrHasQueue()) {
removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

jobThread = null;
}
} else {
// just queue trigger
}
}

// replace thread (new or exists invalid)
//起线程执行任务
if (jobThread == null) {
jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
}

// push data to queue
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
return pushResult;
}

总结:任务执行器提供web服务,任务调度中心根据任务分组及分组服务器发送http请求,任务执行器收到请求,根据请求中的数据调用对应的任务。

XXL-JOB原理--定时任务框架简介的更多相关文章

  1. XXL-JOB原理--定时任务框架简介(一)

    https://blog.csdn.net/qq924862077/article/details/82595948 https://blog.csdn.net/qq924862077/article ...

  2. 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

    1.引言 Netty 是一个广受欢迎的异步事件驱动的Java开源网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端. 本文基于 Netty 4.1 展开介绍相关理论模型,使用场景,基本组件 ...

  3. Netty高性能原理和框架架构解析

    1.引言 Netty 是一个广受欢迎的异步事件驱动的Java开源网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端. 本文基于 Netty 4.1 展开介绍相关理论模型,使用场景,基本组件 ...

  4. 《HiWind企业快速开发框架实战》(0)目录及框架简介

    <HiWind企业快速开发框架实战>(0)目录及框架简介 本系列主要介绍一款企业管理系统快速开发框架,该框架旨在快速完成企业管理系统,并实现易维护可移植的目标. 使用逐个系统模块进行编码的 ...

  5. hdwiki 框架简介

    虽然HDwiki是一个开源的wiki系统,并且代码简洁易懂,但如果想在系统上做做进一步开发还需要对框架有一个整体的认识.熟悉了HDwiki的框架以后完全可以独立出来做其他功能的开发,当做一个开源的PH ...

  6. 定时任务框架APScheduler学习详解

    APScheduler简介 在平常的工作中几乎有一半的功能模块都需要定时任务来推动,例如项目中有一个定时统计程序,定时爬出网站的URL程序,定时检测钓鱼网站的程序等等,都涉及到了关于定时任务的问题,第 ...

  7. Elastic-Job - 分布式定时任务框架

    Elastic-Job - 分布式定时任务框架 摘要 Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架.去掉了和dd-job中的监控和ddframe接入规范 ...

  8. Django框架简介,wsgiref 与 jinja2 模块

    目录 框架简介 wsgiref模块 jinja2 模块 框架简介 Django是一个web开发框架,用来开发web应用,本质就是, web框架+socket服务端 MVC框架和MTV框架 MVC,全名 ...

  9. 阿里 AndFix 热修复框架简介

    阿里AndFix热修复框架简介 热修复原理: Android的类加载机制 Android的类加载器分为两种,PathClassLoader和DexClassLoader,两者都继承自BaseDexCl ...

  10. [JavaEE] DWR框架简介

    DWR框架简介 DWR框架是一个可以允许你去创建AJAX WEB站点的JAVA开源库.它可以让你在浏览器的JavaScript代码中调用Web服务器的Java代码,就像Java代码在浏览器中一样.DW ...

随机推荐

  1. Centos7下Docker搭建Matomo

    1.docker安装和启动 wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repo ...

  2. WebService WCF 请求通道在等待 00:00:58.9616639 以后答复时超时。增加传递给请求调用的超时值,或者增加绑定上的 SendTimeout 值。分配给此操作的时间可能是更长超时的一部分。

    查看使用什么方式调用的接口 如果是使用的 HttpWebRequest 请检查Timeout属性,默认值是 100,000 毫秒(100 秒)如果超过便会返回超时错误.

  3. 系统框架(delphi)

    写了一个简单的框架,参考ERP系统写的,可使用两层(client+DB),或三层(client+app<datasnap>+DB)的方式运行,非com+方式. 哈哈,登录好俗...... ...

  4. 话说ReferenceQueue

    也是几年前写的,在内部邮件列表里发过,在这里保存一下. 看到了这篇帖子: <WeakHashMap的神话>http://www.javaeye.com/topic/587995 因为Jav ...

  5. 推荐UML插件Green UML、AmaterasUML

    项目上要求release时需要同时给出详细的类关系图,可惜本人之前只是使用XMind手工画过很简单的类关系图(只是类的继承关系),可苦了我呀. 这两天一直在网上查找能够在Eclipse 已有的代码基础 ...

  6. 2022-2023 ACM-ICPC Nordic Collegiate Programming Contest (NCPC 2022)

    F. Foreign Football 一共有\(n\)支队伍,每支队伍的名称为\(s_i\),给定一个\(n \times n\)的矩阵,\(a_{i,j}\)代表第\(i\)支队伍和第\(j\)支 ...

  7. 【解决方案】Error running,Command line is too long

    一.现象 IDEA 提示 Error running,Command line is too long 二.原因 Java 命令行启动举例如下图,当命令行字符过多的时候,就会出现 Error runn ...

  8. springboot 前后端大打包成一个JAR

    1.概述 现在开发使用前后端开发机制,在部署的时候,我们需要将前后端分别打包,使用nginx 进行统一部署.这样就比较复杂,我们可以使用前后端打包到一个jar中,这样我们只需要一个包就可以了. 2.实 ...

  9. 下列哪个选项是对ICMP FLOOD攻击的正确描述?

    A.  通过重复发送HTTP GET请求,将内容传输的负载施加到攻击目标服务器上. B.  通过使用ping命令发送大量请求包,导致通向被攻击服务器过载并阻止访问. C.  通过发送与连接启动请求对应 ...

  10. 内存Fuzz和WinAFL

    文章一开始发表在微信公众号 https://mp.weixin.qq.com/s/XSPrmBb44J8BUpKsj0cwGQ 内存Fuzz和WinAFL FoxitReader 软件分析 目前Fuz ...