Browse Source

- 1、【新增】新增AI执行器示例,并与spring-ai集成打通;内置一系列AIXxlJob,支持快速开发AI类任务(xxl-job-executor-sample-springboot-ai)。
- 2、【新增】新增通用OllamaChat任务(ollamaJobHandler),支持自定义prompt、input等输入信息,示例参数如下;

xuxueli 7 months ago
parent
commit
67da6864d1

+ 19 - 6
doc/XXL-JOB官方文档.md

@@ -2447,18 +2447,31 @@ public void execute() {
 - 3、【优化】IP获取逻辑优化,优先遍历网卡来获取可用IP;
 - 4、【优化】通用命令行任务(“commandJobHandler”)优化,支持多参数执行,命令及参数之间通过空格隔开;如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据;
 - 5、【优化】通用HTTP任务(httpJobHandler)优化,任务参数格式调整为json格式;
-- 6、【升级】多个项目依赖升级至较新稳定版本,涉及 gson、groovy、spring/springboot 等;
+- 6、【升级】多个项目依赖升级至较新稳定版本,涉及 netty、groovy、spring/springboot 等;
 
 **备注:**
 - a、本次升级数据模型及通讯协议向前兼容,v2.4.*及后续版本可无缝升级;
 - b、版本3.x开始要求Jdk17;版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求,可选择接入不同版本;
 
 ### 7.38 版本 v3.0.1 Release Notes[规划中]
-- 1、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题(ISSUE-2081)。
-- 2、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题(ISSUE-2220)。
-- 3、【优化】Glue IDE优化,版本回溯支持查看修改时间;
-- 4、[规划中]登陆态Token生成逻辑优化,混淆登陆时间属性,降低token泄漏风险;
-- 5、[规划中]组件扫描改为BeanPostProcessor方式,避免小概率情况下提前初始化;底层组件移除单例写法,汇总factory统一管理;
+- 1、【新增】新增AI执行器示例,并与spring-ai集成打通;内置一系列AIXxlJob,支持快速开发AI类任务(xxl-job-executor-sample-springboot-ai)。
+- 2、【新增】新增通用OllamaChat任务(ollamaJobHandler),支持自定义prompt、input等输入信息,示例参数如下;
+```
+{
+    "input": "{输入信息,必填信息}",
+    "prompt": "{模型prompt,可选信息}"
+}
+```
+- 3、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题(ISSUE-2081)。
+- 4、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题(ISSUE-2220)。
+- 5、【优化】Glue IDE调整,版本回溯支持查看修改时间;
+- 6、【优化】任务RollingLog调整,XSS过滤支持白名单排出,提升日志易读性;
+- 7、【升级】多个项目依赖升级至较新稳定版本,涉及 gson、groovy、spring/springboot 等;
+
+
+### 7.39 版本 v3.0.2 Release Notes[规划中]
+- 1、[规划中]登陆态Token生成逻辑优化,混淆登陆时间属性,降低token泄漏风险;
+- 2、[规划中]组件扫描改为BeanPostProcessor方式,避免小概率情况下提前初始化;底层组件移除单例写法,汇总factory统一管理;
 
 ### TODO LIST
 - 1、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。

+ 6 - 6
pom.xml

@@ -29,20 +29,20 @@
 		<maven-javadoc-plugin.version>3.11.2</maven-javadoc-plugin.version>
 		<maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version>
 		<!-- base -->
-		<slf4j-api.version>2.0.16</slf4j-api.version>
-		<junit-jupiter.version>5.11.4</junit-jupiter.version>
+		<slf4j-api.version>2.0.17</slf4j-api.version>
+		<junit-jupiter.version>5.12.1</junit-jupiter.version>
 		<jakarta.annotation-api.version>3.0.0</jakarta.annotation-api.version>
 		<!-- net -->
-		<netty.version>4.1.117.Final</netty.version>
+		<netty.version>4.2.0.Final</netty.version>
 		<gson.version>2.12.1</gson.version>
 		<!-- spring -->
-		<spring-boot.version>3.4.2</spring-boot.version>
-		<spring.version>6.2.2</spring.version>
+		<spring-boot.version>3.4.4</spring-boot.version>
+		<spring.version>6.2.5</spring.version>
 		<!-- db -->
 		<mybatis-spring-boot-starter.version>3.0.4</mybatis-spring-boot-starter.version>
 		<mysql-connector-j.version>9.2.0</mysql-connector-j.version>
 		<!-- dynamic language -->
-		<groovy.version>4.0.25</groovy.version>
+		<groovy.version>4.0.26</groovy.version>
 	</properties>
 
 	<build>

+ 34 - 3
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java

@@ -127,7 +127,7 @@ public class JobLogController {
 	public String logDetailPage(@RequestParam("id") int id, Model model){
 
 		// base check
-		ReturnT<String> logStatue = ReturnT.SUCCESS;
+		//ReturnT<String> logStatue = ReturnT.SUCCESS;
 		XxlJobLog jobLog = xxlJobLogDao.load(id);
 		if (jobLog == null) {
             throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid"));
@@ -162,8 +162,7 @@ public class JobLogController {
 
 			// fix xss
 			if (logResult.getContent()!=null && StringUtils.hasText(logResult.getContent().getLogContent())) {
-				String newLogContent = logResult.getContent().getLogContent();
-				newLogContent = HtmlUtils.htmlEscape(newLogContent, "UTF-8");
+				String newLogContent = filter(logResult.getContent().getLogContent());
 				logResult.getContent().setLogContent(newLogContent);
 			}
 
@@ -174,6 +173,38 @@ public class JobLogController {
 		}
 	}
 
+	/**
+	 * filter xss tag
+	 *
+	 * @param originData
+	 * @return
+	 */
+	private String filter(String originData){
+
+		// exclude tag
+		Map<String, String> excludeTagMap = new HashMap<String, String>();
+		excludeTagMap.put("<br>", "###TAG_BR###");
+		excludeTagMap.put("<b>", "###TAG_BOLD###");
+		excludeTagMap.put("</b>", "###TAG_BOLD_END###");
+
+		// replace
+		for (String key : excludeTagMap.keySet()) {
+			String value = excludeTagMap.get(key);
+			originData = originData.replaceAll(key, value);
+		}
+
+		// htmlEscape
+		originData = HtmlUtils.htmlEscape(originData, "UTF-8");
+
+		// replace back
+		for (String key : excludeTagMap.keySet()) {
+			String value = excludeTagMap.get(key);
+			originData = originData.replaceAll(value, key);
+		}
+
+		return originData;
+	}
+
 	@RequestMapping("/logKill")
 	@ResponseBody
 	public ReturnT<String> logKill(@RequestParam("id") int id){

+ 1 - 0
xxl-job-executor-samples/pom.xml

@@ -12,6 +12,7 @@
     <modules>
         <module>xxl-job-executor-sample-frameless</module>
         <module>xxl-job-executor-sample-springboot</module>
+        <module>xxl-job-executor-sample-springboot-ai</module>
     </modules>
 
     <properties>

+ 83 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/pom.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xuxueli</groupId>
+        <artifactId>xxl-job-executor-samples</artifactId>
+        <version>3.0.1-SNAPSHOT</version>
+    </parent>
+    <artifactId>xxl-job-executor-sample-springboot-ai</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Example executor project for spring boot.</description>
+    <url>https://www.xuxueli.com/</url>
+
+    <properties>
+        <spring-ai.version>1.0.0-M6</spring-ai.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-parent</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- spring-boot-starter-web (spring-webmvc + tomcat) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- xxl-job-core -->
+        <dependency>
+            <groupId>com.xuxueli</groupId>
+            <artifactId>xxl-job-core</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <!-- spring-ai -->
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <!-- spring-boot-maven-plugin (提供了直接运行项目的插件:如果是通过parent方式继承spring-boot-starter-parent则不用此插件) -->
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <!--<version>${spring-boot.version}</version>-->
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <fork>true</fork>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 16 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/XxlJobAIExecutorApplication.java

@@ -0,0 +1,16 @@
+package com.xxl.job.executor;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author xuxueli 2018-10-28 00:38:13
+ */
+@SpringBootApplication
+public class XxlJobAIExecutorApplication {
+
+	public static void main(String[] args) {
+        SpringApplication.run(XxlJobAIExecutorApplication.class, args);
+	}
+
+}

+ 91 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java

@@ -0,0 +1,91 @@
+package com.xxl.job.executor.config;
+
+import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
+import org.springframework.ai.chat.memory.InMemoryChatMemory;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * xxl-job config
+ *
+ * @author xuxueli 2017-04-28
+ */
+@Configuration
+public class XxlJobConfig {
+    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
+
+    @Value("${xxl.job.admin.addresses}")
+    private String adminAddresses;
+
+    @Value("${xxl.job.admin.accessToken}")
+    private String accessToken;
+
+    @Value("${xxl.job.admin.timeout}")
+    private int timeout;
+
+    @Value("${xxl.job.executor.appname}")
+    private String appname;
+
+    @Value("${xxl.job.executor.address}")
+    private String address;
+
+    @Value("${xxl.job.executor.ip}")
+    private String ip;
+
+    @Value("${xxl.job.executor.port}")
+    private int port;
+
+    @Value("${xxl.job.executor.logpath}")
+    private String logPath;
+
+    @Value("${xxl.job.executor.logretentiondays}")
+    private int logRetentionDays;
+
+
+    @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.setTimeout(timeout);
+        xxlJobSpringExecutor.setLogPath(logPath);
+        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
+
+        return xxlJobSpringExecutor;
+    }
+
+    /**
+     * ChatClient
+     *
+     * @param ollamaChatModel
+     * @return
+     * @throws Exception
+     */
+    @Bean
+    public ChatClient chatClient(OllamaChatModel ollamaChatModel) throws Exception {
+        // init ollamaiChatClient
+        ChatClient ollamaiChatClient  = ChatClient
+                .builder(ollamaChatModel)
+                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))    // 管理对话上下文记忆
+                .defaultAdvisors(new SimpleLoggerAdvisor())                                 // 记录日志的Advisor,
+                .defaultOptions(OllamaOptions.builder().topP(0.7).build())                  // 设置ChatModel参数
+                .build();
+
+        return ollamaiChatClient;
+    }
+
+
+}

+ 63 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/controller/IndexController.java

@@ -0,0 +1,63 @@
+package com.xxl.job.executor.controller;//package com.xxl.job.executor.mvc.controller;
+
+import com.xxl.job.executor.config.XxlJobConfig;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import reactor.core.publisher.Flux;
+
+@Controller
+@EnableAutoConfiguration
+public class IndexController {
+    private static Logger logger = LoggerFactory.getLogger(IndexController.class);
+
+
+    @RequestMapping("/")
+    @ResponseBody
+    String index() {
+        return "xxl job executor running.";
+    }
+
+
+    @Resource
+    private ChatClient chatClient;
+    private static String prompt = "你好,你是一个研发工程师,擅长解决技术类问题。";
+
+
+    /**
+     * ChatClient 简单调用
+     */
+    @GetMapping("/chat/simple")
+    @ResponseBody
+    public String simpleChat(@RequestParam(value = "input") String input) {
+        String result = chatClient
+                .prompt(prompt)
+                .user(input)
+                .call()
+                .content();
+        System.out.println("result: " + result);
+        return result;
+    }
+
+    /**
+     * ChatClient 流式调用
+     */
+    @GetMapping("/chat/stream")
+    public Flux<String> streamChat(HttpServletResponse response, @RequestParam(value = "input") String input) {
+        response.setCharacterEncoding("UTF-8");
+        return chatClient
+                .prompt(prompt)
+                .user(input)
+                .stream()
+                .content();
+    }
+
+}

+ 79 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java

@@ -0,0 +1,79 @@
+package com.xxl.job.executor.jobhandler;
+
+import com.xxl.job.core.context.XxlJobHelper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import com.xxl.job.core.util.GsonTool;
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * AI 任务开发示例
+ *
+ * @author xuxueli 2025-04-06
+ */
+@Component
+public class AIXxlJob {
+
+    @Resource
+    private ChatClient chatClient;
+
+    /**
+     * 1、ollama Chat任务
+     *
+     *  参数示例:
+     *  <pre>
+     *      {
+     *          "input": "{输入信息,必填信息}",
+     *          "prompt": "{模型prompt,可选信息}"
+     *      }
+     *  </pre>
+     */
+    @XxlJob("ollamaJobHandler")
+    public void ollamaJobHandler() throws Exception {
+
+        // param
+        String param = XxlJobHelper.getJobParam();
+        if (param==null || param.trim().isEmpty()) {
+            XxlJobHelper.log("param is empty.");
+
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+        // param parse
+        String prompt = "你是一个研发工程师,擅长解决技术类问题。";
+        String input;
+        try {
+            Map<String, String> paramMap =GsonTool.fromJson(param, Map.class);
+            if (paramMap.containsKey("prompt")) {
+                prompt = paramMap.get("prompt");
+            }
+            input = paramMap.get("input");
+            if (input == null || input.trim().isEmpty()) {
+                XxlJobHelper.log("input is empty.");
+
+                XxlJobHelper.handleFail();
+                return;
+            }
+        } catch (Exception e) {
+            XxlJobHelper.log(e);
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+        // input
+        XxlJobHelper.log("<br><br><b>【Input】: " + input + "</b><br><br>");
+
+        // invoke
+        String result = chatClient
+                .prompt(prompt)
+                .user(input)
+                .call()
+                .content();
+        XxlJobHelper.log("<br><br><b>【Output】: " + result+ "</b><br><br>");
+    }
+
+}

+ 271 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/SampleXxlJob.java

@@ -0,0 +1,271 @@
+package com.xxl.job.executor.jobhandler;
+
+import com.xxl.job.core.context.XxlJobHelper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import com.xxl.job.core.util.GsonTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * XxlJob开发示例(Bean模式)
+ *
+ * 开发步骤:
+ *      1、任务开发:在Spring Bean实例中,开发Job方法;
+ *      2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
+ *      3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
+ *      4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
+ *
+ * @author xuxueli 2019-12-11 21:52:51
+ */
+@Component
+public class SampleXxlJob {
+    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);
+
+
+    /**
+     * 1、简单任务示例(Bean模式)
+     */
+    @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
+    }
+
+
+    /**
+     * 2、分片广播任务
+     */
+    @XxlJob("shardingJobHandler")
+    public void shardingJobHandler() throws Exception {
+
+        // 分片参数
+        int shardIndex = XxlJobHelper.getShardIndex();
+        int shardTotal = XxlJobHelper.getShardTotal();
+
+        XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
+
+        // 业务逻辑
+        for (int i = 0; i < shardTotal; i++) {
+            if (i == shardIndex) {
+                XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
+            } else {
+                XxlJobHelper.log("第 {} 片, 忽略", i);
+            }
+        }
+
+    }
+
+
+    /**
+     * 3、命令行任务
+     *
+     *  参数示例:"ls -a" 或者 "pwd"
+     */
+    @XxlJob("commandJobHandler")
+    public void commandJobHandler() throws Exception {
+        String command = XxlJobHelper.getJobParam();
+        int exitValue = -1;
+
+        BufferedReader bufferedReader = null;
+        try {
+            // valid
+            if (command==null || command.trim().length()==0) {
+                XxlJobHelper.handleFail("command empty.");
+                return;
+            }
+
+            // command split
+            String[] commandArray = command.split(" ");
+
+            // command process
+            ProcessBuilder processBuilder = new ProcessBuilder();
+            processBuilder.command(commandArray);
+            processBuilder.redirectErrorStream(true);
+
+            Process process = processBuilder.start();
+            //Process process = Runtime.getRuntime().exec(command);
+
+            BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream());
+            bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
+
+            // command log
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                XxlJobHelper.log(line);
+            }
+
+            // command exit
+            process.waitFor();
+            exitValue = process.exitValue();
+        } catch (Exception e) {
+            XxlJobHelper.log(e);
+        } finally {
+            if (bufferedReader != null) {
+                bufferedReader.close();
+            }
+        }
+
+        if (exitValue == 0) {
+            // default success
+        } else {
+            XxlJobHelper.handleFail("command exit value("+exitValue+") is failed");
+        }
+
+    }
+
+
+    /**
+     * 4、跨平台Http任务
+     *
+     *  参数示例:
+     *  <pre>
+     *      {
+     *          "url": "http://www.baidu.com",
+     *          "method": "get",
+     *          "data": "hello world"
+     *      }
+     *  </pre>
+     */
+    @XxlJob("httpJobHandler")
+    public void httpJobHandler() throws Exception {
+
+        // param
+        String param = XxlJobHelper.getJobParam();
+        if (param==null || param.trim().length()==0) {
+            XxlJobHelper.log("param["+ param +"] invalid.");
+
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+        // param parse
+        String url;
+        String method;
+        String data;
+        try {
+            Map<String, String> paramMap =GsonTool.fromJson(param, Map.class);
+            url = paramMap.get("url");
+            method = paramMap.get("method");
+            data = paramMap.get("data");
+        } catch (Exception e) {
+            XxlJobHelper.log(e);
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+        // param valid
+        if (url==null || url.trim().length()==0) {
+            XxlJobHelper.log("url["+ url +"] invalid.");
+
+            XxlJobHelper.handleFail();
+            return;
+        }
+        if (method==null || !Arrays.asList("GET", "POST").contains(method.toUpperCase())) {
+            XxlJobHelper.log("method["+ method +"] invalid.");
+
+            XxlJobHelper.handleFail();
+            return;
+        }
+        method = method.toUpperCase();
+        boolean isPostMethod = method.equals("POST");
+
+        // request
+        HttpURLConnection connection = null;
+        BufferedReader bufferedReader = null;
+        try {
+            // connection
+            URL realUrl = new URL(url);
+            connection = (HttpURLConnection) realUrl.openConnection();
+
+            // connection setting
+            connection.setRequestMethod(method);
+            connection.setDoOutput(isPostMethod);
+            connection.setDoInput(true);
+            connection.setUseCaches(false);
+            connection.setReadTimeout(5 * 1000);
+            connection.setConnectTimeout(3 * 1000);
+            connection.setRequestProperty("connection", "Keep-Alive");
+            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
+
+            // do connection
+            connection.connect();
+
+            // data
+            if (isPostMethod && data!=null && data.trim().length()>0) {
+                DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
+                dataOutputStream.write(data.getBytes("UTF-8"));
+                dataOutputStream.flush();
+                dataOutputStream.close();
+            }
+
+            // valid StatusCode
+            int statusCode = connection.getResponseCode();
+            if (statusCode != 200) {
+                throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
+            }
+
+            // result
+            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+            StringBuilder result = new StringBuilder();
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                result.append(line);
+            }
+            String responseMsg = result.toString();
+
+            XxlJobHelper.log(responseMsg);
+
+            return;
+        } catch (Exception e) {
+            XxlJobHelper.log(e);
+
+            XxlJobHelper.handleFail();
+            return;
+        } finally {
+            try {
+                if (bufferedReader != null) {
+                    bufferedReader.close();
+                }
+                if (connection != null) {
+                    connection.disconnect();
+                }
+            } catch (Exception e2) {
+                XxlJobHelper.log(e2);
+            }
+        }
+
+    }
+
+    /**
+     * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;
+     */
+    @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
+    public void demoJobHandler2() throws Exception {
+        XxlJobHelper.log("XXL-JOB, Hello World.");
+    }
+    public void init(){
+        logger.info("init");
+    }
+    public void destroy(){
+        logger.info("destroy");
+    }
+
+
+}

+ 34 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties

@@ -0,0 +1,34 @@
+# web port
+server.port=8082
+# no web
+#spring.main.web-environment=false
+
+# log config
+logging.config=classpath:logback.xml
+
+
+### 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-admin
+### xxl-job, access token
+xxl.job.admin.accessToken=default_token
+### xxl-job timeout by second, default 3s
+xxl.job.admin.timeout=3
+
+### xxl-job executor appname
+xxl.job.executor.appname=xxl-job-executor-sample-ai
+### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
+xxl.job.executor.address=
+### xxl-job executor server-info
+xxl.job.executor.ip=
+xxl.job.executor.port=9999
+### xxl-job executor log-path
+xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
+### xxl-job executor log-retention-days
+xxl.job.executor.logretentiondays=30
+
+
+### ollama
+spring.ai.ollama.base-url=http://localhost:11434
+spring.ai.ollama.chat.enabled=true
+spring.ai.ollama.chat.options.model=qwen2.5:1.5b
+spring.ai.ollama.chat.options.temperature=0.8

+ 29 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/logback.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration debug="false" scan="true" scanPeriod="1 seconds">
+
+    <contextName>logback</contextName>
+    <property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot-ai.log"/>
+
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
+            </pattern>
+        </encoder>
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="console"/>
+        <appender-ref ref="file"/>
+    </root>
+
+</configuration>

+ 14 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java

@@ -0,0 +1,14 @@
+package com.xxl.job.executor.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+public class XxlJobExecutorExampleBootApplicationTests {
+
+	@Test
+	public void test() {
+		System.out.println(11);
+	}
+
+}