SpringBoot的竞争对手——Micronaut之控制反转(IOC)

上一篇我已经带大家学习了Micronaut的入门教程,介绍了Micronaut是什么,有哪些特点,今天我们就带大家来了解一下Micronaut之控制反转

1.控制反转

当大多数开发者想到控制反转时,会想到Spring框架。控制反转又被称为依赖注入

MicronautSpring框架获得灵感,事实上,Micronaut的核心开发人员是前SpringSource / Pivotal工程师,现在在OCI工作。

Spring完全依赖于运行时反射和代理,而Micronaut使用编译时数据来实现依赖注入

Google的Dagger等工具采用的类似方案,其主要是为Android而设计一套依赖注入框架。另一方面,Micronaut设计用于构建服务端微服务,并提供许多与Spring相同的工具和实用程序,不使用反射或缓存过多的反射元数据。

Micronaut IoC容器的目标总结如下:

  • 使用反射作为最后的手段

  • 避免使用代理

  • 优化启动时间

  • 减少内存占用

  • 提供清晰,易懂的错误处理

注意: 由于Micronaut的IoC部分可以完全独立于Micronaut本身使用,我在构建应用之前,我们需要添加包含micronaut-inject-java依赖项作为注解处理器。 例如Maven

  <dependencies>
    <dependency>
      <groupId>io.micronaut</groupId>
      <artifactId>micronaut-inject</artifactId>
      <scope>compile</scope>
    </dependency>
    ...
  </dependencies>
  <build>
    <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.7.0</version>
          <configuration>
            <compilerArgs>
              <arg>-parameters</arg>
            </compilerArgs>
            <annotationProcessorPaths>
                  <path>
                    <groupId>io.micronaut</groupId>
                    <artifactId>micronaut-inject-java</artifactId>
                    <version>${micronaut.version}</version>
                  </path>
            </annotationProcessorPaths>
          </configuration>
          <executions>
            <execution>
              <id>test-compile</id>
              <goals>
                <goal>testCompile</goal>
              </goals>
              <configuration>
                <compilerArgs>
                  <arg>-parameters</arg>
                </compilerArgs>
                <annotationProcessorPaths>
                  <path>
                    <groupId>io.micronaut</groupId>
                    <artifactId>micronaut-inject-java</artifactId>
                    <version>${micronaut.version}</version>
                  </path>
                  <path>
                    <groupId>io.micronaut</groupId>
                    <artifactId>micronaut-validation</artifactId>
                    <version>${micronaut.version}</version>
                  </path>

                </annotationProcessorPaths>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

micronaut-inject-java: 添加Annotation Processing插件
micronaut-inject:执行依赖注入所需的依赖项

然后,IoC的入口点是ApplicationContext接口,其中包含一个run方法。 以下示例演示如何使用它:

运行ApplicationContext

try (ApplicationContext context = ApplicationContext.run()) { 
    MyBean myBean = context.getBean(MyBean.class); 
    // do something with your bean
}
  • 运行ApplicationContext
  • 检索已依赖注入的bean

注意:该示例使用Java的try-with-resources语法来确保在应用程序退出时干净地关闭ApplicationContext

2.定义Bean

Micronaut实现了JSR-330(javax.inject) - Java规范的依赖注入,因此使用Micronaut只需使用javax.inject提供的注解。

以下是一个简单的例子:

/**
*定义了通用`Engine`接口
*/
public interface Engine { 
    int getCylinders();
    String start();
}
/**
*定义`V8Engine`实现并添加`@Singleton`注解,
*标记该类的作用域`Singleton`,则在整个应用中,只创建bean的一个实
*/
@Singleton
public class V8Engine implements Engine {
    public String start() {
        return "Starting V8";
    }

    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }

    private int cylinders = 8;
}

@Singleton
public class Vehicle {
    private final Engine engine;

    // 通过构造函数注入注入`Engine`
    public Vehicle(Engine engine) {
        this.engine = engine;
    }

    public String start() {
        return engine.start();
    }
}
  • 定义了通用Engine接口
  • 定义V8Engine实现并添加@Singleton注解,标记该类的作用域Singleton,则在整个应用中,只创建bean的一个实例。
  • 通过构造函数注入注入Engine

要执行依赖项注入,只需使用run()方法运行BeanContext并使用getBean(Class)查找bean,如下例所示:

Vehicle vehicle = BeanContext.run().getBean(Vehicle.class);
System.out.println(vehicle.start());

在这里插入图片描述

Micronaut将自动发现在classpath下依赖注入的元数据,根据我们定义的注入点将Bean连接在一起。

Micronut支持以下类型的依赖注入

  • 构造函数注入(必须是一个公共构造函数或使用@Inject注解的单个构造函数)
  • Field注入
  • Java Bean属性注入
  • 方法参数注入

3 依赖注入的工作原理

上面我们已经了解了Micronaut的依赖注入和Bean的定义,此时,我最想知道Micronaut如何在不需要反射的情况下执行上述依赖注入。

关键是一组AST转换(用于Groovy)注解处理器(用于Java),它们生成实现BeanDefinition接口的类。

ASM字节码库用于生成类,并且由于Micronaut提前知道注入点,因此不需要像运行Spring等其他框架一样在运行时扫描所有方法字段构造函数等。

此外,由于在构造bean时不使用反射,因此JVM可以更好地内联和优化代码,从而提高运行时性能并减少内存消耗。 这对于非Singleton作用域的bean尤为重要,因为应用程序性能取决于bean创建的性能

此外,使用Micronaut,我们的应用程序的启动时间内存消耗不会像使用反射的框架一样与代码库的大小有关系。 基于反射的IoC框架为代码中的每个字段,方法和构造函数加载和缓存反射数据。 因此,随着代码规模的扩大,您的内存需求也会增加,而使用Micronaut则不是这样。

4 BeanContext 介绍

BeanContext是所有bean定义的容器对象(它还实现了BeanDefinitionRegistry)。

这也是Micronaut的初始化点。 但是,一般来说,我们不必直接与BeanContext API交互,只需使用javax.inject注解和io.micronaut.context.annotation包中定义的注解来满足依赖注入需求。

5 可注入容器类型

除了能够注入Bean,Micronaut本身还支持注入以下类型:

类型 描述 示例
java.util.Optional An Optionalof a bean. If the bean doesn’t exist empty() is injected Optional<Engine>
java.lang.Iterable An Iterable or subtype of Iterable (example List, Collection etc.) Iterable<Engine>
java.util.stream.Stream A lazy Stream of beans Stream<Engine>
Array A native array of beans of a given type Engine[]
Provider A javax.inject.Provider if a circular dependency requires it or to instantiate a prototype for each getcall. Provider<Engine>

注意:属性bean将在每个注入bean的位置创建一个实例。 当一个属性bean作为Provider注入时,每次调用get()都会创建一个新实例。

6.Bean限定符

如果要为要注入的给定接口提供多种可能的实现,则需要使用限定符

Micronaut又一次利用JSR-330QualifierNamed 来支持Bean的限定符。

6.1 Named 限定符

要按名称限定,我们可以使用@Named注解 。 如下面的类:

public interface Engine { 
    int getCylinders();
    String start();
}

@Singleton
public class V6Engine implements Engine {  
    public String start() {
        return "Starting V6";
    }

    public int getCylinders() {
        return 6;
    }
}

@Singleton
public class V8Engine implements Engine {
    public String start() {
        return "Starting V8";
    }

    public int getCylinders() {
        return 8;
    }

}

@Singleton
public class Vehicle {
    private final Engine engine;

    @Inject
    public Vehicle(@Named("v8") Engine engine) {
        this.engine = engine;
    }

    public String start() {
        return engine.start();
    }
}
  • 定义了通用Engine接口
  • 定义V6Engine实现Engine接口
  • 定义V8Engine实现Engine接口
  • @Named("v8") 注解用于指示需要V8Engine实现
  • 调用start 方法 输出了: "Starting V8"

我们还可以在bean的类级别声明@Named以显式定义bean的名称。

6.2 Qualifier 修饰符

除了能够按名称限定之外,我们还可以使用Qualifier构建自己的限定符。请参考以下注解:

import javax.inject.Qualifier;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}

上面的注解本身使用@Qualifier注解进行定义,以将其指定为限定符。 然后,我们可以在代码中的任何注入点使用V8注解。 例如:

 @Inject Vehicle(@V8 Engine engine) {
        this.engine = engine;
    }

6.3 PrimarySecondary 限定Bean

Primary是用来限定bean是在多个可能的接口实现的情况下应该选择的主bean。

请参考以下示例:

public interface ColorPicker {
    String color();
}

定义一个通用的ColorPicker接口,定义了两个实现Green,Blue,其中Green添加了@Primary

import io.micronaut.context.annotation.Primary;
import javax.inject.Singleton;

@Primary
@Singleton
class Green implements ColorPicker {

    @Override
    public String color() {
        return "green";
    }
}

@Singleton
public class Blue implements ColorPicker {

    @Override
    public String color() {
        return "blue";
    }
}

Blue bean也实现了ColorPicker接口,因此在注入ColorPicker接口我们有两个可能。 由于Green是添加了@Primary,它总是默认实现。

package hello.world;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/test")
public class TestController {

    protected final ColorPicker colorPicker;

    public TestController(ColorPicker colorPicker) {
        this.colorPicker = colorPicker;
    }

    @Get
    public String index() {
        return colorPicker.color();
    }
}

通过构造方法注入,默认注入是带有@Primary注解的Green的bean。
在这里插入图片描述

如果存在多个可能的实现Bean且未定义@Primary,则将抛出NonUniqueBeanException

除了@Primary之外,还有一个@Secondary 注解,它会产生相反的效果,并允许对bean进行去优先级排序。

6.4 作用域

Micronaut具有基于JSR-330的可扩展bean作用域机制。 支持以下默认范围:

6.4.1 内建的作用域

类型 描述
@Singleton Singleton作用域表示只应存在一个bean实例
@Context Context作用域表示应该与ApplicationContext同时创建bean
@Prototype Prototype作用域表示每次注入时都会创建一个新的bean实例
@Infrastructure Infrastructure作用域表示使用@Replaces无法覆盖或替换的bean,因为它对系统的运行至关重要。
@ThreadLocal @ThreadLocal作用域是一个自定义作用域,它通过ThreadLocal将每个线程的bean关联起来
@Refreshable @Refreshable 作用域是一个自定义作用域,允许通过/ refresh端点刷新bean的状态
@RequestScope @RequestScope作用域是一个自定义作用域,表示创建bean的新实例并将其与每个HTTP请求关联

注意@Prototype注解是@Bean的同义词,因为默认作用域是Prototype

其他作用域的实现可以通过定义一个@Singleton的 bean 并实现CustomScope接口。

注意:默认情况下,使用Micronaut启动ApplicationContext时,Singleton 作用域的bean会根据需要随时创建。 这样设计可以优化启动时间。

如果我们需要在应用程序启动时,创建bean。那么我们可以选择使用@Context注解将对象的生命周期绑定到ApplicationContext的生命周期。 换句话说,当ApplicationContext启动时,将会创建bean。

或者,我们可以使用@Parallel注解的任何@Singleton作用域bean,它允许并行初始化bean而不会影响整体启动时间。

如果我们的bean无法并行初始化,那么应用程序将自动关闭

6.4.2 Refreshable作用域

Refreshable作用域是一个作用域,允许通过以下方式刷新bean的状态:

以下示例说明了@Refreshable作用域的行为。

@Refreshable 
public static class WeatherService {
    private String forecast;

    @PostConstruct
    public void init() {
        forecast = "Scattered Clouds " + new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(new Date());
    }

    public String latestForecast() {
        return forecast;
    }
}
  • WeatherService使用@Refreshable作用域进行注解,该作用域存储实例直到触发刷新事件
  • 创建bean时,forecast属性的值设置为固定值,并且在刷新bean之前不会更改

如果我们两次调用latestForecast(),我们将看到相同的响应结果,例如Scattered Clouds 01 / Feb / 18 10:29.199

当我们调用/ refresh端点或发布RefreshEvent事件时,实例将失效,并在下次请求对象时创建新实例。 例如:

try (ApplicationContext applicationContext = ApplicationContext.run()) {
            applicationContext.publishEvent(new RefreshEvent());
            WeatherService weatherService = applicationContext.getBean(WeatherService.class);
//            // do something with your bean
           System.out.println(weatherService.latestForecast());
        }

结果显示如下,已经刷新成功。
在这里插入图片描述

6.4.3元注释的作用域

作用域可以在Meta注解上定义,然后可以应用于我们的类。 请参考以下示例元注解:

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.Requires;

import javax.inject.Singleton;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

@Requires(classes = Car.class ) 
@Singleton 
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
  • 作用域使用@Requires声明对Car类的关联
  • 注解声明为@Singleton

在上面的示例中,@ Singleton注解应用于@Driver注解,这导致每个使用@Driver注解的的类被视为单例

注意: 在这种情况下,应用注解时无法更改作用域。 例如,以下内容不会覆盖@Driver声明的作用域,并且无效:

声明另外一个作用域

@Driver
@Prototype
class Foo {
}

如果希望作用域可以覆盖,则应该在@Driver上使用DefaultScope注解,如果没有其他作用域,则允许指定默认作用域:

使用@DefaultScope

@Requires(classes = Car.class )
@DefaultScope(Singleton.class) 
@Documented
@Retention(RUNTIME)
public @interface Driver {
}

DefaultScope用于声明在没有作用域的时使用的作用域。

7 总结

这篇主要介绍了Micronaut控制反转(IOC)Bean限定符Bean的作用域,下一篇我们继续学习Bean 工厂的基本知识。

发布了140 篇原创文章 · 获赞 278 · 访问量 81万+

猜你喜欢

转载自blog.csdn.net/u010046908/article/details/100821360