Quartz—学习02—Quartz源码阅读

Quartz源码阅读


Quartz—学习01—Quartz入门
Quartz—学习02—Quartz源码阅读

一、Quartz的整套流程

1-1、初始化过程

我以Quartz—学习01—Quartz入门quartz-learn-001的demo为列进行源码阅读。

在Test类中有scheduler =schedulerFactory.getScheduler(); 这样一段代码,用于从SchedulerFactory工厂中获取Scheduler对象。下面以StdSchedulerFactory类中的getScheduler() 方法为例看一下其是怎么创建Scheduler 对象的。

// ########################## StdSchedulerFactory类  ######################
public Scheduler getScheduler() throws SchedulerException {
    if (cfg == null) {
        initialize();	// 初始加载配置文件,默认加载quartz.properties
        //如果没有quartz.properties然后去加载/quartz.properties;
        //如果都没有的话,会加载自带的org/quartz/quartz.properties文件
    }
    // 初始化一个SchedulerRepository;SchedulerRepository中有一个Map,这个Map用于存放Scheduler,key为Scheduler的Name,value为Scheduler对象
    SchedulerRepository schedRep = SchedulerRepository.getInstance();
    // 从SchedulerRepository
    Scheduler sched = schedRep.lookup(getSchedulerName());
    if (sched != null) {
        if (sched.isShutdown()) {
            schedRep.remove(getSchedulerName());
        } else {
            return sched;
        }
    }
    // 初始化一个Scheduler的实例,这个instantiate()方法代码很长,将在下面进行介绍。
    sched = instantiate();
    return sched;
}

加载自带的 org/quartz/quartz.properties 文件内容。

##############################
# scheduler的配置属性
##############################
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
# Scheduler通过RMI作为服务器导出本身。
org.quartz.scheduler.rmi.export: false
# 是否是连接远程服务的调用程序,如果“是”需要指定RMI注册表进程的主机和端口
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

##############################
# threadPool的配置属性		##
##############################
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

##############################
# jobStore的配置属性			##
##############################
# 触发超时时间
org.quartz.jobStore.misfireThreshold: 60000
# 默认使用RAMJobStore存,也就是把调度信息存储在内存中
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

下面是StbSchedulerFactoryinstantiate() 方法代码片段,这个方法主要作用是实例化一个Scheduler对象并对其进行一系列初始化操作,这个方法的代码有几百行,就不把代码粘贴出来了,具体细节可以参考源码进行阅读,这里就粘贴几个重要的步骤的代码。

....
// 从配置文件中获取配置信息
// 01-线程池配置
// 02-JobStore配置
// 03-数据源配置
// 04-SchedulerPlugin设置
// 05-JobListener设置
// 06-TriggerListener设置
// 08-ThreadExecutor设置
// 09-QuartzSchedulerResources设置

// JobRunShellFactory初始化配置
jrsf.initialize(scheduler);

// tp为ThreadPool,initialize主要作用是创建WorkerThread,并启动线程;具体代码看:代码001
tp.initialize();
...
// 实例化QuartzScheduler,具体代码看:代码002
qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);

代码001
ThreadPool【具体实现以SimpleThreadPool为例】的instantiate()方法,该方法主要作用是获取WorkerThread线程并启动线程。

################ SimpleThreadPool的instantiate()方法 ############
public void initialize() throws SchedulerConfigException {
	// 已经初始化过了
	if(workers != null && workers.size() > 0) 
		return;
	
	// 线程数量必须大于0
	if (count <= 0) {
		throw new SchedulerConfigException("Thread count must be > 0");
	}
	
	// 线程优先级必须在大于0小于等于9
	if (prio <= 0 || prio > 9) {
		throw new SchedulerConfigException("Thread priority must be > 0 and <= 9");
	}
	
	// isThreadsInheritGroupOfInitializingThread默认返回ture
	if(isThreadsInheritGroupOfInitializingThread()) {
		// 获取该线程对应的线程组
		threadGroup = Thread.currentThread().getThreadGroup();
	} else {
		// 按照threadGroup树到根线程组。
		threadGroup = Thread.currentThread().getThreadGroup();
		ThreadGroup parent = threadGroup;
		while ( !parent.getName().equals("main") ) {
			threadGroup = parent;
			parent = threadGroup.getParent();
		}
		threadGroup = new ThreadGroup(parent, schedulerInstanceName + "-SimpleThreadPool");
		if (isMakeThreadsDaemons()) {
			threadGroup.setDaemon(true);
		}
	}

	// 默认为false
	if (isThreadsInheritContextClassLoaderOfInitializingThread()) {
		getLog().info("Job execution threads will use class loader of thread: " + Thread.currentThread().getName());
	}

	// 创建WorkerThread,WorkerThread默认为权重为5,非守护线程。count为10,
	Iterator<WorkerThread> workerThreads = createWorkerThreads(count).iterator();
	while(workerThreads.hasNext()) {
		WorkerThread wt = workerThreads.next();
		// 启动线程,WorkerThread的run方法如下所示
		wt.start();
		// 把线程放到存放可用线程的Map中,初始化为10个
		availWorkers.add(wt);
	}
}

#####################  WorkThread类  ################
@Override
public void run() {
	boolean ran = false;
	
	// run默认为ture,调用shutdown()方法后会修改为false,
	// run.get():用于判断Quartz是否暂停了。如果线程启动
	while (run.get()) {
		try {
			// 上锁操作,
			synchronized(lock) {
				// 判断是否有工作,或者工作是否暂停,如果是就没0.5检查一次
				while (runnable == null && run.get()) {
					lock.wait(500);
				}

				if (runnable != null) {
					ran = true;
					// 执行runnable的run方法
					runnable.run();
				}
			}
		} catch (InterruptedException unblock) {
			try {
				getLog().error("Worker thread was interrupt()'ed.", unblock);
			} catch(Exception e) {
				
			}
		} catch (Throwable exceptionInRunnable) {
			try {
				getLog().error("Error while executing the Runnable: ", exceptionInRunnable);
			} catch(Exception e) {
				
			}
		} finally {
			synchronized(lock) {
				runnable = null;
			}
			// 修复线程,【没看明白】
			if(getPriority() != tp.getThreadPriority()) {
				setPriority(tp.getThreadPriority());
			}

			// 是否只跑一次,默认为false,当有runnable的初始化的WorkThread的时候,会修改为ture
			if (runOnce) {
				run.set(false);
				// 从busyWorkers中移除
				clearFromBusyWorkersList(this);
			} else if(ran) {
				ran = false;
				// availWorkers添加改线程,并从busyWorkers中移除
				makeAvailable(this);
			}

		}
	}

	try {
		getLog().debug("WorkerThread is shut down.");
	} catch(Exception e) {
		
	}
}

代码002
QuartzScheduler的构造函数QuartzScheduler(QuartzSchedulerResources resources, long idleWaitTime, @Deprecated long dbRetryInterval)。在QuartzScheduler初始化过程中主要做了:通过给定的QuartzSchedulerResources创建QuartzSchedulerThread实例,并启动QuartzSchedulerThread实例,并把对应的监听器加入到QuartzScheduler中。QuartzSchedulerThreadrun方法主要做了:判断线程是否启动和Quartz是否启动,如果没有启动,就一直检查;如果启动以后,检查当前时候有可用的Trigger

// ####################   QuartzScheduler的构造函数。   ##############
public QuartzScheduler(QuartzSchedulerResources resources, long idleWaitTime, @Deprecated long dbRetryInterval) throws SchedulerException {

	// 把SchedulerFactory初始化的QuartzSchedulerResources赋给QuartzScheduler的QuartzSchedulerResources
	this.resources = resources;
	
	// 判断JobStore是否是JobListener的子类
	if (resources.getJobStore() instanceof JobListener) {
		addInternalJobListener((JobListener)resources.getJobStore());
	}

	// 实例一个QuartzSchedulerThread对象通过QuartzScheduler和QuartzSchedulerResources
	this.schedThread = new QuartzSchedulerThread(this, resources);
	
	// 获取QuartzSchedulerResources中的ThreadExecutor
	ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
	
	// 把QuartzSchedulerThread启动起来,下面是QuartzSchedulerThread的run方法。
	schedThreadExecutor.execute(this.schedThread);
	
	if (idleWaitTime > 0) {
		this.schedThread.setIdleWaitTime(idleWaitTime);
	}

	// 添加JobListener监听
	jobMgr = new ExecutingJobsManager();
	addInternalJobListener(jobMgr);
	
	// 添加schedulerListener监听
	errLogger = new ErrorLogger();
	addInternalSchedulerListener(errLogger);

	signaler = new SchedulerSignalerImpl(this, this.schedThread);
	
	if(shouldRunUpdateCheck()) 
		updateTimer = scheduleUpdateCheck();
	else
		updateTimer = null;
	
	getLog().info("Quartz Scheduler v." + getVersion() + " created.");
}

// ####################   QuartzSchedulerThread   ##################
@Override
public void run() {
	boolean lastAcquireFailed = false;

	// halted初始化为false,用于判断quartz是否是暂停状态,默认不是暂停状态;
	// paused初始化默认为ture,用于判断是否暂停状态,默认为暂停状态。
	while (!halted.get()) {
		try {
			// 初始化的时候Quartz不是暂停状态,但是是未启动状态。
			// 没过1秒检查一次,Quartz是否启动并且不是暂停状态。如果QuartzScheduler的start()方法被调用以后paused会被置为ture.
			// 只有启动的时候才会把paused设为false,所以初始化的时候一直在等待,直到启动为止。
			synchronized (sigLock) {
				while (paused && !halted.get()) {
					try {
						sigLock.wait(1000L);
					} catch (InterruptedException ignore) {
					
					}
				}

				if (halted.get()) {
					break;
				}
			}
			
			// 获取可用线程数。
			int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
			
			// 这个总是为ture
			if(availThreadCount > 0) { 
			
				List<OperableTrigger> triggers = null;
				long now = System.currentTimeMillis();
				
				// 清除信号记录,把signaled设为ture
				clearSignaledSchedulingChange();
				
				try {
					// acquireNextTriggers()方法获取到OperableTrigger的集合,并按执行时间的先后顺序放入list中。这个方法可以看一下源码
					//(当前时间 + 30s, [availThreadCount,maxBatchSize初始化为1]中小的一个, batchTimeWindow默认为0)
					triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow()); 
					lastAcquireFailed = false;
					if (log.isDebugEnabled()) 
						log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
				} catch (JobPersistenceException jpe) {
					if(!lastAcquireFailed) {
						qs.notifySchedulerListenersError("An error occurred while scanning for the next triggers to fire.", jpe);
					}
					lastAcquireFailed = true;
					continue;
				} catch (RuntimeException e) {
					if(!lastAcquireFailed) {
						getLog().error("quartzSchedulerThreadLoop: RuntimeException " + e.getMessage(), e);
					}
					lastAcquireFailed = true;
					continue;
				}

				// 如果triggers是否为空
				if (triggers != null && !triggers.isEmpty()) {
					now = System.currentTimeMillis();
					long triggerTime = triggers.get(0).getNextFireTime().getTime();
					long timeUntilTrigger = triggerTime - now;
					
					//【不明白】为什么要大于2?
					while(timeUntilTrigger > 2) {
						synchronized (sigLock) {
							// quartz是否是在暂停状态
							if (halted.get()) {
								break;
							}
							
							//QuartzSchedule是否被改变了,没有改变isCandidateNewTimeEarlierWithinReason返回false,
							if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
								try {
									// 重新计算时间
									now = System.currentTimeMillis();
									timeUntilTrigger = triggerTime - now;
									
									// 如果没有到达触发时间,等到触发时间。
									if(timeUntilTrigger >= 1)
										sigLock.wait(timeUntilTrigger);
								} catch (InterruptedException ignore) {
								}
							}
						}
						
						if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
							break;
						}
						
						now = System.currentTimeMillis();
						timeUntilTrigger = triggerTime - now;
					}

					// 再次检查triggers是否为空。
					if(triggers.isEmpty()) {
						continue;
					}						

					List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();
					boolean goAhead = true;
					
					// 判断是否暂停。
					synchronized(sigLock) {
						goAhead = !halted.get(); // 没有出现暂停情况goAhead都为ture
					}
					
					if(goAhead) {
						// 从QuartzSchedulerResources中获取对应的TriggerFiredResult集合,并赋给bndles;
						try {
							List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
							if(res != null) {
								bndles = res;
							}
						} catch (SchedulerException se) {
							qs.notifySchedulerListenersError("An error occurred while firing triggers '" + triggers + "'", se);
							for (int i = 0; i < triggers.size(); i++) {
								qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
							}
							continue;
						}
					}

					for (int i = 0; i < bndles.size(); i++) {
						TriggerFiredResult result =  bndles.get(i);
						// 获取TriggerFiredBundle从TriggerFiredResult中,
						TriggerFiredBundle bndle =  result.getTriggerFiredBundle();
						Exception exception = result.getException();

						if (exception instanceof RuntimeException) {
							getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
							qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
							continue;
						}

						if (bndle == null) {
							qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
							continue;
						}

						//#################################################
						// 创建对应的JobRunShell,并初始化
						//#################################################
						JobRunShell shell = null;
						try {
							shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
							shell.initialize(qs);
						} catch (SchedulerException se) {
							qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
							continue;
						}

						//#################################################
						// 获取对应的JobRunShell,具体代码请看:代码004
						//#################################################
						if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
							getLog().error("ThreadPool.runInThread() return false!");
							qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
						}

					}
					continue; 
				}
			} else { 
				continue; 
			}

			long now = System.currentTimeMillis();
			long waitTime = now + getRandomizedIdleWaitTime();
			long timeUntilContinue = waitTime - now;
			
			synchronized(sigLock) {
				try {
				  if(!halted.get()) {
					if (!isScheduleChanged()) {
					  sigLock.wait(timeUntilContinue);
					}
				  }
				} catch (InterruptedException ignore) {
				}
			}

		} catch(RuntimeException re) {
			getLog().error("Runtime error occurred in main trigger firing loop.", re);
		}
	}

	qs = null;
	qsRsrcs = null;
}

代码004
SimpleThreadPool

//####################  SimpleThreadPool类  ########################
// 以runnable为JobRunShell实例为例
public boolean runInThread(Runnable runnable) {
	if (runnable == null) {
		return false;
	}

	synchronized (nextRunnableLock) {
		handoffPending = true;

		// 等待工作线程可用。
		while ((availWorkers.size() < 1) && !isShutdown) {
			try {
				nextRunnableLock.wait(500);
			} catch (InterruptedException ignore) {
			
			}
		}

		// 判断Quartz是否停止了。
		if (!isShutdown) {
			// 从可用的线程Map中获取到一个可用线程。
			WorkerThread wt = (WorkerThread)availWorkers.removeFirst();
			// 把获取到的可用线程放到busyWorkers中。
			busyWorkers.add(wt);
			// 
			wt.run(runnable);
		} else {
			// 如果线程池正在关闭,在新的其他工作线程执行Runnable
			WorkerThread wt = new WorkerThread(this, threadGroup, "WorkerThread-LastJob", prio, isMakeThreadsDaemons(), runnable);
			busyWorkers.add(wt);
			workers.add(wt);
			wt.start();
		}
		nextRunnableLock.notifyAll();
		handoffPending = false;
	}

	return true;
}

//#################   WorkerThread类的  ##################
public void run(Runnable newRunnable) {
	synchronized(lock) {
		if(runnable != null) {
			throw new IllegalStateException("Already running a Runnable!");
		}

		runnable = newRunnable;
		lock.notifyAll();
	}
}

//#################   WorkerThread类的  ##################
@Override
public void run() {
	boolean ran = false;
	
	while (run.get()) {
		try {
			synchronized(lock) {
				while (runnable == null && run.get()) {
					lock.wait(500);
				}

				if (runnable != null) {
					ran = true;
					runnable.run();
				}
			}
		} catch (InterruptedException unblock) {
			// do nothing (loop will terminate if shutdown() was called
			try {
				getLog().error("Worker thread was interrupt()'ed.", unblock);
			} catch(Exception e) {
				// ignore to help with a tomcat glitch
			}
		} catch (Throwable exceptionInRunnable) {
			try {
				getLog().error("Error while executing the Runnable: ",
					exceptionInRunnable);
			} catch(Exception e) {
				// ignore to help with a tomcat glitch
			}
		} finally {
			synchronized(lock) {
				runnable = null;
			}
			// repair the thread in case the runnable mucked it up...
			if(getPriority() != tp.getThreadPriority()) {
				setPriority(tp.getThreadPriority());
			}

			if (runOnce) {
				   run.set(false);
				clearFromBusyWorkersList(this);
			} else if(ran) {
				ran = false;
				makeAvailable(this);
			}

		}
	}

	//if (log.isDebugEnabled())
	try {
		getLog().debug("WorkerThread is shut down.");
	} catch(Exception e) {
		// ignore to help with a tomcat glitch
	}
}

// #####################  JobRunShell类  ################
// 这里才真正的执行了Job。
public void run() {

	qs.addInternalSchedulerListener(this);
	
	try {
		OperableTrigger trigger = (OperableTrigger) jec.getTrigger();
		JobDetail jobDetail = jec.getJobDetail();

		do {
			JobExecutionException jobExEx = null;
			Job job = jec.getJobInstance();

			try {
				// 本身begin是没有做任何事的,但是如果有事务的话就要进行处理。
				begin();
			} catch (SchedulerException se) {
				qs.notifySchedulerListenersError("Error executing Job (" + jec.getJobDetail().getKey() + ": couldn't begin execution.", se);
				break;
			}

			// notify job & trigger listeners...
			
			try {
				if (!notifyListenersBeginning(jec)) {
					break;
				}
			} catch(VetoedException ve) {
				try {
					CompletedExecutionInstruction instCode = trigger.executionComplete(jec, null);
					qs.notifyJobStoreJobVetoed(trigger, jobDetail, instCode);
					
					// 即使Trigger被否决,仍然需要检查它是否是Trigger的最终运行。
					if (jec.getTrigger().getNextFireTime() == null) {
						qs.notifySchedulerListenersFinalized(jec.getTrigger());
					}
					
					complete(true);
					
				} catch (SchedulerException se) {
					qs.notifySchedulerListenersError("Error during veto of Job (" + jec.getJobDetail().getKey() + ": couldn't finalize execution.", se);
				}
				break;
			}

			long startTime = System.currentTimeMillis();
			long endTime = startTime;
			
			//##############################################
			// 执行Job
			//##############################################
			try {
				log.debug("Calling execute on job " + jobDetail.getKey());
				job.execute(jec);
				endTime = System.currentTimeMillis();
			} catch (JobExecutionException jee) {
				endTime = System.currentTimeMillis();
				jobExEx = jee;
				getLog().info("Job " + jobDetail.getKey() + " threw a JobExecutionException: ", jobExEx);
			} catch (Throwable e) {
				endTime = System.currentTimeMillis();
				getLog().error("Job " + jobDetail.getKey() + " threw an unhandled Exception: ", e);
				SchedulerException se = new SchedulerException("Job threw an unhandled exception.", e);
				qs.notifySchedulerListenersError("Job (" + jec.getJobDetail().getKey() + " threw an exception.", se);
				jobExEx = new JobExecutionException(se, false);
			}

			jec.setJobRunTime(endTime - startTime);

			// 通知所有的Job监听器,Job处理完成。  notify all job listeners
			if (!notifyJobListenersComplete(jec, jobExEx)) {
				break;
			}

			CompletedExecutionInstruction instCode = CompletedExecutionInstruction.NOOP;

			//  更新trigger, update the trigger
			try {
				instCode = trigger.executionComplete(jec, jobExEx);
			} catch (Exception e) {
				SchedulerException se = new SchedulerException("Trigger threw an unhandled exception.", e);
				qs.notifySchedulerListenersError("Please report this error to the Quartz developers.", se);
			}

			// 通知所有的trigger监听器,
			if (!notifyTriggerListenersComplete(jec, instCode)) {
				break;
			}

			// 更新job和trigger,或者重新执行job
			if (instCode == CompletedExecutionInstruction.RE_EXECUTE_JOB) { 	
				jec.incrementRefireCount();
				try {
					complete(false);
				} catch (SchedulerException se) {
					qs.notifySchedulerListenersError("Error executing Job (" + jec.getJobDetail().getKey() + ": couldn't finalize execution.", se);
				}
				continue;
			}

			try {
				complete(true);
			} catch (SchedulerException se) {
				qs.notifySchedulerListenersError("Error executing Job ("+ jec.getJobDetail().getKey() + ": couldn't finalize execution.", se);
				continue;
			}

			qs.notifyJobStoreJobComplete(trigger, jobDetail, instCode);
			break;
		} while (true);
	} finally {
		qs.removeInternalSchedulerListener(this);
	}
}

1-2、启动流程

在上一节中Test类中调用了scheduler.start();启动线程。

// ########################  QuartzScheduler  ############################
public void start() throws SchedulerException {
	
	// 判断Quartz是否停止或者关闭,如果停止或者关闭抛出异常,Quartz是没有重启操作的。
	if (shuttingDown|| closed) {
		throw new SchedulerException("The Scheduler cannot be restarted after shutdown() has been called.");
	}


	// 获取所有scheduler listeners并进行通知
	notifySchedulerListenersStarting();

	if (initialStart == null) {
		initialStart = new Date();
		this.resources.getJobStore().schedulerStarted();            
		startPlugins();
	} else {
		resources.getJobStore().schedulerResumed();
	}

	//###########################
	// 启动Quartz操作
	//###########################
	schedThread.togglePause(false);

	getLog().info(
			"Scheduler " + resources.getUniqueIdentifier() + " started.");
	
	notifySchedulerListenersStarted();
}

// #################### QuartzSchedulerThread类 #########  
void togglePause(boolean pause) {
	synchronized (sigLock) {
		// 把暂停设置为false,启动Quartz。
		paused = pause;
		
		if (paused) {
			signalSchedulingChange(0);
		} else {
			// 通知所有被
			sigLock.notifyAll();
		}
	}
}
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值