百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

SpringMvc项目仿 springcloud openfeign 实现微服务下接口调用

yuyutoo 2024-12-06 20:39 1 浏览 0 评论

SpringMvc项目集成nacos、openfeign、Ribbon,仿 springcloud openfeign 实现微服务下接口调用

背景

近几年,公司新开发项目转为微服务架构,但有很多基于 SpringMvc 老系统,若都进行系统重构会消耗很大的人力、时间成本。故尝试在 SpringMvc 系统中通过集成 nacosfeign 的方式让老系统焕发第二春。

已知

1、nacos官方已提供SpringMvc集成示例
2、openfeign基于feign的微服务架构下服务之间调用解决方案,官方只提供了Spring Cloud版本

问题

1、公司当前SpringMvc项目基于Spring 4.x版本,尝试对Spring版本升级发现存在大量问题,本人能力有限故放弃。
2、SpringMvc项目为独立单体项目,存在独立的用户权限配置体系。

分析

1、nacos官方已提供了SpringMvc集成示例
2、openfeign虽没有SpringMvc版本,但好在作为开源项目,有项目源码可以参考

实现

SpringMvc集成nacos

添加依赖

<dependency>
   <groupId>com.alibaba.nacos</groupId>
   <artifactId>nacos-spring-context</artifactId>
   <version>{nacos.version}</version>
   <exclusions>
      <exclusion>
         <groupId>org.springframework</groupId>
         <artifactId>spring-context</artifactId>
      </exclusion>
   </exclusions>
</dependency>

spring-context与项目中引用的有冲突,故排除。 通过添加 @EnableNacosDiscovery 注解开启 Nacos Spring 的服务发现功能:

@Configuration
@EnableNacosDiscovery(globalProperties = @NacosProperties(serverAddr = "127.0.0.1:8848"))
public class NacosConfiguration {

}

注意:按照 nacos 官方集成到 spring 的例子配置后会发现 nacos 管理端可以查看到服务,但是一会就消失了,怀疑是 spring 服务未定时发送心跳链接导致。 查看nacos源代码中发送心跳链接部分:

# BeatReactor.java
private final ScheduledExecutorService executorService;

public BeatReactor(NamingProxy serverProxy, int threadCount) {
    this.serverProxy = serverProxy;
    this.executorService = new ScheduledThreadPoolExecutor(threadCount, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setDaemon(true);
            thread.setName("com.alibaba.nacos.naming.beat.sender");
            return thread;
        }
    });
}

/**
 * Add beat information.
 *
 * @param serviceName service name
 * @param beatInfo    beat information
 */
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
    String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
    BeatInfo existBeat = null;
    //fix #1733
    if ((existBeat = dom2Beat.remove(key)) != null) {
        existBeat.setStopped(true);
    }
    dom2Beat.put(key, beatInfo);
    executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

BeatReactor 在构造器中实例化了一个 ScheduledThreadPoolExecutor 在调用注册方法(addBeatInfo)时创建定时任务,在给定的延时后给 nacos 发送心跳信息

class BeatTask implements Runnable {

    BeatInfo beatInfo;

    public BeatTask(BeatInfo beatInfo) {
        this.beatInfo = beatInfo;
    }

    @Override
    public void run() {
        if (beatInfo.isStopped()) {
            return;
        }
        long nextTime = beatInfo.getPeriod();
        try {
            JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
            long interval = result.get("clientBeatInterval").asLong();
            boolean lightBeatEnabled = false;
            if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
                lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
            }
            BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
            if (interval > 0) {
                nextTime = interval;
            }
            int code = NamingResponseCode.OK;
            if (result.has(CommonParams.CODE)) {
                code = result.get(CommonParams.CODE).asInt();
            }
            if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                Instance instance = new Instance();
                instance.setPort(beatInfo.getPort());
                instance.setIp(beatInfo.getIp());
                instance.setWeight(beatInfo.getWeight());
                instance.setMetadata(beatInfo.getMetadata());
                instance.setClusterName(beatInfo.getCluster());
                instance.setServiceName(beatInfo.getServiceName());
                instance.setInstanceId(instance.getInstanceId());
                instance.setEphemeral(true);
                try {
                    serverProxy.registerService(beatInfo.getServiceName(),
                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                } catch (Exception ignore) {
                }
            }
        } catch (NacosException ex) {
            NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());

        }
        # 循环发送心跳信息
        executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
    }
}

BeatTask#run 方法中可以看到在执行 registerService 后会重复创建定时任务以达到在特定时间重复向 nacos 注册服务信息。

综上可知,spring 服务想要持续向 nacos 发送心跳信息,需手动调用一次nacos的实例注册方法,nacos 配置类修改为:

/**
 * @author: kkfan
 * @create: 2021-07-08 15:54:44
 * @description: nacos 配置
 */
@Configuration
@EnableNacosDiscovery(globalProperties = @NacosProperties)
// 加载 nacos 服务配置信息
@PropertySource(value = "classpath:nacos.properties")
public class NacosConfiguration {

    @Value("${nacos.group-name:PLATFORM-01}")
    private String groupName;

    @Value("${server.port}")
    private String port;

    @Value("${nacos.service-name:platform1}")
    private String serviceName;

    @NacosInjected
    private NamingService namingService;

    @NacosInjected(properties = @NacosProperties(encode = "UTF-8"))
    private NamingService namingServiceUTF8;

    @PostConstruct
    public void init() {
        try {
            InetAddress address = InetAddress.getLocalHost();
            if (namingService != namingServiceUTF8) {
                throw new RuntimeException("nacos service registration failed");
            } else {
                namingService.registerInstance(serviceName, groupName, address.getHostAddress(), Integer.parseInt(port));
            }
        } catch (UnknownHostException | NacosException e) {
            e.printStackTrace();
        }
    }

}

  • @NacosInjected 是一个核心注解,用于在 Spring Beans 中注入ConfigServiceNamingService 实例,并使这些实例可缓存。 这意味着如果它们的 @NacosProperties 相等,则实例将是相同的,无论属性是来自全局还是自定义的 Nacos 属性。

spring 集成 openfeign

openfeign 是一种声明式的web服务客户端,在 spring cloud 中,仅需创建一个接口并对其进行几行注释即可实现调用远程服务就像调用本地方法一样,开发者完全感知不到是在调用远程方法,更没有像 HttpClient 那样相对繁琐的请求参数封装与响应解析。但遗憾的是官方只提供了 Spring Cloud 版本。本文将参照 spring-cloud-openfeignspring mvc 项目中使用 feign 实现远程服务的调用。

本文参考 spring-cloud-starter-openfeign 版本为 2.0.0.RELEASE,以下简称 openfeign

spring-cloud-openfeign 源码分析

  1. 从开启 openfeign 服务注解 @EnableFeignClients 开始
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

   ...
}

EnableFeignClientsspringIOC 容器导入了一个 FeignClientsRegistrar 实例。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
      ResourceLoaderAware, EnvironmentAware {

}

FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,使用 @Import,如果括号中导入的类是 ImportBeanDefinitionRegistrar 的实现类,则会调用接口方法 registerBeanDefinitions,将其中要注册的类注册成 bean

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   // 注册默认配置
   registerDefaultConfiguration(metadata, registry);
   // 注册 feignClients
   registerFeignClients(metadata, registry);
}

BeanDefinitionRegistryspring 中动态注册 beanDefinition 的接口。

registerDefaultConfiguration 用来注册 EnableFeignClients 中提供的自定义配置类中的 Bean,我们主要来看 registerFeignClients

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   // 类扫描
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);
   // 存储类扫描路径
   Set<String> basePackages;
   // 获取EnableFeignClients注解属性
   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   // 注解filter -> FeignClient
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   // 获取EnableFeignClients上是否配置clients属性
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   // if ... else 主要是确定类扫描路径和添加扫描过滤器
   if (clients == null || clients.length == 0) {
      // 类路径扫描器添加过滤器
      scanner.addIncludeFilter(annotationTypeFilter);
      // 获取EnableFeignClients上配置的扫描路径 若不存在则获取EnableFeignClients类所在路径
      basePackages = getBasePackages(metadata);
   }
   // 若配置了clients
   else {
      final Set<String> clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      // 获取 clients 配置类所在的包路径
      for (Class<?> clazz : clients) {
         basePackages.add(ClassUtils.getPackageName(clazz));
         clientClasses.add(clazz.getCanonicalName());
      }
      // 定义filter 根据给定的 ClassMetadata 对象确定匹配项。
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
         @Override
         protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\#34;, ".");
            return clientClasses.contains(cleaned);
         }
      };
      // 添加filter
      scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
   }
   // 开始根据包路径扫描 FeignClient
   for (String basePackage : basePackages) {
      // 扫描 FeignClient bean 定义
      Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
      for (BeanDefinition candidateComponent : candidateComponents) {
         // 判断类是否为带注解的Bean
         if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // 验证注解类是否是一个接口(注意是接口)
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(),
                  "@FeignClient can only be specified on an interface");
            // 获取FeignClient上配置的属性
            Map<String, Object> attributes = annotationMetadata
                  .getAnnotationAttributes(
                        FeignClient.class.getCanonicalName());
            // 获取 FeignClient 定义名称
            String name = getClientName(attributes);
            registerClientConfiguration(registry, name,
                  attributes.get("configuration"));
            # 注册 feign client
            registerFeignClient(registry, annotationMetadata, attributes);
         }
      }
   }
}

注意: FeignClient 注解标注的是接口 registerFeignClients 方法主要是为了获取 FeignClient 注解标注的接口

下面看注册 FeignClient 方法:

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   // 利用 BeanDefinitionBuilder 向 spring 容器中注入 bean

   String className = annotationMetadata.getClassName();

   // 这里要注意 FeignClientFactoryBean 将会在集成 ribbon 说明
   BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

   ...

   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

   ...

   // 到此完成了从 FeignClient 注释的接口到 BeanDefinition 转化
   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
         new String[] { alias });
   // 将转化后的 BeanDefinition 注入 spring 容器
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

到此 openfeign 完成了将 FeignClient 注解注释的接口信息注入通过 BeanDefinition 注入 spring 容器。

仿 openfeign 实现 FeignClient 接口发现与注册

  1. openfeign 中复制以下源码修改:
  1. 仿照 openfeignFeignClientsConfiguration 添加 FeignConfig 配置类
/**
 * @author: kkfan
 * @create: 2021-07-08 15:54:44
 * @description: feign 配置
 */
@Configuration
@EnableFeignClients(basePackages = "com.kk.feign")
public class FeignConfig {

    public FeignConfig() {
        try {
            // ribbon全局配置读入
            ConfigurationManager.loadPropertiesFromResources("ribbon.properties");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @NacosInjected
    private NamingService namingService;

    @Value("${nacos.group-name:PLATFORM-01}")
    private String groupName;

    @Bean
    public static FeignContext feignContext() {
        return new FeignContext();
    }

    @Bean
    public FeignLoggerFactory feignLoggerFactory() {
        return new DefaultFeignLoggerFactory(null);
    }

    @Bean
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder()
                .retryer(retryer);
    }

    @Bean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

    @Bean
    public Decoder feignDecoder() {
        return new JacksonDecoder();
    }

    @Bean
    public Encoder feignEncoder() {
        return new JacksonEncoder();
    }

    @Bean
    public Contract feignContract() {
        return new Contract.Default();
    }

    @Bean
    public FeignClientProperties feignClientProperties() {
        return new FeignClientProperties();
    }

    @Bean
    public Targeter feignTargeter() {
        return new Targeter.DefaultTargeter();
    }

}

至此完成了 feign 的集成,但还存在以下问题:

  1. FeignClient 注解类中的 SpringMvc 的注解不支持;
  2. 未和 nacos 集成使用,只能在 FeignClient 中指明调用地址。

下面来解决上面两个问题:

  1. 支持 SpringMvc 注解 参考 openfeign 中的 SpringMvcContract 把相关代码拷出来,相关代码如下:

注意由 spring 版本不同导致的兼容问题

修改 FeignConfig#feignContract 如下:

@Bean
public Contract feignContract() {
    return new SpringMvcContract();
}

  1. feign + nacos 集成 这部分实现主要为从 nacos 中获取已注册服务列表,feign 根据在 FeignClient 上配置的服务名来调用对应的服务,这部分将在下一节关于集成 ribbon 实现负载均衡中体现。

集成Ribbon

在集成完 nacos + feign 后下一个问题是 nacosfeign 都集成好了,如何把他们合在一起使用呢,我们接着看在上节中注册 feignClient 是说到的 FeignClientFactoryBean

class FeignClientFactoryBean
      implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    ...
}

其实现了 FactoryBean 接口,我们知道如果要使用 Bean 工厂,可以手动实现一个 FactoryBean 的类,改接口有三个方法如下:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

其中 isSingleton 是用来判断生产的 bean 是否是单例,有默认实现,我们不需要手动实现。getObject 方法是获得生产出来的 bean 对象,getObjectType 是用于获得生产对象的类。

现在来找下 FeignClientFactoryBeangetObject 的实现,代码如下:

@Override
public Object getObject() throws Exception {
    return getTarget();
}

/**
 * @param <T> the target type of the Feign client
 * @return a {@link Feign} client created with the specified data and the context
 * information
 */
<T> T getTarget() {
    FeignContext context = this.applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(this.url)) {
        if (!this.name.startsWith("http")) {
            this.url = "http://" + this.name;
        }
        else {
            this.url = this.name;
        }
        this.url += cleanPath();
        return (T) loadBalance(builder, context,
                new HardCodedTarget<>(this.type, this.name, this.url));
    }
    ...
}

可以看到调用了一个 loadBalance 方法,从字面意思上看负载均衡,应该就是想要的,接着往下看:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
        HardCodedTarget<T> target) {
    Client client = getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        Targeter targeter = get(context, Targeter.class);
        return targeter.target(this, builder, context, target);
    }

    throw new IllegalStateException(
            "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}

该方法接收一个 feign builder 和一个 feign context,打个断点调试下这段代码:

可以看到 getOption 从上下文中获取了一个 Client 实例 LoadBalancerFeignClient 后添加到 feign builder 中,现在问题就解决了,在 spring 集成 openfeign 一节中有创建 feignBuilder,在其中加入ribbon client 即可,代码如下:

@Bean
public Feign.Builder feignBuilder(Retryer retryer) {
    return Feign.builder()
            .retryer(retryer)
            .client(ribbonClient())
            .requestInterceptor(new KkRequestInterceptor(new ObjectMapper()));
}

/**
 * 构建负载均衡
 * @return
 */
private RibbonClient ribbonClient() {
    return RibbonClient.builder().lbClientFactory(clientName -> {
        log.info("初始化客户端: ---------》" + clientName);
        IClientConfig config = ClientFactory.getNamedConfig(clientName);

//            ZoneAwareLoadBalancer zb = new ZoneAwareLoadBalancer(config, zoneAvoidanceRule(), ribbonPing(), ribbonServerList(), ribbonServerListFilter(), ribbonServerListUpdater());
        ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName);
        ZoneAwareLoadBalancer zb = (ZoneAwareLoadBalancer) lb;
        zb.setRule(zoneAvoidanceRule());
        zb.setServersList(getByServerName(clientName));
        return LBClient.create(zb, config);
    }).build();
}

其中 ribbon 负载均衡策略如下:

/**
 * Ribbon负载均衡策略实现
 * 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,
 * 剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。
 * @return
 */
private IRule zoneAvoidanceRule() {
    return new ZoneAvoidanceRule();
}

可用服务列表根据服务名称从nacos中读取:

/**
 * 从nacos读取服务, 封装节点
 * @param name
 * @return
 */
private List<Server> getByServerName(String name) {
    List<Server> servers = new ArrayList<>();
    try {
        List<Instance> allInstances = namingService.getAllInstances(name, groupName);
        allInstances.forEach(x -> {
            Server server = new Server(x.getIp(), x.getPort());
            server.setZone(name);
            servers.add(server);
        });
    } catch (NacosException e) {
        e.printStackTrace();
    }
    return servers;
}

集成完 ribbon 后至此就完成了 spring 集成 openfeign 中的 feign + nacos 集成小节。

相关推荐

史上最全的浏览器兼容性问题和解决方案

微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...

平面设计基础知识_平面设计基础知识实验收获与总结
平面设计基础知识_平面设计基础知识实验收获与总结

CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...

2025-02-21 16:01 yuyutoo

写作排版简单三步就行-工具篇_作文排版模板

和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...

写一个2048的游戏_2048小游戏功能实现

1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...

今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……

  最近的天气暖和得让很多小伙伴们喊“热”!!!  昨天的气温到底升得有多高呢?你家有没有榜上有名?...

CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式

之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...

柠檬科技肖勃飞:大数据风控助力信用社会建设

...

你的自我界限够强大吗?_你的自我界限够强大吗英文

我的结果:A、该设立新的界限...

行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?

行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...

让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华

去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...

2025-02-21 16:00 yuyutoo

今年国家综合性消防救援队伍计划招录消防员15000名

记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...

一起盘点最新 Chrome v133 的5大主流特性 ?

1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...

竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使

style="text-align:center;"data-mce-style="text-align:...

学物理能做什么?_学物理能做什么 卢昌海

作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...

你不知道的关于这只眯眼兔的6个小秘密
你不知道的关于这只眯眼兔的6个小秘密

在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...

2025-02-21 16:00 yuyutoo

取消回复欢迎 发表评论: