背景
在一些以freemarker
或者jsp
为模板引擎的spring mvc
项目开发过程中,通常会在本地联调不同环境的服务,毕竟部署到线上调试是极其不方便的,本文将以实际项目为例捋清楚请求的各链路环节,以及如何切换到不同环境。
流程
以agentBuy(Spring MVC)
项目为例,梳理出从浏览器访问页面,到服务端controller
层接收到请求,调用service
层服务或者远程服务调用,访问redis
,查库MySQL
,消费消息,最终将业务数据注入到模板中,由引擎编译成html
返回给浏览器渲染。
agentBuy-service
服务是一个Spring MVC
服务,controller
层接收到请求后,可能调用自身service
服务或者其他远程service
服务获取业务数据注入ftl
模板,然后freemarker
引擎将模板编译成html
并返回给浏览器渲染。这一过程的业务数据可能来源于redis
缓存,或者MySQL
数据库,再或者ES
;- 浏览器加载解析
html
过程中可能会拉取CassEC-Static
静态资源服务上的静态资源; - 页面加载完后,交互过程中可能会请求其他
web
服务,获取业务数据; - 页面加载完后,
message-service
消息服务也可能会向浏览器推送消息;
以上所有流程默认走的alpha
环境配置,如果想本地联调hwbeta
、gamma
环境,就需要搞清楚各个链路的调用过程以及配置。
远程服务调用
Spring Cloud核心组件:Eureka
假设现在开发一个电商网站,agentBuy-service
需要向inquiry-service
获取用户地址,但是agentBuy-service
压根就不知道所依赖的服务在那台机器上,他就算想要发起一个请求,都不知道发送给谁,有心无力!
这时候,就轮到Spring Cloud Eureka
出场了。Eureka
是微服务架构中的注册中心,专门负责服务的注册与发现,其官方结构图如下:
Spring-Cloud Euraka
是Spring Cloud
集合中一个组件,它是对Euraka
的集成,用于服务注册和发现。Eureka
是Netflix
中的一个开源框架。它和 zookeeper
、Consul
一样,都是用于服务注册管理的,同样,Spring-Cloud
还集成了Zookeeper
和Consul
。
Eureka
由多个instance
(服务实例)组成,这些服务实例可以分为两种:Eureka Server
和Eureka Client
。为了便于理解,我们将Eureka client
再分为Service Provider
和Service Consumer
。
Eureka Server
为服务注册中心,向外暴露自己的地址,负责管理、记录服务提供者的信息,同时将符合要求的服务提供者地址列表返回服务消费者Service Provider
为服务提供者,在服务启动后,服务提供者向Eureka
注册自己的IP
、端口、提供服务等信息,并定时续约更新自己的状态等Service Consumer
为服务消费者,通过Eureka Server
发现得到所需服务的提供者地址信息,然后向服务提供者发起远程调用
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=false
和eureka.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
时自行封装服务调用客户端的开发量。不需要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理。
这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!
看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事儿压根儿就不是地球人能干的。
Feign
早已为我们提供好了优雅的解决方案。来看看如果用Feign
的话,你的订单服务调用库存服务的代码会变成啥样?
看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 FeignClient
接口,然后调用那个接口就可以了。人家Feign Client
会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign
全给你干了。
那么问题来了,Feign
是如何做到这么神奇的呢?很简单,Feign
的一个关键机制就是使用了动态代理。咱们一起来看看下面的图,结合图来分析:
- 首先,如果你对某个接口定义了
@FeignClient
注解,Feign
就会针对这个接口创建一个动态代理 - 接着你要是调用那个接口,本质就是会调用
Feign
创建的动态代理,这是核心中的核心 Feign
的动态代理会根据你在接口上的@RequestMapping
等注解,来动态构造出你要请求的服务的地址- 最后针对这个地址,发起请求、解析响应
agentBuy-service
是一个集成Spring MVC
搭建的web
项目,只要生成了bean
就可以使用@Autowired
注入实例,但agentBuy-service
要生成inquiry-service
的Feign
接口实例发起远程调用,不能使用@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
@EnableAutoConfiguration
是Spring Boot
实现自动化配置的核心注解,通过这个注解把spring
应用所需的bean注入容器中。@EnableAutoConfiguration
源码通过@Import
注入了一个ImportSelector
的实现类
AutoConfigurationImportSelector
,这个ImportSelector
最终实现根据我们的配置,动态加载所需的bean
因此,使用@Autowired
注入实例
另外,如果生成bean
时指定FeignClientBuilder
类中configAgentUrl
属性(远程服务地址),那么可以跨Eureka
定位服务(不需要通过Eureka
定位)。Spring Boot
服务Feign
接口配置中没有指定服务调用的url
地址,而采用指定服务名的方式,导致依赖方服务如果想调用Feign
接口,必须要和被依赖方服务注册在同一个注册中心上
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
就会针对这台机器,构造并发起请求。
以订单服务远程调用库存服务为例,描述Eureka
、Ribbon
、Feign
三者之间的协同关系:
参考:一文读懂SpringCloud与Eureka,Feign,Ribbon,Hystrix,Zuul核心组件间的关系
案例
【维修厂端】采购确认页
采购确认页是agentBuy-service Spring MVC
项目中的页面,调用过程过程相对复杂,依此为例讲解如何在本地切换到其他环境联调。整体来说需要更改以下配置:
(1)走本地网关,修改网关配置
routes.yml
如下配置
(2)agentBuy
可能调用其他service
层服务,默认会调用alpha
环境的service
服务,需要切换到hwbeta
环境内网域名
agentBuy-service
采用FeignClientBuilder
动态创建FeignClient
实例去发起远程调用,生成bean
时指定FeignClientBuilder
中configAgentUrl
,可以跨Eureka
调用服务,不需要通过Eureka Server
定位服务
common.yml
如下配置:
(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
的配置也需要更改
(5)服务端还会访问数据库,因此连接MySQL
的配置也需要更改
(6)网关默认调用alpha
环境服务OAuth2.0
授权登录,也需要切换到hwbeta
环境
(7)拉取CassEC-Static
静态资源
【供应商端】报价页面
报价页面也比较特殊,在webagent
和agentBuy
之间存在一个seller-center
异常网关服务,卖家中心请求都有经过这个服务,该服务会提供卖家中心slider
、topbar
、footer
公共模块,用sitemesh
嵌套agentBuy
中的内容页面
(1)更改网关配置
config/routes.yml
# 注意与保持上下文的缩进
...
- 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'
...
目的:
sellerMgr
分拆出两项(id命名不同),让agentBuy/seller/**
的请求路由到本地启动的seller-center
服务中;让其他不相关的请求路由到测试环境部署的seller-center
中。使侧边栏等其他信息可以正常显示。agentbuy-service
的uri
改为本地服务地址+端口,使除agentBuy/seller/**
外的其他的agentBuy/**
请求可以路由到本地的agentBuy
服务中。
(2)seller-center
工程配置
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
环境数据被污染了。
(2)假如agentBuy-service
依赖了web-seller
和web-market
两个web
服务,web-seller
远程调用了A
服务,web-market
远程调用了B
服务,但是本地联调只更改了web-seller
指向A
服务的远程调用地址(hwbeta
),而没有更改web-market
指向B
服务远程地址(还是alpha
),导致页面提交接口入参数据有部分来自于alpha
,有些数据来自hwbeta
,因此写入的数据是错误的,hwbeta
环境也被污染了。
hwbeta
、gamma
环境类似于一个沙箱,几乎完全隔离,确保了测试的可靠性,一旦环境被污染,在该环境测试结果的可靠性就得不到保证,因此并不建议大家通过上述方式本地联调
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。