背景

在一些以freemarker或者jsp为模板引擎的spring mvc项目开发过程中,通常会在本地联调不同环境的服务,毕竟部署到线上调试是极其不方便的,本文将以实际项目为例捋清楚请求的各链路环节,以及如何切换到不同环境。

流程

agentBuy(Spring MVC)项目为例,梳理出从浏览器访问页面,到服务端controller层接收到请求,调用service层服务或者远程服务调用,访问redis,查库MySQL,消费消息,最终将业务数据注入到模板中,由引擎编译成html返回给浏览器渲染。

本地联调不同测试环境.png

  • agentBuy-service服务是一个Spring MVC服务,controller层接收到请求后,可能调用自身service服务或者其他远程service服务获取业务数据注入ftl模板,然后freemarker引擎将模板编译成html并返回给浏览器渲染。这一过程的业务数据可能来源于redis缓存,或者MySQL数据库,再或者ES
  • 浏览器加载解析html过程中可能会拉取CassEC-Static静态资源服务上的静态资源;
  • 页面加载完后,交互过程中可能会请求其他web服务,获取业务数据;
  • 页面加载完后,message-service消息服务也可能会向浏览器推送消息;

以上所有流程默认走的alpha环境配置,如果想本地联调hwbetagamma环境,就需要搞清楚各个链路的调用过程以及配置。

远程服务调用

Spring Cloud核心组件:Eureka

假设现在开发一个电商网站,agentBuy-service需要向inquiry-service获取用户地址,但是agentBuy-service压根就不知道所依赖的服务在那台机器上,他就算想要发起一个请求,都不知道发送给谁,有心无力!

这时候,就轮到Spring Cloud Eureka出场了。Eureka是微服务架构中的注册中心,专门负责服务的注册与发现,其官方结构图如下:

eureka.png

Spring-Cloud EurakaSpring Cloud集合中一个组件,它是对Euraka的集成,用于服务注册和发现。EurekaNetflix中的一个开源框架。它和 zookeeperConsul一样,都是用于服务注册管理的,同样,Spring-Cloud 还集成了ZookeeperConsul

Eureka由多个instance(服务实例)组成,这些服务实例可以分为两种:Eureka ServerEureka Client。为了便于理解,我们将Eureka client再分为Service ProviderService Consumer

  • Eureka Server 为服务注册中心,向外暴露自己的地址,负责管理、记录服务提供者的信息,同时将符合要求的服务提供者地址列表返回服务消费者
  • Service Provider 为服务提供者,在服务启动后,服务提供者向Eureka注册自己的IP、端口、提供服务等信息,并定时续约更新自己的状态等
  • Service Consumer为服务消费者,通过Eureka Server发现得到所需服务的提供者地址信息,然后向服务提供者发起远程调用

Eureka (1).png

agentBuy-service是一个Spring MVC服务,不需要向其他服务提供服务,也就不需要将自己注册到Eureka Server,只需要作为Service Consumer调用其他远程服务。agentBuy-service配置成消费者服务如下:

(1)在pom文件中添加Eureka-client依赖

<dependency>
  <groupId>com.netflix.eureka</groupId>
  <artifactId>eureka-client</artifactId>
  <exclusions>
    <exclusion>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
    </exclusion>
  </exclusions>
</dependency>

(2)在eureka-client.properties配置文件中增加相关配置信息,设置不需要注册到Eureka Server,添加eureka.registration.enabled=falseeureka.registerWithEureka=false,详细如下:

// eureka-client.properties
###Eureka Client configuration for Sample Eureka Client

# see the README in eureka-examples to see an overview of the example set up

# note that for a purely client usage (e.g. only used to get information about other services,
# there is no need for registration. This property applies to the singleton DiscoveryClient so
# if you run a server that is both a service provider and also a service consumer,
# then don't set this property to false.
eureka.registration.enabled=false # 表示是否注册到Eureka Server,默认为true

## configuration related to reaching the eureka servers
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.registerWithEureka=false # 表示是否将自己注册到Eureka Server,默认为true

eureka.serviceUrl.default=http://10.118.71.204:8761/eureka/ # 注册中心地址

eureka.appinfo.replicate.interval=10

eureka.vipAddress=agentbuy-service # 定义服务名变量
eureka.name=${eureka.vipAddress} # 服务名,SpringCloud中服务调用的依据
eureka.port=8001 # 服务端口
#eureka.ipAddr=192.168.29.170
#eureka.instanceId=${eureka.vipAddress}:${eureka.port}
#eureka.hostname=${eureka.ipAddr}
eureka.homePageUrlPath=/${eureka.vipAddress}
#eureka.statusPageUrlPath=/${eureka.vipAddress}/status
#eureka.healthCheckUrlPath=/${eureka.vipAddress}/health
eureka.lease.renewalInterval=5
eureka.lease.duration=15
eureka.decoderName=JacksonJson

Spring Cloud核心组件:Feign

agentBuy-service还集成Feign实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。不需要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理。

这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!

img

看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事儿压根儿就不是地球人能干的。

Feign早已为我们提供好了优雅的解决方案。来看看如果用Feign的话,你的订单服务调用库存服务的代码会变成啥样?

img

看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。

那么问题来了,Feign是如何做到这么神奇的呢?很简单,Feign的一个关键机制就是使用了动态代理。咱们一起来看看下面的图,结合图来分析:

  • 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
  • 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
  • Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
  • 最后针对这个地址,发起请求、解析响应

Feign.png

agentBuy-service是一个集成Spring MVC搭建的web项目,只要生成了bean就可以使用@Autowired注入实例,但agentBuy-service要生成inquiry-serviceFeign接口实例发起远程调用,不能使用@Autowired注入实例,因为会报错找不到bean “Could not autowire. No beans of 'InquiryClient' type found”,所以项目中使用FeignClientBuilder动态创建FeignClient实例发起远程服务请求,缺陷是每次都需要手动创建,然后在作用域生命周期结束后销毁。

Spring Boot在启动类中使用了@SpringBootApplication,其内部封装了@ComponentScan,@EnableAutoConfiguration,@SpringBootConfiguration

  • @ComponentScan注解就是用来自动扫描被这些注解(@Service,@Repository,@Component,@Controller)标识的类,最终生成IOC容器里的bean
  • @SpringBootConfiguration用来声明当前类是一个配置类,可以通过@Bean注解生成IOC容器管理的bean
  • @EnableAutoConfigurationSpring Boot实现自动化配置的核心注解,通过这个注解把spring应用所需的bean注入容器中。@EnableAutoConfiguration源码通过@Import注入了一个ImportSelector的实现类
    AutoConfigurationImportSelector,这个ImportSelector最终实现根据我们的配置,动态加载所需的bean

因此,使用@Autowired注入实例

参考:简单讲讲@SpringBootApplication

另外,如果生成bean时指定FeignClientBuilder类中configAgentUrl属性(远程服务地址),那么可以跨Eureka定位服务(不需要通过Eureka定位)。Spring Boot服务Feign接口配置中没有指定服务调用的url地址,而采用指定服务名的方式,导致依赖方服务如果想调用Feign接口,必须要和被依赖方服务注册在同一个注册中心上

动态FeignClientBuilder.png

feign接口.png

bean.png

carbon (3).png

carbon (1).png

Spring Cloud核心组件:Ribbon

说完了Feign,还没完。现在新的问题又来了,如果人家库存服务部署在了5台机器上,如下所示:

  • 192.168.169:9000
  • 192.168.170:9000
  • 192.168.171:9000
  • 192.168.172:9000
  • 192.168.173:9000

这下麻烦了!人家Feign怎么知道该请求哪台机器呢?

这时Spring Cloud Ribbon就派上用场了。Ribbon就是专门解决这个问题的。它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上

Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。

此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:

  • 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
  • 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
  • Feign就会针对这台机器,构造并发起请求。

以订单服务远程调用库存服务为例,描述EurekaRibbonFeign三者之间的协同关系:

Eureka、Ribbon、Feign三者之间的关系.png

参考:一文读懂SpringCloud与Eureka,Feign,Ribbon,Hystrix,Zuul核心组件间的关系

案例

【维修厂端】采购确认页

采购确认页是agentBuy-service Spring MVC项目中的页面,调用过程过程相对复杂,依此为例讲解如何在本地切换到其他环境联调。整体来说需要更改以下配置:

(1)走本地网关,修改网关配置

routes.yml如下配置

image-20220111183527675.png

(2)agentBuy可能调用其他service层服务,默认会调用alpha环境的service服务,需要切换到hwbeta环境内网域名

agentBuy-service采用FeignClientBuilder动态创建FeignClient实例去发起远程调用,生成bean时指定FeignClientBuilderconfigAgentUrl,可以跨Eureka调用服务,不需要通过Eureka Server定位服务

common.yml如下配置:

image2022-1-5_15-56-10.png

image-20220111184644237.png

image-20220111183800956.png

(3)在页面交互过程还可能请求其他web服务,而被请求的web服务可能又调用了其他service服务(spi),此过程需要更改网关路由配置,使页面请求转发到本地web服务,web服务工程还需要更改配置,使之调用hwbeta环境的service服务,下面以web-market为例更改配置:

# webagent/config/routes.yml
- id: web-market
  predicates:
      - Path=/market/**
  #uri: 'lb://web-market'
  uri: 'http://localhost:9801' # 改成走本地web-market服务

web-market的环境变量是拉取的config-center配置工程,因此只需要更改环境变量spring.profiles.active=hwbeta,启用hwbeta环境的配置

# bootstrap.yml
server:
  port: 9801
  contextPath: /market
  tomcat:
    protocolHeader: X-Forwarded-Proto
encrypt.failOnError: false
spring:
  application:
    name: web-market
  cloud:
    config:
      uri: http://config-center.alpha-intra.casstime.com/conf
      label: alpha
management:
  port: 29801
  contextPath: /hellgate
  security.enabled: false
xmwlServiceUrl: https://xmwltest.casstime.com/edi/kaisi/getLogisticOrderTrace
wenjuanxing.key: 8560a160-d42b-40d6-a97e-48b12cf51e8b
---
spring:
  profiles: hwbeta
  cloud:
    config:
      uri: http://config-center.hwbeta-intra.casstime.com/conf # 改成去配置中心服务拉取hwbeta的配置,里面包含Eureka Server注册中心的地址
      label: hwbeta

---
spring:
  profiles: prod
  cloud:
    config:
      uri: http://config-server.intra.cassmall.com/conf
      label: master
xmwlServiceUrl: https://xmwl.cassmall.com/edi/kaisi/getLogisticOrderTrace

(4)服务端还可能会访问缓存redis,因此连接redis的配置也需要更改

image-20220725160631278.png

(5)服务端还会访问数据库,因此连接MySQL的配置也需要更改

image-20220725003603103.png

(6)网关默认调用alpha环境服务OAuth2.0授权登录,也需要切换到hwbeta环境

image-20220725160826820.png

(7)拉取CassEC-Static静态资源

image-20220729140026271.png

【供应商端】报价页面

报价页面也比较特殊,在webagentagentBuy之间存在一个seller-center异常网关服务,卖家中心请求都有经过这个服务,该服务会提供卖家中心slidertopbarfooter公共模块,用sitemesh嵌套agentBuy中的内容页面

(1)更改网关配置

config/routes.yml

image2021-4-8_17-48-58.png

image2021-4-8_17-43-43.png

# 注意与保持上下文的缩进
...
      - id: sellerMgr
        predicates:
        - Path=/sellerMgr/**, /storemgr/**, /casswallet/seller/**, /bill/seller/**, /invoice/seller/**, /scf/seller/**, /orders/seller/**, /member/seller/**
        uri: 'lb://sellercenter-portal'
      - id: sellerMgr2
        predicates:
        - Path=/agentBuy/seller/**
        # uri: 'lb://sellercenter-portal'
        uri: 'http://localhost:10102'
...

目的:

  1. sellerMgr分拆出两项(id命名不同),让agentBuy/seller/**的请求路由到本地启动的seller-center服务中;让其他不相关的请求路由到测试环境部署的seller-center中。使侧边栏等其他信息可以正常显示。
  2. agentbuy-serviceuri改为本地服务地址+端口,使除agentBuy/seller/**外的其他的agentBuy/**请求可以路由到本地的agentBuy服务中。

(2)seller-center工程配置

image2021-4-8_17-23-8.png

zuul.routes.agentBuy.url=http://localhost:8001
目的:让前面通过agentBuy/seller/**过来的请求,经本地的seller-center,路由至本地的agentBuy服务中。

本地联调其他环境的缺点

本地联调hwbeta、gamma环境会导致环境被污染,以下面两个场景为例作下说明:

(1)如果本地打通了hwbeta环境,本地agentBuy-service能调通hwbeta环境的远程服务quote-service,当quote-service发送一个消息推送,此时本地agentBuy-service会接收到消息,消费此消息。假如这条消息表示供应商报价完成,agentBuy-service接收到此消息后需要将shoppinglist表中的询价单状态更改为报价完成,但是如果本地连接的是alpha环境的数据库,这时是查找不到该询价单的,也就无法更改其报价状态,而该询价单正确状态应该是报价完成,实际却是未完成报价,导致hwbeta环境数据被污染了。

本地联调其他环境的缺点.png

(2)假如agentBuy-service依赖了web-sellerweb-market两个web服务,web-seller远程调用了A服务,web-market远程调用了B服务,但是本地联调只更改了web-seller指向A服务的远程调用地址(hwbeta),而没有更改web-market指向B服务远程地址(还是alpha),导致页面提交接口入参数据有部分来自于alpha,有些数据来自hwbeta,因此写入的数据是错误的,hwbeta环境也被污染了。

hwbetagamma环境类似于一个沙箱,几乎完全隔离,确保了测试的可靠性,一旦环境被污染,在该环境测试结果的可靠性就得不到保证,因此并不建议大家通过上述方式本地联调


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。