android 使用apt(编译时注解) 自动生成第三方的狗皮膏药代码

本文介绍如何利用APT(Annotation Processing Tool)自动生成特定的Android组件,例如微信登录所需的WXEntryActivity,以此减少重复代码并提高开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    在日常的Android项目开发中,免不了集成大量第三方库,由于各个公司开发风格不一,导致在项目集成过程中东粘一块西粘一块,对于有代码洁癖的人来说无疑是场灾难,面对第三方库如此强大的代码侵入性,我们无所适从,只能尽量整合,用良好的编码结构来规避混乱,不过java也为我们提供了一套在编译期自动生成代码的利器,让我们不再面对那些某些狗皮膏药似的代码,以微信登录为例(抛砖引玉),做微信登录的朋友都知道集成它要强制编写一个WXEntryActivity类,包名还有一定规范,必须是apk包名下的.wxapi下,实在不能接受这种写法的朋友可以继续往下看,当然看完这篇文章,那么你对ButterKnife(通过自动生成代码),XUtils(通过反射实现比ButterKnife逊色)等诸多注解框架的核心原理也就知道了。
       首先为什么我们要自动生成微信Activity,直接写上去不是更方便吗?答案是为了抽取到基准库,我们作为开发人员都有自己的库文件,当需要开发项目时直接把库集成进来就可以了,而第三方微信的集成如登录,支付在正常情况下需要已知包名的情况下去书写类文件,而底层库不依赖于具体项目而存在,抽取有诸多不便。大致思路如下:首先我们在底层库中创建一个activity,在这个activity中完成微信登录后的所有操作逻辑并发出回调,对于上层用户(项目开发者)来说,他只关心微信的最终openId是否能得到,至于中间的过程就交给底层框架来处理,框架负责发出登录请求,接收微信返回并回馈给上层项目,最大限度实现解耦,现在切入正题。


 
  第一步:创建整个项目架构,如图:

       


 我们把整个项目拆分成四个部分。
  1. android application模块—相当于主项目。
  2. 注解模块— java库模块(定义注解的模块,因为annotation属于java范畴,所以只要是java工程即可)。
  3. 注解编译模块—(同样是java模块,因为需要使用java及第三方的注解处理api)。
  4. 核心库模块— (android库,因为核心库涉及到UI,网络以及抽取的基类如BaseActivity等,所以必须是android库)。

对于模块的拆分因人而异,不过在实际项目中模块的拆分我觉得是很有必要的,他可以灵活搭配,适度取舍。甚至以上这种拆分我都闲太集中,我们完全可以把apt-lib拆分为net library,ui library, utils library,还有各种thirdpart library,甚至可以更细,谁都不知道实际项目需要用到什么,当把没必要的库引入到了项目中,不但增加了包体大小,同时也影响了审美,对代码洁癖者来说是无法忍受的,不过在这里我就只用四个模块吧。

第二:梳理依赖关系
  • apt-android 依赖 apt-lib
  • apt-lib 依赖 apt-annotations 
  • apt-android 编译期依赖apt-compiler  (就是在编译的时候使用apt-compiler来产生java代码在apt-android下的gradle配置中只要加上annotationProcessor project(':apt-compile')
  • apt-compiler 依赖apt-annotations 同时也依赖部分第三方jar 
    compile 'com.squareup:javapoet:1.9.0’    (生成java类的关键库,提供了很多非常简洁的api)
    compile 'com.google.auto.service:auto-service:1.0-rc3
    compile 'com.google.auto:auto-common:0.8’ 
    compile project(':apt-annotations)


第三:编写java代码
在apt-annotations中定义一个WXActivityGenerator 微信生成器注解。
package com.late.apt.annotations ;
import java.lang.annotation.ElementType;
import java.lang.annotation. Retention ;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation. Target ;
/**
* Created by xuhongliang on 2017/11/15.
*/
//声明注解作用范围是作用在类,接口,注解,枚举上
@Target(ElementType. TYPE )
//声明注解的有效期为源码期
@Retention(RetentionPolicy. SOURCE )
public @ interface WXActvityGenerator {
    //声明该注解所要生成的包名规则
    String getPackageName () ;
    //声明该注解所生成java类需要继承哪个父类
    Class<?> getSupperClass () ;
}
在apt-lib库中定义模板类,继承自AppCompatActivity
package com.late.apt.lib ;
import android.os.Bundle ;
import android.support.annotation. Nullable ;
import android.support.v7.app.AppCompatActivity ;
/**
* Created by xuhongliang on 2017/11/15.
*/
public class WXTemplateActivity extends AppCompatActivity {
    @Override
    protected void onCreate( @Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState) ;
        //执行登录回调逻辑,包括请求token,获取openid,回给真实项目模块
    }
}
在apt-android中随便哪个类上使用注解@WXActivityGenerator,这里在MainActivity上使用
package com.xhl.apt ;
import android.os.Bundle ;
import android.support.v7.app.AppCompatActivity ;
import com.late.apt.annotations. WXActvityGenerator ;
import com.late.apt.lib.WXTemplateActivity ;
@WXActvityGenerator(
        getPackageName = "com.xhl.apt" ,
        getSupperClass = WXTemplateActivity. class
)
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState) ;
        setContentView(R.layout. activity_main) ;
    }
}
以上三步是定义并使用了注解,具体的注解处理生成代码还未写,我们在apt-compiler中写相关注解处理代码.
定义一个WXActivityAnnotationProcessor
import com.google.auto.service. AutoService ;
import com.late.apt.annotations. WXActvityGenerator ;
import com.squareup.javapoet.JavaFile ;
import com.squareup.javapoet.TypeName ;
import com.squareup.javapoet.TypeSpec ;
import java.io.IOException ;
import java.util.Collections ;
import java.util.List ;
import java.util.Map ;
import java.util.Set ;
import javax.annotation.processing.AbstractProcessor ;
import javax.annotation.processing.Filer ;
import javax.annotation.processing.ProcessingEnvironment ;
import javax.annotation.processing.Processor ;
import javax.annotation.processing.RoundEnvironment ;
import javax.lang.model.element.AnnotationMirror ;
import javax.lang.model.element.AnnotationValue ;
import javax.lang.model.element.Element ;
import javax.lang.model.element.ExecutableElement ;
import javax.lang.model.element.Modifier ;
import javax.lang.model.element.TypeElement ;
import javax.lang.model.type.TypeMirror ;
import javax.lang.model.util.SimpleAnnotationValueVisitor7 ;
/**
* Created by xuhongliang on 2017/11/15.
*/
@AutoService(Processor. class)
public class WXActivityAnnotationProcessor extends AbstractProcessor {
    //可以理解为编译期的文档管理员
    private Filer filer ;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment) ;
        this. filer = processingEnvironment.getFiler() ;
    }
    //这个函数式注解处理的核心函数,env对象是编译器环境,可获取整个包中的所有元素属性
    @Override
    public boolean process(Set<? extends TypeElement> set , RoundEnvironment env) {
        generatorJavaCode( filer , env) ;
        return true;
    }
    private void generatorJavaCode(Filer filer , RoundEnvironment env) {
        //首先创建一个注解属性解析器
        final WXActvityAnnotationVisitor visitor = new WXActvityAnnotationVisitor(filer) ;
        //获取标注了WXActvityGenerator注解的所有元素集合
        final Set<? extends Element> elementsWithWXAnnotation = env.getElementsAnnotatedWith( WXActvityGenerator. class) ;
        //遍历
        for (Element element : elementsWithWXAnnotation) {
            //取得带有属性的注解元素
            final List<? extends AnnotationMirror> annotationMirrors = element.getAnnotationMirrors() ;
            //遍历
            for (AnnotationMirror annotationMirror : annotationMirrors) {
                //获取注解属性值
                final Map<? extends ExecutableElement , ? extends AnnotationValue> map = annotationMirror.getElementValues() ;
                for (AnnotationValue annotationValue : map.values()) {
                    //把属性值丢给注解解析器处理
                    annotationValue.accept(visitor , null) ;
                }
            }
        }
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //因为就一个注解,所以创建只存在一个注解的集合
        final Set<String> annotationNames = Collections. singleton( WXActvityGenerator. class.getCanonicalName()) ;
        return annotationNames ;
    }
    final static class WXActvityAnnotationVisitor extends SimpleAnnotationValueVisitor7<Void , Void> {
        private final Filer filer ;
        private String packageName ;
        public WXActvityAnnotationVisitor(Filer filer) {
            this. filer = filer ;
        }
        @Override
        public Void visitString(String s , Void aVoid) {
            //解析得到包名
            this. packageName = s ;
            return aVoid ;
        }
        @Override
        public Void visitType(TypeMirror typeMirror , Void aVoid) {
            //生成java类也就是WXEntryActivity
            generatorJavaCode(typeMirror) ;
            return aVoid ;
        }
        private void generatorJavaCode(TypeMirror typeMirror) {
            try {
                //创建类的描述
                final TypeSpec classType = TypeSpec. classBuilder( "WXEntryActivity")
                        .addModifiers(Modifier. FINAL , Modifier. PUBLIC)
                        .superclass(TypeName. get(typeMirror)).build() ;
                //创建java类文件
                final JavaFile javaFile = JavaFile. builder( packageName + ".wxapi" , classType)
                        .addFileComment( "WXActivity")
                        .build() ;
                //写入编译文档文件
                javaFile.writeTo( filer) ;

            } catch (IOException e) {
                e.printStackTrace() ;
            }

        }
    }
}
因为注释已经很详细了,在这边也不解释具体怎么写了,主要是定义一个注解处理器,这个处理器继承自AbstractProcessor,
这个类是java包中的专门处理注解的抽象类,我们只要实现process方法即可拿到注解上的元素信息,接着丢给一个注解元素访问者即AnnotationVisitor,我们通过他的visit+类型这类方法就可以拿到注解中定义的元素值,也就是packageName与需要继承的类。

最后我们编译工程就会发现我们的WXEntryActivity神不知鬼不觉的出现在了项目中,而且不用被你看见就可以使用他,毕竟眼不见为净。


而相关的处理逻辑我们已经在模板类中执行,这些逻辑是通用的,不管在哪个项目里都一样。等有新项目我们只要引入包,配置下依赖,配置下清单文件,再写个注解即可,根本不用重复在写登录分享逻辑了。

在这里只是以生成微信特有类为例来使用了一下apt,其实在我们的项目中apt还可以干更多的事,比如根据json文件自动生成java类,都是可以在编译期来实现的,再也不用你手写javaBean了,当然网上有这种工具,那如果我们自己实现岂不是很牛逼?

ButterKnife作为非常知名的注解框架也是使用了这一原理,无非就多了些处理逻辑,本质是一样的,我们来简单看下源码。

这就相当于我们的apt-annotations库,只是butterknife定义的注解多了点而已,注解本质就是为了识别属性与提供属性值而存在的。

这个类就是执行绑定的整个逻辑的库,本质最终处理还是会走findViewById(R.id.btn);
public final class ButterKnife {
@NonNull @UiThread
public static Unbinder bind( @NonNull Activity target) {
  View sourceView = target.getWindow().getDecorView() ;
  return createBinding(target , sourceView) ;
}
private static Unbinder createBinding( @NonNull Object target , @NonNull View source) {
  Class<?> targetClass = target.getClass() ;
  if ( debug) Log. d( TAG , "Looking up binding for " + targetClass.getName()) ;
  Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass) ;

  if (constructor == null) {
    return Unbinder. EMPTY ;
  }

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    return constructor.newInstance(target , source) ;
  } catch (IllegalAccessException e) {
    throw new RuntimeException( "Unable to invoke " + constructor , e) ;
  } catch (InstantiationException e) {
    throw new RuntimeException( "Unable to invoke " + constructor , e) ;
  } catch (InvocationTargetException e) {
    Throwable cause = e.getCause() ;
    if (cause instanceof RuntimeException) {
      throw (RuntimeException) cause ;
    }
    if (cause instanceof Error) {
      throw (Error) cause ;
    }
    throw new RuntimeException( "Unable to create binding instance." , cause) ;
  }
}
}
这句是核心
 Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
我们拿到了compiler生成的java类的构造器生成对象,在生成对象的时候已经将具体view赋值了,以下是具体的生成类。
package com.diabin.fastec.example ;
import android.support.annotation. CallSuper ;
import android.support.annotation. UiThread ;
import android.view.View ;
import butterknife.Unbinder ;
import butterknife.internal.DebouncingOnClickListener ;
import butterknife.internal.Utils ;
import java.lang.IllegalStateException ;
import java.lang. Override ;
public class ExampleDelegate_ViewBinding implements Unbinder {
  private ExampleDelegate target ;
  private View view2131624084 ;
  @UiThread
  public ExampleDelegate_ViewBinding( final ExampleDelegate target , View source) {
    this. target = target ;
    View view ;
    view = Utils. findRequiredView(source , R.id. btn_test , "method 'onClickTest'") ;
    view2131624084 = view ;
    view.setOnClickListener( new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClickTest() ;
      }
    }) ;
  }
  @Override
  @CallSuper
  public void unbind() {
    if ( target == null) throw new IllegalStateException( "Bindings already cleared.") ;
    target = null;
     view2131624084.setOnClickListener( null) ;
    view2131624084 = null;
  }
}
在来看butterknife的compiler包

ButterKnifeProcessor为核心类.
process方法如初一折,目的就是生成像 ExampleDelegate_ViewBinding这样的类(对于Activity来说是中间赋值类)。
@Override public boolean process(Set<? extends TypeElement> elements , RoundEnvironment env) {
  Map<TypeElement , BindingSet> bindingMap = findAndParseTargets(env) ;
  for (Map.Entry<TypeElement , BindingSet> entry : bindingMap.entrySet()) {
    TypeElement typeElement = entry.getKey() ;
    BindingSet binding = entry.getValue() ;
    JavaFile javaFile = binding.brewJava( sdk) ;
    try {
      javaFile.writeTo( filer) ;
    } catch (IOException e) {
      error(typeElement , "Unable to write binding for type %s: %s" , typeElement , e.getMessage()) ;
    }
  }
  return false;
}

附github地址: https://github.com/lategege/apt-android/tree/master

在本文结尾向ButterKnife的作者致敬,让程序员摆脱了很多重复的没意义的代码,实属丰功伟绩。
      

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值