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

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

posted @ 2025-12-08 09:04  失足成万古风流人物  阅读(173)  评论(5)    收藏  举报