自定义retrofit框架(三)使用编译时注解处理请求

前言

自定义retrofit框架(二)编写基本框架模型中,使用动态代理+反射的方式处理注解,实现了和retrofit一样的网络请求方式。这篇文章将采用java-apt(编译时注解处理器)来处理注解(编译时注解技术在EventBus,Butterknife,ARounter等框架中均有使用),这样做可以避免在代码中使用反射。当然在实际的网络请求中,网络延迟时长远比反射处理时长高出许多倍,这篇文章重在学习编译时注解操作。

效果展示

首先来看一下我们需要的效果

  1. 定义接口 (为了缩短篇幅,这里接口定义只定义一个方法)
@ApiService
public interface PhpService {

    @LRequest("login")
    Observable<HttpResult<UserInfo>> login(
                        @Param("name") String name, 
                        @Param("password") String password);
}
  1. 通过菜单栏Build(在AndroidStudio中点击build -> make project)生成实现类,然后即可使用
 PhpService service =  new PhpServiceImpl("basr-url");//入参填写域名
       service.login("luqihua","123456")
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(new Consumer<HttpResult<UserInfo>>() {
                   @Override
                   public void accept(HttpResult<UserInfo> userInfoHttpResult) throws Exception {
                       
                   }
               });

以上就是改造好的库的使用方式。看上去也是很简单的,我们只编写了一个类似retrofit接口请求类,便可以使用它的实现类来完成具体的请求,new出来的实现类可以使用单例模式包装。在app/build/generated/source/apt/debug路径下可以查看到我们生成的实现类如下:

public class PhpServiceImpl implements PhpService {
  private final String _baseUrl;

  private HttpRequest _request = new HttpRequest();

  public PhpServiceImpl(final String baseUrl) {
    this._baseUrl=baseUrl;
  }

  @Override
  public Observable<HttpResult<UserInfo>> login(String name, String password) {
    String _url = "login";
    if(_url.length()==0) {
      throw new RuntimeException("incorrect url");
    }
    if(!_url.startsWith("http")) {
      _url = _baseUrl+_url;
    }
    final Type _type = new TypeToken<HttpResult<UserInfo>>(){}.getType();
    final Map<String,String> _params = new HashMap<>();
    final Map<String,String> _headers = new HashMap<>();
    _params.put("name",name);
    _params.put("password",password);
    return (Observable<HttpResult<UserInfo>>)_request.form(_url,LMethod.POST,_headers,_params,_type);
  }
}

可以看到生成的实现类中有一个baseUrl,这个是域名。另一个全局变量HttpRequest,这是网络请求的包装类,用来具体构建网络请求,从而在接口实现类中做简单的入参即可,减少代码生成量。HttpRequest 的代码如下:

@RequestWrapper
public class HttpRequest implements IRequestWrapper<Observable<?>> {
    private static Gson sGson = new Gson();

    @Override
    public Observable<?> form(final String url,
                              final Enum<LMethod> method,
                              Map<String, String> headers,
                              Map<String, String> params,
                              final Type type) {
        return new FormRequest()
                .url(url)
                .params(params)
                .headers(headers)
                .method(method)
                .observerResponseBody()
                .map(new Function<ResponseBody, Object>() {
                    @Override
                    public Object apply(ResponseBody responseBody) throws Exception {
                        Object o = sGson.fromJson(responseBody.string(), type);
                        return o;
                    }
                });
    }

}

代码编写

在阅读这部分代码之前需要掌握以下基础

  • java-apt的实现之Element详解,可以理解一下在编译时注解中的元素概念
  • javapoet 的使用,这是一个帮助我们生成java源文件的开源库
  1. 注解编写
  • 创建一个java工程lib-annotation,所有的注解都在该工程下
//注解的编写这里只介绍一个LRequest,其余的在源码中可查看。注意把Retention设置为SOURCE
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface LRequest {
    String value();//访问路径

    LMethod method() default LMethod.POST;//访问方法  post或者get

    ContextType type() default ContextType.FORM;//访问类型,form、json、multipart
}
  1. 注解处理器编写
  • 创建一个java工程,名为lib-processor
  • 引入需要的库
    implementation 'com.google.auto:auto-common:0.10'
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'
    implementation project(':lib-annotation')
  • 创建一个类HttpProcessor集成自AbstractProcessor,下面具体分析这个类的代码

    1.初始化类操作

     private Elements mElementUtils;
    
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        mElementUtils = env.getElementUtils();
    }
    
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //该方法添加所编写的注解,不过貌似这里面不添加的,在process方法中也可以获取处理
        Set<String> types = new HashSet<>();
        types.add(ApiService.class.getCanonicalName());
        types.add(Param.class.getCanonicalName());
        types.add(ParamMap.class.getCanonicalName());
        types.add(RequestWrapper.class.getCanonicalName());
        return types;
    }
    
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    
    
    
    1. 核心的处理工作都在process方法中
     @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //这一步我们获取所有带有ApiService标记的元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ApiService.class);
        //这一步骤判断元素的类型,必须是接口类型(实际上我们在定义ApiService的时候可以限制只能在Type上使用,但Type有可能是类和接口,因此这一步过滤掉类,只保留接口元素)
        for (Element element : elements) {
            if (element.getKind() != ElementKind.INTERFACE || !(element instanceof TypeElement))
                continue;
            //生成接口实现类的具体操作    
            createSourceFile(roundEnv, element);
        }
        return true;
    }
    
    1. createSourceFile方法

    从前面我们看到的接口实现类可以知道,

    • 在接口实现类中应该有一个_baseUrl变量,有一个_request变量
    • 需要重写接口的方法
     private void createSourceFile(RoundEnvironment roundEnv, Element element) {
        //新生成的类命名为接口名+Impl
        final String classImpName = element.getSimpleName().toString() + Constants.IMPL_CLASS_SUFFIX;
        //TypeSpec即代表一个类文件
        TypeSpec.Builder builder = TypeSpec.classBuilder(classImpName)
                .addModifiers(Modifier.PUBLIC)
                //添加一个全局变量  String _baseUrl
                .addField(String.class, "_baseUrl", Modifier.FINAL, Modifier.PRIVATE)
                .addMethod(MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(String.class, "baseUrl", Modifier.FINAL)
                        .addStatement("this._baseUrl=baseUrl")
                        .build())
                //全局变量HttpRequest,一个用于请求http的request,这个request是事先写好的
                .addField(getRequestField(roundEnv))
                .addSuperinterface(TypeName.get(element.asType()));
    
        //构建方法
        MethodCreate methodCreate = new MethodCreate();
        for (Element enclosedElement : element.getEnclosedElements()) {
            //因为我们只生成接口方法的重写方法,因此过滤掉不是方法的元素
            if (!(enclosedElement instanceof ExecutableElement) || enclosedElement.getKind() != ElementKind.METHOD) {
                continue;
            }
            ExecutableElement executableElement = (ExecutableElement) enclosedElement;
            //过滤掉不带有LRequest注解的方法
            if (executableElement.getAnnotation(LRequest.class) == null) {
                continue;
            }
            //生成方法
            builder.addMethod(methodCreate.createMethod(executableElement));
        }
    
        //生成java类
        JavaFile javaFile = JavaFile.builder(mElementUtils.getPackageOf(element).toString(), builder.build())
                .addFileComment(" This codes are generated automatically. Do not modify!")
                .build();
        try {
            //写入文件
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    1. getRequestField方法用于生成_request变量
     private FieldSpec getRequestField(RoundEnvironment roundEnv) {
    
        TypeName requestType = null;
    
        Set<? extends Element> requestElements = roundEnv.getElementsAnnotatedWith(RequestWrapper.class);
        if (requestElements.size() == 0) {
            requestType = Constants.HTTP_REQUEST;
        } else {
            for (Element element : requestElements) {
                requestType = ClassName.get(element.asType());
                break;
            }
        }
        return FieldSpec.builder(requestType, "_request")
                .addModifiers(Modifier.PRIVATE)
                .initializer("new $T()", requestType)
                .build();
    
    }
    
    1. MethodCreate类是用于根据方法元素生成方法重写的操作
  • MethodCreate

public class MethodCreate {

    /**
     * 根据方法元素生成每个接口方法公共的代码
     *
     * @param executableElement
     * @return
     */

    public MethodSpec createMethod(ExecutableElement executableElement) {
        LRequest LRequest = executableElement.getAnnotation(LRequest.class);
        //返回值类型
        TypeMirror returnType = executableElement.getReturnType();
        //因为返回值类型是形如 Observable<HttpResult<UserInfo>> ,因此我们需要得到HttpResult<UserInfo>
        if (returnType instanceof DeclaredType) {
            DeclaredType type = (DeclaredType) returnType;
            returnType = type.getTypeArguments().get(0);
        }

        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(executableElement.getSimpleName().toString())
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(ClassName.get(executableElement.getReturnType()));
        // 处理url
        String url = LRequest.value();
        methodBuilder.addStatement("String _url = $S", url)
                .beginControlFlow("if(_url.length()==0)")
                .addStatement("throw new $T(\"incorrect url\")", RuntimeException.class)
                .endControlFlow()
                .beginControlFlow("if(!_url.startsWith(\"http\"))")
                .addStatement("_url = _baseUrl+_url")
                .endControlFlow();


        // 生成代码 final Type _type = new TypeToken<returnType>(){}.getType();
        methodBuilder.addStatement("final $T _type = new $T<$T>(){}.getType()", Type.class,
                ClassName.get("com.google.gson.reflect", "TypeToken")
                , ClassName.get(returnType));

        // 生成代码 Map<String,String> _params = new HashMap<>();
        methodBuilder.addStatement("final $T<String,String> _params = new $T<>()", Map.class, HashMap.class);

        // 生成代码 Map<String,String> _headers = new HashMap<>();
        methodBuilder.addStatement("final $T<String,String> _headers = new $T<>()", Map.class, HashMap.class);

        //如果是MULTIPART  则添加一个Map<String,File> _fileMap = new HashMap<>();用于存放文件数据
        if (LRequest.type() == ContextType.MULTIPART) {
            methodBuilder.addStatement("final $T<String,$T> _fileMap = new $T<>()", Map.class, File.class, HashMap.class);
        }

        if (LRequest.type() == ContextType.JSON) {
            methodBuilder.addStatement("$T _jsonBody=null", Object.class);
        }

        //解析方法的形参
        for (VariableElement variableElement : executableElement.getParameters()) {
            //形参的名称
            final String parameterName = variableElement.getSimpleName().toString();
            //形参传入实现类的方法
            methodBuilder.addParameter(ClassName.get(variableElement.asType()), parameterName);

            Header header = variableElement.getAnnotation(Header.class);
            //将头信息放入_headers
            if (header != null) {
                methodBuilder.addStatement("_headers.put($S,$L)", header.value(), parameterName);
                continue;
            }

            //将单个的参数键值对放入_params
            Param param = variableElement.getAnnotation(Param.class);
            if (param != null) {
                methodBuilder.addStatement("_params.put($S,$L)", param.value(), parameterName);
                continue;
            }

            //将参数集合放入_params
            ParamMap paramMap = variableElement.getAnnotation(ParamMap.class);
            if (paramMap != null) {
                methodBuilder.addStatement("_params.putAll($L)", variableElement.getSimpleName().toString());
                continue;
            }
            //如果是上传文件的   处理文件参数
            if (LRequest.type() == ContextType.MULTIPART) {
                FileParam fileParam = variableElement.getAnnotation(FileParam.class);
                if (fileParam != null) {
                    methodBuilder.addStatement("_fileMap.put($S,$L)", fileParam.value(), parameterName);
                }

                FileMap fileMap = variableElement.getAnnotation(FileMap.class);
                if (fileMap != null) {
                    methodBuilder.addStatement("_fileMap.putAll($L)", parameterName);
                }

                ProgressListener listener = variableElement.getAnnotation(ProgressListener.class);
                if (listener != null) {
                    methodBuilder.addStatement("final $T _listener = $L",
                            ClassName.get("com.lu.http.Interface", "IProgressListener"),
                            parameterName);
                }
            } else if (LRequest.type() == ContextType.JSON) {

                Body jsonBody = variableElement.getAnnotation(Body.class);
                if (jsonBody != null) {
                    methodBuilder.addStatement("_jsonBody= $L", parameterName);
                }
            }
        }

        final String targetMethod = LRequest.type().name().toLowerCase();

        if (LRequest.type() == ContextType.MULTIPART) {
            methodBuilder.addStatement("return ($T)_request.$L(_url,_headers,_params,_fileMap,_listener,_type)",
                    executableElement.getReturnType(),
                    targetMethod);
        } else if (LRequest.type() == ContextType.FORM) {
            methodBuilder.addStatement("return ($T)_request.$L(_url,$T.$L,_headers,_params,_type)",
                    executableElement.getReturnType(),
                    targetMethod,
                    LMethod.class,
                    LRequest.method());
        } else if (LRequest.type() == ContextType.JSON) {
            methodBuilder.addStatement("return ($T)_request.$L(_url,_headers,_params,_jsonBody,_type)",
                    executableElement.getReturnType(),
                    targetMethod);
        }

        return methodBuilder.build();
    }
}

至此我们的代码编写完毕,即可运行测试

源码地址

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

推荐阅读更多精彩内容