依赖注入Dagger初学习

第一章 一堆废话

第01节 前置内容

最近在学习 Android 源码,在研究 SystemUI 源代码的过程中,发现了一段代码:

java 复制代码
public interface StatusBarIconController ... {
    
}


protected StatusBarIconController mIconController;
protected void getDependencies(){
    mIconController = Dependency.get(StatusBarIconController.class);
}

// 后面发现存在大量的  Dependency.get(XXXX.class) 的操作!

我当时特别好奇,这个 Dependency.get(XXXX.class) 在源代码当中,大量的出现,他是啥??

通过 AI 搜索,我发现了 他给我的解释是 Dependency 他是 Android 系统服务内部的一种轻量级依赖注入/服务定位器实现,用于解耦、简化构造、支持测试。

它虽然不如 Dagger 强大,但在 AOSP 这种对性能和启动速度敏感的场景下,足够高效且易于理解。

我发现这样的解释,说的有点不够清晰,不明白。

因为这里又有一些新的概念。依赖注入?

依赖注入(Dependency Injection,简称 DI) 是一种软件设计模式,用于实现控制反转(Inversion of Control,IoC)

它的核心思想是:对象的依赖(即它所需要的其他对象)不由自身创建或查找,而是由外部容器或框架在运行时"注入"进来

简单来说:"我不要自己找我要的东西,你直接给我。"

哦,这里我看到了几个核心词汇 DIIOC

这两个词汇,我可以说并不陌生啊。

复制代码
DI: 依赖注入,一种软件设计模式。
IOC: 了解过 Java Spring 框架的应该知道两大核心  IOC 和 AOP

了解过 IOC 的同学,应该清楚知道。

在 Java 当中,一般的写法创建对象可以写为

复制代码
Car  mCar = new Car();

这种是我们常规的写法,学过多态就知道,左右两侧是可以拆分的。

复制代码
Car  mCar1 = new CarBMW();   // 创建了一辆宝马汽车
Car  mCar2 = new CarAudi();  // 创建了一辆奥迪汽车

注意:等号左边可以看成是固定的,等号右边 由于 Java 多态的关系,我们可以看成不同的 Car 的创建方式。

如果将 new 的操作,我们自己最初的时候,并不去做,而是交给别人去做,别人做好了,创建好了,我只是拿过来使用 对象的内容!

这种不就是 IOC 的思想吗?

举个 生活中的例子

自己租房(传统方式) 入住酒店(IOC)
控制权 我们需要自己找房源、签合同、交水电费、维修家具 我们只需要预定房间,酒店负责清洁、维修、换床单
依赖管理 我们需要自己管理水电、网络、家具等所有的依赖 酒店系统统一管理所有的服务
灵活性 我们可以随意的改造房间,但是承担所有责任 我们无法改变房间的布局,但是享受一站式服务

IOC 的体现: 我们不再控制 "如何维护居住的环境",而是将控制权交给酒店(容器)。我只需要提出需求(入住),酒店自动提供所有的配套服务。

当然,我这里不是去学习 IOC 的内容,只是去理解 IOC 的设计方式,整体来说: IOC 就是一种 DI 依赖注入的方式。

那么和我们今天探讨的话题,Dagger 有什么关系呢?

回到主题来说:

IOC 控制反转,他是一种设计原则,而 Dagger 是一种具体的实现框架。

简单举例:

复制代码
IOC 就像 "去餐厅吃饭"  这个概念。(我们不再需要自己做饭了)
Dagger 就像是一家具体的餐厅, 比如 "海底捞" 它实现了 "去餐厅吃饭" 这个理念。

对比一下 DaggerSpring IOC 的区别

特性 Dagger Spring
注入的时机 编译时(静态生成代码) 运行时(反射)
性能 高(无反射) 较低(反射有开销)
配置方式 注解 + 手动编写 Module 注解 + XML / Java Config / 自动扫描
适用平台 Android 为主,也可以用于 Java 企业级 Java 后端
灵活性 较严格,需要显式声明所有依赖 更灵活,支持自动装载,懒加载等

通过上面的介绍,我们最终确定几点内容

复制代码
1、Dagger 是一种 DI 依赖注入的方式
2、Dagger 主要应用在 Android 上
3、Dagger 性能高
4、Dagger 注入在编译时,会生成代码

第02节 准备工作

Dagger 我想要从一个最简单的 JavaSE 项目入手。

作为一个最简单的 JavaSE 项目,那么应该做哪些操作呢? (这里不存在 maven 依赖,有的仅有 JavaSE 代码 和 lib jar 包)

因为没有 gradlemaven 相关的内容,我全部采用的是 lib jar 包,所以前面需要去准备相关的 jar

相关 jar 包的下载地址:

maven 中央仓库地址 https://central.sonatype.com/

需要下载的依赖和版本:

编号 依赖名称 版本号
1 annotation-api 1.3.0
2 auto-common 0.1.0
3 checker-qual 2.5.2
4 dagger 2.24
5 dagger-complier 2.24
6 dagger-complier-with-poet
7 dagger-producers 2.24
8 dagger-spi 2.24
9 failureaccess 1.0.1
10 guava 27.1
11 javapoet 1.11.0
12 javax.inject 1

第03节 项目鸟瞰

在学习的过程中,在项目文档结构中,踩了不少的坑,防止后续有更多的人,踩坑,将项目的 鸟瞰图,拿出来对照,能够清晰的看明白代码结构。

配置文件 studyDagger/studyDagger.iml 内容如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_17" inherit-compiler-output="true">
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/out/production/studyDagger/generated" isTestSource="false" generated="true" />
      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="library" name="lib" level="project" />
  </component>
</module>

第二章 入门案例

第01节 案例代码

汽车工厂接口

java 复制代码
package com.study;

import dagger.Component;

@Component
public interface CarFactory {

    CarBMW getBMW();

    CarAudi getAudi();
}

宝马汽车类

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarBMW {

    @Inject
    public CarBMW(){

    }

    public String getName() {
        return "宝马汽车";
    }
}

奥迪汽车类

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarAudi {

    @Inject
    public CarAudi(){

    }

    public String getName() {
        return "奥迪汽车";
    }
}

测试类

java 复制代码
package com.study;

public class Demo {

    public static void main(String[] args) {
        
        CarFactory factory = DaggerCarFactory.create();

        System.out.println("success: " + factory.getBMW().getName());   // success: 宝马汽车
        System.out.println("success: " + factory.getAudi().getName());  // success: 奥迪汽车
    }
}

第02节 流程图解

编译时的图解

运行时图解

第03节 总结思考

总结:

复制代码
1、你看,在 Demo 的运行过程中, 并没有出现 new 的代码。

2、过程说明:
	A. Demo 想要车, 那么问 Factory 要!
	B. Factory 怎么知道 如何造车的呢?
	C. 我们在 Car 的构造方法上, 贴上了 @Inject 标签
	D. 承上启下的纽带就是 Dagger 帮我们自动生成的 DaggerCarFactory

因此整个过程中, 我们并没有写一行的 new 代码, 但是车子却被 稳稳当当的造出来了!

思考:

复制代码
上面代码当中,仅仅是作为一个入门级的案例,目的是为了我们方便去理解代码运行过程。
如果,将它作为工业级项目,他存在一些致命的问题。

致命缺陷:  不符合"面向接口编程"原则

1、问题所在:
	CarFactory 直接返回了具体的实现类  CarBMW 和 CarAudi 
	如果未来有 100种车型, 工厂接口就要写 100个方法, 并且调用方 Demo 必须依赖这些具体的类, 违背了低耦合的设计原则。
	
2、拓展方向: 接口多实现与多态
	A. 解决方案: 引入一个统一的 Car 接口, 让 CarBMW 和 CarAudi 去实现它
	B. 在 Dagger 当中存在知识点:  
		学习使用 @Mudule 与 @Binds (或者 @Provides) 来告诉 Dagger 
		当别人管你要 Car 接口时, 我们应该实例化哪一个具体的实现类

第三章 基础用法

第01节 案例代码

统一的接口 Car

java 复制代码
package com.study;

public interface Car {

    String getName();
}

基础子类 宝马汽车

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarBMW implements Car {

    @Inject
    public CarBMW() {

    }

    public String getName() {
        return "宝马汽车";
    }
}

基础子类 奥迪汽车

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarAudi implements Car {

    @Inject
    public CarAudi() {

    }

    public String getName() {
        return "奥迪汽车";
    }
}

模块管理类 CarModule

java 复制代码
package com.study;

import dagger.Binds;
import dagger.Module;

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

@Module
public abstract class CarModule {

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BMW {
    }

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Audi {
    }

    @Binds
    @BMW
    public abstract Car bindBMW(CarBMW carBMW);

    @Binds
    @Audi
    public abstract Car bindAudi(CarAudi carAudi);
}

汽车工厂类

java 复制代码
package com.study;

import dagger.Component;

@Component(modules = CarModule.class)
public interface CarFactory {

    @CarModule.BMW
    Car getBMW();

    @CarModule.Audi
    Car getAudi();
}

测试类

java 复制代码
package com.study;

public class Demo {

    public static void main(String[] args) {
        CarFactory factory = DaggerCarFactory.create();

        System.out.println("success: " + factory.getBMW().getName());   // success: 宝马汽车
        System.out.println("success: " + factory.getAudi().getName());  // success: 奥迪汽车
    }
}

第02节 总结思考

总结

复制代码
上面的这套基础用法,是 Dagger 解决  "同一个接口, 有多个实现类" 的时候, 经典做法。

基于 @Qualifier 自定义注解的显示绑定

优点

复制代码
1、强制类型安全与编译期检查
	每个类都有明确的自定义 注解(如  @BMW  @Audi)
	在 Component 接口也是显式声明的方法(如: getBMW)
	如果配置错误, 编译期间, 直接报错, 绝对不会把奥迪错当成为宝马
	
2、直观易读
	对于使用者来说, API 语义非常清晰
	调用 factory.getBMW() 就可以明确知道拿到的是什么对象

缺点 存在的痛点!

复制代码
1、样板代码严重
	每增加一个汽车品牌(如 奔驰 CarBenz) 
	我们必须成套的增加下面的内容:
		A. 一个新自定义的 @Qualifier 注解
		B. 在 CarModule 当中添加一条 @Binds 抽象方法
		C. 在 CarFactory 接口当中增加一个显式获取的方法 (如: getBenz())
		
2、违反开闭原则(OCP)
	核心工厂接口 CarFactory 被迫感知了所有的具体实现。
	一旦需要支持新的车型,就必须修改 CarFactory 接口, 
	这种操作, 在大型项目或者插件化架构中是不可以接受的。
	
3、无法动态, 动态获取
	所有的调用都是硬编码, 如果业务上需要根据用户输入的字符串 或者类名称 动态获取对应的汽车实例。
	基础用法的代码, 很难优雅的做到, 通常需要写一堆的 if...else... 或者 switch....

如何解决上述缺点?

复制代码
为了解决上述的问题:
	每增加一个类, 都需要改动多处接口, 以及 无法动态拓展的 痛点!
	我们需要一种 能够自动手机实现类, 并且支持动态路由的机制。
	
这种方案正是 Dagger 的 Multibindings  多重绑定 派上用场的地方。

第四章 多重绑定

第01节 案例代码

车辆品牌的接口

java 复制代码
package com.study;

public interface CarBrand {
    String getName();
}

车辆工厂接口

java 复制代码
package com.study;

import dagger.Component;

import java.util.Map;

@Component(modules = CarModule.class)
public interface CarFactory {
    Map<Class<?>, CarBrand> getCarMap();
}

基础子类 奥迪汽车

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarAudi implements CarBrand {

    @Inject
    public CarAudi(){

    }

    @Override
    public String getName() {
        return "奥迪汽车";
    }
}

基础子类,宝马汽车

java 复制代码
package com.study;

import javax.inject.Inject;

public class CarBMW implements CarBrand {

    @Inject
    public CarBMW() {


    }

    public String getName() {
        return "宝马汽车";
    }
}

模块类

java 复制代码
package com.study;

import dagger.Binds;
import dagger.Module;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;

@Module
public abstract class CarModule {

    @Binds
    @IntoMap
    @ClassKey(CarBMW.class)
    public abstract CarBrand bindCarBrandBMW(CarBMW carBMW);

    @Binds
    @IntoMap
    @ClassKey(CarAudi.class)
    public abstract CarBrand bindCarBrandAudi(CarAudi carAudi);
}

测试类

java 复制代码
package com.study;

import java.util.Map;

public class Demo {

    public static void main(String[] args) {
        CarFactory factory = DaggerCarFactory.create();
        Map<Class<?>, CarBrand> carMap = factory.getCarMap();
        CarBrand brandAudi = carMap.get(CarAudi.class);
        CarBrand brandBMW = carMap.get(CarBMW.class);
        
        System.out.println("success: " + brandAudi.getName());   // success: 奥迪汽车
        System.out.println("success: " + brandBMW.getName());    // success: 宝马汽车
    }
}

第02节 总结思考

总结

复制代码
1、极佳的架构扩展性(符合开闭原则 OCP)
	A. Component 接口彻底解耦: 
		CarFactory 只需要暴露一个统一的接口 getCarMap()
	B. 新增品牌零修改: 
		当需要增加奔驰(CarBenz)时,完全不需要修改 CarFactory 接口。
		你只需要写好 CarBenz 类,并在 CarModule 中增加一个 @IntoMap 的绑定方法即可。
		这极大地保护了核心架构的稳定性
		
2、支持动态路由与运行时查找
	A. 在实际业务中,我们经常需要根据配置、数据库数据、或者用户的输入来决定实例化哪辆车。
	B. 使用 Map 结构,你可以直接通过传递变量(如 Class<?>、或是将 Key 改为 String 后的字符串标识)来动态获取对应的实例。
	   这在第一套方案(每个品牌对应一个硬编码方法)中是无法直接做到的。
	C. 告别自定义注解的样板代码
		基础用法中,每加一个品牌都要手写一个 @Qualifier 注解(如 @BMW、@Audi)。
		多重绑定方案直接利用了 Dagger 内置的 @ClassKey(或 @StringKey),省去了大量自定义注解的声明,代码结构更加干净整洁。

思考 虽然多重绑定非常优雅,但在特定场景下它也有一些不可忽视的"隐患":

复制代码
1、丧失了编译期的"强类型安全"约束
	A. 必须通过 carMap.get(CarAudi.class) 来获取实例。
	B. 如果某个品牌忘记在 Module 中注册(或者拼写错了 Key),编译期不会报错,但在运行期 carMap.get(...) 会直接返回 null。
	C. 如果你没有做非空校验(如 brand.getName()),就会触发 NullPointerException(空指针异常)
	
2、存在不必要的内存与性能开销(非懒加载问题)
	A. 即时初始化: 
		默认情况下,当你调用 factory.getCarMap() 时,
		Dagger 会一次性将 Map 里的所有汽车实例全部初始化(包括奥迪、宝马以及未来可能加入的几十个品牌)
	B. 性能隐患: 
		如果某些汽车类的初始化非常消耗资源(比如要连接数据库或加载大文件),
		而你其实只需要使用其中一种,这种"全量初始化"就会造成严重的内存和性能浪费。
	C. 解决方案: 
		在实际开发中,通常需要将 Map 的 Value 类型定义为双子(Provider)或懒加载(Lazy),
		例如:Map<Class<?>, Provider<CarBrand>>,从而实现用哪个才初始化哪个。

说到了懒加载的方式,我们回头看看, SystemUI 的代码不就是 懒加载的吗!

第五章 注意事项

第01节 编译的依赖

因为在编译期间,需要找到相关的配置

复制代码
主要位置:
	Settings  --->  Build, Execution, Deployment  ---> Compiler ---> Annoation Processors

1、注意需要配置 lib 文件夹下面的所有 jar 包
2、可选项:  Use --processor--module-path  compiler option (for Java 9 and later)
3、操作完毕之后, 点击 OK

第02节 生成的out

这里生成的文件,会出现在 out 目录下面,需要对文件,作为 Generated Sources Root

第03节 重新编译

每次修改完毕之后,建议重新编译一下代码,编译代码的方式是

复制代码
在工具栏,找到  Build  ---> Rebuild Project
相关推荐
@@@@@@@@541 小时前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
网小鱼的学习笔记1 小时前
Agent学习001
学习·迁移学习
এ慕ོ冬℘゜1 小时前
深度解析 JavaScript:赋予 Web 灵魂的“全栈语言”
开发语言·前端·javascript
茗创科技1 小时前
CGE连接组:用基因表达解码脑疾病的异质性(附高分文献下载)
python·matlab·脑网络
汤米粥1 小时前
Python调用百度搜索触发安全验证
开发语言·python
白帽小阳1 小时前
我的AI辅助开发工具链2026版:从编码到部署的全栈智能实践
网络·人工智能·学习·安全·自动化
杠杠的blog1 小时前
企业级提示词管理
python·agent·提示词·生产级实践
影寂ldy2 小时前
C# 五大加密算法全套实战(AES/DES对称、RSA非对称、MD5/SHA256哈希)
开发语言·c#·哈希算法
小小放舟、2 小时前
VS Code Code Runner 中文乱码修复与 IDEA 风格输出配置
vscode·python·code runner
罗超驿2 小时前
6.Java LinkedList深度解析:从链表原理到源码实践
java·开发语言·链表