小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Java注解与代码生成

java是一门静态语言,语言层面缺乏灵活性,这使得我们项目中很容易出现大量重复的代码。虽然这些重复代码必不可少,但我们也不想用粘贴复制来解决问题,所以我们希望通过代码自动生成的方式来提高我们的工作效率。C#有非常棒的生成工具CodeSmith,而java我们可以借助注解来完成代码生成工作。

Java注解

java注解是java5引入的功能,我们能够经常看到,如@Override,但未必对其有深入的了解。了解这方面的知识有助于我们深入理解一些框架,比如ButterKnifeDraggerRetrofit等。

定义一个注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface BoolValue {
    boolean value() default false;
}

这里简单定义了一个注解BoolValue

  • 首先,注解是用@interface进行声明的;
  • 然后我们用@Target规定其修饰的对象范围,@Target的取值为:TYPE、 Field、 METHOD、 PARAMETER、 CONSTRUCTOR、LOCALVARIABLE、ANNOTATIONTYPE、PACKAGE。
  • 其次我们用@Retention约束其有效时期:可以是源码文件中(SOURCE)、可以是源码文件和class文件中(CLASS)、也可以是运行时(RUNTIME)

apt

我们声明了注解后,我们就需要工具去扫描和处里它们,这个工具就是apt(Annotation processing tool)。因此如果我们项目中需要引入apt,在gradle项目中,引入也是很简单的,只需在项目build.gradle中的dependencies加入依赖:

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

那么开发者如何参与apt过程呢?我们需要了解AbstractProcessor

AbstractProcessor

AbstractProcessor是javac扫描和处理注解的关键类。我们需要继承自这个类来完成代码生成的功能。

@AutoService(Processor.class)
public class FeatureProcessor extends AbstractProcessor {
    private static final String ANNOTATION_BOOL = "@" + BoolValue.class.getSimpleName();
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BoolValue.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

首先简要对重写的各个方法的作用进行介绍:

  • init(ProcessingEnvironment env): javac 会在 Processor 创建时调用并执行的初始化操作,该方法会传入 一个参数 ProcessingEnvironment env ,通过 env 可以访问 Elements、Types、Filer等工具类。
  • getSupportedAnnotationTypes(): 指定哪些注解需要注册
  • getSupportedSourceVersion(): 指定支持的 java 版本,通常返回SourceVersion.latestSupported(),如果只想支持到 Java 7 可以返回 SourceVersion.RELEASE_7
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 它是每个 processor 的主方法,可以在这个方法中扫描和处理注解,并生成新的 java 源代码,通过参数 RoundEnvironment env 可以找到我们想要的某一个被注解的元素

我们最重要的方法是 process(Set<? extends TypeElement> annotations, RoundEnvironment env),但在重写我们会遇到下面一些类型,你需要知道它的含义后才能够写出想要的代码:

  • Element: 可以表示java文件的任意元素,也许是类名,也许是方法名,也许是操作符;
  • TypeElement: 表示java文件的class或者interface
  • TypeMirror: 表示java文件的class的类型
  • Messeger:在process过程中可能会遭遇exception,我们可以用Messeger来收集错误并展示在构建结果中,点击构建结果的错误可以跳转到错误源头,是一个方便的debug工具。

现在我们就可以在process()去拿取注解信息以及其注解的元素:

for(Element e: env.getElementsAnnotatedWith(BoolValue.class)){
    TypeElement annotatedClass = (TypeElement)annotatedElement;
    // write the generate code
}

代码生成本质是拼接字符串,而javapoet为我们提供了一个优雅的拼接方式。给项目加上依赖就可以享受它带来的便捷性了:

compile 'com.squareup:javapoet:1.4.0'

我们可以到 https://github.com/square/javapoet 去学习其使用方式。然后就可以按照自己的需求生成自己想要的代码了。

另外一个很重要的问题:如何让javac执行时调用我们自定义的FeatureProcessor?答案是我们需要注册。注册流程是比较麻烦的:

  1. FeatureProcessor 需要打包到 jar 包中,就像其它普通的 .jar 文件一样,假如我们可以命名为 FeatureProcessor.jar
  2. 但 FeatureProcessor.jar 中多了一个特殊的文件;javax.annotation.processing.Processor 它存储在 jar/META-INF/services/ 文件夹下,javax.annotation.processing.Processor 文件列出了要注册的 Processor;
  3. 构建项目时 javac 自动检测并读取 javax.annotation.processing.Processor 来注册列出来的 Processor,这样我们的代码生成逻辑就能够得到执行。

注册流程如此复杂,但google提供了auto-service,可以自动化完成注册功能。要想使用auto-service服务,我们只需要如下两步:

  1. 在gradle依赖中加入依赖:

    compile 'com.google.auto.service:auto-service:1.0-rc2'
    
  2. 在自定义的Processor前加上注解@AutoService(Processor.class)。

这样就轻松完成注册了。。下面我们来看看他的实际应用。

项目实战

一般好的项目都会分模块管理,利用代码生成也应该运用合理的工程模块,例如下图的模块划分:

  • app: 主项目;
  • feature-api: 这个module提供注解定义以及提供外界使用的相关接口;
  • feature-compile: 这个是用于处理processor相关的逻辑,完全是编译期行为,应该独立一个module;

比较重要的一点是看如何配置build.gradle的,这里贴一下示例代码:

项目根目录build.gradle:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        // 引入apt
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

ext {
    minSdkVersion = 15
    targetSdkVersion = 23
    compileSdkVersion = 23
    buildToolsVersion = '23.0.2'
    sourceCompatibilityVersion = JavaVersion.VERSION_1_7
    targetCompatibilityVersion = JavaVersion.VERSION_1_7
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

app module的build.gradle:

apply plugin: 'com.android.application'
// 应用plugin
apply plugin: 'com.neenbedankt.android-apt'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        applicationId "com.example.feature"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile project(':feature-api')
    // 重要:调用apt
    apt project(':feature-compiler')
}

feature-compiler module的build.gradle:

apply plugin: 'java'

// This is important even if Android Studio claims it isn't
// used. Android can't interpret Java 8 byte code.
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

dependencies {
    compile project(':feature')
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.0.0'
}

feature-compiler与feature-api基本都是java domain层面的,为了可以不局限与android的使用,因此应用的plugin是java。开发者应该根据需要应用不同的plugin。

feature这个名字取自于我们微信读书上用到的A/B Test框架,其实现过程就利用了注解生成业务代码,使得使用起来非常简洁与方便。

比如我们定义了一个扩展自Feature接口:

@Key(name="plan")
@Default(false)
public interface Plan extends Feature {
    void doSomeThing();
}

然后添加其两种实现并添加相应注解:

@BoolValue(true)
public class PlanA implements Plan {
    @Override
    public void doSomeThing() {
        Log.d("plan","planA");
    }
}

@BoolValue(false)
public class PlanB implements Plan {
    @Override
    public void doSomeThing() {
        Log.d("plan","planB");
    }
}

在项目中,我们通过

Features.init(storage,factory)

来配置从服务器下发的配置,然后在具体业务逻辑中就可以调用下面的方法:

Features.of(Plan.class).doSomeThing()

我们以上述的例子来阐述他的处理过程:

我们在编译阶段可以扫描所有子类实现,生成相应的FeatureChooser,然后把相应的Feature接口与FeatureChooser建立联系并生成FeatureFactory类。如果手工维护的话,每增加一个Feature,都要去实现相应的FeatureChooser,并更新FeatureFactory,这样就会非常繁琐。采用java注解自动生成代码后,开发者可以更加关注于业务逻辑了。

像工厂模式这样的设计模式,其实很容易产生大量的模板代码,如果合理利用java注解,这将很大程度上解放开发者的双手,让开发者有更多的精力去做更有意义的事情。其次,开发者也应该尽早使用诸如ButterknifeRetrofit这样的框架,从重复劳动中解放出来。

参考文章:

深入浅出Java注解

AndroidSutdio 编译时自动生成源代码

Android Annotation Processing: POJO string generator

←支付宝← →微信 →
comments powered by Disqus