面试官问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 的区别,
他大概率会觉得 “这小子是真懂,不是背八股”。
以此表达作者对故乡的思念之情,对童年时光的怀念之情,爱国之情,对小日子的痛恨之情等等。
浙公网安备 33010602011771号