1、前言
我对dagger2技术有强烈的原理探索欲望,但是其主要原理又在代码生成中,所以,只有自己会用,才会理解dagger2的精髓;也希望有和我一样探知欲望的小伙伴们,一起在dagger2原理的道路上,一起成长。
了解过dagger2的小伙伴,都知道,它有一个核心思想,叫依赖注入,也叫控制反转;可能也有小伙伴不知道,dagger2利用了apt技术进行代码生成中间件而达到此效果的。本章内容如下:
- 控制反转(依赖注入)
- java apt技术
- javapoet库简单介绍
- dagger2一步一步手写
2、控制反转(依赖注入)
听起来很高深,不过别着急;它从效果上确实是可以达到松耦合,但是它并不难理解。依赖注入关键理解的两个点
- 焦点是实例
- 关键是实例生成和赋值的中间件
这样两点一说是不是感觉也就不是那么难懂,如果还没有理解,下面给你个代码示例
正常的场景:生成一个apple,使用new生成
public class Test {
Apple apple = new Apple();
public static class Apple {
}
}
复制代码
依赖注入:
public class Test {
Apple apple;
public Test() {
TestInjector.inject(this);
}
public static class Apple {
}
public static class AppleFactory {
Apple get() {
return new Apple();
}
}
public static class TestInjector {
static void inject(Test test) {
test.apple = new AppleFactory().get();
}
}
}
复制代码
类变量apple未使用new生成,而是通过其注入器进行内部赋值;注入器内部是需要创建工厂AppleFactory提供实例;创建工厂只和提供者联系。
如果代码直接写,也没有什么优势啊
是的;所以需要代码生成技术支撑,而注入器和工厂类,通过注解识别,生成相关代码,这样需求方和提供方不必知道对方,而就可以赋值;所以注入器和创建工厂的生成是依赖注入的关键
3、Java apt技术
这是一种代码生成技术;帮忙我们生成通用代码,把我们从固定套路代码逻辑中解脱出来。效率必备神器啊
APT:注解处理器,代码编译期间,对注解信息读取和写入信息的工具;而java类的写入借助javapoet库更加丝滑
注解器运行规则
注释处理按顺序进行。在每一轮中,可能会要求处理器processor在上一轮产生的源文件和类文件上发现的注释的一个子集。第一轮处理的输入是工具运行的初始输入;如果一个处理器被要求在给定的一轮进行处理,那么将被要求处理后续的回合,包括最后一轮,即使没有注释来处理它。工具基础设施还可能要求处理器处理由工具操作隐含生成的文件。不会为每一轮创建一个新的Processor对象。
3.1 注解
注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Provider {
boolean single() default true;
String tag() default "";
String name() default "";
}
复制代码
- @interface 注解类标志
- 注解必须在编译时有效,也就是@Retention注解配置内容
- @Target 表明注解作用地方
具体内容可以查看作者之前的文章4 注解与反射部分
3.2 注解处理器
处理器不能android模块中进行创建,需要java模块,进行处理;步骤
- 创建java模块
- 创建处理器类,自己的类名(下面写入配置的内容,也是这个类的完整名字),我这里是InjectionProcessor
- 继承类extends AbstractProcessor
- 进行配置(别着急,配置在后面章节会讲到)
这样最简单的注解处理器就完成了。其中有几个方法需要注意
- init方法:获取一些工具方法处理类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
processingEnvironment.getFiler();
filer = processingEnvironment.getFiler(); // 用于创建新的源,类或辅助文件的文件管理器
elements = processingEnvironment.getElementUtils(); // 一些用于操作元素的实用方法
}
复制代码
- process方法:进行注解处理;对来自前一轮的类型元素处理一组注释类型,并返回此处理器是否声明这些注释类型。如果返回true,则会声明注释类型,并且不会要求后续处理器处理它们;如果返回false,则注释类型是无人认领的,并且后处理器可能被要求处理它们。处理器可以总是返回相同的布尔值,或者可以根据其自己选择的标准来改变结果。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Provider.class); // 获取使用注解的信息
return false;
}
复制代码
- getSupportedAnnotationTypes方法,需要处理的注解类型集合;
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(Inject.class.getName());
types.add(Provider.class.getName());
return types;
}
复制代码
- getSupportedSourceVersion方法,支持的版本,按照下面写即可
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
复制代码
APT配置
编译时的注解处理器,需要被识别,需要通过配置;通过下面两种方式均可
- AutoService注解
引入包:com.google.auto.service:auto-service:1.0-rc4 会自动在build/classes输入目录下生成文件META-INF/services/javax.annotation.processing.Processor文件 复制代码
- 手动配置
首先创建相关目录,使src/main/resources/META-INF/services/ 目录存在,然后再此目录下创建文件javax.annotation.processing.Processor,并在其中写入注解处理器类完整名称; 文件内容示例: com.liko.yuko.injection_compile.InjectionProcessor 复制代码
建议使用手动配置,使用注解处理时需要注意库的兼容,否则不会生效;
javapoet技术
JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。
源码地址,最新版本: com.squareup:javapoet:1.13.0
官网上有详细的使用,这里就不献丑了
4、手写
项目地址
当前代码实现【日期2022-03-26】,作者构思有两种注解即可:生成注解,注入注解
- 生成注解:作用于方法或者构造器,方法需要存在普通类、静态内部类中;可显示定义提供类类型,否则按照默认构造器类或者方法返回类型处理;提供类型不支持泛型;也可以通过定义标识对同一个类提供不同的实现
- 注入注解:作用成员变量,需要存在于普通类、静态内部类中;可显示定义提供类类型,也可以默认类型,也可以提供标识来对同提供类进行进一步匹配
分为一下几步
- 注解定义,以及中间件的生成格式;帮助反射查找
- 注解收集
- 注解的写入
使用示例如下:(项目中也有调试用例)
public static class Main{
@Inject
InnerUser user; // 需求方,需求InnerUser对象
public Main() {
Injection.inject(this); //进行注入,也可调用_DI_MainActivity_Main_Injector中inject进行注入,_DI_MainActivity_Main_Injector这个类只有在生成之后才可以调用
if (user != null) {
user.print();
}
}
}
public static class InnerUser implements User {
@Provider()
public InnerUser() {} // 提供方法,提供InnerUser对象
@Override
public String print() {
return "InnerUser";
}
}
复制代码
4.1 注解定义
生成注解:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Provider {
boolean single() default true;
String tag() default "";
String name() default "";
}
复制代码
注入注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Inject {
String tag() default "";
}
复制代码
对于中间生成,提供接口:
生成工厂接口:
public interface Factory<T>{
T get();
}
复制代码
注入器接口:
public interface Injector<T> {
void inject(T instance) throws Throwable;
}
复制代码
注入入口类:调用静态方法inject
public final class Injection {
private final static HashMap<Class, Injector> injectors = new HashMap<>();
private Injection() {
throw new RuntimeException("no need instance!");
}
public static void inject(Object obj) {
if (obj == null) {
return;
}
Injector inject = injectors.get(obj.getClass());
if (inject == null) {
try {
inject = Reflects.getInjector(obj);
injectors.put(obj.getClass(), inject);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
if (inject != null) {
try {
inject.inject(obj);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
复制代码
- 通过已经定义好的规则,通过需要注入对象,查找注入器全限定类名
- 缓存注入器实例以备后续使用;并进行注入
也可以直接使用生成的注入器,进行生成调用,这样这就不需要反射来处理了
关键的地方就在注入器和工厂实现了:大致如下
代码生成工厂:
public class _DI_MainActivity_InnerUser_Factory implements Factory<MainActivity.InnerUser> {
private static final _DI_MainActivity_InnerUser_Factory _instance = new _DI_MainActivity_InnerUser_Factory();
private static MainActivity.InnerUser _MainActivity_InnerUser;
public static _DI_MainActivity_InnerUser_Factory _getFactory() {
return _instance;
}
@Override
public MainActivity.InnerUser get() {
if (_MainActivity_InnerUser == null) {
_MainActivity_InnerUser = new MainActivity.InnerUser();
}
return _MainActivity_InnerUser;
}
}
复制代码
代码生成的注入器:
public class _DI_MainActivity_Main_Injector implements Injector<MainActivity.Main> {
@Override
public void inject(MainActivity.Main _MainActivity_Main) {
_MainActivity_Main.user = _DI_MainActivity_InnerUser_Factory._getFactory().get();
}
}
复制代码
注入器中可能能使用反射;这里注入器和实现工厂在同一轮任务处理中,也就是在同一个模块中,所以不需要反射;
4.2 注解收集与代码生成
由于代码过多,就仅仅对一种注解处理代码进行粘贴;详细解释,请看代码中注释;解释中主要说明一些重要的地方
注解收集
基类
public interface Collector<T> {
Set<T> collect(RoundEnvironment roundEnvironment, Elements elementUtils);
}
复制代码
Provider收集具体处理:
@Override
public Set<ProviderBean> collect(RoundEnvironment roundEnvironment, Elements elementUtils) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Provider.class); // 获取所有使用此注解的元素
if (elements == null || elements.isEmpty()) {
return null;
}
HashSet<ProviderBean> beans = new HashSet<>(elements.size() * 3 / 4 + 1);
for (Element element : elements) {
if (element.getKind() != ElementKind.CONSTRUCTOR && element.getKind() != ElementKind.METHOD) {
continue; // 作用于方法或者构造器的此种注解,才是需要处理的
}
if (element.getEnclosingElement().getKind() != ElementKind.CLASS) {
throw new RuntimeException("provider method must in class, " +
element.getEnclosingElement().asType().toString() + ","
+ element.getSimpleName()); // 包含此元素的结构一定得是类
}
ProviderBean bean = new ProviderBean();
TypeElement cls = ((TypeElement)element.getEnclosingElement()); // 获取包含当前元素的元素
bean.clsPkg = elementUtils.getPackageOf(cls).toString(); // 获取元素的包信息
bean.clsName = cls.getQualifiedName().toString().replace(bean.clsPkg + '.', ""); // 元素的全限定名字,去除包名和.
Element pre = element.getEnclosingElement();
Element now = pre.getEnclosingElement();
while (now != null && now.getKind() != ElementKind.PACKAGE) {
if (!pre.getModifiers().contains(Modifier.STATIC)) {
throw new RuntimeException("Inner class must static, " +
bean.clsPkg + '.' + bean.clsName); // 静态内部才可
}
pre = now;
now = pre.getEnclosingElement();
}
bean.isConstructor = element.getKind() == ElementKind.CONSTRUCTOR; // 构造器判断
bean.isStatic = element.getModifiers().contains(Modifier.STATIC);// 是否是静态方法
bean.methodName = element.getSimpleName().toString();// 此种方法获取方法名字或者属性名字
String provideCls = element.getAnnotation(Provider.class).name();
if (provideCls == null || "".equals(provideCls)) {
if (element.getKind() == ElementKind.CONSTRUCTOR) {
bean.providerPkg = bean.clsPkg;
bean.providerName = bean.clsName;
} else {
provideCls = ((ExecutableElement) element).getReturnType().toString();
TypeElement proCls = elementUtils.getTypeElement(provideCls);
bean.providerPkg = elementUtils.getPackageOf(proCls).toString();
bean.providerName = proCls.getQualifiedName().toString()
.replace(bean.providerPkg + '.', "");
}
} else {
TypeElement proCls = elementUtils.getTypeElement(provideCls);
bean.providerPkg = elementUtils.getPackageOf(proCls).toString();
bean.providerName = proCls.getQualifiedName().toString()
.replace(bean.providerPkg + '.', "");
}
bean.tag = element.getAnnotation(Provider.class).tag();
bean.isSingle = element.getAnnotation(Provider.class).single();
beans.add(bean);
}
return beans;
}
复制代码
对上面的一些方法和概念稍微解释下
- Element分为很多种,最外层是PackageElement,类是TypeElement,变量VariableElement,方法ExecutableElement;
- getEnclosingElement方法:包含此元素的,紧邻外层元素类型,比如普通类外部是packageElement,静态内部类,类成员,类方法外部是TypeElement
- asType().toString,全限定类名
- getKind类别,也是元素类型,在ElementKind里面,PACKEAGE对应包,CLASS对应类,METHOD方法,FIELD属性等
- Elements.getTypeElement():通过类的全限定名,获取对应Element
- Elements.getPackageOf(),通过Element获取其所在包属性
- 特别注意,在运行时,静态内部类是$来分割和外部类的,而代码编译时,就是.
Provider注解处理
public void handle(Filer filer, Set<String> writeFiles, Set<ProviderBean> collectBean) {
if (collectBean == null || collectBean.isEmpty()) {
return;
}
for (ProviderBean bean : collectBean) {
MethodSpec.Builder get = MethodSpec.methodBuilder("get") //方法构造,方法名get
.addModifiers(Modifier.PUBLIC) // public 方法
.addAnnotation(ClassName.get(Override.class)) // Override注解
.returns(ClassName.get(bean.providerPkg, bean.providerName)); // 返回类型
String clsName = Reflects.getFactoryName(bean.providerName, bean.tag);
TypeSpec.Builder factory = TypeSpec.classBuilder(clsName) //类构造
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(
ClassName.get(Factory.class), ClassName.get(bean.providerPkg, bean.providerName)
)); // 增加实现接口,接口类型是泛型
FieldSpec instance = FieldSpec.builder(
ClassName.get(bean.providerPkg, clsName), "_instance") // 属性构建,属性类,属性名
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) // 属性限定符
.initializer("new $L()", clsName) // 属性初始化
.build();
MethodSpec getInstance = MethodSpec.methodBuilder(Reflects.STATIC_METHOD_NAME_IN_FACTORY)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(ClassName.get(bean.providerPkg, clsName))
.addStatement("return $L", "_instance") // 增加执行语句
.build();
factory.addField(instance);
factory.addMethod(getInstance);
if (bean.isSingle) {
String singleName = "_" + bean.providerName.replace('.', '_');
FieldSpec single = FieldSpec.builder(
ClassName.get(bean.providerPkg, bean.providerName), singleName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.build();
factory.addField(single);
get.beginControlFlow("if ($L == null)", singleName); // 条件语句开始
if (bean.isStatic) {
get.addStatement("$L = $T.$L()", singleName,
ClassName.get(bean.clsPkg, bean.clsName),
bean.methodName);
} else if (bean.isConstructor) {
get.addStatement("$L = new $T()", singleName,
ClassName.get(bean.clsPkg, bean.clsName));
} else {
get.addStatement("$L = new $T().$L()", singleName,
ClassName.get(bean.clsPkg, bean.clsName),
bean.methodName);
}
get.endControlFlow()
.addStatement("return $L", singleName); // 条件语句结束
} else {
if (bean.isStatic) {
get.addStatement("return $T.$L()",
ClassName.get(bean.clsPkg, bean.clsName),
bean.methodName);
} else if (bean.isConstructor) {
get.addStatement("return new $T()",
ClassName.get(bean.clsPkg, bean.clsName));
} {
get.addStatement("return new $T().$L()",
ClassName.get(bean.clsPkg, bean.clsName),
bean.methodName);
}
}
factory.addMethod(get.build());
try {
JavaFile.builder(bean.providerPkg, factory.build()).build().writeTo(filer);
writeFiles.add(bean.providerPkg + "." + clsName);
} catch (IOException e) {
e.printStackTrace();
}
}
return;
}
复制代码
- 写入操作是通过JavaFile对象witeTo(Filer)来进行的
- javapoet中:TypeSpec表示类,FieldSpec表示属性,MethodSpec表示方法或者构造器;这些类都采用构造器模式进行构造
- addStatement方法增加语句,其中有占位符,使用$ + 字母(T、L、S、N),T类替换(自动引入类),L值替换,S字符串替换,N替换MethodSpec或者FieldSpec或者TypeSpe等,表示相应类的名字的L替换,不过N替换可以不必要的手误
- ClassName,类似于class的概念;参数泛型ParameterizedTypeName
- 流程控制,其实就相当于语句块{}的作用,beginControlFlow(条件语句) 相当于在条件语句后加了{,endControlFlow()相当于}
5、断点调试
首先要创建构建调试:(操作顺序如下文字,后面有跟图标注)
在AndroidStudio编译类型 -> Edit Configurations -> Templates -> Remote(有些可能是Remote for jvm) -> creat Configuration ->写上名字和,选择模块 -> 应用
复制代码
编译类型位置
点击Edit Configurations后的界面
最后步骤相关位置
每次调试,都应该先clean编译;然后按照下面两步处理
- 当前应用目录下命令行执行:gradlew –no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
- 选择创建的构建调试,然后点击debug
debug位置(我的构建名字为apt-debug)
代码生成位置查看
- 主工程,aar模块:build\generated\ap_generated_sources\debug\out
- jar包模块:build\generated\sources\annotationProcessor\java\main
6、总结
把握依赖注入的概念以及实现方式;并对静态编译时的处理以及元素模型进行理解,我叫这些api为‘静态反射’,这些和动态反射概念是相同应的;javapoet是和类结构相关概念比较接近的,把这些多了解比对,就很容易理解,也很容找所需求用的api了;由于apt进行代码生成的相关的文章和基础比较少,所以还是先了解其它的一些概念来类似理解和作为依据查找api。另外也可以通过上面的调试手段进行信息打磨。
在作者对代码生成使用不断趟坑后,会写一篇基础,关于注解收集以及异常处理提示的内容;一起加油!!
如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!