手書きの短剣2をステップバイステップで教えます

1はじめに

私はdagger2テクノロジーの原理を探求したいと強く望んでいますが、その主な原理はコード生成であるため、自分で使用できる場合にのみ、dagger2の本質を理解できます。dagger2の原理の道で、一緒に成長します。

dagger2を知っている友人は、依存性注入と呼ばれるコアアイデア(制御の反転とも呼ばれる)があることを知っています。dagger2がaptテクノロジーを使用してコード生成ミドルウェアを生成し、この効果を実現していることを知らない友人もいます。この章の内容は次のとおりです。

  1. 制御の反転(依存性注入)
  2. javaaptテクノロジー
  3. javapoetライブラリの簡単な紹介
  4. dagger2手書きステップバイステップ

2.制御の反転(依存性注入)

深く聞こえますが、心配しないでください。実際には緩い結合を実現しますが、理解するのは難しくありません。依存性注入の重要な理解の2つのポイント

  • フォーカスはインスタンスです
  • 重要なのは、インスタンスの生成と割り当てのためのミドルウェアです

このように、2つのポイントを理解するのがそれほど難しくないかどうかを理解することはそれほど難しくありません。まだ理解していない場合は、ここにコード例を示します。

通常のシナリオ:リンゴを生成し、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.Javaaptテクノロジー

これはコード生成手法です。ジェネリックコードを生成し、固定ルーチンのロジックから解放するのに役立ちます。効率のための必需品

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模块,进行处理;步骤

  1. 创建java模块
  2. 创建处理器类,自己的类名(下面写入配置的内容,也是这个类的完整名字),我这里是InjectionProcessor
  3. 继承类extends AbstractProcessor
  4. 进行配置(别着急,配置在后面章节会讲到)

这样最简单的注解处理器就完成了。其中有几个方法需要注意

  1. init方法:获取一些工具方法处理类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    processingEnvironment.getFiler();
    filer = processingEnvironment.getFiler(); // 用于创建新的源,类或辅助文件的文件管理器
    elements = processingEnvironment.getElementUtils(); // 一些用于操作元素的实用方法
}
复制代码
  1. process方法:进行注解处理;对来自前一轮的类型元素处理一组注释类型,并返回此处理器是否声明这些注释类型。如果返回true,则会声明注释类型,并且不会要求后续处理器处理它们;如果返回false,则注释类型是无人认领的,并且后处理器可能被要求处理它们。处理器可以总是返回相同的布尔值,或者可以根据其自己选择的标准来改变结果。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Provider.class); // 获取使用注解的信息
    return false;
}
复制代码
  1. getSupportedAnnotationTypes方法,需要处理的注解类型集合;
@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new HashSet<>();
    types.add(Inject.class.getName());
    types.add(Provider.class.getName());
    return types;
}
复制代码
  1. 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】,作者构思有两种注解即可:生成注解,注入注解

  1. 生成注解:作用于方法或者构造器,方法需要存在普通类、静态内部类中;可显示定义提供类类型,否则按照默认构造器类或者方法返回类型处理;提供类型不支持泛型;也可以通过定义标识对同一个类提供不同的实现
  2. 注入注解:作用成员变量,需要存在于普通类、静态内部类中;可显示定义提供类类型,也可以默认类型,也可以提供标识来对同提供类进行进一步匹配

分为一下几步

  1. 注解定义,以及中间件的生成格式;帮助反射查找
  2. 注解收集
  3. 注解的写入

使用示例如下:(项目中也有调试用例)

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();
            }
        }
    }
}
复制代码
  1. 通过已经定义好的规则,通过需要注入对象,查找注入器全限定类名
  2. 缓存注入器实例以备后续使用;并进行注入

也可以直接使用生成的注入器,进行生成调用,这样这就不需要反射来处理了

关键的地方就在注入器和工厂实现了:大致如下

代码生成工厂

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;
}
复制代码

对上面的一些方法和概念稍微解释下

  1. Element分为很多种,最外层是PackageElement,类是TypeElement,变量VariableElement,方法ExecutableElement;
  2. getEnclosingElement方法:包含此元素的,紧邻外层元素类型,比如普通类外部是packageElement,静态内部类,类成员,类方法外部是TypeElement
  3. asType().toString,全限定类名
  4. getKind类别,也是元素类型,在ElementKind里面,PACKEAGE对应包,CLASS对应类,METHOD方法,FIELD属性等
  5. Elements.getTypeElement():通过类的全限定名,获取对应Element
  6. Elements.getPackageOf(),通过Element获取其所在包属性
  7. 特别注意,在运行时,静态内部类是$来分割和外部类的,而代码编译时,就是.

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;
}
复制代码
  1. 写入操作是通过JavaFile对象witeTo(Filer)来进行的
  2. javapoet中:TypeSpec表示类,FieldSpec表示属性,MethodSpec表示方法或者构造器;这些类都采用构造器模式进行构造
  3. addStatement方法增加语句,其中有占位符,使用$ + 字母(T、L、S、N),T类替换(自动引入类),L值替换,S字符串替换,N替换MethodSpec或者FieldSpec或者TypeSpe等,表示相应类的名字的L替换,不过N替换可以不必要的手误
  4. ClassName,类似于class的概念;参数泛型ParameterizedTypeName
  5. 流程控制,其实就相当于语句块{}的作用,beginControlFlow(条件语句) 相当于在条件语句后加了{,endControlFlow()相当于}

5、断点调试

首先要创建构建调试:(操作顺序如下文字,后面有跟图标注)

在AndroidStudio编译类型 -> Edit Configurations  -> Templates  -> Remote(有些可能是Remote for jvm)   -> creat Configuration ->写上名字和,选择模块  -> 应用
复制代码

编译类型位置 1648278133(1).png 点击Edit Configurations后的界面

1648278199(1).png 最后步骤相关位置

1648278247(1).png

每次调试,都应该先clean编译;然后按照下面两步处理

  1. 当前应用目录下命令行执行:gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
  2. 选择创建的构建调试,然后点击debug

debug位置(我的构建名字为apt-debug)

1648278293(1).png

代码生成位置查看

  • 主工程,aar模块:build\generated\ap_generated_sources\debug\out
  • jar包模块:build\generated\sources\annotationProcessor\java\main

6、总结

把握依赖注入的概念以及实现方式;并对静态编译时的处理以及元素模型进行理解,我叫这些api为‘静态反射’,这些和动态反射概念是相同应的;javapoet是和类结构相关概念比较接近的,把这些多了解比对,就很容易理解,也很容找所需求用的api了;由于apt进行代码生成的相关的文章和基础比较少,所以还是先了解其它的一些概念来类似理解和作为依据查找api。另外也可以通过上面的调试手段进行信息打磨。

在作者对代码生成使用不断趟坑后,会写一篇基础,关于注解收集以及异常处理提示的内容;一起加油!!

如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!

おすすめ

転載: juejin.im/post/7079304676121772062