java quartz 性能_聊聊quartz的调度及性能

本文主要研究下quartz的QuartzSchedulerThread的调度以及quartz的性能问题。

SchedulerFactoryBean

spring-context-support-4.3.7.RELEASE-sources.jar!/org/springframework/scheduling/quartz/SchedulerFactoryBean.java

@Override

public void afterPropertiesSet() throws Exception {

if (this.dataSource == null && this.nonTransactionalDataSource != null) {

this.dataSource = this.nonTransactionalDataSource;

}

if (this.applicationContext != null && this.resourceLoader == null) {

this.resourceLoader = this.applicationContext;

}

// Create SchedulerFactory instance...

SchedulerFactory schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass);

initSchedulerFactory(schedulerFactory);

if (this.resourceLoader != null) {

// Make given ResourceLoader available for SchedulerFactory configuration.

configTimeResourceLoaderHolder.set(this.resourceLoader);

}

if (this.taskExecutor != null) {

// Make given TaskExecutor available for SchedulerFactory configuration.

configTimeTaskExecutorHolder.set(this.taskExecutor);

}

if (this.dataSource != null) {

// Make given DataSource available for SchedulerFactory configuration.

configTimeDataSourceHolder.set(this.dataSource);

}

if (this.nonTransactionalDataSource != null) {

// Make given non-transactional DataSource available for SchedulerFactory configuration.

configTimeNonTransactionalDataSourceHolder.set(this.nonTransactionalDataSource);

}

// Get Scheduler instance from SchedulerFactory.

try {

this.scheduler = createScheduler(schedulerFactory, this.schedulerName);

populateSchedulerContext();

if (!this.jobFactorySet && !(this.scheduler instanceof RemoteScheduler)) {

// Use AdaptableJobFactory as default for a local Scheduler, unless when

// explicitly given a null value through the "jobFactory" bean property.

this.jobFactory = new AdaptableJobFactory();

}

if (this.jobFactory != null) {

if (this.jobFactory instanceof SchedulerContextAware) {

((SchedulerContextAware) this.jobFactory).setSchedulerContext(this.scheduler.getContext());

}

this.scheduler.setJobFactory(this.jobFactory);

}

}

finally {

if (this.resourceLoader != null) {

configTimeResourceLoaderHolder.remove();

}

if (this.taskExecutor != null) {

configTimeTaskExecutorHolder.remove();

}

if (this.dataSource != null) {

configTimeDataSourceHolder.remove();

}

if (this.nonTransactionalDataSource != null) {

configTimeNonTransactionalDataSourceHolder.remove();

}

}

registerListeners();

registerJobsAndTriggers();

}复制代码这里在afterPropertiesSet的时候,createScheduler(schedulerFactory, this.schedulerName);

SchedulerFactoryBean.createSchedulerprotected Scheduler createScheduler(SchedulerFactory schedulerFactory, String schedulerName)

throws SchedulerException {

// Override thread context ClassLoader to work around naive Quartz ClassLoadHelper loading.

Thread currentThread = Thread.currentThread();

ClassLoader threadContextClassLoader = currentThread.getContextClassLoader();

boolean overrideClassLoader = (this.resourceLoader != null &&

!this.resourceLoader.getClassLoader().equals(threadContextClassLoader));

if (overrideClassLoader) {

currentThread.setContextClassLoader(this.resourceLoader.getClassLoader());

}

try {

SchedulerRepository repository = SchedulerRepository.getInstance();

synchronized (repository) {

Scheduler existingScheduler = (schedulerName != null ? repository.lookup(schedulerName) : null);

Scheduler newScheduler = schedulerFactory.getScheduler();

if (newScheduler == existingScheduler) {

throw new IllegalStateException("Active Scheduler of name '" + schedulerName + "' already registered " +

"in Quartz SchedulerRepository. Cannot create a new Spring-managed Scheduler of the same name!");

}

if (!this.exposeSchedulerInRepository) {

// Need to remove it in this case, since Quartz shares the Scheduler instance by default!

SchedulerRepository.getInstance().remove(newScheduler.getSchedulerName());

}

return newScheduler;

}

}

finally {

if (overrideClassLoader) {

// Reset original thread context ClassLoader.

currentThread.setContextClassLoader(threadContextClassLoader);

}

}

}复制代码这里调用schedulerFactory.getScheduler()来创建

quartz-2.3.0-sources.jar!/org/quartz/impl/StdSchedulerFactory.java

public Scheduler getScheduler() throws SchedulerException {

if (cfg == null) {

initialize();

}

SchedulerRepository schedRep = SchedulerRepository.getInstance();

Scheduler sched = schedRep.lookup(getSchedulerName());

if (sched != null) {

if (sched.isShutdown()) {

schedRep.remove(getSchedulerName());

} else {

return sched;

}

}

sched = instantiate();

return sched;

}复制代码这里调用instantiate来初始化。里头初始化了一个QuartzScheduler

qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);复制代码

QuartzScheduler

quartz-2.3.0-sources.jar!/org/quartz/core/QuartzScheduler.java

public QuartzScheduler(QuartzSchedulerResources resources, long idleWaitTime, @Deprecated long dbRetryInterval)

throws SchedulerException {

this.resources = resources;

if (resources.getJobStore() instanceof JobListener) {

addInternalJobListener((JobListener)resources.getJobStore());

}

this.schedThread = new QuartzSchedulerThread(this, resources);

ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();

schedThreadExecutor.execute(this.schedThread);

if (idleWaitTime > 0) {

this.schedThread.setIdleWaitTime(idleWaitTime);

}

jobMgr = new ExecutingJobsManager();

addInternalJobListener(jobMgr);

errLogger = new ErrorLogger();

addInternalSchedulerListener(errLogger);

signaler = new SchedulerSignalerImpl(this, this.schedThread);

getLog().info("Quartz Scheduler v." + getVersion() + " created.");

}复制代码这个又初始化了QuartzSchedulerThread

QuartzSchedulerThread这个是调度的核心类

quartz-2.3.0-sources.jar!/org/quartz/core/QuartzSchedulerThread.java

public class QuartzSchedulerThread extends Thread {

//......

/**

*

* The main processing loop of the QuartzSchedulerThread.

*

*/

@Override

public void run() {

int acquiresFailed = 0;

while (!halted.get()) {

try {

// check if we're supposed to pause...

synchronized (sigLock) {

while (paused && !halted.get()) {

try {

// wait until togglePause(false) is called...

sigLock.wait(1000L);

} catch (InterruptedException ignore) {

}

// reset failure counter when paused, so that we don't

// wait again after unpausing

acquiresFailed = 0;

}

if (halted.get()) {

break;

}

}

// wait a bit, if reading from job store is consistently

// failing (e.g. DB is down or restarting)..

if (acquiresFailed > 1) {

try {

long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed);

Thread.sleep(delay);

} catch (Exception ignore) {

}

}

int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();

if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...

List triggers;

long now = System.currentTimeMillis();

clearSignaledSchedulingChange();

try {

triggers = qsRsrcs.getJobStore().acquireNextTriggers(

now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());

acquiresFailed = 0;

if (log.isDebugEnabled())

log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");

} catch (JobPersistenceException jpe) {

if (acquiresFailed == 0) {

qs.notifySchedulerListenersError(

"An error occurred while scanning for the next triggers to fire.",

jpe);

}

if (acquiresFailed < Integer.MAX_VALUE)

acquiresFailed++;

continue;

} catch (RuntimeException e) {

if (acquiresFailed == 0) {

getLog().error("quartzSchedulerThreadLoop: RuntimeException "

+e.getMessage(), e);

}

if (acquiresFailed < Integer.MAX_VALUE)

acquiresFailed++;

continue;

}

if (triggers != null && !triggers.isEmpty()) {

now = System.currentTimeMillis();

long triggerTime = triggers.get(0).getNextFireTime().getTime();

long timeUntilTrigger = triggerTime - now;

while(timeUntilTrigger > 2) {

synchronized (sigLock) {

if (halted.get()) {

break;

}

if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {

try {

// we could have blocked a long while

// on 'synchronize', so we must recompute

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;

}

// this happens if releaseIfScheduleChangedSignificantly decided to release triggers

if(triggers.isEmpty())

continue;

// set triggers to 'executing'

List bndles = new ArrayList();

boolean goAhead = true;

synchronized(sigLock) {

goAhead = !halted.get();

}

if(goAhead) {

try {

List res = qsRsrcs.getJobStore().triggersFired(triggers);

if(res != null)

bndles = res;

} catch (SchedulerException se) {

qs.notifySchedulerListenersError(

"An error occurred while firing triggers '"

+ triggers + "'", se);

//QTZ-179 : a problem occurred interacting with the triggers from the db

//we release them and loop again

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 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;

}

// it's possible to get 'null' if the triggers was paused,

// blocked, or other similar occurrences that prevent it being

// fired at this time... or if the scheduler was shutdown (halted)

if (bndle == null) {

qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));

continue;

}

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;

}

if (qsRsrcs.getThreadPool().runInThread(shell) == false) {

// this case should never happen, as it is indicative of the

// scheduler being shutdown or a bug in the thread pool or

// a thread pool being used concurrently - which the docs

// say not to do...

getLog().error("ThreadPool.runInThread() return false!");

qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);

}

}

continue; // while (!halted)

}

} else { // if(availThreadCount > 0)

// should never happen, if threadPool.blockForAvailableThreads() follows contract

continue; // while (!halted)

}

long now = System.currentTimeMillis();

long waitTime = now + getRandomizedIdleWaitTime();

long timeUntilContinue = waitTime - now;

synchronized(sigLock) {

try {

if(!halted.get()) {

// QTZ-336 A job might have been completed in the mean time and we might have

// missed the scheduled changed signal by not waiting for the notify() yet

// Check that before waiting for too long in case this very job needs to be

// scheduled very soon

if (!isScheduleChanged()) {

sigLock.wait(timeUntilContinue);

}

}

} catch (InterruptedException ignore) {

}

}

} catch(RuntimeException re) {

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

}

} // while (!halted)

// drop references to scheduler stuff to aid garbage collection...

qs = null;

qsRsrcs = null;

}

}复制代码这里有几个关键的信息

blockForAvailableThreads

就是qsRsrcs.getThreadPool().blockForAvailableThreads(),如果线程池满了的话,则会阻塞,因而会影响调度的准确性。

acquireNextTriggerstriggers = qsRsrcs.getJobStore().acquireNextTriggers(

now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize())复制代码

timeUntilContinue

这里进行等待

long now = System.currentTimeMillis();

long waitTime = now + getRandomizedIdleWaitTime();

long timeUntilContinue = waitTime - now;

synchronized(sigLock) {

try {

if(!halted.get()) {

// QTZ-336 A job might have been completed in the mean time and we might have

// missed the scheduled changed signal by not waiting for the notify() yet

// Check that before waiting for too long in case this very job needs to be

// scheduled very soon

if (!isScheduleChanged()) {

sigLock.wait(timeUntilContinue);

}

}

} catch (InterruptedException ignore) {

}

}复制代码

而getRandomizedIdleWaitTime如下

private static long DEFAULT_IDLE_WAIT_TIME = 30L * 1000L;

private long idleWaitTime = DEFAULT_IDLE_WAIT_TIME;

private long getRandomizedIdleWaitTime() {

return idleWaitTime - random.nextInt(idleWaitVariablness);

}复制代码idleWaitTime默认是30秒,通过org.quartz.scheduler.idleWaitTime这个来配置

computeDelayForRepeatedErrors// wait a bit, if reading from job store is consistently

// failing (e.g. DB is down or restarting)..

if (acquiresFailed > 1) {

try {

long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed);

Thread.sleep(delay);

} catch (Exception ignore) {

}

}复制代码这里优化了对数据库挂的时候,避免频繁轮询的问题

private static final long MIN_DELAY = 20;

private static final long MAX_DELAY = 600000;

private static long computeDelayForRepeatedErrors(JobStore jobStore, int acquiresFailed) {

long delay;

try {

delay = jobStore.getAcquireRetryDelay(acquiresFailed);

} catch (Exception ignored) {

// we're trying to be useful in case of error states, not cause

// additional errors..

delay = 100;

}

// sanity check per getAcquireRetryDelay specification

if (delay < MIN_DELAY)

delay = MIN_DELAY;

if (delay > MAX_DELAY)

delay = MAX_DELAY;

return delay;

}复制代码

connectionProvider.class

比如

org.quartz.jobStore.dataSource=myDataSourceName

org.quartz.dataSource.myDataSourceName.connectionProvider.class=org.quartz.utils.HikariCpPoolingConnectionProvider

org.quartz.dataSource.myDataSourceName.driver=org.postgresql.Driver

org.quartz.dataSource.myDataSourceName.URL=jdbc:postgresql://192.168.99.100:5432/postgres

org.quartz.dataSource.myDataSourceName.user:postgres

org.quartz.dataSource.myDataSourceName.password:postgres

org.quartz.dataSource.myDataSourceName.maxConnection:30

org.quartz.dataSource.myDataSourceName.validationQuery: select 1复制代码

这样设置,貌似报错

Caused by: org.quartz.SchedulerException: ConnectionProvider class 'org.quartz.utils.HikariCpPoolingConnectionProvider' could not be instantiated.

at org.quartz.impl.StdSchedulerFactory.instantiate(StdSchedulerFactory.java:936) ~[quartz-2.3.0.jar:na]

at org.quartz.impl.StdSchedulerFactory.getScheduler(StdSchedulerFactory.java:1559) ~[quartz-2.3.0.jar:na]复制代码

quartz-2.3.0-sources.jar!/org/quartz/impl/StdSchedulerFactory.java

ConnectionProvider cp = null;

try {

cp = (ConnectionProvider) loadHelper.loadClass(cpClass).newInstance();

} catch (Exception e) {

initException = new SchedulerException("ConnectionProvider class '" + cpClass

+ "' could not be instantiated.", e);

throw initException;

}复制代码不过我是在spring中直接配置连接池的,详见springboot集成quartz2.3.0,因而就不管了

性能问题

批量参数

采用jdbc store的话,定时轮询数据库,比较消耗时间,一种解决方案就是尽量批量查询,这里可以设置两个参数如下

org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow=1 ## 时间窗口,默认0

org.quartz.scheduler.batchTriggerAcquisitionMaxCount=5 ##这里默认是1,可以设置跟thread pool的core size相等复制代码

quartz-2.3.0-sources.jar!/org/quartz/core/QuartzSchedulerResources.java

batchTriggerAcquisitionMaxCount对应的就是QuartzSchedulerResources中的maxBatchSize

batchTriggerAcquisitionFireAheadTimeWindow对应的就是QuartzSchedulerResources中的batchTimeWindow

triggers = qsRsrcs.getJobStore().acquireNextTriggers(

now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());复制代码这里实际取availThreadCount与maxBatchSize的最小值作为maxCount

最后作用的是selectTriggerToAcquire

quartz-2.3.0-sources.jar!/org/quartz/impl/jdbcjobstore/JobStoreSupport.java

protected List acquireNextTrigger(Connection conn, long noLaterThan, int maxCount, long timeWindow)

throws JobPersistenceException {

if (timeWindow < 0) {

throw new IllegalArgumentException();

}

List acquiredTriggers = new ArrayList();

Set acquiredJobKeysForNoConcurrentExec = new HashSet();

final int MAX_DO_LOOP_RETRY = 3;

int currentLoopCount = 0;

do {

currentLoopCount ++;

try {

List keys = getDelegate().selectTriggerToAcquire(conn, noLaterThan + timeWindow, getMisfireTime(), maxCount);

// No trigger is ready to fire yet.

if (keys == null || keys.size() == 0)

return acquiredTriggers;

long batchEnd = noLaterThan;

// ...

// if we didn't end up with any trigger to fire from that first

// batch, try again for another batch. We allow with a max retry count.

if(acquiredTriggers.size() == 0 && currentLoopCount < MAX_DO_LOOP_RETRY) {

continue;

}

// We are done with the while loop.

break;

} catch (Exception e) {

throw new JobPersistenceException(

"Couldn't acquire next trigger: " + e.getMessage(), e);

}

} while (true);

// Return the acquired trigger list

return acquiredTriggers;

}复制代码

sql实例

quartz-2.3.0-sources.jar!/org/quartz/impl/jdbcjobstore/StdJDBCDelegate.java

public List selectTriggerToAcquire(Connection conn, long noLaterThan, long noEarlierThan, int maxCount)

throws SQLException {

PreparedStatement ps = null;

ResultSet rs = null;

List nextTriggers = new LinkedList();

try {

ps = conn.prepareStatement(rtp(SELECT_NEXT_TRIGGER_TO_ACQUIRE));

// Set max rows to retrieve

if (maxCount < 1)

maxCount = 1; // we want at least one trigger back.

ps.setMaxRows(maxCount);

// Try to give jdbc driver a hint to hopefully not pull over more than the few rows we actually need.

// Note: in some jdbc drivers, such as MySQL, you must set maxRows before fetchSize, or you get exception!

ps.setFetchSize(maxCount);

ps.setString(1, STATE_WAITING);

ps.setBigDecimal(2, new BigDecimal(String.valueOf(noLaterThan)));

ps.setBigDecimal(3, new BigDecimal(String.valueOf(noEarlierThan)));

rs = ps.executeQuery();

while (rs.next() && nextTriggers.size() <= maxCount) {

nextTriggers.add(triggerKey(

rs.getString(COL_TRIGGER_NAME),

rs.getString(COL_TRIGGER_GROUP)));

}

return nextTriggers;

} finally {

closeResultSet(rs);

closeStatement(ps);

}

}复制代码这里的maxCount对应ps.setMaxRows(maxCount)

noLaterThan = noLaterThan + timeWindow ; noEarlierThan = getMisfireTime() 分别作用在NEXT_FIRE_TIME的范围上

NEXT_FIRE_TIME <= noLaterThan && NEXT_FIRE_TIME >= noEarlierThan

查询sql实例

SELECT TRIGGER_NAME, TRIGGER_GROUP, NEXT_FIRE_TIME, PRIORITY FROM QRTZ_TRIGGERS WHERE SCHED_NAME = 'schedulerFactoryBean' AND TRIGGER_STATE = ? AND NEXT_FIRE_TIME <= ? AND (MISFIRE_INSTR = -1 OR (MISFIRE_INSTR != -1 AND NEXT_FIRE_TIME >= ?)) ORDER BY NEXT_FIRE_TIME ASC, PRIORITY DESC复制代码

小结

由于是定时轮询trigger,那么在采用jdbc store的话,则这里的轮询可能是个不小的开销,对数据库有潜在的压力,另外在数据库挂掉的时候,可能也有问题,虽然做了延迟等待。另外尽量配置连接池,还有如果需要的话,调整batch参数。

doc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在现有省、市港口信息化系统进行有效整合基础上,借鉴新 一代的感知-传输-应用技术体系,实现对码头、船舶、货物、重 大危险源、危险货物装卸过程、航管航运等管理要素的全面感知、 有效传输和按需定制服务,为行政管理人员和相关单位及人员提 供高效的管理辅助,并为公众提供便捷、实时的水运信息服务。 建立信息整合、交换和共享机制,建立健全信息化管理支撑 体系,以及相关标准规范和安全保障体系;按照“绿色循环低碳” 交通的要求,搭建高效、弹性、高可扩展性的基于虚拟技术的信 息基础设施,支撑信息平台低成本运行,实现电子政务建设和服务模式的转变。 实现以感知港口、感知船舶、感知货物为手段,以港航智能 分析、科学决策、高效服务为目的和核心理念,构建“智慧港口”的发展体系。 结合“智慧港口”相关业务工作特点及信息化现状的实际情况,本项目具体建设目标为: 一张图(即GIS 地理信息服务平台) 在建设岸线、港口、港区、码头、泊位等港口主要基础资源图层上,建设GIS 地理信息服务平台,在此基础上依次接入和叠加规划建设、经营、安全、航管等相关业务应用专题数据,并叠 加动态数据,如 AIS/GPS/移动平台数据,逐步建成航运管理处 "一张图"。系统支持扩展框架,方便未来更多应用资源的逐步整合。 现场执法监管系统 基于港口(航管)执法基地建设规划,依托统一的执法区域 管理和数字化监控平台,通过加强对辖区内的监控,结合移动平 台,形成完整的多维路径和信息追踪,真正做到问题能发现、事态能控制、突发问题能解决。 运行监测和辅助决策系统 对区域港口与航运业务日常所需填报及监测的数据经过科 学归纳及分析,采用统一平台,消除重复的填报数据,进行企业 输入和自动录入,并进行系统智能判断,避免填入错误的数据, 输入的数据经过智能组合,自动生成各业务部门所需的数据报 表,包括字段、格式,都可以根据需要进行定制,同时满足扩展 性需要,当有新的业务监测数据表需要产生时,系统将分析新的 需求,将所需字段融合进入日常监测和决策辅助平台的统一平台中,并生成新的所需业务数据监测及决策表。 综合指挥调度系统 建设以港航应急指挥中心为枢纽,以各级管理部门和经营港 口企业为节点,快速调度、信息共享的通信网络,满足应急处置中所需要的信息采集、指挥调度和过程监控等通信保障任务。 设计思路 根据项目的建设目标和“智慧港口”信息化平台的总体框架、 设计思路、建设内容及保障措施,围绕业务协同、信息共享,充 分考虑各航运(港政)管理处内部管理的需求,平台采用“全面 整合、重点补充、突出共享、逐步完善”策略,加强重点区域或 运输通道交通基础设施、运载装备、运行环境的监测监控,完善 运行协调、应急处置通信手段,促进跨区域、跨部门信息共享和业务协同。 以“统筹协调、综合监管”为目标,以提供综合、动态、实 时、准确、实用的安全畅通和应急数据共享为核心,围绕“保畅通、抓安全、促应急"等实际需求来建设智慧港口信息化平台。 系统充分整合和利用航运管理处现有相关信息资源,以地理 信息技术、网络视频技术、互联网技术、移动通信技术、云计算 技术为支撑,结合航运管理处专网与行业数据交换平台,构建航 运管理处与各部门之间智慧、畅通、安全、高效、绿色低碳的智 慧港口信息化平台。 系统充分考虑航运管理处安全法规及安全职责今后的变化 与发展趋势,应用目前主流的、成熟的应用技术,内联外引,优势互补,使系统建设具备良好的开放性、扩展性、可维护性。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值