强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PaperMC 插件开发完全指南 / 第 18 章:最佳实践

第 18 章:最佳实践

汇集代码规范、性能优化、安全建议和兼容性策略,帮助你写出高质量的插件。


18.1 代码规范

命名规范

类型 规范 示例
包名 全小写,逆域名 com.example.myplugin
类名 PascalCase PlayerManager, EconomyService
方法名 camelCase getBalance(), sendTitle()
常量 UPPER_SNAKE_CASE MAX_PLAYERS, DEFAULT_LANG
变量 camelCase playerCount, isReady
配置键 snake_case database.host, max-players

项目结构

com.example.myplugin/
├── MyPlugin.java           # 主类
├── commands/                # 命令处理
│   ├── MainCommand.java
│   ├── HealCommand.java
│   └── WarpCommand.java
├── listeners/               # 事件监听
│   ├── PlayerListener.java
│   └── CombatListener.java
├── managers/                # 管理器
│   ├── EconomyManager.java
│   ├── WarpManager.java
│   └── ConfigManager.java
├── models/                  # 数据模型
│   ├── PlayerData.java
│   └── Warp.java
├── database/                # 数据层
│   ├── DatabaseManager.java
│   └── PlayerDataDAO.java
├── utils/                   # 工具类
│   ├── MessageUtil.java
│   └── ItemBuilder.java
└── tasks/                   # 任务
    ├── AutoSaveTask.java
    └── ScoreboardTask.java

设计模式

// 1. 单例模式(Singleton)
public class EconomyManager {
    private static EconomyManager instance;

    public static EconomyManager getInstance() {
        if (instance == null) {
            instance = new EconomyManager();
        }
        return instance;
    }
}

// 2. 建造者模式(Builder)
public class ItemBuilder {
    private final ItemStack item;
    private final ItemMeta meta;

    public ItemBuilder(Material material) {
        this.item = new ItemStack(material);
        this.meta = item.getItemMeta();
    }

    public ItemBuilder name(String name) {
        meta.displayName(Component.text(name));
        return this;
    }

    public ItemBuilder lore(String... lines) {
        meta.lore(Arrays.stream(lines)
            .map(Component::text)
            .collect(Collectors.toList()));
        return this;
    }

    public ItemStack build() {
        item.setItemMeta(meta);
        return item;
    }
}

// 使用
ItemStack sword = new ItemBuilder(Material.DIAMOND_SWORD)
    .name("§6传说之剑")
    .lore("§7攻击 +100", "§7速度 +20%")
    .build();

18.2 性能优化

高频事件优化

// 不好:每次移动都处理
@EventHandler
public void onMove(PlayerMoveEvent event) {
    checkRegion(event.getPlayer());
}

// 好:只在跨越区块时处理
@EventHandler(ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) {
    Location from = event.getFrom();
    Location to = event.getTo();

    if (to == null) return;
    if (from.getBlockX() >> 4 == to.getBlockX() >> 4
        && from.getBlockZ() >> 4 == to.getBlockZ() >> 4) {
        return; // 同一区块,跳过
    }

    checkRegion(event.getPlayer());
}

数据结构选择

场景 推荐 避免
频繁查找 HashMap / HashSet ArrayList.contains()
有序数据 TreeMap / TreeSet 手动排序
只读列表 List.of() / Set.of() Collections.unmodifiableXxx()
大量数值 原生数组 (int[]) ArrayList<Integer>
并发访问 ConcurrentHashMap synchronized HashMap

字符串拼接

// 不好:循环中拼接
String result = "";
for (String s : list) {
    result += s + ", "; // 每次创建新 String 对象
}

// 好:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : list) {
    if (!sb.isEmpty()) sb.append(", ");
    sb.append(s);
}
String result = sb.toString();

// 更好:使用 Stream
String result = String.join(", ", list);

缓存策略

public class CacheManager<K, V> {

    private final Map<K, CacheEntry<V>> cache = new HashMap<>();
    private final long ttl; // 缓存过期时间(毫秒)

    public CacheManager(long ttlMillis) {
        this.ttl = ttlMillis;
    }

    public void put(K key, V value) {
        cache.put(key, new CacheEntry<>(value, System.currentTimeMillis()));
    }

    public V get(K key) {
        CacheEntry<V> entry = cache.get(key);
        if (entry == null) return null;

        if (System.currentTimeMillis() - entry.timestamp() > ttl) {
            cache.remove(key); // 过期移除
            return null;
        }

        return entry.value();
    }

    public void clear() {
        cache.clear();
    }

    private record CacheEntry<V>(V value, long timestamp) {}
}

实体管理

// 限制实体数量
public void spawnMobSafely(Location loc, EntityType type, int maxNearby) {
    long count = loc.getWorld().getNearbyEntities(loc, 32, 32, 32).stream()
        .filter(e -> e.getType() == type)
        .count();

    if (count < maxNearby) {
        loc.getWorld().spawnEntity(loc, type);
    }
}

// 定期清理
Bukkit.getScheduler().runTaskTimer(plugin, () -> {
    for (World world : Bukkit.getWorlds()) {
        int removed = cleanupEntities(world, EntityType.DROPPED_ITEM, 100);
        if (removed > 0) {
            plugin.getLogger().info("清理了 " + removed + " 个掉落物");
        }
    }
}, 6000L, 6000L); // 每 5 分钟

18.3 内存管理

防止内存泄漏

public class PlayerDataManager {

    // 不好:玩家退出后数据仍在内存
    private final Map<UUID, PlayerData> data = new HashMap<>();

    // 好:玩家退出时清理
    @EventHandler
    public void onQuit(PlayerQuitEvent event) {
        UUID uuid = event.getPlayer().getUniqueId();
        PlayerData playerData = data.remove(uuid); // 移除并获取
        if (playerData != null) {
            saveToDatabase(playerData); // 持久化
        }
    }
}

取消任务

public class MyPlugin extends JavaPlugin {

    private final List<BukkitTask> tasks = new ArrayList<>();

    @Override
    public void onDisable() {
        // 取消所有任务
        tasks.forEach(BukkitTask::cancel);
        tasks.clear();

        // 或者一次性取消所有
        Bukkit.getScheduler().cancelTasks(this);
    }

    private void startTask() {
        BukkitTask task = Bukkit.getScheduler().runTaskTimer(this, () -> {
            // 逻辑
        }, 0L, 20L);
        tasks.add(task);
    }
}

18.4 安全建议

验证用户输入

public class InputValidator {

    /**
     * 验证玩家名
     */
    public static boolean isValidPlayerName(String name) {
        return name != null
            && name.length() >= 3
            && name.length() <= 16
            && name.matches("[a-zA-Z0-9_]+");
    }

    /**
     * 验证数值范围
     */
    public static boolean isInRange(double value, double min, double max) {
        return value >= min && value <= max;
    }

    /**
     * 清理聊天消息(防注入)
     */
    public static String sanitize(String input) {
        if (input == null) return "";
        return input.replaceAll("[§&][0-9a-fk-or]", "") // 移除颜色代码
                    .trim();
    }

    /**
     * 验证配置文件路径(防路径遍历)
     */
    public static boolean isSafePath(String path) {
        return path != null
            && !path.contains("..")
            && !path.startsWith("/");
    }
}

权限检查

// 始终在执行操作前检查权限
public void handleCommand(CommandSender sender, String[] args) {
    // 不好:先执行后检查
    // executeAction();
    // if (!sender.hasPermission("perm")) { return; }

    // 好:先检查后执行
    if (!sender.hasPermission("myplugin.admin")) {
        sender.sendMessage("§c没有权限!");
        return;
    }
    executeAction();
}

数据保护

// 敏感数据加密
public class DataEncryptor {

    private static final String SECRET_KEY = getConfigSecretKey();

    public static String encrypt(String data) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            SecretKeySpec key = new SecretKeySpec(
                SECRET_KEY.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] encrypted = cipher.doFinal(data.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败", e);
        }
    }

    public static String decrypt(String encrypted) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            SecretKeySpec key = new SecretKeySpec(
                SECRET_KEY.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decoded = Base64.getDecoder().decode(encrypted);
            return new String(cipher.doFinal(decoded));
        } catch (Exception e) {
            throw new RuntimeException("解密失败", e);
        }
    }
}

SQL 注入防护

// 不好:字符串拼接 SQL
String sql = "SELECT * FROM players WHERE name = '" + playerName + "'";

// 好:参数化查询
String sql = "SELECT * FROM players WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, playerName);

18.5 兼容性策略

多版本兼容

public class VersionCompat {

    private static final int CURRENT_VERSION = getMinecraftVersion();

    private static int getMinecraftVersion() {
        String version = Bukkit.getBukkitVersion(); // "1.21.4-R0.1-SNAPSHOT"
        String[] parts = version.split("-")[0].split("\\.");
        return Integer.parseInt(parts[0]) * 100 + Integer.parseInt(parts[1]);
    }

    /**
     * 根据版本使用不同 API
     */
    public static void sendActionBar(Player player, String message) {
        if (CURRENT_VERSION >= 1210) {
            // Paper 1.21+ Adventure API
            player.sendActionBar(Component.text(message));
        } else {
            // 旧版兼容
            player.spigot().sendMessage(
                ChatMessageType.ACTION_BAR,
                TextComponent.fromLegacyText(message)
            );
        }
    }
}

优雅降级

public class FeatureManager {

    private boolean papiEnabled = false;
    private boolean vaultEnabled = false;

    public void initialize(MyPlugin plugin) {
        // PlaceholderAPI
        if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
            papiEnabled = true;
            new MyPluginExpansion(plugin).register();
            plugin.getLogger().info("PlaceholderAPI 已集成");
        } else {
            plugin.getLogger().info("PlaceholderAPI 未安装,跳过集成");
        }

        // Vault
        if (Bukkit.getPluginManager().getPlugin("Vault") != null) {
            vaultEnabled = true;
            setupEconomy();
            plugin.getLogger().info("Vault 经济已集成");
        }
    }

    public boolean isPAPIEnabled() { return papiEnabled; }
    public boolean isVaultEnabled() { return vaultEnabled; }
}

18.6 日志与调试

分级日志

public class PluginLogger {

    private final Logger logger;
    private boolean debug = false;

    public void info(String message) {
        logger.info(message);
    }

    public void warning(String message) {
        logger.warning(message);
    }

    public void severe(String message) {
        logger.severe(message);
    }

    public void debug(String message) {
        if (debug) {
            logger.info("[DEBUG] " + message);
        }
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }
}

性能监控

public class PerformanceMonitor {

    /**
     * 测量方法执行时间
     */
    public static <T> T measure(String name, Supplier<T> supplier) {
        long start = System.nanoTime();
        T result = supplier.get();
        long elapsed = (System.nanoTime() - start) / 1_000_000; // 毫秒

        if (elapsed > 50) { // 超过 50ms 警告
            Bukkit.getLogger().warning(String.format(
                "[Performance] %s 耗时 %dms", name, elapsed));
        }

        return result;
    }
}

18.7 错误处理

优雅的异常处理

public class SafeExecutor {

    /**
     * 安全执行任务(捕获异常不影响服务器)
     */
    public static void safeRun(Runnable task) {
        try {
            task.run();
        } catch (Exception e) {
            Bukkit.getLogger().severe("任务执行失败: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 带返回值的安全执行
     */
    public static <T> Optional<T> safeGet(Supplier<T> supplier) {
        try {
            return Optional.ofNullable(supplier.get());
        } catch (Exception e) {
            Bukkit.getLogger().warning("获取数据失败: " + e.getMessage());
            return Optional.empty();
        }
    }
}

用户友好的错误提示

@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
    try {
        // 处理命令
        handleCommand(event);
    } catch (Exception e) {
        event.setCancelled(true);
        event.getPlayer().sendMessage("§c执行命令时出错,请联系管理员。");
        plugin.getLogger().severe("命令执行失败: " + e.getMessage());
        e.printStackTrace();
    }
}

18.8 配置管理

配置热重载

public class ConfigManager {

    private FileConfiguration config;
    private final JavaPlugin plugin;

    public ConfigManager(JavaPlugin plugin) {
        this.plugin = plugin;
        reload();
    }

    public void reload() {
        plugin.saveDefaultConfig();
        plugin.reloadConfig();
        config = plugin.getConfig();
    }

    public String getString(String path, String def) {
        return config.getString(path, def);
    }

    public int getInt(String path, int def) {
        return config.getInt(path, def);
    }

    public boolean getBoolean(String path, boolean def) {
        return config.getBoolean(path, def);
    }

    public List<String> getStringList(String path) {
        return config.getStringList(path);
    }
}

18.9 更新检查

public class UpdateChecker {

    private final JavaPlugin plugin;
    private final String resourceId; // SpigotMC 资源 ID

    public UpdateChecker(JavaPlugin plugin, String resourceId) {
        this.plugin = plugin;
        this.resourceId = resourceId;
    }

    /**
     * 异步检查更新
     */
    public void check(Consumer<String> callback) {
        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                HttpClient client = HttpClient.newHttpClient();
                HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(
                        "https://api.spigotmc.org/legacy/resource.php?"
                        + resourceId + "&command=getversion"))
                    .build();

                HttpResponse<String> response = client.send(
                    request, HttpResponse.BodyHandlers.ofString());

                String latest = response.body().trim();
                String current = plugin.getDescription().getVersion();

                if (!current.equals(latest)) {
                    Bukkit.getScheduler().runTask(plugin, () ->
                        callback.accept(latest));
                }
            } catch (Exception e) {
                plugin.getLogger().warning("检查更新失败: " + e.getMessage());
            }
        });
    }
}

18.10 代码审查清单

类别 检查项
功能 所有功能是否正常工作
权限 权限检查是否正确
输入验证 用户输入是否已验证
异常处理 异常是否被优雅处理
性能 是否有性能瓶颈
内存 是否有内存泄漏
线程安全 异步操作是否安全
配置 配置是否有默认值
文档 代码是否有注释
测试 关键功能是否有测试

18.11 推荐的库

用途 说明
Lombok 减少样板代码 @Getter, @Setter
HikariCP 数据库连接池 高性能连接池
Jedis Redis 客户端 缓存和发布订阅
ProtocolLib 数据包操作 高级网络功能
Adventure 文本组件 现代化消息系统
CommandAPI 命令框架 Brigadier 封装
Configurate 配置管理 Paper 原生配置库

18.12 常见反模式

反模式 问题 改进
主线程 I/O TPS 下降 异步处理
硬编码配置 不灵活 使用配置文件
魔法数字 难以维护 使用常量
God Class 过大的类 拆分为多个类
重复代码 维护困难 提取公共方法
忽略异常 隐藏问题 记录日志
不使用版本控制 无法回溯 使用 Git

18.13 扩展阅读


18.14 本章小结

要点 内容
代码规范 统一命名、合理结构、设计模式
性能优化 避免高频事件开销、合理缓存、限制实体
安全 验证输入、防注入、权限检查
兼容性 多版本适配、优雅降级
错误处理 捕获异常、记录日志、用户友好提示
持续改进 代码审查、自动化测试、定期重构

恭喜你完成了全部 18 章的学习!🎉

回顾学习路线:

入门 → 第 1-5 章(环境 + 基础 API)
进阶 → 第 6-10 章(物品/GUI/世界/实体/计分板)
深入 → 第 11-14 章(数据包/数据库/调度/占位符)
工程 → 第 15-18 章(Docker/测试/发布/最佳实践)

下一步行动建议:

  1. 🛠️ 动手写一个完整的插件项目
  2. 📖 阅读 PaperMC 官方文档获取最新 API
  3. 💬 加入 SpigotMC / PaperMC Discord 社区
  4. 🧪 为你的项目添加测试
  5. 🚀 发布到 SpigotMC 或 Hangar

祝你的插件开发之旅顺利!🚀