自己动手实现简易版Feign

Feign的功能

主要功能是让开发人员只使用简单注解就能像调用一般RPC一样调用HTTP请求,在此基础上,框架扩展了原SpringMVC参数只能为一个类的功能。

基本思路

代理实现->bean的注册->多参数实现

代码实现

注解定义

需要定义三个注解。
第一个“HttpConsumer”,用于消费端注册服务。

/**
 * 标注需要代理为http请求发送
 *
 * @author kun
 * @data 2022/1/15 17:50
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpConsumer {
    /**
     * 域名
     *
     * @return  域名
     */
    String domain();

    /**
     * 端口,默认80
     *
     * @return  端口号
     */
    String port() default "80";
}

第二个“HttpRequest”,用于服务提供方定义服务。

/**
 * 用于标记可以通过http访问的服务
 *
 * @author kun
 * @data 2022/1/15 17:55
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpRequest {
    /**
     * http请求url
     *
     * @return  url
     */
    String value();
}

第三个“MultiRequestBody”,用于实现多变量传参。

/**
 * 标注方法参数
 *
 * @author kun
 * @data 2022/1/15 17:57
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
    /**
     * 解析参数时用到JSON中的key
     *
     * @return  JSON格式参数
     */
    String value();
}

动态代理

创建代理的目的是将方法转化为http调用。

/**
 * HttpConsumer注解代理类
 *
 * @author kun
 * @data 2022/1/15 19:30
 */
public class HttpConsumerInterceptor implements MethodInterceptor {

    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String HTTP_HEAD = "http://";

    private final Class<?> proxyKlass;

    private final HttpDomain httpDomain;

    private final OkHttpClient okHttpClient;

    public HttpConsumerInterceptor(Class<?> proxyKlass, HttpDomain httpDomain, OkHttpClient okHttpClient) {
        this.proxyKlass = proxyKlass;
        this.httpDomain = httpDomain;
        this.okHttpClient = okHttpClient;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        HttpRequest httpRequest = method.getAnnotation(HttpRequest.class);
        if (Objects.isNull(httpRequest)) {
            throw new IllegalStateException("method[" + method.getName() + "] must annotated with @HttpRequest!");
        }
        HttpRequest klassAnnotation = proxyKlass.getAnnotation(HttpRequest.class);
        String namespace = Objects.isNull(klassAnnotation) ? null : klassAnnotation.value();
        String url = getUrl(httpRequest.value(), namespace);
        Request request = buildRequest(method, args, url);
        Call call = okHttpClient.newCall(request);
        Response response = call.execute();
        ResponseBody body = response.body();
        if (Objects.isNull(body)) {
            return null;
        }
        byte[] bytes = body.bytes();
        String res = new String(bytes, StandardCharsets.UTF_8);
        if (StringUtils.isEmpty(res)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(res, method.getReturnType());
        } catch (Throwable t) {
            Map<?, ?> map = OBJECT_MAPPER.readValue(res, Map.class);
            Object err = map.get("error");
            if (Objects.nonNull(err)) {
                throw new RuntimeException(err.toString());
            }
            throw new RuntimeException(t);
        }
    }

    private String getUrl(String requestPath, String namespace) {
        if (Objects.isNull(namespace)) {
            return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + requestPath;
        }
        return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + namespace + "/" + requestPath;
    }

    private Request buildRequest(Method method, Object[] args, String url) throws JsonProcessingException {
        Request.Builder builder = new Request.Builder();
        Map<String, Object> paramMap = new HashMap<>(4);
        Parameter[] parameters = method.getParameters();
        for (int i=0; i<parameters.length; i++) {
            Parameter parameter = parameters[i];
            MultiRequestBody multiRequestBody = parameter.getAnnotation(MultiRequestBody.class);
            if (Objects.isNull(multiRequestBody)) {
                throw new IllegalStateException("method[" + method.getName() + "] param must annotated with @MultiRequest!");
            }
            paramMap.put(multiRequestBody.value(), args[i]);
        }
        // 将参数构建为一个大JSON
        String param = OBJECT_MAPPER.writeValueAsString(paramMap);
        RequestBody requestBody = RequestBody.create(param, JSON);
        builder.post(requestBody);
        builder.url(url);
        return builder.build();
    }
}

自定义FactoryBean

使用FactoryBean生成bean

/**
 * @author kun
 * @data 2022/1/16 14:50
 */
public class HttpConsumerProxyFactoryBean implements FactoryBean<Object> {

    private final Class<?> proxyKlass;

    private final HttpDomain httpDomain;

    private final OkHttpClient okHttpClient;

    public HttpConsumerProxyFactoryBean(Class<?> proxyKlass, HttpDomain httpDomain, OkHttpClient okHttpClient) {
        this.proxyKlass = proxyKlass;
        this.httpDomain = httpDomain;
        this.okHttpClient = okHttpClient;
    }

    @Override
    public Object getObject() {
        Enhancer enhancer = new Enhancer();
        if (proxyKlass.isInterface()) {
            enhancer.setInterfaces(new Class[]{proxyKlass});
        } else {
            enhancer.setSuperclass(proxyKlass);
        }
        HttpConsumerInterceptor httpConsumerInterceptor = new HttpConsumerInterceptor(proxyKlass, httpDomain, okHttpClient);
        enhancer.setCallback(httpConsumerInterceptor);
        return enhancer.create();
    }

    @Override
    public Class<?> getObjectType() {
        return proxyKlass;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

注册bean

使用BeanDefinition方式注册bean

/**
 * @author kun
 * @data 2022/1/16 13:59
 */
@EnableConfigurationProperties(HttpConsumerProperties.class)
public class HttpConsumerPostProcessor implements BeanClassLoaderAware, EnvironmentAware, BeanFactoryPostProcessor, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(HttpConsumerPostProcessor.class);

    private ClassLoader classLoader;

    private ApplicationContext context;

    private ConfigurableEnvironment environment;

    private ConfigurableListableBeanFactory beanFactory;

    private final Map<String, BeanDefinition> httpClientBeanDefinitions = new HashMap<>(4);

    private OkHttpClient okHttpClient;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        this.beanFactory = configurableListableBeanFactory;
        this.okHttpClient = buildOkHttpClient(environment);
        postProcessBeanFactory(beanFactory, (BeanDefinitionRegistry) beanFactory);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment) environment;
    }

    private OkHttpClient buildOkHttpClient(ConfigurableEnvironment environment) {
        HttpConsumerProperties properties = BinderUtils.bind(environment, HttpConsumerProperties.PREFIX, HttpConsumerProperties.class);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(properties.getCoreThreads(), properties.getMaxThreads(),
                properties.getKeepAliveTime(), TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Custom Dispatcher", false));
        Dispatcher dispatcher = new Dispatcher(executor);
        dispatcher.setMaxRequests(properties.getMaxRequests());
        dispatcher.setMaxRequestsPerHost(properties.getMaxRequests());
        builder.dispatcher(dispatcher);
        builder.connectTimeout(properties.getConnectTimeOut(), TimeUnit.SECONDS);
        builder.readTimeout(properties.getReadTimeOut(), TimeUnit.SECONDS);
        builder.writeTimeout(properties.getWriteTimeOut(), TimeUnit.SECONDS);
        builder.connectionPool(new ConnectionPool(properties.getMaxIdleConnections(), properties.getConnectionKeepAliveTime(), TimeUnit.SECONDS));
        return builder.build();
    }

    private void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) {
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
            String beanClassName = definition.getBeanClassName();
            // 当用@Bean 返回的类型是Object时,beanClassName是null
            if (Objects.isNull(beanClassName)) {
                continue;
            }
            Class<?> clazz = ClassUtils.resolveClassName(definition.getBeanClassName(), this.classLoader);
            ReflectionUtils.doWithFields(clazz, this::parseElement, this::annotatedWithHttpConsumer);
        }
        for (String beanName : httpClientBeanDefinitions.keySet()) {
            if (context.containsBean(beanName)) {
                throw new IllegalArgumentException("[HttpConsumer Starter] Spring context already has a bean named " + beanName
                 + ", please change @HttpConsumer field name.");
            }
            registry.registerBeanDefinition(beanName, httpClientBeanDefinitions.get(beanName));
            logger.info("registered HttpConsumer factory bean \"{}\" in spring context.", beanName);
        }
    }

    private void parseElement(Field field) {
        Class<?> interfaceClass = field.getType();
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException("field [" + field.getName() + "] annotated with @HttpConsumer must be interface!");
        }
        HttpConsumer httpConsumer = AnnotationUtils.getAnnotation(field, HttpConsumer.class);
        HttpDomain httpDomain = HttpDomain.from(httpConsumer);
        // 支持占位符${}
        httpDomain.setDomain(beanFactory.resolveEmbeddedValue(httpDomain.getDomain()));
        httpDomain.setPort(beanFactory.resolveEmbeddedValue(httpDomain.getPort()));
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                .rootBeanDefinition(HttpConsumerProxyFactoryBean.class)
                .addConstructorArgValue(interfaceClass)
                .addConstructorArgValue(httpDomain)
                .addConstructorArgValue(okHttpClient);
        BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.setPrimary(true);
        beanDefinition.setAutowireCandidate(true);
        httpClientBeanDefinitions.put(field.getName(), beanDefinition);
    }

    private boolean annotatedWithHttpConsumer(Field field) {
        return field.isAnnotationPresent(HttpConsumer.class);
    }
}

多参数适配

主要是多SpringMVC的参数进行配置

/**
 * 参数解析
 *
 * @author kun
 * @data 2022/1/15 21:06
 */
public class MultiRequestBodyResolver implements HandlerMethodArgumentResolver {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String JSON_REQUEST_BODY = "JSON_REQUEST_BODY";

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(MultiRequestBody.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        String requestBody = getRequestBody(nativeWebRequest);
        OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        JsonNode rootNode = OBJECT_MAPPER.readTree(requestBody);
        if (StringUtils.isEmpty(rootNode)) {
            throw new IllegalArgumentException("requestBody must not empty!");
        }
        MultiRequestBody multiRequestBody = methodParameter.getParameterAnnotation(MultiRequestBody.class);
        if (Objects.isNull(multiRequestBody)) {
            throw new IllegalArgumentException("param must annotated with @MultiRequestBody!");
        }
        String key = !StringUtils.isEmpty(multiRequestBody.value()) ? multiRequestBody.value() : methodParameter.getParameterName();
        JsonNode value = rootNode.get(key);
        if (Objects.isNull(value)) {
            return null;
        }
        Class<?> paramType = methodParameter.getParameterType();
        return OBJECT_MAPPER.readValue(value.toString(), paramType);
    }

    /**
     *
     * 获取请求体的JSON字符串
     */
    private String getRequestBody(NativeWebRequest webRequest) {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) webRequest.getAttribute(JSON_REQUEST_BODY, NativeWebRequest.SCOPE_REQUEST);
        if (!StringUtils.isEmpty(jsonBody)) {
            return jsonBody;
        }
        try (BufferedReader reader = servletRequest.getReader()) {
            jsonBody = IOUtils.toString(reader);
            webRequest.setAttribute(JSON_REQUEST_BODY, jsonBody, NativeWebRequest.SCOPE_REQUEST);
            return jsonBody;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
/**
 * JSON解析数据
 *
 * @author kun
 * @data 2022/1/15 21:03
 */
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer pathMatchConfigurer) {

    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer contentNegotiationConfigurer) {

    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer asyncSupportConfigurer) {

    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer defaultServletHandlerConfigurer) {

    }

    @Override
    public void addFormatters(FormatterRegistry formatterRegistry) {

    }

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {

    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {

    }

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {

    }

    @Override
    public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {

    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry viewResolverRegistry) {

    }

    /**
     * 参数解析器
     *
     * @param list  解析器
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
        list.add(new MultiRequestBodyResolver());
    }

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> list) {
        MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
        LinkedList<MediaType> mediaTypes = new LinkedList<>();
        mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        jacksonConverter.setSupportedMediaTypes(mediaTypes);
        jacksonConverter.setDefaultCharset(StandardCharsets.UTF_8);
        list.add(jacksonConverter);
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> list) {

    }

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {

    }

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {

    }

    @Override
    public Validator getValidator() {
        return null;
    }

    @Override
    public MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

编写为Spring starter

/**
 * 服务调用方生效
 *
 * @author kun
 * @data 2022/1/16 15:36
 */
@ConditionalOnClass(OkHttpClient.class)
public class HttpClientAuthConfig {

    @Bean
    @ConditionalOnMissingBean
    public HttpConsumerPostProcessor httpConsumerPostProcessor() {
        return new HttpConsumerPostProcessor();
    }
}
/**
 * 服务提供方生效
 *
 * @author kun
 * @data 2022/1/16 15:40
 */
@ConditionalOnClass(WebMvcConfigurer.class)
public class SpringMvcConfigurerAutoConfig {

    @Bean
    @ConditionalOnMissingBean
    public WebMvcConfig webMvcConfig() {
        return new WebMvcConfig();
    }

    @Bean
    @ConditionalOnMissingBean
    public HttpRequestValidator httpRequestValidator() {
        return new HttpRequestValidator();
    }
}

测试

服务提供方

/**
 * @author kun
 * @data 2022/1/22 14:25
 */
@HttpRequest("demo")
public interface DemoHttpService {

    @HttpRequest("checkSuccess")
    String checkSuccess(@MultiRequestBody("param1") String param1, @MultiRequestBody("param2") String param2);
}

服务调用方

/**
 * @author kun
 * @data 2022/1/22 14:37
 */
@Configuration
public class HttpConfig {

    @HttpConsumer(domain = "localhost", port = "8080")
    private DemoHttpService demoHttpService;
}
/**
 * @author kun
 * @data 2022/1/22 14:40
 */
@Component
public class DemoConsumer {
    @Resource
    private DemoHttpService demoHttpService;

    public String checkSuccess() {
        return demoHttpService.checkSuccess("param1", "param2");
    }
}

完整代码地址

https://github.com/wanggangkun/myFeign

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容