依赖注入Dagger初学习
第一章 一堆废话第01节 前置内容最近在学习 Android 源码在研究SystemUI源代码的过程中发现了一段代码publicinterfaceStatusBarIconController...{}protectedStatusBarIconControllermIconController;protectedvoidgetDependencies(){mIconControllerDependency.get(StatusBarIconController.class);}// 后面发现存在大量的 Dependency.get(XXXX.class) 的操作我当时特别好奇这个Dependency.get(XXXX.class)在源代码当中大量的出现他是啥通过 AI 搜索我发现了 他给我的解释是Dependency他是 Android 系统服务内部的一种轻量级依赖注入/服务定位器实现用于解耦、简化构造、支持测试。它虽然不如 Dagger 强大但在 AOSP 这种对性能和启动速度敏感的场景下足够高效且易于理解。我发现这样的解释说的有点不够清晰不明白。因为这里又有一些新的概念。依赖注入依赖注入Dependency Injection简称 DI是一种软件设计模式用于实现控制反转Inversion of ControlIoC。它的核心思想是对象的依赖即它所需要的其他对象不由自身创建或查找而是由外部容器或框架在运行时“注入”进来。简单来说“我不要自己找我要的东西你直接给我。”哦这里我看到了几个核心词汇DI和IOC这两个词汇我可以说并不陌生啊。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 就像是一家具体的餐厅 比如 海底捞 它实现了 去餐厅吃饭 这个理念。对比一下Dagger和Spring IOC的区别特性DaggerSpring注入的时机编译时静态生成代码运行时反射性能高无反射较低反射有开销配置方式注解 手动编写 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包因为没有gradle和maven相关的内容我全部采用的是lib jar包所以前面需要去准备相关的jar包相关 jar 包的下载地址maven 中央仓库地址 https://central.sonatype.com/需要下载的依赖和版本编号依赖名称版本号1annotation-api1.3.02auto-common0.1.03checker-qual2.5.24dagger2.245dagger-complier2.246dagger-complier-with-poet7dagger-producers2.248dagger-spi2.249failureaccess1.0.110guava27.111javapoet1.11.012javax.inject1为了方便大家快速学习我已经将使用的 jar 包提供了下载地址。点我进入jar包的下载地址第03节 项目鸟瞰在学习的过程中在项目文档结构中踩了不少的坑防止后续有更多的人踩坑将项目的 鸟瞰图拿出来对照能够清晰的看明白代码结构。配置文件studyDagger/studyDagger.iml内容如下?xml version1.0 encodingUTF-8?moduletypeJAVA_MODULEversion4componentnameNewModuleRootManagerLANGUAGE_LEVELJDK_17inherit-compiler-outputtrueexclude-output/contenturlfile://$MODULE_DIR$sourceFolderurlfile://$MODULE_DIR$/out/production/studyDagger/generatedisTestSourcefalsegeneratedtrue/sourceFolderurlfile://$MODULE_DIR$/srcisTestSourcefalse//contentorderEntrytypeinheritedJdk/orderEntrytypesourceFolderforTestsfalse/orderEntrytypelibrarynameliblevelproject//component/module第二章 入门案例第01节 案例代码汽车工厂接口packagecom.study;importdagger.Component;ComponentpublicinterfaceCarFactory{CarBMWgetBMW();CarAudigetAudi();}宝马汽车类packagecom.study;importjavax.inject.Inject;publicclassCarBMW{InjectpublicCarBMW(){}publicStringgetName(){return宝马汽车;}}奥迪汽车类packagecom.study;importjavax.inject.Inject;publicclassCarAudi{InjectpublicCarAudi(){}publicStringgetName(){return奥迪汽车;}}测试类packagecom.study;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactoryDaggerCarFactory.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节 案例代码统一的接口 Carpackagecom.study;publicinterfaceCar{StringgetName();}基础子类 宝马汽车packagecom.study;importjavax.inject.Inject;publicclassCarBMWimplementsCar{InjectpublicCarBMW(){}publicStringgetName(){return宝马汽车;}}基础子类 奥迪汽车packagecom.study;importjavax.inject.Inject;publicclassCarAudiimplementsCar{InjectpublicCarAudi(){}publicStringgetName(){return奥迪汽车;}}模块管理类 CarModulepackagecom.study;importdagger.Binds;importdagger.Module;importjavax.inject.Qualifier;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;ModulepublicabstractclassCarModule{QualifierRetention(RetentionPolicy.RUNTIME)publicinterfaceBMW{}QualifierRetention(RetentionPolicy.RUNTIME)publicinterfaceAudi{}BindsBMWpublicabstractCarbindBMW(CarBMWcarBMW);BindsAudipublicabstractCarbindAudi(CarAudicarAudi);}汽车工厂类packagecom.study;importdagger.Component;Component(modulesCarModule.class)publicinterfaceCarFactory{CarModule.BMWCargetBMW();CarModule.AudiCargetAudi();}测试类packagecom.study;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactoryDaggerCarFactory.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节 案例代码车辆品牌的接口packagecom.study;publicinterfaceCarBrand{StringgetName();}车辆工厂接口packagecom.study;importdagger.Component;importjava.util.Map;Component(modulesCarModule.class)publicinterfaceCarFactory{MapClass?,CarBrandgetCarMap();}基础子类 奥迪汽车packagecom.study;importjavax.inject.Inject;publicclassCarAudiimplementsCarBrand{InjectpublicCarAudi(){}OverridepublicStringgetName(){return奥迪汽车;}}基础子类宝马汽车packagecom.study;importjavax.inject.Inject;publicclassCarBMWimplementsCarBrand{InjectpublicCarBMW(){}publicStringgetName(){return宝马汽车;}}模块类packagecom.study;importdagger.Binds;importdagger.Module;importdagger.multibindings.ClassKey;importdagger.multibindings.IntoMap;ModulepublicabstractclassCarModule{BindsIntoMapClassKey(CarBMW.class)publicabstractCarBrandbindCarBrandBMW(CarBMWcarBMW);BindsIntoMapClassKey(CarAudi.class)publicabstractCarBrandbindCarBrandAudi(CarAudicarAudi);}测试类packagecom.study;importjava.util.Map;publicclassDemo{publicstaticvoidmain(String[]args){CarFactoryfactoryDaggerCarFactory.create();MapClass?,CarBrandcarMapfactory.getCarMap();CarBrandbrandAudicarMap.get(CarAudi.class);CarBrandbrandBMWcarMap.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 例如MapClass?, ProviderCarBrand从而实现用哪个才初始化哪个。说到了懒加载的方式我们回头看看SystemUI的代码不就是 懒加载的吗前面我们说到了多重绑定 和 懒加载 的实现方式关于后续的思考如何操作呢怎样代码才能符合 商用级的标准 怎样的代码才能符合 工业级标准想要了解更多点击下面进入我的下一篇文章吧。点我进入 依赖注入Dagger懒加载第五章 注意事项第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