一、前言
自从有了AI、大模型、DeepSeek、豆包、GPT......,就再也没写过技术文章了。毕竟,在它们面前写什么内容都是多余的。我甚至问过AI"AI 时代写技术博客还有意义吗"这个问题,它给出了如下结论:
结论:AI 时代,技术博客不仅有意义,反而变得 更重要 了。 虽然 AI 能快速生成内容,但真正的价值恰恰在于人类的独特思考和实践经验, 这是 AI 无法完全复制的 "极值"(而 AI 只能产出 "平均值")。 行动建议: 不要犹豫是否开始,而是思考如何将 AI 变成你的创作助手。 本周就选定一个小而精的技术主题,尝试 "人类构思 + AI 辅助" 的创作模式,体验效率与质量的双重提升。 记住:在 AI 时代,最有价值的不是 "写什么",而是 "你为什么这样写" 的独特视角。
总感觉它在一本正经的胡说八道,但又拿不出来证据,偶尔还会被它的"心灵鸡汤"戳中。有时候想想也是:
AI 是很牛逼,但它写不出我深夜改 bug 改到脱发的崩溃;
AI 是很万能,但它写不出"一个 bug 是 bug,两个 bug 是 feature"的玄学代码;
AI 是能给方案,但它给不出在办公室用底层原理驯服测试小姐姐的那种拿捏感。
以上便是今天这篇博文的引子,字里行间表达了作者对故乡的思念之情,对童年时光的怀念之情,爱国之情,对小日子的痛恨之情等等。
二、背景
问:MyBatis为什么写一个public interface UserMapper接口类,就能访问数据库?
问:OpenFeign为什么写一个public interface UserFeignClient接口类,就能发送HTTP请求?
此时你一脸懵逼的说:我平常项目就是这么开发的,接口会调用xml中我写好的sql,接口会调用我注解中的url地址。
遗憾的是,要是面试时你这么答,面试官大概率直接给你打零分 ------ 他要的不是 "怎么用",而是 "为什么能这么用"。
要搞懂这些问题的核心,就绕不开 "动态代理"------ 这正是面试官想考察的底层思维,咱们今天就掰烂了揉碎了说说"动态代理的那些事儿"。接下来咱们不背八股,直接手搓一个类 OpenFeign 的 "MyHttp",把动态代理扒明白。
三、手搓一个"MyHttp"
我们要实现的东西暂且叫做"MyHttp",他的目标就是像OpenFeign一样,定义一个接口就能发送HTTP请求,不需要任何配置和任何实现类。
我们首先来看,常规调用HTTP接口的代码大概长下面这个样子:
private static RegisterResponse registerUser(RegisterUserRequest requestParam) throws IOException, ParseException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
//构建POST请求
HttpPost httpPost = new HttpPost("http://localhost:8080/api/user/register");
//设置请求头
httpPost.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
//将请求参数序列化为JSON字符串
String requestJson = OBJECT_MAPPER.writeValueAsString(requestParam);
HttpEntity requestEntity = new StringEntity(requestJson, ContentType.APPLICATION_JSON);
httpPost.setEntity(requestEntity);
//执行请求,获取响应
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
//解析响应实体
HttpEntity responseEntity = response.getEntity();
if (responseEntity == null) {
throw new RuntimeException("注册接口返回空响应");
}
//将响应JSON字符串反序列化为实体类
String responseJson = EntityUtils.toString(responseEntity);
return OBJECT_MAPPER.readValue(responseJson, RegisterResponse.class);
}
}
}
"MyHttp"的目标是这个样子:
@HttpClient(baseUrl="http://localhost:8080/api")
public interface UserHttp {
@HttpPost(url = "/user/register")
RegisterResponse registerUser(RegisterUserRequest requestParam);
}
看起来是不是很清爽?接下来我们基于动态代理一步一步实现它。
四、先搞几个核心注解
**@HttpClient:**放到接口类上,表示这是一个基于"MyHttp"的接口,放一个属性baseUrl,定义这个接口下的所有HTTP调用的url根路径:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface HttpClient {
String baseUrl() default "";
}
当然,如果你愿意,还可以扩展其它属性,比如你想设置连接超时时间、读超时时间,再比如你的baseUrl是动态的,或者是个地址列表要负载均衡去调用等等:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface HttpClient {
String baseUrl() default "";
int connectTimeout() default -1;
int readTimeout() default -1;
Class<? extends BaseUrlSource> baseUrlSource() default BaseUrlSource.class;
}
public interface BaseUrlSource {
List<String> getBaseUrls();
}
**@HttpPost:**放在接口方法上,表示这个方法具体要调用哪个接口,报文头怎么设置,超时参数等等:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface HttpPost {
String url() default "";
String contentType() default MediaType.APPLICATION_JSON_VALUE;
int connectTimeout() default -1;
int readTimeout() default -1;
}
当然,如果你愿意,仍然可以扩展很多很多属性,但这不是本文重点。
五、再实现一下InvocationHandler
简单说一下InvocationHandler :是 JDK 动态代理的 "调用处理器",当我们通过Proxy.newProxyInstance(JDK 动态代理的核心方法,作用是 "绑定接口和代理逻辑,生成最终可用的代理对象")的方式生成对象并调用目标方法时,JVM 会自动将调用转发到 InvocationHandler 的 invoke 方法,由该方法完成最终的方法执行 + 自定义增强逻辑。
翻译成人话:用 InvocationHandler 把 "被代理的接口" 包一层,生成一个 "代理对象";之后调用接口方法时,其实是在调代理对象的方法,自然就会走进 invoke 里咱们写的逻辑。
以下例子中,我们便在invoke方法中拿到了被代理的接口类和接口方法,这时候我们就能拿到所有注解,进而根据注解信息组装HTTP报文并发送请求。
/**
* HTTP动态代理处理器:拦截接口方法调用,自动发送HTTP请求
*/
public class HttpInvocationHandler implements InvocationHandler {
// JSON序列化工具(全局复用)
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// HttpClient客户端
private static final CloseableHttpClient HTTP_CLIENT = HttpClients.createDefault();
// 目标接口的Class对象(用于解析注解)
private final Class<?> targetInterface;
public HttpInvocationHandler(Class<?> targetInterface) {
this.targetInterface = targetInterface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 解析接口级@HttpClient注解,获取基础URL
HttpClient httpClientAnnotation = targetInterface.getAnnotation(HttpClient.class);
String baseUrl = httpClientAnnotation.baseUrl();
// 解析方法级@HttpPost注解,获取子路径
HttpPost httpPostAnnotation = method.getAnnotation(HttpPost.class);
String subUrl = httpPostAnnotation.url();
String contentType = httpPostAnnotation.contentType();
// 拼接完整请求URL
String fullUrl = baseUrl + subUrl;
//处理请求参数
String requestJson = OBJECT_MAPPER.writeValueAsString(args[0]);
//发送HTTP POST请求
HttpPost httpPost = new HttpPost(fullUrl);
// 设置请求头:JSON格式
httpPost.setHeader("Content-Type", contentType);
// 设置请求体
HttpEntity requestEntity = new StringEntity(requestJson);
httpPost.setEntity(requestEntity);
// 执行请求并获取响应
try (var response = HTTP_CLIENT.execute(httpPost)) {
HttpEntity responseEntity = response.getEntity();
// 解析响应JSON
String responseJson = EntityUtils.toString(responseEntity);
//响应结果反序列化为方法返回类型
Type returnType = method.getGenericReturnType();
return OBJECT_MAPPER.readValue(responseJson, OBJECT_MAPPER.constructType(returnType));
}
}
}
六、再写个代理工厂
现在我们只差如何创建代理对象了,这也是最后一步,这时候我们用到了Proxy.newProxyInstance。这个方法你可以想象成:被代理对象,通过Proxy.newProxyInstance的方式与代理对象绑定了起来,这样当被代理对象的方法被调用时,实际就变成了代理对象在帮你调用,那么就会进入代理对象的invoke方法,从而执行我们的增强逻辑。
/**
* HTTP代理工厂:封装动态代理对象的创建逻辑
*/
public class HttpProxyFactory {
/**
* 创建HTTP接口的代理对象
* @param interfaceClass 目标接口Class(如UserHttp.class)
* @return 接口代理对象
* @param <T> 接口类型
*/
public static <T> T createProxy(Class<T> interfaceClass) {
// 创建自定义InvocationHandler
HttpInvocationHandler handler = new HttpInvocationHandler(interfaceClass);
// 生成动态代理对象
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass},
handler
);
}
}
七、核心逻辑串一串
进度条走到这里,核心逻辑基本梳理完了,咱们先简单总结一下整体流程:接口注解定义 → 代理工厂创建代理对象 → 调用接口方法触发 invoke → 解析注解组装 HTTP 请求 → 响应反序列化返回。
八、测试一下
@RestController
@RequestMapping("/demo")
public class DemoController {
@PostMapping("/register")
public RegisterResponse register(@RequestBody RegisterUserRequest request) {
//创建UserHttp接口的代理对象
UserHttp userHttp = HttpProxyFactory.createProxy(UserHttp.class);
// 调用接口方法,底层自动发送HTTP请求
return userHttp.registerUser(request);
}
}
至此,上面的实现已经能跑通,但总觉得还缺点什么?
九、还缺点什么?
有人说,你怎么通过UserHttp userHttp = HttpProxyFactory.createProxy(UserHttp.class);的方式才能调用?我平常项目里都是这样就能调用了:
@Autowired
private UserHttp userHttp;
这里就涉及到Spring 的 FactoryBean 接口和注解扫描注册器,并不是本文重点,但还是给大家补全这个 "实战最后一公里"。
首先要实现FactoryBean (Spring 的 "特殊 Bean 工厂",专门用来创建 "不是简单 new 出来" 的 Bean,比如咱们的动态代理对象):
/**
* 自定义FactoryBean:生成HTTP接口的动态代理对象
* @param <T> 目标接口类型(如UserHttp)
*/
public class HttpProxyFactoryBean<T> implements FactoryBean<T> {
// 目标接口的Class对象
private Class<T> interfaceClass;
// 构造器注入接口类型
public HttpProxyFactoryBean(Class<T> interfaceClass) {
this.interfaceClass = interfaceClass;
}
/**
* 创建Bean实例(返回动态代理对象)
*/
@Override
@Nullable
public T getObject() throws Exception {
// 调用之前的动态代理工厂生成代理对象
return HttpProxyFactory.createProxy(interfaceClass);
}
/**
* 返回Bean的类型(接口类型)
*/
@Override
public Class<?> getObjectType() {
return interfaceClass;
}
/**
* 单例模式(代理对象复用)
*/
@Override
public boolean isSingleton() {
return true;
}
}
然后实现ImportBeanDefinitionRegistrar扫描所有标记了@HttpClient的接口,自动注册为 Spring Bean:
public class HttpProxyBeanRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//创建扫描器:只扫描标记@HttpClient的接口
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(HttpClient.class));
//扫描指定包,需替换为实际包名
String basePackage = "com.demo.http";
scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> {
try {
//获取接口的Class对象
String className = beanDefinition.getBeanClassName();
Class<?> interfaceClass = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());//构建BeanDefinition:指定Bean类型为HttpProxyFactoryBean
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HttpProxyFactoryBean.class);
//构造器注入接口Class对象
builder.addConstructorArgValue(interfaceClass);
//注册Bean,Bean名称默认用接口类名首字母小写,如userHttp
registry.registerBeanDefinition(ClassUtils.getShortNameAsProperty(interfaceClass), builder.getBeanDefinition());
} catch (ClassNotFoundException e) {
throw new RuntimeException("扫描HTTP接口失败:" + e.getMessage(), e);
}
});
}
}
最后别忘了关键的一步:增加一个配置类,导入自定义bean注册器
@Configuration
@Import(HttpProxyBeanRegistrar.class)
public class HttpProxyAutoConfiguration {
}
十、再测试一下
@RestController
@RequestMapping("/demo")
public class DemoController {
//自动注入UserHttp接口
@Autowired
private UserHttp userHttp;
@PostMapping("/register")
public RegisterResponse register(@RequestBody RegisterUserRequest request) {
// 调用接口方法,底层自动发送HTTP请求
return userHttp.registerUser(request);
}
}
这个代码,是不是就非常有感觉了。有了这一套,面试时被问 "OpenFeign 为什么能直接注入接口用",你不光能说清动态代理,还能说清 Spring 是怎么管理这些代理 Bean 的,直接碾压八股文选手。
十一、结语:手搓的意义,不止于 "会用"
好像没啥可说的了,用AI生成一段吧:
写到这,咱们的 "MyHttp" 就彻底跑通了 ------ 从注解定义到动态代理拦截,再到 Spring 自动注入,核心逻辑和 OpenFeign、MyBatis 的接口代理思想完全一致。 可能有人会说:"有现成的框架用,为啥还要手搓?" 答案很简单: 面试时,"会用" 只能拿及格分,"懂原理 + 能手搓" 才能拿 Offer; 工作中,遇到框架适配问题时,底层原理才是你解决问题的底气。 就像面试官问 "OpenFeign 为什么能直接调用接口",你要是能把今天这一套手搓逻辑讲清楚,再对比一下 JDK 动态代理和 CGLIB 的区别, 他大概率会觉得 "这小子是真懂,不是背八股"。
以此表达作者对故乡的思念之情,对童年时光的怀念之情,爱国之情,对小日子的痛恨之情等等。