APT技术学习以及发布到jCenter

首先APT是什么,APT可以通过配合注解在编译期生成代码的一种技术,当下比较流行的第三方库,比如Dagger2 、 ButterKnife都用到了APT技术,使用这些库可以减少编写一些重复的代码,提高效率。

本文主要是通过一个简单的需求来一步一步了解怎么使用APT技术,然后将写好的组件发布到jCenetr仓库。


首先定需求

现在有好多文章都是用绑定控件(findViewById)来作为例子,我决定换一种思路,刚好我之前做了一个项目,里面有很多的表单输入,在点击提交按钮前都要写很长的代码用来校验输入的格式,这种工作比较枯燥无味,所以想着是否可以通过注解的方式,在编译期自动生成这些校验的代码,于是需求就来了。

先展示一下组件最终的效果:

public class MainActivity extends AppCompatActivity {

    @NotEmpty(R.string.hello_empty)
    EditText helloEt;

    @Match(regex = "^1(3|4|5|7|8)\\d{9}$", message = R.string.dont_match)
    EditText matchEt;

    @NotEmpty(R.string.hello_empty)
    EditText equal1Et;

    @NotEmpty(R.string.hello_empty)
    @Equal(target = "equal1Et", message = R.string.dont_equal)
    EditText equal2Et;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         
        // 初始化控件
        ...
    }
    //  在layout申明的onClick事件
    public void confirm(View view) {
        String errorMessage = InputCheck.check(this);

        if (errorMessage != null) {
            Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show();
            return;
        }

        // 业务处理
        Toast.makeText(this, "confirm", Toast.LENGTH_SHORT).show();
    }
}

下面来一步步实现这个组件(基于Android Studio)。

1、先创建一个java library,取名为inputcheck-annotation,这个library主要是用来放注解的,目前只有三个注解。

@NotEmpty:不允许为空,接收一个int参数,为空时的错误信息

@Equal:两个控件内容相等,接收两个参数,target:字符串,目标控件的变量名;message:不相等时的错误信息

@Match:匹配指定的正则表达式,接收两个参数,regex:正则表达式,message:不匹配时的错误信息

这三个注解都只能用于变量且只保留到class,所以申明如下
@Target(ElementType.FIELD)

@Retention(RetentionPolicy.CLASS)

2、创建一个android library,取名为inputcheck,这个library是用来提供api用于编写代码时调用。主工程会引入这个库,所以这个库需要引入刚才创建的annotation库,就不用主工程再去引入annotation了。

代码编写思路:我们需要在点击提交的时候去调用在编译期生成好的校验输入的代码,然后将错误信息返回出来,供业务来使用,期望自动生成的代码应该是下面这样的

// 类名是用到了inputcheck注解的类的类名加上“$$Check”,后面会用到这个类
// 由于开始是不知道会生成哪些类,所以采取面向接口的方法
public class MainActivity$$Check implements Check<MainActivity> {
  @Override
  public String check(final MainActivity host) {
    if (android.text.TextUtils.isEmpty(host.helloEt.getText().toString())) {
      return host.getString(2131099668);
    }
    if (android.text.TextUtils.isEmpty(host.equal1Et.getText().toString())) {
      return host.getString(2131099668);
    }
    if (android.text.TextUtils.isEmpty(host.equal2Et.getText().toString())) {
      return host.getString(2131099668);
    }
    if (!host.matchEt.getText().toString().matches("^1(3|4|5|7|8)\\d{9}$")) {
      return host.getString(2131099667);
    }
    if (!host.equal2Et.getText().toString().equals(host.equal1Et.getText().toString())) {
      return host.getString(2131099666);
    }
    return null;
  }
}

// Check定义如下,接收一个泛型表示使用了inputcheck注解的类
public interface Check<T> {
    // 返回值为校验表单后返回的错误提示,如果为null,表示校验通过
    String check(T host);
}

创建一个InputCheck类,有一个check的静态方法,返回值为字符串,接收当前调用的类

public class InputCheck {
    
    private static final Map<String, Check> CHECK_MAP = new HashMap<>();

    public static String check(Object target) {
        String className = target.getClass().getName();

        try {
            Check check = CHECK_MAP.get(className);
            if (check == null) {
                Class<?> finderClass = Class.forName(className + "$$Check");
                check = (Check) finderClass.newInstance();
                CHECK_MAP.put(className, check);
            }
            return check.check(target);
        } catch (Exception e) {
            throw new RuntimeException("Unable to check for " + className, e);
        }
    }
}

这样inputcheck库就编写完成,剩下的就是apt的核心,根据这些注解来生成上面的MainActivity$$Check类

3,创建一个java library(一定要是java库,因为android库不能使用jdk提供的AbstractProcessor类),取名为inputcheck-compiler(注解处理器),需要依赖inputcheck-annotation,另外还需要依赖两个工具库:

javapoet:生成代码就是用过它来完成的;

auto-service:google提供的库,用来生成必要的META-INF相关文件。

在编码前我们需要了解一个概念,我们的java源码的每一个部分在注解处理器中都属于一个Element(基类)的一个特定类型,如下面这个例子:

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {}
}

element.getEnclosingElement();//返回封装此元素(非严格意义上)的最里层元素。
element.getEnclosedElements();//返回此元素直接封装(非严格意义上)的元素。

下面开始编写处理器:

创建一个类:InputCheckProcessor继承AbstractProcessor


public class InputCheckProcessor extends AbstractProcessor {

    private Filer mFiler;   // 生成文件需要的类
    private Elements mElementUtils; // element的工具类
    private Messager messager;  // 可以通过它打印一些日志

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        messager = processingEnv.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 将使用到的注解名字添加到集合里
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(NotEmpty.class.getCanonicalName());
        annotations.add(Match.class.getCanonicalName());
        annotations.add(Equal.class.getCanonicalName());
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 推荐写法
        return SourceVersion.latestSupported();
    }

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

接下来一步步完善process方法:

通过process的第二个参数我们可以取到一个java类里面所有的注解信息,比如:

List<Element> notEmpties = roundEnv.getElementsAnnotatedWith(NotEmpty.class);
List<Element> matches = roundEnv.getElementsAnnotatedWith(Match.class);
List<Element> equals = roundEnv.getElementsAnnotatedWith(Equal.class);

根据面向对象的思路,我们将所有的注解都封装成一个类,另外再创建一个类来封装这些注解,用来生成代码。

NotEmpty注解的封装如下,代码很简单应该很好理解,其它的注解也可以类似这样去封装。

public class NotEmptyField {

    // 错误信息
    private int messageId;

    // 被notempty注解的变量
    private VariableElement mField;

    public NotEmptyField(Element element) {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(
                    String.format("Only fields can be annotated with @%s", NotEmpty.class.getSimpleName()));
        }

        if (element.getModifiers().contains(Modifier.PRIVATE) || element.getModifiers().contains(Modifier.STATIC)) {
            throw new IllegalArgumentException(
                    String.format("@%s can't used on private or static", NotEmpty.class.getSimpleName()));
        }

        mField = (VariableElement) element;

        NotEmpty notEmpty = mField.getAnnotation(NotEmpty.class);
        messageId = notEmpty.value();
    }

    public int getMessage() {
        return messageId;
    }

    public String getFieldName() {
        return mField.getSimpleName().toString();
    }
}

用一个类封装这些注解,以及封装生成java代码的内容,代码也很好理解,生成代码的部分采用javaPoet库进行封装,可以百度一下它的使用,也很简单。

public class AnnotatedClass {

    private TypeElement mClassElement;

    private List<NotEmptyField> mNotEmptyFields;
    private List<MatchField> mMatchFields;
    private List<EqualField> mEqualFields;

    private Elements mElementUtils;

    public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
        this.mClassElement = classElement;
        this.mElementUtils = elementUtils;

        mNotEmptyFields = new ArrayList<>();
        mMatchFields = new ArrayList<>();
        mEqualFields = new ArrayList<>();
    }

    public void addNotEmptyField(NotEmptyField field) {
        mNotEmptyFields.add(field);
    }

    public void addMatchField(MatchField field) {
        mMatchFields.add(field);
    }

    public void addEqualField(EqualField field) {
        mEqualFields.add(field);
    }

    /**
     * 生成Check代码,根据想要得到的内容,通过javaPoet拼接。
     */
    public JavaFile generateCheck() {
        MethodSpec.Builder checkBuilder = MethodSpec.methodBuilder("check")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
                .returns(String.class);

        for (NotEmptyField field : mNotEmptyFields) {
            checkBuilder.beginControlFlow("if (android.text.TextUtils.isEmpty(host.$L.getText().toString()))", field.getFieldName());

            checkBuilder.addStatement("return host.getString($L)", field.getMessage());
            checkBuilder.endControlFlow();
        }

        for (MatchField field : mMatchFields) {
            checkBuilder.beginControlFlow("if (!host.$L.getText().toString().matches($S))", field.getFieldName(), field.getRegex());

            checkBuilder.addStatement("return host.getString($L)", field.getMessage());
            checkBuilder.endControlFlow();
        }

        for (EqualField field : mEqualFields) {
            checkBuilder.beginControlFlow("if (!host.$L.getText().toString().equals(host.$L.getText().toString()))", field.getFieldName(), field.getTarget());

            checkBuilder.addStatement("return host.getString($L)", field.getMessage());
            checkBuilder.endControlFlow();
        }

        checkBuilder.addStatement("return null");

        TypeSpec checkClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Check")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.CHECK, TypeName.get(mClassElement.asType())))
                .addMethod(checkBuilder.build())
                .build();

        String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();

        return JavaFile.builder(packageName, checkClass).build();
    }

}

下面完成process方法:

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(NotEmpty.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            NotEmptyField field = new NotEmptyField(element);
            annotatedClass.addNotEmptyField(field);
        }

        for (Element element : roundEnv.getElementsAnnotatedWith(Match.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            MatchField field = new MatchField(element);
            annotatedClass.addMatchField(field);
        }

        for (Element element : roundEnv.getElementsAnnotatedWith(Equal.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            EqualField field = new EqualField(element);
            annotatedClass.addEqualField(field);
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateCheck().writeTo(mFiler);
            } catch (IOException e) {
                return true;
            }
        }
        return true;
    }

    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = classElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(classElement, mElementUtils);
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

这样注解编译器就写完了,然后再主工程通过apt引用这个库,使用过ButterKnife都应该知道怎么配置gradle的环境,这里就不多说了。

接下来就可以在主工程中使用这些注解了,使用方法在文章最前面。

以上就是apt的简单用法,如果想提供给别人用,就可以发布到jCenter方便使用。将android library发布到jCenter网上有很多这样的文章,这里就不讲了,我主要说一下关于发布apt这样既有android library又有java library的库。

首先我们总共有三个library: inputcheck,inputcheck-annotation,inputcheck-compiler。inputcheck和inputcheck-compiler都要依赖inputcheck-annotation,所以我们必须先将inputcheck-annotation的java library发布到jCenter,那么inputcheck-annotation的build.gradle文件内容如下:

apply plugin: 'java'
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'

version = "1.0.2"   // 版本号,自己定义,每次修改代码后,版本号必须修改

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

// compile 'com.yzd:inputcheck-annotation:1.0.2'
group = "com.yzd"   // groupId就是上面一行冒号前面的一截。
def siteUrl = 'https://github.com/yanzhaodi/InputCheck' // 项目的主页
def gitUrl = 'https://github.com/yanzhaodi/InputCheck.git' // Git仓库的url
install {
    repositories.mavenInstaller {
        // This generates POM.xml with proper parameters
        pom {
            project {
                packaging 'jar'
                // Add your description here
                name 'inputcheck-annotation' //项目描述
                url siteUrl
                // Set your license
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id 'yanzhaodi'    //填写的一些基本信息
                        name 'yanzhaodi'
                        email 'yzd1014@outlook.com'
                    }
                }
                scm {
                    connection gitUrl
                    developerConnection gitUrl
                    url siteUrl
                }
            }
        }
    }
}

// 下面的几个task是与发布android library不一样的地方
task sourcesJar(type: Jar) {
    from sourceSets.main.allSource
    classifier = 'sources'
}
//task javadoc(type: Javadoc) {
//    source = sourceSets.main.allSource
//    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
//}
//task javadocJar(type: Jar, dependsOn: javadoc) {
//    classifier = 'javadoc'
//    from javadoc.destinationDir
//}
artifacts {
//    archives javadocJar
    archives sourcesJar
}
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
bintray {
    user = properties.getProperty("bintray.user")
    key = properties.getProperty("bintray.apikey")
    configurations = ['archives']
    pkg {
        repo = "maven"
        name = "inputcheck-annotation"    //发布到JCenter上的项目名字,上面注释里面冒号后面的一截
        websiteUrl = siteUrl
        vcsUrl = gitUrl
        licenses = ["Apache-2.0"]
        publish = true
    }
}

顶层的build.gradle文件需要添加两个classpath:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
        
        // 发布组件需要的两个
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
        // apt需要的插件
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

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

写好之后就在terminal控制台通过命令上传组件:

首先执行gradle install指令,用来生成javadoc,javasources,pom-default.xml
然后执行gradle bintrayUpload

inputcheck-annotation发布成功之后,就将inputcheck和inputcheck-compiler引入inputcheck-annotation的方式改为

compile 'com.yzd:inputcheck-annotation:1.0.2'

如果一切顺利的话,编译会成功。接下来就发布inputcheck和inputcheck-compiler

inputcheck-compiler工程的build.gradle的文件编写与inputcheck-annotation类似,拷贝过来修改一下就可以了。

inputcheck工程的build.gradle文件编写如下:

apply plugin: 'com.android.library'
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'

version = "1.0.3"

android {
    compileSdkVersion 22
    buildToolsVersion '21.1.0'

    resourcePrefix "inputcheck__"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.yzd:inputcheck-annotation:1.0.2'
}

group = "com.yzd"
def siteUrl = 'https://github.com/yanzhaodi/InputCheck' // 项目的主页
def gitUrl = 'https://github.com/yanzhaodi/InputCheck.git' // Git仓库的url
install {
    repositories.mavenInstaller {
        // This generates POM.xml with proper parameters
        pom {
            project {
                packaging 'aar'
                // Add your description here
                name 'inputcheck' //项目描述
                url siteUrl
                // Set your license
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id 'yanzhaodi'    //填写的一些基本信息
                        name 'yanzhaodi'
                        email 'yzd1014@outlook.com'
                    }
                }
                scm {
                    connection gitUrl
                    developerConnection gitUrl
                    url siteUrl
                }
            }
        }
    }
}
task sourcesJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier = 'sources'
}
task javadoc(type: Javadoc) {
    source = android.sourceSets.main.java.srcDirs
    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}
artifacts {
    archives javadocJar
    archives sourcesJar
}
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
bintray {
    user = properties.getProperty("bintray.user")
    key = properties.getProperty("bintray.apikey")
    configurations = ['archives']
    pkg {
        repo = "maven"
        name = "inputcheck"    //发布到JCenter上的项目名字
        websiteUrl = siteUrl
        vcsUrl = gitUrl
        licenses = ["Apache-2.0"]
        publish = true
    }
}

完成后在控制台先执行gradle install指令,用来生成javadoc,javasources,pom-default.xml
然后输入gradle bintrayUpload指令,最终结果应该是失败的,因为inputcheck-annotation已经发布过来,而现在版本号没有变,所以会失败,不过没关系,如果配置没问题的话,我们进入bintray应该就可以看到我们发布的工程了。

如此,大功告成,我们就可以使用很简单的方式来继承我们的组件了。


写的比较乱,希望不会误导各位_

整个工程我已经放在了github,有需求的朋友可以去看下,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容