Skip to content

定时任务 ​

自 v6.2.0 起

@Scheduled 注解自 UltiTools-API v6.2.0 起可用。

UltiTools 提供了声明式的方式来调度重复或延迟任务。无需手动创建 BukkitRunnable 对象,只需在方法上添加 @Scheduled 注解,框架会自动处理其余工作。

基本用法 ​

在任意受容器管理的 Bean(如 @Service)中,给 void、无参方法添加 @Scheduled:

java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class AutoSaveService {

    @Scheduled(period = 6000) // Every 5 minutes (6000 ticks)
    public void autoSave() {
        // This method is called automatically by the framework
        Bukkit.getLogger().info("Auto-saving data...");
    }
}

Tick 换算

Minecraft 以每秒 20 tick 的速率运行:1 秒等于 20 tick,1 分钟等于 1,200 tick,5 分钟等于 6,000 tick,30 分钟等于 36,000 tick。

注解属性 ​

属性类型默认值说明
delaylong0首次执行前的延迟 tick 数
periodlong-1重复间隔 tick 数。-1 表示只执行一次
asyncbooleanfalse在异步线程而非主线程上运行

自 v6.3.0 起,period 和 delay 也可以通过 config、periodKey、delayKey 从配置项读取,见绑定到配置项的时间。

一次性延迟任务 ​

只设置 delay(period 保持默认 -1)即可在延迟后执行一次:

java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class WelcomeService {

    @Scheduled(delay = 100) // Run once, 5 seconds after plugin loads
    public void sendWelcomeMessage() {
        Bukkit.broadcastMessage("Plugin loaded successfully!");
    }
}

重复任务 ​

设置 period 为正数即可创建重复任务:

java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class ScoreboardService {

    @Scheduled(delay = 20, period = 200) // Start after 1 second, repeat every 10 seconds
    public void updateScoreboard() {
        for (Player player : Bukkit.getOnlinePlayers()) {
            // Update each player's scoreboard
        }
    }
}

异步任务 ​

对于不需要直接访问 Bukkit API 的任务(如数据库操作、HTTP 请求),设置 async = true:

java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class InterestService {

    @Autowired
    private UltiToolsPlugin plugin;

    @Scheduled(period = 36000, async = true) // Every 30 minutes, async
    public void distributeInterest() {
        DataOperator<AccountEntity> dataOperator =
            plugin.getDataOperator(AccountEntity.class);
        List<AccountEntity> accounts = dataOperator.getAll();
        for (AccountEntity account : accounts) {
            account.setBalance(account.getBalance() * 1.01);
            try {
                dataOperator.update(account);
            } catch (IllegalAccessException e) {
                Bukkit.getLogger().warning("Failed to update account: " + e.getMessage());
            }
        }
    }
}

async = true 只适用于字面量时间。自 v6.3.0 起,绑定到配置项的任务必须是同步任务。

Bukkit 线程安全

当 async = true 时,任务在非主线程上运行。你不能从异步线程调用大多数 Bukkit API 方法。如果需要在异步任务中与 Bukkit API 交互,请切换回主线程:

java
Bukkit.getScheduler().runTask(UltiTools.getInstance(), () -> {
    // 在这里可以安全调用 Bukkit API
    player.sendMessage("操作完成!");
});

绑定到配置项的时间 ​

自 v6.3.0 起

@Scheduled 可以从模块自己的配置文件中读取执行间隔和首次延迟,服主因此可以直接调整间隔,你不需要再手写第二套调度逻辑。

不写 period 和 delay 字面量,改用 periodKey 和 delayKey 指定一个 @ConfigEntry 路径,并用 config 指定配置实体类。读取到的值以秒为单位,乘以 20 换算为 tick。默认值只写在配置字段上:

java
@Getter
@Setter
@ConfigEntity("config/economy.yml")
public class EconomyConfig extends AbstractConfigEntity {
    @ConfigEntry(path = "interest.interval", comment = "利息发放间隔(秒)")
    private int interestInterval = 1800;

    public EconomyConfig(String configFilePath) {
        super(configFilePath);
    }
}
java
@Service
public class InterestService {

    @Scheduled(config = EconomyConfig.class, periodKey = "interest.interval", delayKey = "interest.interval")
    public void distributeInterest() {
        // 每隔 interest.interval 秒在主线程上执行一次
    }
}

delayKey 可以与 periodKey 指向同一个键,这样首次执行会先等待一个完整的间隔。像发放利息这类不应在加载时立即执行的任务,通常就需要这样写。键按 @ConfigEntry(path = ...) 声明的路径匹配;path 为空时按字段名匹配。

属性类型默认值说明
configClass<? extends AbstractConfigEntity>AbstractConfigEntity.class(未绑定)periodKey 与 delayKey 所指键所在的配置实体类
periodKeyString""(未绑定)其值(秒)作为重复间隔的键,取代 period
delayKeyString""(未绑定)其值(秒)作为首次延迟的键,取代 delay

加载时检查 ​

绑定在模块加载时检查。任何一项不通过,只有该模块被拒绝加载,日志中会给出键名和值。以下情况会拒绝模块:

  • 字面量与取代它的键同时设置(period 与 periodKey,或 delay 与 delayKey);
  • 该配置类没有为模块恰好注册一次。指向目录的 @ConfigEntity 不能用于绑定;
  • 键没有匹配到任何 @ConfigEntry 路径;
  • 被绑定字段的类型不是 int、long、Integer 或 Long;
  • 值为 null、小于 1 秒,或大于 107,374,182 秒(Integer.MAX_VALUE / 20,约 3.4 年)。0 不表示关闭。

重载行为 ​

修改后的值在执行 /ul reload 时生效,任务保持它在当前周期中的位置:下一次执行时间为上一次执行时间加上新的间隔;如果任务还没有执行过,则为任务启动时间加上新的延迟。如果这个时间点已经过去,任务会在下一个 tick 执行。重载不会让任务提前执行,也不会因为重新计时而推迟它。

值没有变化的任务不受影响。重载时读到无效的值不会被应用:保留当前正在使用的值,并输出一条指明该键的 WARNING。通过面板所做的修改在下一次 /ul reload 时生效。面板写入如果把被绑定的键设为上述范围之外的值(例如 0),会像违反 @Range 一样被拒绝,不会写入任何内容。

绑定的限制 ​

  • 只支持同步任务。 被绑定的方法不能设置 async = true,这种组合会在加载时被拒绝。请绑定一个同步任务,再在方法体中通过 Bukkit.getScheduler().runTaskAsynchronously(...) 执行耗时工作。使用字面量的 async 任务不受影响。允许异步绑定的工作由 issue #535 跟踪。
  • 只查找本类声明的方法。 与未绑定的 @Scheduled 一样,绑定只在 Bean 类自身声明的方法上查找。从父类继承的方法既不会被调度,也不会被检查,因此请把被绑定的方法声明在 Bean 类本身(#532)。
  • 被绑定的字段不要加 @Range。上述绑定范围就是该字段的范围。如果同一字段上还有模块自己的 @Range,重载时超出范围的值会让配置重载本身抛出异常,进而中止该模块其余的重载步骤(#509),而不是保留当前正在使用的值。如果字段在绑定之前已经带有 @Range,请将其删除。
  • 只支持 UltiTools 模块。 外部插件 API 插件的 Bean 上的绑定会被拒绝。

必需的 api-version ​

使用绑定的模块必须在 plugin.yml 中声明 api-version: 630。6.2.x 框架不认识这些属性,会静默忽略它们,被绑定的任务于是只会在加载时执行一次,而不是按间隔重复执行。声明这个下限后,旧框架会直接拒绝加载该模块。6.3.0 自身也会拒绝使用了绑定、却声明了更低 api-version 的模块。为什么只提高 pom.xml 中的 pin 不够,见模块版本规范。

绑定命令冷却

@CmdCD 也支持同样的绑定方式,见命令冷却。

自动生命周期管理 ​

使用 @Scheduled 注解的任务由框架自动管理:

  • 注册:插件加载时自动发现并启动任务
  • 取消:当所属插件卸载或服务器关闭时,所有任务自动取消

你无需手动追踪或取消任务。

使用要求 ​

  • 被注解的方法必须是 void 且无参数
  • 方法必须在受容器管理的 Bean 中(如 @Service)
  • Bean 必须在 @UltiToolsModule(scanBasePackages = {...}) 扫描的包内
java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@UltiToolsModule(scanBasePackages = {"com.example.plugin"})
public class MyPlugin extends UltiToolsPlugin {
    @Override
    public boolean registerSelf() { return true; }
    @Override
    public void unregisterSelf() { }
}

完整示例 ​

java
package com.ultikits.docs.scheduled;

import com.ultikits.ultitools.UltiTools;
import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.aop.ExceptionHandler;
import com.ultikits.ultitools.interfaces.DataOperator;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class ServerMonitorService {

    @Autowired
    private UltiToolsPlugin plugin;

    // Check server health every minute
    @Scheduled(delay = 1200, period = 1200, async = true)
    public void checkServerHealth() {
        Runtime runtime = Runtime.getRuntime();
        long usedMemory = runtime.totalMemory() - runtime.freeMemory();
        long maxMemory = runtime.maxMemory();
        double memoryUsage = (double) usedMemory / maxMemory * 100;

        if (memoryUsage > 90) {
            Bukkit.getScheduler().runTask(UltiTools.getInstance(), () -> {
                Bukkit.broadcastMessage("[Monitor] Warning: Memory usage at "
                    + String.format("%.1f", memoryUsage) + "%");
            });
        }
    }

    // Clean expired data daily (24 hours = 1,728,000 ticks)
    @Scheduled(period = 1728000, async = true)
    public void cleanExpiredData() {
        DataOperator<TempDataEntity> dataOperator =
            plugin.getDataOperator(TempDataEntity.class);
        dataOperator.query()
            .where("expireTime").lt(System.currentTimeMillis())
            .delete();
    }
}

贡献者

The avatar of contributor named as Ling Bao Ling Bao
The avatar of contributor named as Claude Opus 5.5 (1M context) Claude Opus 5.5 (1M context)

基于 MIT 许可发布