首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Retrofit框架分析(二):注解、反射以及动态代理,Retrofit框架动态代理的源码分析

  • 25-04-23 20:41
  • 2341
  • 7660
juejin.cn

Retrofit框架分析(二):反射、泛型以及动态代理,手写Retrofit框架

  1. 注解是什么,为什么出现

  2. 反射是什么,为什么出现

    2.1. 使用反射实现ButterKnife的一个小功能:为TextView赋值

  3. 动态代理,和Retrofit有什么关系?

  4. Retrofit框架动态代理的源码分析


一、注解是什么,为什么出现

在注解出现之前,我们进行一些信息配置,都是采用XML 配置文件,这种比较繁琐、容易与代码不同步。注解出现以后,才来的的好处:

  1. ​简化配置​​
  2. ​减少样板代码​​
  3. ​运行时动态处理​​

注解是 Java 5 引入的一种​​元数据(metadata)​​机制,用于为代码元素(类、方法、字段等)添加结构 化信息。这些信息本身不直接影响代码逻辑,但可以被编译器、开发工具或运行时框架读取和处理。

注解单独本身没有什么作用,需要搭配其他功能来使用,比如反射。


二、反射是什么,为什么出现

一般情况下,我们使用某个类时必定知道它是什么类是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

java
代码解读
复制代码
String name = new String()

反射则是一开始并不知道我要初始化的类对象是什么自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK提供的反射 API进行反射调用。反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。是Java被视为动态语言的关键。

​类似于现实生活​​:

  • ​​X光机​​:不拆开物体即可观察内部结构(反射不修改代码即可分析类)。
  • ​​镜子​​:通过镜像观察自身(反射对象获取类的元数据)。

为什么要学习反射,一方面是主流的框架(如Retrofit、ButterKnife)底层大量依赖反射,如果不了解,那么我们无法知道他们是如何实现的;一方面,如果我们要写出可移植性的代码,给别人用的代码,那么反射是我们必须掌握的知识。

2.1 使用反射实现ButterKnife的一个小功能:为TextView赋值

java
代码解读
复制代码
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyId { int value(); }

@Retention(RetentionPolicy.RUNTIME):必须保留到运行时才能通过反射获取。 @Target(ElementType.FIELD):使用在变量上。

java
代码解读
复制代码
@MyId(R.id.tv_myid) TextView tvMyid;
java
代码解读
复制代码
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_back); //反射进行赋值。 ButtId.init(this); tvMyid.setText("您年的是否啊手动阀手动阀士大夫阿斯蒂芬阿斯蒂芬阿斯蒂芬啊的发射点阿斯顿阿斯顿发撒"); }
java
代码解读
复制代码
public class ButtId { private static final String TAG = "ButtId"; public static void init(Activity activity){ Classextends Activity> aClass = activity.getClass(); //获得所有的成员,包括private Field[] declaredFields = aClass.getDeclaredFields(); for (Field declaredField : declaredFields) { //获得包含指定注解的 Log.d(TAG, "init: "+declaredField.isAnnotationPresent(MyId.class)); if (declaredField.isAnnotationPresent(MyId.class)) { //拿到注解信息 MyId annotation = declaredField.getAnnotation(MyId.class); //拿到注解的参数 int value = annotation.value(); View viewById = activity.findViewById(value); Log.d(TAG, "viewById init: "+viewById); try { //给成员赋值 declaredField.set(activity,viewById); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } } }

调用 Field.set(Object obj, Object value) 方法,将 declaredField 表示的字段值赋值为 viewById。


三、动态代理

3.1 是什么

动态代理确实可以帮助我们“​​动态创建一个类的代理对象​​”,但这个“类”并不是你手动编写的,而是​​在程序运行时自动生成的新类​​。它就像一个“中间商”,在不修改原有代码的情况下,帮你给某个类的方法“加料”(比如加日志、加权限检查等)。

3.2 动态代理如何“创建类”?​​

  • ​​核心目的​​:在运行时生成一个“代理类”,让它代替原始类去执行方法,并在方法执行前后插入额外逻辑。

  • ​​怎么生成​​:
    你不需要手动写这个代理类的代码,Java通过Proxy类和InvocationHandler接口,在运行时用​​反射技术​​动态生成字节码,创建一个新的类。

假设你有一个​​客服系统​​,用户打电话进来时,系统会直接接通客服人员处理问题。现在你想​​在每次通话前后自动记录通话日志​​,但不想修改客服人员原有的工作流程。怎么办?

  • ​​传统做法​​(笨办法):
    让每个客服人员自己记录日志(​​修改原有代码​​)。但如果有100个客服,就要改100次,麻烦!
  • ​​动态代理做法​​:
    雇一个​​“代理客服”​​,所有电话先转给这个代理客服。代理客服在转接给真实客服前记录日志,通话结束后再记录一次。​​真实客服完全不需要修改​​!

3.3 代码举例

typescript
代码解读
复制代码
// 原始接口(比如客服接电话的流程) public interface CustomerService { void answerCall(String phoneNumber); } // 真实实现类(真正的客服人员) public class RealCustomerService implements CustomerService { public void answerCall(String phoneNumber) { System.out.println("接听电话:" + phoneNumber); } } // 动态代理的“统一处理逻辑”(代理客服) public class LogHandler implements InvocationHandler { private Object target; // 被代理的真实对象(比如真实的客服) public LogHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("【开始记录日志】方法名:" + method.getName()); Object result = method.invoke(target, args); // 转接给真实客服 System.out.println("【结束记录日志】"); return result; } } // 使用动态代理 public class Main { public static void main(String[] args) { // 真实客服对象 CustomerService realService = new RealCustomerService(); // 创建代理对象(动态生成的类) CustomerService proxy = (CustomerService) Proxy.newProxyInstance( CustomerService.class.getClassLoader(), new Class[]{CustomerService.class}, // 代理的接口 new LogHandler(realService) // 统一的处理逻辑 ); // 调用方法:实际会先走代理的invoke方法 proxy.answerCall("123456789"); } }

3.4 分析Proxy.newProxyInstance方法

swift
代码解读
复制代码
public static Object newProxyInstance( ClassLoader loader, // 参数1:类加载器 Class[] interfaces, // 参数2:接口数组 InvocationHandler handler // 参数3:调用处理器 )
1. ClassLoader loader:类加载器​​
  • ​​作用​​:
    负责​​加载动态生成的代理类的字节码​​到内存中,让 JVM 能识别这个新生成的类。

  • ​​通俗理解​​:
    代理类是在运行时动态生成的“新类”,这个类加载器就像“翻译官”,负责把代理类的字节码“翻译”成 JVM 能理解的格式。

  • ​​怎么选​​:
    一般直接使用​​目标类的类加载器​​(比如 TargetClass.class.getClassLoader()),确保代理类和目标类在同一个类加载环境下。

  • ​​示例​​:

ini
代码解读
复制代码
// 获取目标接口的类加载器 ClassLoader loader = UserService.class.getClassLoader();

有时候,你会看到,它不一定使用接口的类加载器,有时候,可能使用Activity的,为什么呢?我们应该如何选择呢?

​​1. 基本原则:谁加载了接口,就用谁的类加载器​​

动态代理生成的代理类需要​​访问目标接口的所有信息​​。如果“图书管理员”没见过这个接口的书(无法加载接口类),就会报错。


​​2. 具体场景判断​​

​​场景1:代理系统接口(如Android的View.OnClickListener)​​

  • ​​接口来源​​:
    系统自带的接口(比如Android SDK中的View.OnClickListener)。

  • ​​选择类加载器​​:
    随便找一个能访问系统类的“管理员”即可,比如:

    代码解读
    复制代码
    java
  • ini
    代码解读
    复制代码
    // 使用Activity的类加载器(能访问系统接口) ClassLoader loader = activity.getClass().getClassLoader();
  • ​​为什么可以随便选?​​
    系统接口已经被​​根加载器(BootstrapClassLoader)​​加载过,所有子加载器(如应用类加载器)都能直接访问它,无需重复加载。

​​场景2:代理自定义接口(如UserService)​​

  • ​​接口来源​​:
    你自己写的接口,打包在应用代码中。

  • ​​选择类加载器​​:
    必须用​​加载该接口的类加载器​​,通常是应用类加载器:

    代码解读
    复制代码
    java
  • ini
    代码解读
    复制代码
    // 使用接口自身的类加载器 ClassLoader loader = UserService.class.getClassLoader();
  • ​​为什么不能随便选?​​
    如果你用一个不认识UserService的类加载器(比如系统类加载器),它会说:“我没见过这本书(UserService类)”,导致代理类生成失败。


​​2. Class[] interfaces:接口数组​​
  • ​​作用​​:
    告诉动态代理​​生成的代理类需要实现哪些接口​​。代理对象会拥有这些接口中定义的所有方法。

  • ​​通俗理解​​:
    接口就像“合同”,代理类必须承诺实现这些接口里的所有方法。比如,目标对象实现了 UserService 接口,代理对象也要假装自己是 UserService,这样才能“以假乱真”。

  • ​​关键点​​:

    • 必须传入目标对象​​实际实现的接口​​(比如 new Class[]{UserService.class})。
    • 如果目标类没有实现接口,Java 原生动态代理无法使用(需要改用 ​​CGLIB​​)。
  • ​​示例​​:

ini
代码解读
复制代码
// 目标对象实现的接口 Class[] interfaces = new Class[]{UserService.class};

​​3. InvocationHandler handler:调用处理器​​
  • ​​作用​​:
    定义代理对象的方法​​具体如何被增强​​。所有通过代理对象调用的方法,都会先转到 handler 的 invoke() 方法中处理。

  • ​​通俗理解​​:
    这个参数是代理的“大脑”,决定在调用方法前做什么(比如记录日志)、调用方法后做什么(比如提交事务)。就像一个“中间人”,所有请求都要经过它。

  • ​​关键点​​:

    • 需要自己实现 InvocationHandler 接口,并在 invoke() 方法中编写增强逻辑。
    • 通常会持有目标对象(真实对象)的引用,以便在 invoke() 中调用真实方法。
  • ​​示例​​:

typescript
代码解读
复制代码
InvocationHandler handler = new InvocationHandler() { private Object target; // 真实对象(比如UserServiceImpl的实例) public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before method: " + method.getName()); Object result = method.invoke(target, args); // 调用真实对象的方法 System.out.println("After method: " + method.getName()); return result; } };

3.5 Proxy.newProxyInstancer是如何创建出来代理类的?

Proxy.newProxyInstance() 在运行时动态生成代理类的过程,可以类比为 ​​“代码自动生成器”​​。虽然我们没有手写这个代理类的源代码,但Java在运行时通过​​反射 + 字节码生成技术​​,直接在内存中生成一个新的类,并加载到JVM中使用。

下面我们看看一个例子:我想为R.id.tv_hello,添加点击事件,当点击的时候,就调用这个abc方法。

java
代码解读
复制代码
@Click(idRes = R.id.tv_hello) public void abc(View view){ }
java
代码解读
复制代码
package com.l.onclick; import android.app.Activity; import android.view.View; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * * */ public class ClickBinder { // 绑定 Activity 中所有 @Click 注解的方法 public static void bind(Activity activity) { //1. 首先拿到类,然后拿到类的所有方法 Classextends Activity> clazz = activity.getClass(); Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method method : declaredMethods) { //2. 找到加了注解的方法 Click annotation = method.getAnnotation(Click.class); if (annotation != null) { //3. 取出他的参数 int i = annotation.idRes(); /* 4. 参数拿到了,那么接下来,我们就要构建一个view了。 (1)activity.find(),但这样会又什么问题?不需要动态代理对象了。。。。。 (2)但这样的化,我如何传递到对应的方法里面呢?监听他的点击事件?我们需要创建一个对象,就是监听器 */ bindClick(activity, method,i); } } } private static void bindClick(Activity activity, Method method, int i) { // 创建动态代理的 OnClickListener:以前我们需要创建这个对象,现在放到代理类里面创建对象了。 //创建的过程就是:OnClickListener的class和接口就可以。 View.OnClickListener listener = createProxyListener(activity, method); View view = activity.findViewById(i); view.setOnClickListener(listener); } // 创建 OnClickListener 的动态代理 private static View.OnClickListener createProxyListener(Object target, final Method method) { return (View.OnClickListener) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { //这里会拿到什么?拿到view, //那么OnClickListener里面有实现很多方法,我们只监听 onClick方法的调用。 if (m.getName().equals("onClick")) { //这个时候我们就需要调用 加了onclick注解的方法 method.invoke(target,args[0]); } return null; } }); } }
less
代码解读
复制代码
@Target(ElementType.METHOD) // 标记方法 @Retention(RetentionPolicy.RUNTIME) // 运行时保留 public @interface Click { /** View IDs to which the method will be bound. */ int idRes() ; }

代码解释:

1.通过动态代理生成一个“假”的 OnClickListener 对象,当用户点击 View 时,这个代理对象会拦截 onClick 方法,并调用 Activity 中被 @Click 注解标记的方法。

2.Java 的 Proxy 类会动态生成一个全新的类,这个类:

  1. ​​继承 Proxy​​(所有动态代理类的父类)。
  2. ​​实现 View.OnClickListener 接口​​。
  3. ​​重写接口所有方法​​(例如 onClick 方法),并将方法调用转发给 InvocationHandler.invoke()。

3.加载并实例化代理类

  • ​​加载字节码​​:
    使用 ClassLoader 将生成的代理类(如 $Proxy0)加载到 JVM 中,变成一个可用的类。
  • ​​创建代理对象​​:
    通过反射调用代理类的构造函数,传入 InvocationHandler 对象(即匿名内部类实例),生成代理对象。

4.绑定代理对象到 View

  • ​​代码​​:view.setOnClickListener(listener);
  • ​​效果​​:
    当用户点击 View 时,系统会调用代理对象的 onClick 方法,最终触发 InvocationHandler.invoke()。

四、Retrofit中对动态代理、注解的运用

4.1 create里面就是使用了动态代理

ini
代码解读
复制代码
val create:HomeApiService = retrofit.create(HomeApiService::class.java)

可以看到,我们传递了HomeApiService::class.java进去,然后返回的就是HomeApiService的代理对象,我们看看他们代码里面的实现。

图片.png

4.2 反射获得有注解的方法

我们把接口传递了进来,那么它就就可以通过反射来解析方法,看方法上面的注解是什么。

less
代码解读
复制代码
interface HomeApiService { /** * 请求密码 */ @GET("/index/getPwd") suspend fun getPwd(@QueryMap params: Map): BaseResponse }

图片.png 我们看它传递的方法method

图片.png 拿到所有的注解

图片.png 注解可能有GET,有POST,不同的注解,要创建不同的请求方式。

图片.png

图片.png

注:本文转载自juejin.cn的前行的小黑炭的文章"https://juejin.cn/post/7496338587501920290"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top