首先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,有需求的朋友可以去看下,谢谢。