news 2026/3/28 1:25:26

面试官问MyBatis/OpenFeign的原理?我手搓了个MyHttp怼回去!(反八股版)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
面试官问MyBatis/OpenFeign的原理?我手搓了个MyHttp怼回去!(反八股版)

一、前言

自从有了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 的区别, 他大概率会觉得 “这小子是真懂,不是背八股”。

以此表达作者对故乡的思念之情,对童年时光的怀念之情,爱国之情,对小日子的痛恨之情等等。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/26 20:16:13

【课程设计/毕业设计】基于JavaEE的电子印章管理系统的设计与实现印章申请、印章下发【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/14 3:23:57

雷速体育:赛事数据一手掌握

雷速体育平台介绍 雷速体育是一个专注于体育赛事数据、比分直播和资讯的平台&#xff0c;涵盖足球、篮球、网球等主流赛事。提供实时比分、赛程统计、历史数据查询等功能&#xff0c;适合体育爱好者追踪比赛动态。 核心功能推荐 实时比分与赛程 支持全球多个联赛和杯赛的实时…

作者头像 李华
网站建设 2026/3/13 0:38:43

小折叠屏手机兴起,铰链与屏幕成考量重点及三星实例

于智能手机形态寻觅突破的当下&#xff0c;折叠屏设计无疑是近些年来极为引人注目的方向当中的一个&#xff0c;特别是那横向翻折的“小折叠”手机&#xff0c;依靠着其精巧的设计以及便携性&#xff0c;吸引了大批追求个性与时尚的用户。这类产品在确保基础手机功能的情形下&a…

作者头像 李华
网站建设 2026/3/23 8:35:07

Nginx 平滑升级

Nginx 平滑升级&#xff08;Ubuntu 24.04 环境&#xff09; 一、平滑升级核心原理 Nginx 平滑升级通过向主进程发送特定信号实现&#xff0c;核心优势是不中断现有请求处理&#xff1a; 新请求逐步由新版本 Worker 进程接管&#xff1b;旧 Worker 进程处理完现有连接后优雅退出…

作者头像 李华
网站建设 2026/3/20 23:23:34

C++ 中另一种用户自定义的可用的数据类型

结构用于表示一条记录&#xff0c;假设您想要跟踪图书馆中书本的动态&#xff0c;您可能需要跟踪每本书的下列属性&#xff1a; Title &#xff1a;标题Author &#xff1a;作者Subject &#xff1a;类目Book ID &#xff1a;书的 ID 定义结构 在 C 中&#xff0c;struct 语…

作者头像 李华
网站建设 2026/3/25 4:30:12

Agentic 组织下的终极拷问:康威定律是否已失效?

引言&#xff1a;当“智能体”成为组织的新成员梅尔康威在 1968 年提出的经典洞察——康威定律&#xff0c;在软件开发领域被奉为圭臬&#xff1a;“设计系统的组织&#xff08;广义上的&#xff09;注定会产生与该组织内部沟通结构相对应的设计。”&#xff0c;典型如编译器的…

作者头像 李华