手写OpenFeign(简易版)

news2025/1/10 23:34:16

Remoting组件实现

  • 1. 前言
  • 2. 原理说明
  • 3. 远程调用组件实现---自定义注解
    • 3.1 添加Spring依赖
    • 3.2 编写@EnableRemoting注解
    • 3.3 编写@RemoteClient注解
    • 3.4 编写@GetMapping注解
  • 4. 远程调用组件实现---生成代理类
    • 4.1 编写自定义BeanDefinition注册器
    • 4.2 编写自定义包扫描器
    • 4.3 编写FactoryBean
    • 4.4 编写远程调用接口的默认实现
    • 4.5 http调用工具类
    • 4.6 启动类上添加@EnableRemoting注解
  • 5. 成果展现

1. 前言

今日有一个需求中需要调用其他服务的接口,由于该项目并非SpringCloud项目,所以先使用OkHttp实现远程调用,但是总觉得有点low,于是想手写一个远程调用组件,当然其实你不自己写,使用OpenFeign也是可以的。

关于以下示例代码,若是复制过去有爆红的可以自行修改,比较简单的代码了。

2. 原理说明

项目启动时,扫描带有自定义注解的接口,创建动态代理,并注册到IOC容器。
其实OpenFeign的原理也是如此,只不过OpenFeign中逻辑复杂,但最核心不外乎动态代理、注册BeanDefinition

3. 远程调用组件实现—自定义注解

3.1 添加Spring依赖

新建Module:Remoting,添加以下依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!-- okhttp -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
</dependency>

3.2 编写@EnableRemoting注解

此注解作用与OpenFeign的@EnableFeignClients作用一致,执行一些初始化操作,例如扫描我们自定义@RemoteClient注解。

com.xczs.remoting.register.RemoteClientsRegistrar;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(RemoteClientsRegistrar.class)
public @interface EnableRemoting {

    /**
     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
     * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
     * {@code @ComponentScan(basePackages="org.my.pkg")}.
     * @return the array of 'basePackages'.
     */
    String[] value() default {};

    /**
     * Base packages to scan for annotated components.
     * <p>
     * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
     * <p>
     * @return the array of 'basePackages'.
     */
    String[] basePackages() default {};

}

3.3 编写@RemoteClient注解

此注解作用与OpenFeign的@FeignClient作用一致,用于标识我们需要生成代理的接口,创建代理类并注册到Spring的IOC容器。

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RemoteClient {

    String value() default "";

    /**
     * @return 绝对 URL 或可解析的主机名(协议是可选的)。
     */
    String url() default "";

}

3.4 编写@GetMapping注解

用于标识该方法是get方法。

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetMapping {

    String value() default "";

}

4. 远程调用组件实现—生成代理类

4.1 编写自定义BeanDefinition注册器

RemoteClientsRegistrar.java

import com.xczs.remoting.annotation.EnableRemoting;
import com.xczs.remoting.scanner.RemoteClientClassPathBeanDefinitionScanner;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;
import java.util.Objects;

public class RemoteClientsRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableRemoting.class.getName());
        AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(attributes);
        String[] basePackages = null;
        if (Objects.nonNull(annotationAttributes.getStringArray("value"))) {
            basePackages = annotationAttributes.getStringArray("value");
        }
        if (Objects.isNull(basePackages) || basePackages.length == 0) {
            basePackages = (String[])attributes.get("basePackages");
        }
        if (Objects.isNull(basePackages) || basePackages.length == 0) {
            throw new IllegalArgumentException("@EnableRemoting注解value或basePackages属性不可同时为空.");
        }
        RemoteClientClassPathBeanDefinitionScanner scanner = new RemoteClientClassPathBeanDefinitionScanner(registry, false);
        scanner.doScan(basePackages);
    }
}

4.2 编写自定义包扫描器

RemoteClientClassPathBeanDefinitionScanner.java

import com.xczs.remoting.annotation.RemoteClient;
import com.xczs.remoting.bean.RemoteClientFactoryBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;

import java.util.Set;

@Slf4j
public class RemoteClientClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {

    public RemoteClientClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
        super(registry, useDefaultFilters);
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 添加过滤器, 只扫描添加了 RemoteClient 注解的类
        addIncludeFilter(new AnnotationTypeFilter(RemoteClient.class));
        Set<BeanDefinitionHolder> beanDefinitionHolderSet = super.doScan(basePackages);
        // 对扫描到的数据进行代理处理
        processBeanDefinitions(beanDefinitionHolderSet);
        return beanDefinitionHolderSet;
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitionHolderSet) {
        beanDefinitionHolderSet.forEach(beanDefinitionHolder -> {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
            if (beanDefinition instanceof AnnotatedBeanDefinition) {
                AnnotationMetadata annotationMetadata = ((AnnotatedBeanDefinition) beanDefinition).getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@RemoteClient只能使用在接口上");
            }
            // 设置工厂等操作需要基于GenericBeanDefinition, BeanDefinitionHolder是其子类
            GenericBeanDefinition definition = (GenericBeanDefinition) beanDefinition;
            // 获取接口的全路径名称
            String beanClassName = definition.getBeanClassName();
            // 设置构造函数参数
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            // 设置工厂
            definition.setBeanClass(RemoteClientFactoryBean.class);
            definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
        });
    }
}

4.3 编写FactoryBean

RemoteClientFactoryBean.java

import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.Proxy;

public class RemoteClientFactoryBean<T> implements FactoryBean<T> {

    private Class<T> type;

    public RemoteClientFactoryBean(Class<T> type) {
        this.type = type;
    }

    @Override
    public T getObject() throws Exception {
        // 因为 DefaultRemoteClient 需要Class<T>作为参数, 所以该类包含一个Class<T>的成员, 通过构造函数初始化
        return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type},
                new DefaultRemoteClient<>(type));
    }

    @Override
    public Class<?> getObjectType() {
        // 该方法返回的getObject()方法返回对象的类型,这里是基于type生成的代理对象, 所以类型就是上面定义的type
        return type;
    }
}

4.4 编写远程调用接口的默认实现

DefaultRemoteClient.java

import com.xczs.core.utils.OkHttpUtils;
import com.xczs.remoting.annotation.GetMapping;
import com.xczs.remoting.annotation.RemoteClient;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

@Slf4j
public class DefaultRemoteClient<T> implements InvocationHandler {

    /**
     * 这里声明一个Class, 用来接收接口声明的泛型实际类型的class, T是声明的实体类类型
     */
    private Class<T> type;

    public DefaultRemoteClient(Class<T> type) {
        this.type = type;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Object 方法,走原生方法, 比如 hashCode()
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 其它走动态代理
        Class<?> declaringClass = method.getDeclaringClass();
        RemoteClient remoteClient = declaringClass.getAnnotation(RemoteClient.class);
        String domain = remoteClient.url();

        Annotation[] annotations = method.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof GetMapping) {
                GetMapping getAnno = (GetMapping) annotation;
                String uri = getAnno.value();
                String url = domain + uri;
                Response response = OkHttpUtils.doExecuteGet(url);
                String resp = response.body().string();
                return resp;
            }
        }

        return null;
    }
}

4.5 http调用工具类

OkHttpUtils工具类

import lombok.extern.slf4j.Slf4j;
import okhttp3.*;

import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
public class OkHttpUtils {

    private static OkHttpClient okHttpClient = null;

    private static final MediaType mediaType = MediaType.parse(org.springframework.http.MediaType.APPLICATION_JSON_VALUE);

    public static Response doExecuteGet(String url) throws IOException {
        return doExecuteGet(url, null);
    }

    public static Response doExecuteGet(String url, Headers headers) throws IOException {
        init();
        // 创建request对象
        Request.Builder builder = new Request.Builder().url(url);
        if (Objects.nonNull(headers)) {
            builder.headers(headers);
        }
        Request request = builder.build();
        Response response = okHttpClient.newCall(request).execute();
        if (!response.isSuccessful()) {
            log.error("OkHttpUtils.doExecuteGet:{}", response.body().string());
            throw new RPCException(response.code() + "", response.message());
        }
        return response;
    }

    public static Response doExecutePost(String url, Object body) throws IOException {
        return doExecutePost(url, null, body);
    }

    public static Response doExecutePost(String url, Headers headers, Object body) throws IOException {
        init();
        RequestBody requestBody = RequestBody.create(mediaType, JSONUtil.writeValueAsString(body));

        // 创建request对象
        Request.Builder builder = new Request.Builder().url(url).post(requestBody);
        if (Objects.nonNull(headers)) {
            builder.headers(headers);
        }
        Request request = builder.build();
        Response response = okHttpClient.newCall(request).execute();
        if (!response.isSuccessful()) {
            log.error("OkHttpUtils.doExecutePost:{}", response.body().string());
            throw new RPCException(response.message());
        }
        return response;
    }

    private static synchronized void init() {
        if (Objects.isNull(okHttpClient)) {
            okHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(60, TimeUnit.SECONDS)
                    .writeTimeout(60, TimeUnit.SECONDS)
                    .build();
        }
    }

}

4.6 启动类上添加@EnableRemoting注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRemoting(basePackages = {"com"})
public class TestApplication {

    public static void main(String[] args) {
       SpringApplication.run(TestApplication.class, args);
    }

}

5. 成果展现

服务提供方:
在这里插入图片描述
远程调用接口:
在这里插入图片描述
测试类:
在这里插入图片描述
调用结果:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1386391.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL篇—通过Clone插件进行远程克隆数据(第三篇,总共三篇)

在介绍 Clone 最终篇之前&#xff0c;我们先简要回顾一下前面所讲的内容。在第一篇中&#xff0c;我们探讨了 Clone 的用途、使用的前提条件、存在的限制&#xff0c;以及它的备份原理。Clone 是一种用于复制和备份数据的工具&#xff0c;它能够快速高效地创建数据的精确副本。…

如何在simulink中怎么获取足端轨迹代码解释?

在使用Java代码框架统计用户获取足端轨迹时&#xff0c;我们可以使用Simulink的外部接口功能和Java的网络编程来实现。 首先&#xff0c;我们需要在Simulink中配置外部接口以便与Java进行通信。可以使用Simulink中的TCP/IP或UDP模块来实现网络通信。假设我们选择TCP/IP模块。 …

在线培训系统开发

随着远程学习和数字化教育的兴起&#xff0c;在线培训系统成为了教育领域的重要组成部分。在这篇文章中&#xff0c;我们将探讨在线培训系统开发的一些关键技术和概念。 前端开发 在在线培训系统中&#xff0c;前端开发起着至关重要的作用。使用现代的前端框架如React、Vue或…

Linux系统命令 --- seq tr cut sort uniq

目录 一、seq ---- 输出序列化参数 1、seq 数字 按照顺序打印 2、-s 使用指定字符串分割数字 3、计算1-20&#xff0c;并求和 4、-w 在每一列数字前加零 默认补全 二、tr、对数字进行处理 1、替换 2、删除 3、压缩 4、补集 三、cut 截取 四、sort 排序 …

开源内容管理系统Wagtail本地安装运行并结合内网穿透实现公网访问

文章目录 前言1. 安装并运行Wagtail1.1 创建并激活虚拟环境 2. 安装cpolar内网穿透工具3. 实现Wagtail公网访问4. 固定的Wagtail公网地址 前言 Wagtail是一个用Python编写的开源CMS&#xff0c;建立在Django Web框架上。Wagtail 是一个基于 Django 的开源内容管理系统&#xf…

永不停止,永远在路上!MIAOYUN 2023年度回顾

2023 MIAOYUN年度关键词&#xff1a;坚持/沉淀/成长 2023年&#xff0c;我们身处虚浮遥荡的世界&#xff1a;支原体肺炎、流感接二连三、经济下行成热词、人人思危&#xff1b;更有暴雨成灾&#xff0c;核污水扩散&#xff0c;战火与地震不断。 坏事发生时&#xff0c;你有三种…

Android中两种选择联系人方式

1.在选择联系人方式网上也有很多案例 有的说是使用ContactsContract.CommonDataKinds.Phone.CONTENT_URI也有的说是使用ContactsContract.Contacts.CONTENT_URI其实这两种方式都可以使用 只不过ContactsContract.Contacts.CONTENT_URI这种方式需要多查询一遍 一、使用Contacts…

文心一言 VS 讯飞星火 VS chatgpt (179)-- 算法导论13.4 2题

二、用go语言&#xff0c;在 RB-DELETE 中&#xff0c;如果 x 和 x.p 都是红色的&#xff0c;证明&#xff1a;可以通过调用 RB-DELETE-FIXUP(T&#xff0c;x)来恢复性质 4。 文心一言&#xff1a; 在红黑树中&#xff0c;性质4指的是&#xff1a;每个节点要么是红色&#xf…

前端react入门day03-react获取dom与组件通信

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 受控表单绑定 React中获取DOM 组件通信 父传子 父传子-基础实现 父传子-props说明 父传子 - 特殊的…

Linux定时任务调度以及磁盘分区、挂载

一、定时任务调度 1、crond任务调度 定时任务设置 是指系统在某个时间执行的特定命令或程序 任务调度分类&#xff1a; ①系统工作&#xff1a;有些重要的工作必须周而复始地执行&#xff0c;如病毒扫描 ②个别用户工作&#xff1a;个别用户可能希望执行某些程序&#xff0c;比…

2024抖店选品方法,及侧重方向思路(全新版本),可收藏备用

我是王路飞。 做无货源抖店的商家&#xff0c;牢记【选品重于泰山】这句话。 要知道电商的本质就是产品&#xff0c;你所有的运营手段也都是围绕产品进行的&#xff0c;店铺内的流量也都是冲着产品来的。 产品不行&#xff0c;哪怕再多的流量、再高的曝光率&#xff0c;也带…

Docker镜像构建优化及上传

前言 Dockerfile 是一个文本格式的配置文件&#xff0c; 用户可以使用 Dockerfile 来快速创建自定义的镜像&#xff0c;另外&#xff0c;使 用 Dockerfile 去构建镜像好比使用 pom 去构建 maven 项目一样&#xff0c;有异曲同工之妙 一.Dockerfile镜像构建 1.Dockerfile基本…

【教学类-43-21】20240113 数独(三)11-12-13-14-15宫格 无空行A4模板 上下结构(附加3宫格 4宫格)

作品展示&#xff1a; 11-15宫格 A4 两份 下面空行做一点4-5宫格题目 &#xff1a; 已经制作没有分割线的连在一起的3-10宫格模板 【教学类-43-19】20240113 数独&#xff08;一&#xff09; 3-5-6-7-8-10宫格 无空行A4模板-CSDN博客文章浏览阅读399次&#xff0c;点赞13次…

架构师 - 架构师是做什么的 - 学习总结

架构师核心定义 架构师是什么 架构师是业务和技术之间的桥梁 架构师的核心职责是消除不确定性、和降低复杂性 架构设计环 架构师的三个核心能力 架构师的三个关键思维 架构师主要职责 架构设计 Vs 方案设计 架构设计前期 主要任务 澄清不确定性 明确利益干系人的诉求消除冲…

Python中执行定时任务详细教程与示例代码

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 定时任务是自动化执行的一种方式&#xff0c;它可以在指定的时间间隔或特定时间点运行Python代码。无论是自动化数据备份、定期清理文件还是其他周期性任务&#xff0c;Python都提供了多种方式来执行定时任务。本…

Java实现天然气工程运维系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程 12.2.2 流程 22.3 各角色功能2.3.1 系统管理员功能2.3.2 用户服务部功能2.3.3 分公司&#xff08;施工单位&#xff09;功能2.3.3.1 技术员角色功能2.3.3.2 材料员角色功能 2.3.4 安…

2023 China DevOpsDays(DOD) DXCon 国际数字化转型与创新管理企业峰会:核心内容与学习收获(附大会核心PPT下载)

随着科技的飞速发展&#xff0c;数字化转型已成为企业持续发展的必经之路。2023年的China DevOpsDays & DXCon国际数字化转型与创新管理企业峰会&#xff0c;汇集了业界顶尖的专家、学者和企业领袖&#xff0c;共同探讨数字化转型的最新趋势和实践。本文将深入剖析大会的核…

MySQl导入与导出远程备份

文章目录 一. navicat导入导出 二. mysqldump命令导入导出导入导出 三. load data infile命令导入导出导入导出 四. 远程备份导入导出思维导图 一. navicat 导入 右键——>运行SQL文件 导出 选中要导出的表➡右键➡转储SQL文件➡数据和结构 二. mysqldump命令导入导出…

QTabelView使用代理自定义,第一列为QLabel第二列为下拉框

预览界面 代理源文件 CustomParamViewDelegate.cpp #include "CustomParamViewDelegate.h"CustomParamViewDelegate::CustomParamViewDelegate(QObject *parent): QStyledItemDelegate(parent) {}CustomParamViewDelegate::~CustomParamViewDelegate() {}QWidget* …

postgresql16 物理复制与逻辑复制的实现和对比

本文面向想要练习 PostgreSQL 中数据库复制基础知识但可能无法访问远程服务器的初学者。我认为学习新技术时&#xff0c;在自己的机器上运行示例以巩固概念是至关重要的。对于副本来说&#xff0c;这可能很困难&#xff0c;因为许多可用的资源假设用户具有一定的 PostgreSQL 经…