如果常使用 Spring 就会接触到大量内部 IoC(控制反转) 容器, 比较常用 @Component 就是全局实例化能被追踪.
IoC 涵盖以下概念:
控制反转: 抛弃传统开发手动new实例化而让容器来帮助全局实例化管理依赖注入: 通过构造方法(Spring-@Autowired,Java-@Resource)/Setter(写入器-@Bean)/XML等配置将实例注入引用
这样的好处就是把实例化和引用管理移交给到容器统一管理从而能够专注业务, 这里以 spring 为例子说明:
// 定义一个服务接口
public interface HelloService {
void sayHello();
}
// 实现服务接口
public class HelloServiceImpl implements HelloService {
public void sayHello() {
System.out.println("Hello, Spring IoC!");
}
}
// 配置文件(applicationContext.xml)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="helloService" class="com.example.HelloServiceImpl"/>
</beans>
// 使用IoC容器
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloService helloService = (HelloService) context.getBean("helloService");
helloService.sayHello();
}
}
如果习惯用 Java 开发大型应用就需要去自己另外处理这方面功能, 也就是另外额外定制 IoC 容器.
Dagger2
在查看社区之后首先发现谷歌开源的 dagger2 可以做轻量 Ioc 库,
其中主要的几个注解对象如下:
@Inject: 应用于构造方法或者变量, 申请把IoC内部实例化对象注入设置@Module: 应用于类对象用于给第三方库(实例化带自定义参数)提供带参数实例化注入@Provides: 应用于和@Module配合, 对第三方带有构造参数自定义实例化@Component: 应用于接口|类对象, 用于声明组件注入器@Qulifier|@Named: 如果多个相同实例化(同个接口或者继承类衍生), 可以通过这个注解区分哪个具体衍生对象@Scope: 用于接口|注解, 用于当单例实现声明衍生@Singleton: 全局唯一单例(@Scope默认实现), 这里哪怕声明也是需要注入器处理.
@Component其实更像是桥接器, 用于关联类对象和类实例
Dagger2 是在编译阶段通过 apt 利用 Java 注解自动生成Java代码(也就是需要先编译生成中间代码),
然后结合手写的代码来自动帮我们完成依赖注入的工作处理, 下面就是 maven 引入方式:
<!-- 引入 dagger2 第三方库 -->
<dependencies>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.56.2</version>
</dependency>
</dependencies>
<!-- 打包配置 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>2.56.2</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
接下来就说明每种注解使用方法, 才能知道每个注解应用场景.
@Inject
最基础的 @Inject 样例说明:
/**
* 假设需要全局服务器对象用于提供外部访问
*/
public class HttpServer {
/**
* 构造方法目前设定为无参数
* 并且通过声明 @Inject 告诉注入器当有外部是需要实例化该类对象的时候通过该构造方法来实例化
*/
@Inject
public HttpServer() {
}
}
/**
* 构建注入器
* 对于 WebApplication 的类内部给他动态注入
* 也就是如果 WebApplication 这个类实例后续如果内部有 @Inject 声明成员直接帮助注入
* 这里需要对项目重新 build 让 Dagger2 生成相关的Java类, 默认会生成 Dagger组件名.java 工具类
*/
@Component
public interface WebAppComponent {
/**
* 注意这里类型必须具体化, 无法使用抽象类型
*/
void inject(WebApplication application);
}
/**
* 应用入口
*/
public class WebApplication {
/**
* 声明内部需要被注入的成员实例
*/
@Inject
HttpServer server;
/**
* 构造方法
*/
public WebApplication() {
// 这里需要手动构建注入
// 会扫描类内部 @Inject 等相关注解并注入
// 注意编写之后一定要 build 生成下中间代码才会生成 DaggerXXXX 的功能类
DaggerWebAppComponent.builder().build().inject(this);
}
/**
* 启动入口方法
*/
public static void main(String... args) {
WebApplication app = new WebApplication();
}
}
当其他服务引用的时候会自动去生成实例化对象挂载并注入到内部之中,
其实也能看出来外部也还是要设计个全局 IOCFactory 工厂来初始化注册进来.
@Module和@Provides
这两个注解都是需要配合一起用的, 上面例子都是在构造方法当中声明 @Inject,
但是有些第三方库或者自己编写功能必须强制带有传入参数这时候就需要另外做实例化.
/**
* 就像是构建 Web 服务
* 假设需要构建网络服务必须的就是在实例化的时候传入地址和端口参数
*/
public class HttpServer {
private final String address;
private final Integer port;
/**
* 多参数构造方法
* 这里不需要声明 @Inject, 而是在外部 @Module 模块类当中构建
* @param address 监听地址
* @param port 监听端口
*/
public HttpServer(String address, Integer port) {
this.address = address;
this.port = port;
}
}
/**
* 设置模块对象
*/
@Module
public class HttpServerModule {
final String address;
final Integer port;
/**
* 默认模块注册构造方法
* @param address 监听地址
* @param port 监听端口
*/
public HttpServerModule(final String address, final Integer port) {
this.address = address;
this.port = port;
}
/**
* 这里就是关键配置, 构建完成之后注入器默认扫描该注解返回实例化对象
*/
@Provides
HttpServer provideHttpServer() {
return new HttpServer(this.address, this.port);
}
}
/**
* 需要在注入器声明对应模块名
*/
@Component(modules = HttpServerModule.class)
public interface WebComponent {
void inject(WebApplication application);
}
/**
* 这里需要先 build 来生成中间代码
* 最后就是在入口类之中注入即可
* 这里注入依旧还是 @Inject 声明
*/
public class WebApplication {
@Inject
HttpServer server;
public WebApplication() {
DaggerWebComponent
.builder() // 生成构建起
// 写入自定义模型, 首字母小写
// 传入模型对象之后就调用生成自定义传入参数的类实例化
.httpServerModule(new HttpServerModule("127.0.0.1", 8080))
.build() // 构建处理
.inject(this); // 自身要求注入实例
}
/**
* 入口方法
*/
public static void main(String[] args) {
WebApplication app = new WebApplication();
}
}
这里就是对于第三方的多参数构建方法的依赖注入方式, 其实也就是 @Inject 的高级扩展.
@Qualifier和@Named
上面虽然能够自定义参数, 但是这时候还有另外问题就是 怎么继承类和实现接口对象?
比如通用接口 IService 分别有 TcpService 和 UdpService 实现的时候就没办法识别具体继承衍生,
这时候就需要来区分出来实现同个接口|继承同个类的对象:
/**
* 构建 Web 服务
* 这里和往常一样, 主要追加两个识别接口
*/
public class HttpServer {
/**
* 识别 Api 服务接口
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiQualifier {
}
/**
* 识别 View 服务接口
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewQualifier {
}
private final String address;
private final Integer port;
/**
* 多参数构造方法
* 这里不需要声明 @Inject, 而是在外部 @Module 模块类当中构建
* @param address 监听地址
* @param port 监听端口
*/
public HttpServer(String address, Integer port) {
this.address = address;
this.port = port;
}
}
/**
* 设置模块对象
*/
@Module
public class HttpServerModule {
final String address;
final Integer port;
/**
* 默认模块注册构造方法
* @param address 监听地址
* @param port 监听端口
*/
public HttpServerModule(final String address, final Integer port) {
this.address = address;
this.port = port;
}
/**
* 这里就是需要带上签名标识
*/
@Provides
@HttpServer.ApiQualifier
HttpServer provideHttpApiServer() {
return new HttpServer(this.address, this.port);
}
/**
* 另外需要带上签名标识
*/
@Provides
@HttpServer.ViewQualifier
HttpServer provideHttpViewServer() {
return new HttpServer(this.address, this.port);
}
}
/**
* 需要在注入器声明对应模块名, 注入器不需要怎么变动
*/
@Component(modules = HttpServerModule.class)
public interface WebComponent {
void inject(WebApplication application);
}
/**
* 这里需要先 build 来生成中间代码
* 最后就是在入口类之中注入即可
* 这里注入依旧还是 @Inject 声明
* 现在需要挂上签名来识别
*/
public class WebApplication {
/**
* 注入 ApiQualifier 对象
*/
@Inject
@HttpServer.ApiQualifier
HttpServer apiServer;
/**
* 注入 ViewQualifier 对象
*/
@Inject
@HttpServer.ViewQualifier
HttpServer viewServer;
public WebApplication() {
DaggerWebComponent
.builder() // 生成构建起
// 写入自定义模型, 首字母小写
// 传入模型对象之后就调用生成自定义传入参数的类实例化
.httpServerModule(new HttpServerModule("127.0.0.1", 8080))
.build() // 构建处理
.inject(this); // 自身要求注入实例
}
/**
* 入口方法
*/
public static void main(String[] args) {
WebApplication app = new WebApplication();
}
}
这里靠着声明自定义接口打上标签来让其识别出对应继承类, 方便识别衍生接口和集成类处理;
不过这种每次都要定义接口挺麻烦的, 所以就延伸出 @Named 来省略这个过程:
/**
* 直接在 @Module 对象之中声明
*/
@Module
public class HttpServerModule {
final String address;
final Integer port;
/**
* 默认模块注册构造方法
* @param address 监听地址
* @param port 监听端口
*/
public HttpServerModule(final String address, final Integer port) {
this.address = address;
this.port = port;
}
/**
* 这里带上 @Named 字符串签名标识
*/
@Provides
@Named("ApiQualifier")
HttpServer provideHttpApiServer() {
return new HttpServer(this.address, this.port);
}
/**
* 另外需要带上 @Named 签名标识
*/
@Provides
@Named("ViewQualifier")
HttpServer provideHttpViewServer() {
return new HttpServer(this.address, this.port);
}
}
/**
* 这里注入采用 @Inject + @Named 就可以扫描对应实例化
*/
public class WebApplication {
/**
* 注入 ApiQualifier 对象
*/
@Inject
@Named("ApiQualifier")
HttpServer apiServer;
/**
* 注入 ViewQualifier 对象
*/
@Inject
@Named("ViewQualifier")
HttpServer viewServer;
// 其他代码
}
这里就是简化的映射衍生对象识别签名来区分, 这两种方法各自有应用场景;
接口签名方法更适合大批量衍生放在同个类目录归类, 而如果功能比较单一且全局不超过5-8个就采用 @Named.
@Singleton
之前虽然已经做好注入实例化, 但是重复创建共享 @Inject 注入的时候就会发现问题: 修改只影响各自实例.
/**
* 像是以下这种情况
*/
public class WebApplication {
/**
* 入口方法
*/
public static void main(String[] args) {
// 两者内部修改其实是隔离的
// 也就是 tcp 和 udp 内部是对不同实例化对象加载
WebApplication tcp = new WebApplication();
WebApplication udp = new WebApplication();
}
}
目前这种实例化并非全局化, 而是在调用的那瞬间才在构造方法注入到内部, 这种时候就需要 单例化(Singleton).
可以类比
Spring当中的@Bean
单例声明也是很简单, 只需要按照以下操作即可:
- 在
Module对应的Provides方法标明@Singleton, 表明提供的是一个单例对象 - 在
Component注入器标明@Singleton, 表明该Component中有Module使用了@Singleton
import com.sun.net.httpserver.HttpServer;
/**
* 直接在 @Module-@Provides 对象方法之中声明 @Singleton
*/
@Module
public class HttpServerModule {
final String address;
final Integer port;
/**
* 默认模块注册构造方法
* @param address 监听地址
* @param port 监听端口
*/
public HttpServerModule(final String address, final Integer port) {
this.address = address;
this.port = port;
}
/**
* 另外需要带上 @Singleton 签名标识
*/
@Singleton
@Provides
HttpServer provideHttpServer() {
return new HttpServer(this.address, this.port);
}
}
/**
* 需要在注入器声明对应模块名, 注入器不需要怎么变动
*/
@Singleton
@Component(modules = HttpServerModule.class)
public interface WebComponent {
void inject(WebApplication application);
}
/**
* 这里需要先 build 来生成中间代码
* 最后就是在入口类之中注入即可
* 这里注入依旧还是 @Inject 声明
*/
public class WebApplication {
/**
* 保持不变的注入
*/
@Inject
HttpServer server;
/**
* 最后构建方法
*/
public WebApplication() {
DaggerWebComponent
.create() // 构建处理
.inject(this); // 自身要求注入实例
}
}
这就是简单的单例注入仅作为日常使用, 实际上具体引用需要参照官方文档建议来使用.
目前仅仅作为单例注册工厂使用而已, 内部设计原理挺复杂需要学习下其他使用样例
官方示例
这里采用官方的示例来学习构建应用运行时, 参考 官方Command.
官方这个命令行解析工具简要实现了业务注入的命令行功能, 并且转发到自己编写的注入功能:
/**
* 通用的指令接口, 用于被自己编写通用业务注入
*/
public interface Command {
/**
* 指令名称, 用于给注入的指令编写通用启动指令
* 比如 `print XXX` | `add X Y Z` 之类指令注入
*/
String key();
/**
* 执行指令的具体状态
*/
enum Status {
INVALID, // 指令无效
HANDLED // 指令执行
}
/**
* 指令的执行结果, 其实也算是返回状态码
*/
final class Result {
/**
* 状态
*/
private final Status status;
/**
* 私有化状态不允许外部实例化
* @param status 状态
*/
private Result(Status status) {
this.status = status;
}
/**
* 静态获取异常状态
*/
public static Result invalid() {
return new Result(Status.INVALID);
}
/**
* 静态获取执行状态
*/
public static Result handled() {
return new Result(Status.HANDLED);
}
/**
* 获取内部原生状态
*/
Status status() {
return status;
}
}
/**
* 调用处理回调
*/
Result handleInput(List<String> input);
}
上面就是个官方编写的通用注入指令接口, 其实就是等待开发者去继承实现衍生出处理, 之后就是指令路由器编写:
/**
* 命令路由器
*/
public class CommandRouter {
/**
* 注入的内部命令
*/
private final Map<String, Command> commands = Collections.emptyMap();
/**
* 取出字符串左右空白并且按照空格分割出列表对象
*/
private static List<String> split(String input) {
return Arrays.asList(input.trim().split("\\s+"));
}
/**
* 异常指令报错
*/
private Command.Result invalidCommand(String input) {
System.out.printf("couldn't understand \"%s\". please try again.%n", input);
return Command.Result.invalid();
}
/**
* 路由指令调用入口
*/
public Command.Result route(String input) {
// 将命令左右去空并且按照空格分割
List<String> splitInput = split(input);
if (splitInput.isEmpty()) {
// 空字符串代表没有指令错误
return invalidCommand(input);
}
// 取出首个指令参数, 如果不存在内部指令代表不存在该命令
String commandKey = splitInput.get(0);
Command command = commands.get(commandKey);
if (command == null) {
return invalidCommand(input);
}
// 排除首个指令位合并参数, 调用内部的注入指令
List<String> args = splitInput.subList(1, splitInput.size());
Command.Result result = command.handleInput(args);
return result.status().equals(Status.INVALID) ?
invalidCommand(input) : result;
}
}
路由器配置好之后就是编写入口调用方法:
/**
* 命令行应用入口
*/
class CommandLineApp {
/**
* 入口方法
*/
public static void main(String[] args) {
// 获取输入数据行工具
Scanner scanner = new Scanner(System.in);
CommandRouter commandRouter = new CommandRouter();
// 遍历输入指令
while (scanner.hasNextLine()) {
Command.Result unused = commandRouter.route(scanner.nextLine());
}
}
}
官方设计的命令行应用值得学习, 并且加深内部 IoC 功能注入的概念;
上面目前已经设计好基本功能框架, 接下来就是引用 dagger 来做功能注入:
/**
* 路由器注入工厂用于提供外部注入
* `@Component` 注解代表这个接口目前是 dagger2 接管的注入器
*/
@Component
public interface CommandRouterFactory {
/**
* 注入器生成路由器
*/
CommandRouter router();
}
这个工厂类型定义之后需要 build 生成中间代码 DaggerCommandRouterFactory 的具体实现,
启动入口则是需要改成调用注入对象工厂:
/**
* 命令行应用入口
*/
class CommandLineApp {
/**
* 入口方法
*/
public static void main(String[] args) {
// 获取输入数据行工具
Scanner scanner = new Scanner(System.in);
// 创建出注入的工厂对象
CommandRouterFactory routerFactory = DaggerCommandRouterFactory.create();
// 工厂对象提取出对应路由器实现
CommandRouter commandRouter = routerFactory.router();
// 便利出
while (scanner.hasNextLine()) {
Result unused = commandRouter.route(scanner.nextLine());
}
}
}
需要注意目前还没有声明实例化的具体入口方法 @Inject, 按照我们最开始学习之中目前该实例化按照变化:
// 最开始需要手动实例化, 注意该实例化无参数
CommandRouter commandRouter = new CommandRouter();
// 切换成IoC依赖, 需要让该实例化构造方法添加 @Inject
CommandRouterFactory routerFactory = DaggerCommandRouterFactory.create();
CommandRouter commandRouter = routerFactory.router();
那么只需要在路由器之中添加注解即可:
/**
* 命令路由器
*/
public class CommandRouter {
/**
* 通知 dagger2 实例化的时候来此方法实例
*/
@Inject
CommandRouter() {
}
// 其他代码......
}
这样就是完美实现 IoC命令行 的应用, 之后就是准备让开发自己去扩展注入内部方法,
接下来我们编写 echo 命令并且注入到路由器当中:
import java.util.Arrays;
/**
* 实现的底层命令行功能
*/
public final class EchoCommand implements Command {
/**
* 等待被外部注入
*/
@Inject
public EchoCommand() {
}
/**
* 声明自己命令名称
*/
@Override
public String key() {
return "echo";
}
/**
* 调用回调
*/
@Override
public Result handleInput(List<String> input) {
// 没有参数直接返回异常结果
if (input.isEmpty()) {
return Result.invalid();
}
// 参数正确直接打印内容, 并表示已经处理完成
System.out.println(Arrays.toString(input.toArray()));
return Result.handled();
}
}
自己的业务编写完成之后需要手动在路由器 @Inject 来注入依赖:
/**
* 命令路由器
*/
public class CommandRouter {
/**
* 命令行列表
*/
private final Map<String, Command> commands = new HashMap<>();
/**
* 通知 dagger2 实例化的时候来此方法实例
* 并且该构造方法依赖 EchoCommand 实例, 要求让其先实例化并传入
* 也就是该 @Inject 需要依赖 EchoCommand 的 @Inject
*/
@Inject
CommandRouter(EchoCommand echoCommand) {
commands.put(echoCommand.key(), echoCommand);
}
}
注入完成之后就可以启动命令行入口输入 echo xxx yyy zzz 都会被直接打印,
另外目前没有 exit 等强制关闭窗口命令方法, 我们也可以按照上面所说的自己去声明:
public final class ExitCommand implements Command {
@Inject
public ExitCommand() {
}
@Override
public String key() {
return "exit";
}
@Override
public Result handleInput(List<String> input) {
if (input.isEmpty()) {
System.exit(0);
return Result.handled();
}
// 识别退出码
String args = input.get(0);
try {
int status = Integer.parseInt(args);
System.exit(status);
return Result.handled();
} catch (NumberFormatException e) {
System.err.println(e.getMessage());
return Result.invalid();
}
}
}
之后跟随路由器注入即可:
public class CommandRouter {
/**
* 注入的内部命令
*/
private final Map<String, Command> commands = new HashMap<>();
/**
* 声明依赖两个注入实例
*/
@Inject
public CommandRouter(
EchoCommand echoCommand,
ExitCommand exitCommand
) {
commands.put(echoCommand.key(), echoCommand);
commands.put(exitCommand.key(), exitCommand);
}
// 其他代码
}
很巧妙且有趣的注入流程, 很值得学习设计功能设计方法
接下来我们暂时不需要 echo 指令, 需要简化下 ExitCommand 方法, 让路由器看起来像这样:
public class CommandRouter {
/**
* 直接采用 Command 基础接口
* 后续如果开发者改动实现 ExitCommand 方法不会影响到内部类功能
* 内部只关心实现功能
* @param exitCommand 退出指令
*/
@Inject
public CommandRouter(
Command exitCommand
) {
commands.put(exitCommand.key(), exitCommand);
}
}
这里就需要外部设置 @Module + @Binds 来对指令赋予具体实现信息:
/**
* 为 ExitCommand 定义信息模块
*/
@Module
public abstract class ExitCommandModule {
/**
* 为 Command 类绑定具体的子类实现
*
* @param exitCommand 子类实现
* @return Command
*/
@Binds
abstract public Command exitCommand(ExitCommand exitCommand);
}
之后在注入器当中声明引入的模块化类:
/**
* modules 参数是个列表, 也是允许多个 { XXX,YYY } 来引入多个声明模块
*/
@Component(modules = ExitCommandModule.class)
public interface CommandRouterFactory {
CommandRouter router();
}
这样就可以只依靠基础接口类从而调用到具体实现类功能, 但是内部如果出现同个实现 Command 接口类会直接报错,
因为内部 @Module 父接口单独绑定的 ExitCommand 而 EchoCommand 会出现冲突.
主要学习
@Module + @Binds的具体绑定方法
目前现在 CommandRouter 只支持单条 Command 传入, 但是正常应用是多条命令集合, 所以现在就需要考虑怎么将多条依赖合并一起;
其实 Dagger2 内部已经考虑这种情况, 所以追加 @IntoMap / @IntoSet 这类注解来自动保存依赖列表对象组:
/**
* 为 ExitCommand 定义信息模块
*/
@Module
public abstract class ExitCommandModule {
/**
* 为 Command 类绑定具体的子类实现
* 将该依赖声明为 exit 对象保存到 dagger2 内部维护的列表之中
*
* @param exitCommand 子类实现
* @return Command
*/
@Binds
@IntoMap
@StringKey("exit")
abstract public Command exitCommand(ExitCommand exitCommand);
}
/**
* 为 EchoCommand 定义信息模块
*/
@Module
public abstract class EchoCommandModule {
/**
* 为 Command 类绑定具体的子类实现
* 将该依赖声明为 echo 对象保存到 dagger2 内部维护的列表之中
*
* @param echoCommand 子类实现
* @return Command
*/
@Binds
@IntoMap
@StringKey("echo")
abstract public Command echoCommand(EchoCommand echoCommand);
}
声明模块注入:
/**
* 采用多个模块注入内部
*/
@Component(modules = {
ExitCommandModule.class,
EchoCommandModule.class
})
public interface CommandRouterFactory {
CommandRouter router();
}
最后只需要在具体引入的是否声明需要列表对象即可:
public class CommandRouter {
/**
* 注入的内部命令
*/
private final Map<String, Command> commands;
/**
* 直接声明依赖获取 Command 列表组
*
* @param commands 命令列表
*/
@Inject
public CommandRouter(Map<String, Command> commands) {
this.commands = commands;
}
}
OK, 现在已经完美将多条指令注入到 CommandApp 之中, 现在应用已经支持多命令行调用了;
后续单例的具体使用方法可以参照官方文档学习, 都是简单且有趣的设计方法.