【XXL-JOB】执行器架构设计和源码解析

news2024/9/22 11:25:29

简介

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
在这里插入图片描述
XXL-JOB分为B/S架构,调用中心是XXL-JOB服务端,执行器是客户端。

  • 调度中心核心功能:执行器管理、任务管理、任务调度、监控告警和故障转移
  • 执行器核心功能:负责业务任务处理,不关心任务调度

XXL-JOB将任务调度和任务执行隔离,将任务调度和执行进行解耦,让研发人员只关注业务部分,提高搞开发效率和系统扩展性。

集成XXL-JOB

添加依赖

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${最新稳定版}</version>
</dependency>

添加配置

### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

以下是我们需要改动的配置:

  • xxl.job.admin.addresses:调度中心地址,用于自动注册和心跳检测
  • xxl.job.executor.appname:指定执行器名称,每个服务都应该有不同的执行器名称,同一个服务的不同集群节点的执行器名称应该相同
  • xxl.job.accessToken:需要和调度中心配置保持一致,默认是default_token
  • xxl.job.executor.port=9999:如果在单机部署多个执行器时,注意要配置不同执行器端口,否则服务启动时会报端口冲突

启动类配置

执行器启动配置支持两种种方式:

  • XxlJobSpringExecutor:集成Spring框架【推荐】,不会有类爆炸问题,集成方便
  • XxlJobSimpleExecutor:无框架模式,可以参考源码示例xxl-job-executor-sample-frameless中的FrameLessXxlJobConfig类,优点不限制环境和框架,缺点每个任务就是一个类

这里以XxlJobSpringExecutor为例:

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info(">>>>>>>>>>> xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setAddress(address);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

    return xxlJobSpringExecutor;
}

到这里我们的执行器就集成完成啦。

自定义任务处理器

结合上面的配置一个执行器服务就配置好了,现在我们添加一个自定义的任务:

@Component
public class SampleXxlJob {
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");
        
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }
}

开发步骤:

  1. 任务开发:在Spring Bean实例中,开发Job方法;
  2. 注解配置:为Job方法添加注解 “@XxlJob(value=“自定义jobhandler名称”, init = “JobHandler初始化方法”, destroy = “JobHandler销毁方法”)”,注解value值对应的是调度中心新建任务的JobHandler属性的值。
  3. 执行日志:需要通过 “XxlJobHelper.log” 打印执行日志,这个日志可以在管理端的执行日志被查看;
  4. 任务结果:默认任务结果为 “成功” 状态,不需要主动设置;如有需求,比如设置任务结果为失败,可以通过 XxlJobHelper.handleFail/handleSuccess自主设置任务结果;
  5. 在调度中心配置任务调度

在这里插入图片描述

任务类型

这列演示的是最简单的任务,XXL_JOB还支持更为复杂的任务,任务分类:

  • 简单任务示例(Bean模式):定义一个Spring Bean,其中包含要执行的任务方法。
  • 分片广播任务:允许将一个大任务拆分成多个小任务,并在多个执行器实例上并行执行。这通常用于大数据处理或并行计算。
  • 命令行任务:允许直接执行系统命令或脚本。
  • 跨平台Http任务:通过HTTP请求来触发任务。在XXL-JOB调度中心添加任务时,选择HTTP模式,并配置相应的URL、请求方法和参数。
  • 生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑,常用于资源准备和清理工作。;

命令行任务和跨平台Http任务都是通过传入指定的参数在JOB中实现的任务操作,具体实现给可以看源码。

运行模式

在这里插入图片描述
这里的运行模式大致可以分为两种BEAN和````GLUE```。

  • BEAN:就是刚刚提到的简单示例的模式,也是最常用的模式。
  • GLUE:允许你在线编写任务的执行代码。你可以使用Groovy语言编写代码,并在XXL-JOB的Web界面上直接运行和调试。

GLUE示例

在这里插入图片描述
然后在线编辑任务,实现任务调度。
在这里插入图片描述

11:06:56.418 logback [xxl-job, EmbedServer bizThreadPool-1281151494] INFO  c.x.job.core.executor.XxlJobExecutor - >>>>>>>>>>> xxl-job regist JobThread success, jobId:3, handler:com.xxl.job.core.handler.impl.GlueJobHandler@7ae04e7a
GLUE任务测试

任务执行成功。

执行器生命周期

当服务启动后会通过XxlJobSpringExecutor去集成执行器,集成过程中就会完成执行器自动注册。

public class XxlJobExecutor  {
    public void start() throws Exception {
        // init logpath
        XxlJobFileAppender.initLogPath(logPath);
        // init invoker, admin-client
        initAdminBizList(adminAddresses, accessToken);
        // init JobLogFileCleanThread
        JobLogFileCleanThread.getInstance().start(logRetentionDays);
        // init TriggerCallbackThread
        TriggerCallbackThread.getInstance().start();
        // init executor-server
        initEmbedServer(address, ip, port, appname, accessToken);
    }
}
  • XxlJobFileAppender.initLogPath(logPath);:执行日志路和GULE源码路径初始化
  • initAdminBizList(adminAddresses, accessToken);:初始化调用调度中心的RPC工具
  • JobLogFileCleanThread.getInstance().start(logRetentionDays);:初始化日志定期清楚守护线程
  • TriggerCallbackThread.getInstance().start();:初始化任务执行结果通知调度中心的回调守护线程
  • initEmbedServer(address, ip, port, appname, accessToken);:初始化执行器服务端线程

启动内嵌服务 EmbedServer

在上诉执行器完成初始化后会启动嵌入式服务。

embedServer = new EmbedServer();
embedServer.start(address, port, appname, accessToken);

start方法中第一步会启动一个内嵌的netty服务器。

// start server
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel channel) throws Exception {
                channel.pipeline()
                        .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                        .addLast(new HttpServerCodec())
                        .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                        .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
            }
        })
        .childOption(ChannelOption.SO_KEEPALIVE, true);

// bind
ChannelFuture future = bootstrap.bind(port).sync();

logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

内嵌服务启动后会对外暴露接口给调度中心使用,完成执行器与调度中心的通讯。

  • /beat:调度中心故障探测接口,当任路由策略务配置为故障转移时是调用这个接口探活
  • /idleBeat:忙碌检测接口,当任务路由策略配置为忙碌转移时调用这个接口
  • /run:任务触发接口
  • /kill:任务终止接口
  • /log:在调用中心查看任务执行日志时调用这个接口

执行器注册&心跳检测 registryThread

启动内嵌服务后就会开始注册执行器到调度中心,执行器注册和心跳检测调用的是调度中心的同一个接口/registry

ExecutorRegistryThread.getInstance().start(appname, address);
while (!toStop) {
	try {
	    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
	    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
	        try {
	        	// 调用接口registry接口完成上报
	            ReturnT<String> registryResult = adminBiz.registry(registryParam);
	            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
	                registryResult = ReturnT.SUCCESS;
	                logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
	                break;
	            } else {
	                logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
	            }
	        } catch (Exception e) {
	            logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
	        }
	
	    }
	} catch (Exception e) {...}

	try {
	    if (!toStop) {
	    	// 每次循环休眠30秒
	        TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
	    }
	} catch (InterruptedException e) {...}
}
  • ExecutorRegistryThread.getInstance().start(appname, address);这里的start方法会启动一个守护线程,一直循环调用调度中心/registry接口,以此完成对注册器的注册和心跳检测
  • 心跳检测频率为30S一次
  • 调用中心有个守护线程每30秒执行一次,每次检查并删除超过90秒没有上报的执行器

执行器销毁 registryThread

当服务停用时会主动调用/registryRemove接口去调度中心销毁执行器

ExecutorRegistryThread.getInstance().toStop();

public void start(final String appname, final String address){

        // valid
        if (appname==null || appname.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null.");
            return;
        }
        if (XxlJobExecutor.getAdminBizList() == null) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null.");
            return;
        }

        registryThread = new Thread(new Runnable() {
            @Override
            public void run() {
            ...
                // registry remove
               try {
                   RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                   for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                       try {
                       	   // 调用`/registryRemove`接口去调度中心销毁执行器
                           ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                           if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                               registryResult = ReturnT.SUCCESS;
                               logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                               break;
                           } else {
                               logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                           }
                       } catch (Exception e) {...}
                   }
               } catch (Exception e) {...}
            }
        });
        registryThread.setDaemon(true);
        registryThread.setName("xxl-job, executor ExecutorRegistryThread");
        registryThread.start();
    }

执行器生命周期核心流程

在这里插入图片描述

执行定时任务

我们执行器初始化完成后执行器就正常注册到调度中心,当任务在时间轮被拿出来后通过调度策略和阻塞策略,最终通过EmbedServer暴露出来的/run接口触发任务执行。

case "/run":
   TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
   return executorBiz.run(triggerParam);

这里会带上几个核心参数:

  • jobId:任务ID
  • executorHandler:任务执行器名称
  • executorParams:执行参数

任务执行线程管理 JobThread

// 执行线程容器
private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();
public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
    JobThread newJobThread = new JobThread(jobId, handler);
    newJobThread.start();
    logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});

    JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);	// putIfAbsent | oh my god, map's put method return the old value!!!
    if (oldJobThread != null) {
        oldJobThread.toStop(removeOldReason);
        oldJobThread.interrupt();
    }
    return newJobThread;
}
public static JobThread loadJobThread(int jobId){
    return jobThreadRepository.get(jobId);
}

XXL-JOB会给每个任务创建一个执行线程 JobThread,每个线程会有一个任务队列triggerQueue。每当有任务提交过来会先获取到对应的执行线程,并将任务放到这个线程下的队列中。这个线程如果在30个周期内都没有新任务需要执行,那么这个线程将会被回收。

任务阻塞处理策略 ExecutorBlockStrategyEnum

...
// 如果存在 jobThread 说明可能存在有任务在执行,则需要进行阻塞处理策略
if (jobThread != null) {
    ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
    if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
        // 判断是否有任务在执行,如果有则直接丢弃任务
        if (jobThread.isRunningOrHasQueue()) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
        }
    } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
        // 判断是否有任务在执行,如果有则覆盖之前的调度
        if (jobThread.isRunningOrHasQueue()) {
            removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

            jobThread = null;
        }
    } else {
        
    }
}
...
if (jobThread == null) {
    jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
}
...
// 单机串行或者没有任务执行则直接放到线程队列中
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);

任务执行JobThread.run()

// execute
while(!toStop){
	running = false;
	idleTimes++;

	TriggerParam triggerParam = null;
	try {
		// 获取需要执行的任务
		triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
		if (triggerParam!=null) {

			// 生成对应的日志文件, 格式为 "logPath/yyyy-MM-dd/9999.log" 999为调度日志的ID
			String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
			...
			if (triggerParam.getExecutorTimeout() > 0) {
				// limit timeout
				Thread futureThread = null;
				try {
					// 超时任务的执行
					FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
						@Override
						public Boolean call() throws Exception {

							// init job context
							XxlJobContext.setXxlJobContext(xxlJobContext);

							handler.execute();
							return true;
						}
					});
					futureThread = new Thread(futureTask);
					futureThread.start();

					Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
				} catch (TimeoutException e) {
					...
				} finally {
					futureThread.interrupt();
				}
			} else {
				// 普通任务的执行
				handler.execute();
			}

			// 任务回调
			if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) {
				XxlJobHelper.handleFail("job handle result lost.");
			} 
		} else {
			if (idleTimes > 30) {
				if(triggerQueue.size() == 0) {	// avoid concurrent trigger causes jobId-lost
					XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
				}
			}
		}
	} catch (Throwable e) {...} finally {...}
}

这里首先需要在对象获取需要执行的任务,如果获取到任务需要直接修改线程运行状态为运行中,再判断是否是超时任务,如果是需要采用FutureTask来执行任务,否则直接执行。任务的执行直接调用Spring注册过来的hander。

执行器核心流程

在这里插入图片描述

总结

  1. 大量采用异步线程来任务调度的性能问题
  2. 执行器通过心跳机制来保证执行器的可用性
  3. 通过线程隔离、阻塞策略的方式来解决任务调度的可靠性
  4. 通过提调度中心dashboard的来解决系统可维护性和可观测性
  5. 通过accessToken来解决远程通讯的安全性

XXL-JOB完成流程

在这里插入图片描述

源码

https://github.com/xuxueli/xxl-job

引流

GitFlowPlus

GitFlowPlus分支管理IDEA插件

layering-cache

layering-cache 多级缓存开发框架

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1552745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

入门指南|营销中人工智能生成内容的主要类型 [新数据、示例和技巧]

由于人工智能技术的进步&#xff0c;内容生成不再是一项令人头疼的任务。随着人工智能越来越多地接管手动内容制作任务&#xff0c;营销人员明智的做法是了解现有的不同类型的人工智能生成内容&#xff0c;以及哪些内容从中受益最多。这些工具可以帮助我们制作对您的受众和品牌…

3.28C++

复数类的实现&#xff0c;写出三种构造函数&#xff0c;算术运算符、关系运算符、逻辑运算符重载尝试实现自增、自减运算符的重载 #include <iostream> using namespace std; class Num {int rel; //实部int vir; //虚部 public:Num():rel(2),vir(1){}Num(int rel,…

若依 3.8.7版本springboot前后端分离 整合mabatis plus

1.去掉mybatis 这一步我没有操作&#xff0c;看别人的博客有说不去掉可能冲突&#xff0c;也可能不冲突&#xff0c;我试下来就没去掉如需要去除&#xff0c;到总的pom.xml中properties标签下的<mybatis-spring-boot.version>x.x.x</mybatis-spring-boot.version>…

如何在极狐GitLab 自定义 Pages 域名、SSL/TLS 证书

本文作者&#xff1a;徐晓伟 GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 本文主要讲述了在极狐GitLab 用户…

深圳区块链交易所app系统开发,撮合交易系统开发

随着区块链技术的迅速发展和数字资产市场的蓬勃发展&#xff0c;区块链交易所成为了数字资产交易的核心场所之一。在这个快速发展的领域中&#xff0c;区块链交易所App系统的开发和撮合交易系统的建设至关重要。本文将探讨区块链交易所App系统开发及撮合交易系统的重要性&#…

包子凑数(蓝桥杯,闫氏DP分析法)

题目描述&#xff1a; 小明几乎每天早晨都会在一家包子铺吃早餐。 他发现这家包子铺有 N 种蒸笼&#xff0c;其中第 i 种蒸笼恰好能放 Ai 个包子。 每种蒸笼都有非常多笼&#xff0c;可以认为是无限笼。 每当有顾客想买 X 个包子&#xff0c;卖包子的大叔就会迅速选出若干笼…

解决:PytorchStreamWriter failed writing file data

文章目录 问题内容问题分析解决思路 问题内容 今天在炼丹的时候&#xff0c;我发现模型跑到140步的时候保存权重突然报了个问题&#xff0c;详细内容如下&#xff1a; Traceback (most recent call last):File "/public/home/dyedd/.conda/envs/diffusers/lib/python3.8…

uniapp输入框事件(防抖)

一、描述 在输入框输入内容或者说输入关键词的时候&#xff0c;往往都要进行做防抖处理。如果不做防抖&#xff0c;你输入什么&#xff0c;动态绑定的数据就会保持一致。这样不好吗&#xff0c;同步获取。有个业务场景&#xff0c;如果是搜索框&#xff0c;你每次一个字符&…

2024年第十届国际虚拟现实大会(ICVR 2024)即将召开!

会议面向虚拟现实、增强现实、人工智能等互联网新技术领域的专家及学者&#xff0c; 致力于共同促进国内外虚拟现实的发展与应用。 2014年至今&#xff0c;ICVR在全球新加坡&#xff0c;美国洛杉矶&#xff0c;中国成都&#xff0c;香港等国家及地区召开&#xff0c;面向虚拟现…

力扣面试150 二叉搜索树的最小绝对差 中序遍历

Problem: 530. 二叉搜索树的最小绝对差 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 录哥题解 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( 1 ) O(1) O(1) Code // 递归 class Solution {int ans Integer.MAX_VALUE;TreeNode pre;//一开…

C++的非类型模板参数与模板分离编译(模板显式实例化)

非类型模板参数与模板分离编译&#xff08;模板显式实例化&#xff09; 文章目录 非类型模板参数与模板分离编译&#xff08;模板显式实例化&#xff09;前言一、非类型模板参数二、模版分离编译1. 分离编译概念2. 模版的分离编译问题案例解决方法 总结 前言 ​ 本篇博客文章介…

vue2源码解析——Vue.set/$set方法如何给响应式对象添加属性

在Vue 2中需要向响应式对象添加新属性时&#xff0c;可以使用Vue.set或$set方法来实现。这两个方法的作用是向响应式对象添加属性并确保这个新属性也是响应式的。 为什么会有vue.set方法 Vue提供了Vue.set方法主要是为了解决在Vue 2.x 中动态添加属性时可能遇到的响应性问题。 …

八大技术趋势案例(区块链量子计算)

科技巨变,未来已来,八大技术趋势引领数字化时代。信息技术的迅猛发展,深刻改变了我们的生活、工作和生产方式。人工智能、物联网、云计算、大数据、虚拟现实、增强现实、区块链、量子计算等新兴技术在各行各业得到广泛应用,为各个领域带来了新的活力和变革。 为了更好地了解…

钡铼技术R40路由器助力智能船舶航行数据实时传输与分析

钡铼技术R40路由器在智能船舶领域的应用&#xff0c;对于航行数据的实时传输与分析具有重要意义。随着航运业的不断发展和智能化水平的提升&#xff0c;船舶航行数据的及时传输和有效分析对船舶的安全、运营效率等方面至关重要。而引入钡铼技术R40路由器&#xff0c;则可以实现…

前后端分离开发【Yapi平台】【Swagger注解自动生成接口文档平台】

前后端分离开发 介绍开发流程Yapi&#xff08;api接口文档编写平台&#xff09;介绍 Swagger使用方式1). 导入knife4j的maven坐标2). 导入knife4j相关配置类3). 设置静态资源映射4). 在LoginCheckFilter中设置不需要处理的请求路径 查看接口文档常用注解注解介绍 当前项目中&am…

Eclipse+Java+Swing实现斗地主游戏

一. 视频演示效果 java斗地主源码演示 ​ 二.项目结构 代码十分简洁&#xff0c;只有简单的7个类&#xff0c;实现了人机对战 素材为若干的gif图片 三.项目实现 启动类为Main类&#xff0c;继承之JFrame&#xff0c;JFrame 是 Java Swing 库中的一个类&#xff0c;用于创建窗…

Cisco Firepower FMCv修改管理Ip方法

FMCv 是部署在VMWARE虚拟平台上的FMC 部署完成后&#xff0c;如何修改管理IP 1 查看当前版本 show version 可以看到是for VMware 2 修改管理IP步骤 2.1 进入expert模式 expert2.2 进入超级用户 sudo su并输入密码 2.3 查看当前网卡Ip 2.4 修改Ip 命令&#xff1a; /…

Scala第十三章节(作为值的函数及匿名函数、柯里化、闭包及控制抽象以及计算器案例)

章节目标 掌握作为值的函数及匿名函数的用法了解柯里化的用法掌握闭包及控制抽象的用法掌握计算器案例 1.高阶函数介绍 Scala 混合了面向对象和函数式的特性&#xff0c;在函数式编程语言中&#xff0c;函数是“头等公民”&#xff0c;它和Int、String、Class等其他 类型处于…

Unity3d使用Jenkins自动化打包(Windows)(二)

文章目录 前言一、Unity工程准备二、Unity调取命令行实战一实战二实战三实战四实战五 总结 前言 自动化打包的价值在于让程序员更轻松地创建和管理构建工具链&#xff0c;提高编程效率&#xff0c;将繁杂的工作碎片化&#xff0c;变成人人&#xff08;游戏行业特指策划&#x…

ChatGPT 商业金矿(上)

原文&#xff1a;ChatGPT Business Goldmines 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第一章&#xff1a;为什么我写这本书 欢迎阅读《ChatGPT 多源收入&#xff1a;20 个利润丰厚的业务&#xff0c;任何人都可以在一周内使用 ChatGPT 开始》。我很高兴分享我…