上一篇我已经带大家学习了Micronaut
的入门教程,介绍了Micronaut是什么,有哪些特点,今天我们就带大家来了解一下Micronaut之控制反转
。
当大多数开发者想到控制反转
时,会想到Spring
框架。控制反转又被称为依赖注入
。
Micronaut
从Spring
框架获得灵感,事实上,Micronaut
的核心开发人员是前SpringSource / Pivotal
工程师,现在在OCI工作。
Spring
完全依赖于运行时
反射和代理,而Micronaut
使用编译时
数据来实现依赖注入
。
Google的Dagger
等工具采用的类似方案,其主要是为Android
而设计一套依赖注入框架。另一方面,Micronaut
设计用于构建服务端微服务,并提供许多与Spring
相同的工具和实用程序,不使用反射或缓存过多的反射元数据。
Micronaut
IoC容器的目标总结如下:
使用反射作为最后的手段
避免使用代理
优化启动时间
减少内存占用
提供清晰,易懂的错误处理
注意: 由于Micronaut的IoC
部分可以完全独立于Micronaut
本身使用,我在构建应用之前,我们需要添加包含micronaut-inject-java
依赖项作为注解处理器。 例如Maven
:
<dependencies>
<dependency>
<groupId>io.micronautgroupId>
<artifactId>micronaut-injectartifactId>
<scope>compilescope>
dependency>
...
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.7.0version>
<configuration>
<compilerArgs>
<arg>-parametersarg>
compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>io.micronautgroupId>
<artifactId>micronaut-inject-javaartifactId>
<version>${micronaut.version}version>
path>
annotationProcessorPaths>
configuration>
<executions>
<execution>
<id>test-compileid>
<goals>
<goal>testCompilegoal>
goals>
<configuration>
<compilerArgs>
<arg>-parametersarg>
compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>io.micronautgroupId>
<artifactId>micronaut-inject-javaartifactId>
<version>${micronaut.version}version>
path>
<path>
<groupId>io.micronautgroupId>
<artifactId>micronaut-validationartifactId>
<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
}
注意:该示例使用Java的try-with-resources
语法来确保在应用程序退出时干净地关闭ApplicationContext
。
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
注解的单个构造函数)上面我们已经了解了Micronaut
的依赖注入和Bean的定义,此时,我最想知道Micronaut
如何在不需要反射的情况下执行上述依赖注入。
关键是一组AST转换(用于Groovy)
和注解处理器(用于Java)
,它们生成实现BeanDefinition
接口的类。
ASM字节码库
用于生成类,并且由于Micronaut
提前知道注入点,因此不需要像运行Spring
等其他框架一样在运行时
扫描所有方法
,字段
,构造函数
等。
此外,由于在构造bean时不使用反射
,因此JVM可以更好地内联和优化代码,从而提高运行时
性能并减少内存消耗。 这对于非Singleton作用域的bean
尤为重要,因为应用程序性能取决于bean创建的性能
。
此外,使用Micronaut
,我们的应用程序的启动时间
和内存消耗
不会像使用反射的框架一样与代码库的大小有关系。 基于反射的IoC框架
为代码中的每个字段,方法和构造函数加载和缓存反射数据。 因此,随着代码规模的扩大,您的内存需求也会增加,而使用Micronaut
则不是这样。
BeanContext
是所有bean定义的容器对象(它还实现了BeanDefinitionRegistry
)。
这也是Micronaut
的初始化点。 但是,一般来说,我们不必直接与BeanContext
API交互,只需使用javax.inject
注解和io.micronaut.context.annotation
包中定义的注解来满足依赖注入
需求。
除了能够注入Bean,Micronaut
本身还支持注入以下类型:
类型 | 描述 | 示例 |
---|---|---|
java.util.Optional |
An Optional of a bean. If the bean doesn’t exist empty() is injected |
Optional |
java.lang.Iterable |
An Iterable or subtype of Iterable (example List, Collection etc.) |
Iterable |
java.util.stream.Stream |
A lazy Stream of beans |
Stream |
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 get call. |
Provider |
注意:属性bean将在每个注入bean的位置创建一个实例。 当一个属性bean作为Provider
注入时,每次调用get()
都会创建一个新实例。
如果要为要注入的给定接口提供多种可能的实现,则需要使用限定符
。
Micronaut
又一次利用JSR-330
和Qualifier
和 Named
来支持Bean的限定符。
要按名称限定,我们可以使用@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的名称。
除了能够按名称限定之外,我们还可以使用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;
}
Primary
和 Secondary
限定BeanPrimary
是用来限定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进行去优先级
排序。
Micronaut
具有基于JSR-330
的可扩展bean作用域
机制。 支持以下默认范围:
类型 | 描述 |
---|---|
@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无法
并行初始化
,那么应用程序将自动关闭
。
Refreshable作用域是一个作用域,允许通过以下方式刷新bean的状态:
/refresh
刷新端点
发布RefreshEvent
以下示例说明了@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
作用域进行注解,该作用域存储实例直到触发刷新事件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());
}
作用域可以在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
用于声明在没有作用域的时使用的作用域。
这篇主要介绍了Micronaut
的控制反转(IOC)
、Bean限定符
、Bean的作用域
,下一篇我们继续学习Bean 工厂
的基本知识。