Skip to content

IOC 容器 ​

IOC 的全称为 Inversion of Control (反转控制),意在将对象的创建和管理交由容器,而不是由开发者主动新建对象。

UltiTools 拥有自己的 IOC 容器,基于 SimpleContainer 构建,使用三级缓存来解决循环依赖问题。如果你接触过 Spring 开发,你将会对下面的概念感到十分熟悉。

局限性

尽管 UltiTools 尽可能地对涉及的class进行扫描,但仍然可能存在因找不到类使 Bean 注册失败的问题。

模块容器 ​

每个模块都有一个独立的上下文容器 Context,你可以使用主类的 getContext() 方法获取到。

该 Context 由 UltiTools 的 SimpleContainer 支持,本文仅涉及基本的用法。

所有模块的上下文容器都使用了一个公共的容器作为父容器,该父容器拥有一些 UltiTools 的公共 Bean,也有可能存在其他模块注册的公共 Bean。

Bean注册 ​

自动扫描 ​

在你的主类添加 @ComponentScan(...) 注解,UltiTools在初始化你的插件时会自动扫描给定包下所有的类,带有相应注解的将会被自动注册为 Bean。

支持的注解有:

  • @Component
  • @Service
  • @CmdExecutor (UltiTools API 内建)
  • @EventListener (UltiTools API 内建)

手动注册 ​

你可以直接使用容器对象的 registerType() 方法进行注册:

java
import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.UltiToolsModule;

@UltiToolsModule
public class BasicFunctions extends UltiToolsPlugin {

    @Override
    public boolean registerSelf() {
        // 插件启动时执行
        getContext().registerType(MyBean.class, new MyBean());
        return true;
    }
  
  ...
}

依赖获取 ​

自动注入 ​

如果某一类受容器管理,那么可以使用自动注入:

java
//字段注入
@Autowired
MyBean myBean;                  

--- OR ---

//构造函数注入
public MyClass(MyBean myBean) {
    this.myBean = myBean;
}

手动获取 ​

如果需要从容器获取某个依赖,仅需调用容器对象的 getBean() 方法即可:

java
MyBean myBean = context.getBean(MyBean.class);

插件主类 ​

插件主类受容器管理,你可以通过多种方式来获取它。

通过自动注入获取插件主类 ​

前提是该类受容器管理

java
@Autowired
PluginMain pluginMain;                       //字段注入

public MyClass(PluginMain pluginMain) {
    this.pluginMain = pluginMain;            //构造函数注入
}

TIP

如果该类为事件监听器类或命令执行器类,那么可以使用字段注入的方式来实现主类的获取。

手动获取 ​

如果在某些情况下无法通过容器来获取插件主类,那么你仍然可以通过创建 getter 来获取主类。

java
public class MyPlugin extends UltiToolsPlugin {
  private MyPlugin plugin;

  @Override
  public boolean registerSelf() {
    // 插件启动时执行
    this.plugin = this;
    return true;
  }

  public MyPlugin getInstance() {
    return this.plugin;
  }

  ...
}

Bean 生命周期钩子 ​

使用 @PostConstruct 和 @PreDestroy 注解可以在托管 Bean 的特定生命周期阶段自动调用方法。

@PostConstruct ​

@PostConstruct 注解标记一个方法在所有依赖都被注入后且 Bean 完全初始化后被调用。

java
package com.ultikits.docs.ioc;

import com.ultikits.ultitools.annotations.Autowired;
import com.ultikits.ultitools.annotations.PostConstruct;
import com.ultikits.ultitools.annotations.Service;

@Service
public class DatabaseConnection {
    private String connectionUrl;

    @Autowired
    private ConfigService config;

    @PostConstruct
    public void initialize() {
        // Called after injection is complete
        this.connectionUrl = config.getDatabaseUrl();
        // Connect to database
        connectToDatabase();
    }

    private void connectToDatabase() {
        // initialization logic here
    }
}

规则:

  • 方法必须返回 void
  • 方法不能接受任何参数
  • 可以抛出已检查异常
  • 每个 Bean 实例仅调用一次(对于单例)

@PreDestroy ​

@PreDestroy 注解标记一个方法在Bean 销毁前被调用(当插件被禁用或容器关闭时)。

java
package com.ultikits.docs.ioc;

import com.ultikits.ultitools.annotations.PostConstruct;
import com.ultikits.ultitools.annotations.PreDestroy;
import com.ultikits.ultitools.annotations.Service;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

@Service
public class ResourceManager {
    private Connection dbConnection;

    @PostConstruct
    public void connect() throws SQLException {
        dbConnection = createConnection();
    }

    // @PreDestroy methods may declare checked exceptions; they are logged and
    // do not stop shutdown. Note java.sql.Connection exposes isClosed(), not
    // isOpen().
    @PreDestroy
    public void cleanup() throws SQLException {
        // Called before shutdown
        if (dbConnection != null && !dbConnection.isClosed()) {
            dbConnection.close();
        }
    }

    private Connection createConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:sqlite:plugins/MyPlugin/data.db");
    }
}

规则:

  • 方法必须返回 void
  • 方法不能接受任何参数
  • 可以抛出已检查异常
  • 异常将被记录但不会阻止关闭

工厂方法 Bean ​

对于复杂的 Bean 初始化或从第三方库创建 Bean,使用 @Configuration 注解配合 @Bean 工厂方法。

java
package com.ultikits.docs.ioc;

import com.ultikits.ultitools.annotations.Bean;
import com.ultikits.ultitools.annotations.Configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.net.http.HttpClient;
import java.time.Duration;

@Configuration
public class HttpClientConfiguration {

    @Bean
    public HttpClient createHttpClient() {
        // This method's return value becomes a managed bean
        return HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .version(HttpClient.Version.HTTP_2)
            .build();
    }

    @Bean
    public DataSource createDataSource() {
        // This bean is looked up by its default name (the method name): createDataSource
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
        config.setUsername("user");
        config.setPassword("pass");
        return new HikariDataSource(config);
    }
}

何时使用:

  • 从外部库创建 Bean(Gson、HTTP 客户端、数据库连接池)
  • 具有多个步骤的复杂初始化逻辑
  • 基于运行时配置的条件性 Bean 创建
  • 命名 Bean 用于消除多个实现的歧义

规则:

  • 类必须用 @Configuration 注解
  • 方法必须用 @Bean 注解
  • 返回类型变成 Bean 类型
  • Bean 名称默认为方法名,除非设置了 @Bean(name = ...) 或 @Bean(value = ...)(v6.3.0 起)——声明的第一个元素成为注册名称,其余元素作为别名解析到同一实例。
  • 工厂方法以零参数方式被调用;不能接受 @Autowired 形参。

插件实例注入 ​

你的插件主类(扩展 UltiToolsPlugin 的类)会自动在 IoC 容器中注册,并可以被注入到任何托管 Bean 中。

为什么使用此模式 ​

在 v6.2.0 之前,代码通常使用静态 getInstance() 模式:

java
// 旧模式(可行但产生耦合)
public class MyService {
    public void doSomething() {
        MyPlugin plugin = MyPlugin.getInstance();
        // 使用 plugin
    }
}

从 v6.2.0 开始,插件实例由容器自动管理:

java
// 新模式(更好 - 依赖注入)
@Service
public class MyService {
    @Autowired
    private MyPlugin plugin;  // 自动注入

    public void doSomething() {
        // 使用 plugin - 不依赖于静态 getInstance()
    }
}

构造函数注入示例 ​

java
package com.ultikits.docs.ioc;

import com.ultikits.ultitools.annotations.Service;

import java.util.UUID;

@Service
public class PlayerDataService {
    private final MyPlugin plugin;
    private final ConfigService config;

    public PlayerDataService(MyPlugin plugin, ConfigService config) {
        this.plugin = plugin;
        this.config = config;
    }

    public void syncPlayerData(UUID playerId) {
        // Use plugin.getServer(), plugin.getLogger(), etc.
        plugin.getLogger().info("Syncing data for: " + playerId);
    }
}

工作原理 ​

容器在插件初始化期间自动执行此注册:

java
// 在 PluginManager 中的插件初始化期间
UltiToolsPlugin plugin = new YourPlugin();
pluginContext.registerType(UltiToolsPlugin.class, plugin);  // 按父类类型注册
pluginContext.registerType(YourPlugin.class, plugin);       // 也按具体类型注册

这意味着两种注入方式都有效:

java
@Autowired
private UltiToolsPlugin plugin;  // 通过父类类型

@Autowired
private YourPlugin plugin;       // 通过具体类型

优势:

  • 类型安全的依赖注入
  • 更好的可测试性(可以为单元测试模拟插件)
  • 消除了静态 getInstance() 调用
  • 遵循标准依赖注入模式

服务优先级 ​

getBean(Class) 返回首个可赋值的 Bean,而不是优先级最高的

getBean(Class) 遍历 bean 定义并返回首个可赋值的匹配项,随后把结果写进 typeMappings 供后续所有查询复用,而 priority 只被 getServicePriority 读取,后者服务于 getOrderedBeansOfType 与 getHighestPriorityBean。 需要按优先级取实现的地方改调 context.getHighestPriorityBean(PaymentProcessor.class);下面演示的 @Autowired 字段注入无法改变,AutowireFactory 直接委托 getBean(field.getType()),没有任何注解或开关可以影响它。 让 getBean 在类型歧义时委托 getHighestPriorityBean 的修法跟踪于 issue #202。

当同一接口存在多个实现时,使用 @Service 注解的 priority 属性来控制 getBean(Class) 返回哪一个。

java
// 支付处理器的多个实现
@Service(priority = 10)
public class PayPalProcessor implements PaymentProcessor {
    // 优先级高 = 优先处理
}

@Service(priority = 5)
public class StripeProcessor implements PaymentProcessor {
    // 优先级中等
}

@Service  // 默认优先级 = 0
public class DirectBankProcessor implements PaymentProcessor {
    // 优先级最低
}

行为:

  • 更高的 priority 值优先
  • 默认优先级为 0
  • 仅影响接口类型的 getBean(Class) 查找
  • 当多个 Bean 匹配时,返回优先级最高的 Bean
  • 只有 getOrderedBeansOfType() 按优先级排序返回(最高优先级在前);getBeansOfType() 返回的是无序 map。
java
// 使用方式
@Autowired
private PaymentProcessor processor;  // 获得 PayPalProcessor(最高优先级)

// 或获得按优先级排序的所有实现
List<PaymentProcessor> allProcessors = context.getOrderedBeansOfType(PaymentProcessor.class);
// 返回:[PayPalProcessor, StripeProcessor, DirectBankProcessor]

条件注册 ​

从 v6.2.0 开始,你可以使用 @ConditionalOnConfig 注解根据 YAML 配置值来条件性地注册组件。

java
package com.ultikits.docs.ioc;

import com.ultikits.ultitools.annotations.ConditionalOnConfig;
import com.ultikits.ultitools.annotations.Service;

@Service
@ConditionalOnConfig(value = "config/config.yml", path = "features.economy")
public class EconomyService {
    // Only registered if features.economy: true in config.yml
}

这消除了在 registerSelf() 中手动进行 if 判断的需要。详情请参阅条件注册指南。

贡献者

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

基于 MIT 许可发布