Mirage Framework

思追(shaco)大约 13 分钟

介绍

Mirage 的核心框架,其中包含了以下基础功能

应用对象工厂

相关信息

mirage 的对象工厂,完全兼容 JSR 303的依赖注入规范

应用对象工厂,即 IOC(控制反转)+ DI(依赖注入)

即对象的创建,依赖注入的过程都将由对象工厂来完成,那么开发需要做的是在需要对象工厂管理的对象上使用指定注解来标识,这样在应用启动时,会自动扫描这些类并且加载到工厂中进行管理

那么如果定义一个工厂对象呢?,请继续往下看:

定义工厂对象

使用 @Component 注解标识在对象上,即可标识为一个工厂对象。

如果未显式的定义对象的名称,则默认为类名首字母小写驼峰命名法,如果需要显示的定义使用 @Component(value="对象名称")

示例:

@Slf4j
@Component(lazy = false)
class TestComponent : InitializingBean {

    override fun init() {
        log.info("testComponent 初始化成功:${this}")
    }
}

使用 @Configuration 注解标识在对象上,那么该对象将被标识为一个配置工厂对象,在该对象中使用 @Bean 标识方法来表示这是一个工厂对象方法。

如果未显示的定义对象的名称则使用方法名作为对象名称,如果需要显示的定义使用 @Bean(value="对象名称")

示例:

@Configuration
class TestConfiguration {

    @Slf4j
    class TestBean : InitializingBean {
        override fun init() {
            log.info("testBean 初始化成功:${this}")
        }
    }

    @Bean(lazy = false)
    fun testBean(): TestBean {
        return TestBean()
    }
}

懒加载

默认情况下工厂对象都是懒加载的,如果需要定义非懒加载对象可以通过 @Component(lazy = false) 或者 @Bean(lazy = false) 来定义,非懒加载对象将在应用工厂所有的类型扫描完成后,进行实例化。

工厂对象扫描

默认情况下,优先扫描启动类的包路径及其子路径中所有定义的工厂对象,接着将加载 SPI 机制 中的自动装配对象,如果扫描到对象标识着 @ComponentScan 注解,则基于该注解定义的路径继续扫描。

@Import

在工厂的配置对象(即:@Configuration注解标识的对象)上可以使用 @Import 注解来导入指定类,结合 ImportBeanDescriptionRegistrar 接口可以在运行时添加自定义的 BeanDescription 到对象工厂中,以下是一个示例:

导入普通组件示例:

@Component
class MirageComponent

@Configuration
@Import(MirageComponent::class)
class MirageConfiguration

导入BeanDescription的示例:

class MirageBeanDefinitionRegistrar : ImportBeanDescriptionRegistrar {

     override fun registerBeanDescriptions( 
         annotatedMetadata: AnnotatedElementMetadata,
         registry: BeanDescriptionRegistry,
         beanKeyLoader: BeanKeyLoader) {
     // @Import 定义在 @EnableDemo 注解上,目的是可以使用 annotatedMetadata 获取该注解的元数据,以达到作为配置注解的目的
     // 可以使用 beanKeyLoader 来扫描指定目录或指定类的 BeanKey 作为 BeanDescription 的唯一键
     // 使用 registry 注入 BeanDescription
   }
}

@Target(CLASS)
@Retention(RUNTIME)
@Import(MirageBeanDefinitionRegistrar::class)
annotation class EnableDemo

@EnableDemo
@Configuration
public class MirageConfig

注意

需要注意的是:ImportBeanDescriptionRegistrar接口的实现必须存在无参构造函数,将用来初始化该对象。

ImportBeanDescriptionRegistrar接口的实现不会添加到对象工厂中,但是支持 Aware 接口的注入

对象生命周期

作用域

这里值得一提的是,被对象工厂管理的实例,它的作用域一定是单例的,对于 原型对象对象,即:工厂每次都会创建一个对象,创建好后将不会继续管理后续的生命周期,交由使用者进行管理。

如果需要定义原型对象则需要额外使用 @Prototype注解标识

通过以上的生命周期图中可以看出在对象创建的过程中定义了一些回调方法,用于参与对象的创建。

实例化

对象的实例化是通过构造函数完成的,那么如果该对象只存在一个构造函数(无论是否私有),则使用它,如果该对象存在多个构造函数,那么存在一个判断逻辑,构造函数必须的public的且必须标识 @Inject 或者 @Autowired注解,如果匹配到多个那么将抛出例外!

循环依赖

请注意,当前版本不尝试解决任何形式的循环注入问题,即不可以 A实例是B实例的依赖性,同时B实例又是A实例的依赖项。这样的情况将导致获取该实例对象时抛出BeanException异常!

依赖注入

依赖注入分为两种,对象属性注入和对象方法注入。

对象属性注入条件:

  • 非静态属性
  • 非 final 属性
  • 属性上使用 @Inject@Autowired注解

对象方法注入条件:

  • 非静态方法
  • public 标识的方法
  • 方法是上使用 @Inject@Autowired注解
  • 非被重写的方法

依赖名称注入

默认情况下依赖注入使用类型注入,如果出现多个匹配的类型将抛出例外!

如果需要通过名称进行注入,则需要使用 @Named注解定义注入对象的名称。

如果你想为指定对象标识一个类型,通过它来完成依赖注入,可以参考 jakarta.inject.Qualifier,通过自定义注解的方式匹配工厂对象

初始化

如果对象实现了接口 InitializingBean,那么在初始化这个生命周期时,将回调 InitializingBean#init 方法,可以在该方法中进行一些对象初始化逻辑

Aware

Aware即感知,在对象创建的过程中,可以实现 Aware 定义一些子接口,用于获取特定的对象实例。

  • BeanFactoryAware 对象工厂感知接口,可以获取当前应用的工厂对象 BeanFactory
  • BeanNameAware 对象名称感知接口,可以获取当前对象实例在对象工厂中的名称
  • EnvironmentAware 环境对象感知接口,可以获取当前应用的环境配置对象 Environment
  • ApplicationContextAware 应用上下文感知接口,可以获取当前应用的上下文对象 ApplicationContext

BeanPostProcessor

BeanPostProcessor即对象的后置处理器,在工厂对象实例初始化前和初始化后可以做一些特定的操作。

比如:环境配置注解 ConfigurationProperties 的实现就是依赖 BeanPostProcessor 完成的,在实例初始化前判断该对象类是否标识 ConfigurationProperties注解,如果标识则进行配置属性绑定。

您也可以基于 BeanPostProcessor 的机制做一些特定的操作,比如生成代理对象?

AOP

需要注意的是:mirage 目前不支持aop

相关信息

BeanPostProcessor 的对象配置基于 SPI 机制 实现,也被视为对象工厂中的对象,在此我们建议 BeanPostProcessor 不要存在对象依赖,因为这将导致这些依赖对象的创建时机被提到 BeanPostProcessor 之前

工厂对象覆盖

mirage 使用约定大于配置的方式构建应用,那么在一些场景上,可能不太满足,这个时候需要将原有对象排除使用自定义的注入对象,为此我们提供了以下两种方式

条件覆盖

条件覆盖通过在定义工厂对象上标识 @Conditional 注解的方式来完成,@Conditional#value 属性为 Condition接口类型,实现该接口定义工厂对象条件覆盖的具体逻辑即可。

需要注意的是 Condition 的实现对象,必须提供一个无参构造函数,否则将无法初始化该对象。

mirage 内部提供了一些 @Conditional 的实现,可以使用以下注解

  • @ConditionalOnClass :当存在指定类时加载
  • @ConditionalOnMissingBean:当不存在指定的对象,那么将当前对象添加到对象工厂中

对象排除

对象排除一般来说不建议使用,我们更加建议通过条件覆盖的方式。

如果必须要使用对象排除,则需要使用 @ExcludeComponent 注解定义排除的对象类型或者对象名称,也可以通过 SPI 机制 排除指定类型

排除对象

需要注意的是:如果该对象已经完成了初始化,那么该对象就无法被排除

SPI 机制

mirage 的 SPI 机制通过在资源目录下定义 META-INF\mirageFactories.properties 配置文件的方式,以下是一份参考

# 自动装配的类
factories.autoConfiguration=\
  cc.shacocloud.mirage.context.MirageVertxConfiguration,\
  cc.shacocloud.mirage.context.MirageVertxProperties
# 排除的组件
factories.excludeComponent=

如上所示,通过全类名的方式定义,多个英文半角逗号分割

  • factories.autoConfiguration:为自动装配的一些类
  • factories.excludeComponent:为需要排除的对象类型

Vertx Verticle组件

mirage 自定义了 VerticleFactory,所以 VertxVerticle 都可以使用组件的方式存在于 mirage 中,因此我们提供了 @DeployVerticle 注解用于定义 Verticle 组件,以下是一份示例:

@Slf4j
@DeployVerticle(instances = 2)
class TestVerticle : CoroutineVerticle() {

    companion object {
        val i = AtomicInteger(0)
    }

    private var num: Int = -1

    override suspend fun start() {
        num = i.incrementAndGet()
        log.info("TestVerticle start $num")
    }

    override suspend fun stop() {
        log.info("TestVerticle stop $num")
    }
}

以上的示例可以得到2个 TestVerticle 实例,其启动/关闭时打印的日志中都有对应的序号。

相关信息

@DeployVerticle 标识类都是原型对象,即每次获取都将创建一个新的对象

应用环境配置

系统的环境配置提供了2个内置的配置文件,路径都是相对于资源目录,文件不存在则不加载

  • 环境配置文件:environment.yaml 用于定义环境配置信息,可以在其中配置不同环境读取的配置文件
  • 应用配置文件:application.yaml 用于定义应用配置信息,内置配置文件。即:无论环境配置中是否定义,该文件都会加载

相关信息

系统支持3种文件格式,分别是 yaml,json,properties,所以上面的2个配置文件使用 environment.jsonenvironment.propertiesapplication.jsonapplication.properties 也可以

环境配置文件

在环境配置文件中可以定义不同环境下加载的配置文件,示例如下:

mirage:
  environment:
    # 激活的环境
    active: ${mirage_environment_active:dev}
    # 配置文件刷新间隔,默认是5秒
    refresh: 5000
    # 环境配置集
    profiles:
     # 环境名称和 mirage.environment.active 对应
      - id: dev
      # 环境存储信息
        stores:
          # classpath:// 表示类路径加载
          - path: classpath://application-dev.yaml
          # https:// 表示远端加载
          - path: https://gitee.com/lulihu/mirage-demo/raw/master/mirage-kotlin-demo/src/main/resources/application-dev1.yaml
            headers:
              client: mirage demo
            # file:// 表示文件系统加载,绝对路径
          - path: file:///opt/app/application-dev2.yaml
            # optional 如果为 true 表示为可选的,即:文件不存在则忽略,默认为 false 不存在将抛出例外
            optional: true
      - id: uat
        stores:
          - path: classpath://application-uat.yaml
          - path: https://gitee.com/lulihu/mirage-demo/raw/master/mirage-kotlin-demo/src/main/resources/application-uat1.yaml
            headers:
              client: mirage demo
          - path: file:///opt/app/application-uat2.yaml
            optional: true

从以上配置信息示例可以看出,目前系统支持3种配置文件加载方式,分别是

  • 类路径加载:使用 classpath:// 作为路径前缀
  • 文件系统加载:使用 file:// 作为路径前缀
  • 远端请求加载:使用 http://或者https:// 作为路径前缀,如果使用该方式可以通过 headers属性配置请求时携带的自定义头部信息

如果指定配置路径文件有可能不存在,那么可以将 optional 属性定义 true,表示为文件不存在则忽略,默认为 false 不存在将抛出例外

相关信息

在所有的配置文件信息中,字符串的内容都会被当做表达式内容进行表达式替换,比如:${mirage_environment_active:dev} 表示为 如果 mirage_environment_active 配置键的值存在则使用,否则使用 dev 作为值

配置的键可以使用中划线分割字符,例如 mirage.vertx.event-loop-pool-size 等效于 mirage.vertx.eventLoopPoolSize

配置文件加载顺序

配置的顺序非常重要, 因为它定义了覆盖顺序。对于冲突的key, 后声明的配置中心会覆盖之前的。我们举个例子。 我们有两个配置:

  • A 提供 {a:value, b:1} 配置
  • B 提供 {a:value2, c:2} 配置

以 A,B 的顺序声明配置,最终配置应该为: {a:value2, b:1, c:2}

如果您将声明的顺序反过来(B,A),那么您会得到 {a:value, b:1, c:2}

配置注入

系统提供了一下注解,用于注入配置信息到指定的对象中

相关信息

环境配置每隔 mirage.environment.refresh重新加载一次配置信息,如果配置发生了变更将发布应用事件 EnvironmentChangeEvent ,且所有的配置对象都将被重新注入新的值

@ConfigurationProperties

配置属性注解,在对象类上使用了该注解后,该对象的所有Set方法都将被视为配置属性的注入点。

示例:

@ConfigurationProperties(prefix = "mirage.demo")
class MirageDemoProperties {

    var dev: String = ""
    var dev1: String = ""

}

@EnvValue

环境值注解,使用其定义在工厂对象的属性或者Set方法上,即可为这个工厂对象注入环境配置信息。

示例:

属性注入

@Component
class MirageDemoBean {

    @EnvValue("\${mirage.demo.dev1}")
    private var dev1: String = ""

}

方法注入

@Component
class MirageDemoBean {
    
    private lateinit var dev1: String

    @EnvValue("\${mirage.demo.dev1}")
    fun setDev1(dev1: String) {
        this.dev1 = dev1;
    }

}

注意

注意:set 方法必须满足以下几个条件

方法名称必须以 set 为前缀,且 set 的下一个字符大写

方法必须只有一个入参

Vertx 配置

通过应用环境配置机制可以基于MirageVertxProperties对象配置 Vertx 对象的一些属性,以下是一部分配置示例:

mirage:
  vertx: 
    eventLoopPoolSize: 2
    workerPoolSize: 20
    internalBlockingPoolSize: 20
    blockedThreadCheckInterval: 1000
    maxEventLoopExecuteTime: 2000
    maxWorkerExecuteTime: 60000

应用事件系统

当应用运行到某个阶段事件,将会发出应用事件,工厂对象通过实现 ApplicationListener 来定义监听的事件,当该事件发生时将会触发 ApplicationListener#onApplicationEvent方法。

示例:

@Slf4j
@Component
class MirageDemoEventBean : CoroutineApplicationListener<EnvironmentChangeEvent> {

    override suspend fun doApplicationEvent(event: EnvironmentChangeEvent) {
        log.info("配置发生变更...")
    }

}

自定义事件

示例:

// 定义自定义事件对象
class CustomEvent : ApplicationEvent {
}

// 发布事件
MirageHolder.publishEvent(CustomEvent())

所有的事件对象必须继承 ApplicationEvent 接口,以表示自己是一个事件对象