Spring Cloud学习教程2【面试+工作】
之前我们通过RestTemplate调用REST服务,代码是这样的:
虽然使用了Ribbon和Hystrix可以实现负载均衡和容错处理,但是这个编码在实现大量业务时会显得太过于冗余(如,多参数的URL拼接)。
有没有更加优雅的实现呢?
项目主页:https://github.com/OpenFeign/feign
在订单微服务中增加Feign的支持。
测试结果,一切正常。
写这样的代码,就可以访问RESTful服务啦?
流程分析:
1、 由于我们在入口@EnableFeignClients注解,Spring启动后会扫描标注了@FeignClient注解的接口,然后生成代理类2、 我们在@FeignClient接口中指定了value,其实就是指定了在Eureka中的服务名称
3、 在FeignClient中的定义方法以及使用了SpringMVC的注解,Feign就会根据注解中的内容生成对应的URL,然后基于Ribbon的负载均衡去调用REST服务
a) 为什么使用的是SpringMVC的注解?
i. 其实,Feign是有自己的注解的,是因为Spring Cloud对Feign做了增强,兼容了SpringMVC的注解,使我们的学习成本更低
ii. 专业的解释是这样的:
org.springframework.cloud.netflix.feign.FeignClientsConfiguration
设置的默认的契约是SpringMVC契约。
通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?
先来说说这样架构需要做的一些事儿以及存在的不足:
首先,破坏了服务无状态特点。
1.为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
2.从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
其次,无法直接复用既有接口。
1.当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
面对类似上面的问题,我们要如何解决呢? 答案是:服务网关!
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器 è 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
官网:https://github.com/Netflix/zuul
从架构图中可以看出,客户端请求微服务时,先经过Zuul之后再请求,这样就可以将一些类似于校验的业务逻辑放到zuul中完成。
而微服务自身只需要关注自己的业务逻辑即可。
server:
port: 6677 #服务端口
spring:
application:
name: itcasst-microservice-api-gateway #指定服务名
首先,查看Eureka中的服务:
可以看到,当前Eureka中有2个商品的微服务。
接下来,我们编写路由规则:
server:
port: 6677 #服务端口
spring:
application:
name: itcasst-microservice-api-gateway #指定服务名
zuul:
routes:
item-service: #item-service这个名字是任意写的
path: /item-service/** #配置请求URL的请求规则
url: http://127.0.0.1:8081 #真正的微服务地址
可以看到,已经通过zuul访问到了商品微服务。
在快速入门中我们已经通过了URL映射,访问到了商品微服务。这样做会存在一个问题,就是,如果商品微服务的地址发生了改变怎么办?
很自然的能够想到,不应该配置具体的url而是走Eureka注册中心获取地址。
server:
port: 6677 #服务端口
spring:
application:
name: itcasst-microservice-api-gateway #指定服务名
zuul:
routes:
item-service: #item-service这个名字是任意写的
path: /item-service/** #配置请求URL的请求规则
#url: http://127.0.0.1:8081 #真正的微服务地址
serviceId: itcast-microservice-item #指定Eureka注册中心中的服务id
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
查看Eureka注册中:
发现已经有itcasst-microservice-api-gateway在注册中心了。
接下来测试,功能是否正常:
发现一切正常。
过滤器是Zuul的重要组件。
ZuulFilter是一个抽象类,其实现类需要实现4个方法:
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。
2、 run:过滤器的具体业务逻辑。
3、 filterType:返回字符串代表过滤器的类型
a) pre:请求在被路由之前执行
b) routing:在路由请求时调用
c) post:在routing和errror过滤器之后调用
d) error:处理请求时发生错误调用
4、 filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
需求:通过编写过滤器实现用户是否登录的检查。
实现:通过判断请求中是否有token,如果有认为就是已经登录的,如果没有就认为是非法请求,响应401.
可以看到过滤器已经生效。
在我们开发项目时,需要有很多的配置项需要写在配置文件中,如:数据库的连接信息等。
这样看似没有问题,但是我们想一想,如果我们的项目已经启动运行,那么数据库服务器的ip地址发生了改变,我们该怎么办?
如果真是这样,我们的应用需要重新修改配置文件,然后重新启动,如果应用数量庞大,那么这个维护成本就太大了!
有没有好的办法解决呢?当然是有的,Spring Cloud Config提供了这样的功能,可以让我们统一管理配置文件,以及实时同步更新,并不需要重新启动应用程序。
Config Server是一个可横向扩展、集中式的配置服务器,它用于集中管理应用程序各个环境下的配置,默认使用Git存储配置文件内容,也可以使用SVN存储,或者是本地文件存储。
Config Client是Config Server的客户端,用于操作存储在Config Server中的配置内容。微服务在启动时会请求Config Server获取配置文件的内容,请求到后再启动容器。
使用Spring Cloud Config的架构:
准备3个文件:
microservice-dev.properties
microservice-production.properties
microservice-test.properties
该文件的命名规则是:{application}-{profile}.properties
其内容是(另外2个文件内容稍有不同即可):
推送文件到git服务器,这里使用的是我们内网的git服务器(Gogs),当然也可以使用github或者使用svn。
pom依赖:
server:
port: 6688 #服务端口
spring:
application:
name: itcasst-microservice-config-server #指定服务名
cloud:
config:
server:
git: #配置git仓库地址
uri: http://172.16.55.138:10080/zhangzhijun/itcast-config-server.git
#username: zhangzhijun
#password: 123456
测试已经看到了配置文件的内容。
请求配置文件的规则如下:
/{application}/{profile}/[label]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
其中{label}是指分支,默认是master。
我们在itcast-microservice-item项目中添加Config Client的支持。来读取JDBC的配置文件的内容。
spring:
cloud:
config:
name: microservice #对应的配置服务中的应用名称
uri: http://127.0.0.1:6869/
profile: dev #对应配置服务中的{profile}
label: master #对应的分支
编写对象通过@Value注解读取Config Server中的值。
在ItemController中编写test方法:
测试结果显示,已经从Config Server中获取到配置文件的内容。
如果git服务器中的配置文件更新了怎么办?正在运行的应用中的配置内容如何更新?
现在我们更改git服务器中的配置文件内容:
修改成4444:
然后刷新Config Server地址观察:
可以看到这里查询到的是新的数据。
在Config Client中测试:
看到依然是旧的数据。
如何才能在重启应用的情况下,获取到最新的配置文件内容呢? -- 为Config Client添加refresh支持。
需要为动态更新配置内容的bean添加@RefreshScope注解。
server:
port: 8181 #服务端口
spring:
application:
name: itcast-microservice-item #指定服务名
logging:
level:
org.springframework: INFO
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ipAddress: 127.0.0.1
management:
security:
enabled: false #是否开启actuator安全认证
刷新Config Client的地址:
为了测试,需要再次修改配置文件的内容,将原来的端口4444改为5555。
可以看到Config Client中依然是4444:
然后,post请求/refresh来更新配置内容:
响应:
可以看到有配置文件内容更新了。
可以看到应该更新了最新的配置文件内容,我们也就实现了在未重启项目的情况下实现了动态修改配置文件内容。
但是,这并不实用,原因是项目已经发布上线了,不可能人为的守在服务器前,发现有更新了就手动请求/refresh.
是否自动完成呢?
gogs、github等git服务器提供了web hook功能,意思是,在仓库中的资源发生更新时会通知给谁,这里的谁是一个url地址。
查看本机ip地址:
点击“添加Web钩子”。
添加完成。
接下来进行测试,更新配置文件的内容。
测试的结果会发现,配置文件内容会动态更到Bean中。
总结下流程:
在itcast-microservice-item中作为Config Client,在配置文件中了配置了Config Server的地址:
这样的硬编码是不好的,如果配置中心的ip地址发生了改变,那么久需要重新修改并且重启应用了。
想想,有什么好的办法解决呢? 如果将Config Server作为一个微服务,并且将其注册的Eureka中,是不是就可以不用硬编码了?
server:
port: 6869 #服务端口
spring:
application:
name: itcasst-microservice-config-server #指定服务名
cloud:
config:
server:
git: #配置git仓库地址
uri: http://172.16.55.138:10080/zhangzhijun/itcast-config-server.git
#username: zhangzhijun
#password: 123456
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ipAddress: 127.0.0.1
可以看到在Eureka中已经有配置中心的服务。
eureka:
client:
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
spring:
cloud:
config:
name: microservice #对应的配置服务中的应用名称
#uri: http://127.0.0.1:6869/
profile: dev #对应配置服务中的{profile}
label: master #对应的分支
discovery:
enabled: true #启用发现服务功能
service-id: itcasst-microservice-config-server #指定服务名称
疑问:在application.yml中以及配置Eureka的信息,为什么在bootstrap.yml还需要配置?
因为在Spring Boot中bootstrap.yml在application.yml之前加载,所以即使在application.yml中以及配置Eureka的信息,是使用不了的,所以需要在bootstrap.yml中配置Eureka的信息。
测试结果,一切正常。这就完美解决了硬编码的问题。
虽然通过Gogs Git的web hook可以实现自动更新,但是,如果Config Client有很多的话,那么需要在web hook中维护很多地址,这显然是不现实的做法。
有没有更好的方案呢? 通过消息实现通知。
目前Spring Cloud Bus消息总线只是实现了对RabbitMQ以及Kafka的支持。
参考《RabbitMQ-3.4.1安装手册.docx》
eureka:
client:
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
spring:
cloud:
config:
name: microservice #对应的配置服务中的应用名称
#uri: http://127.0.0.1:6869/
profile: dev #对应配置服务中的{profile}
label: master #对应的分支
discovery:
enabled: true #启用发现服务功能
service-id: itcasst-microservice-config-server #指定服务名称
rabbitmq: #RabbitMQ相关的配置
host: 127.0.0.1
port: 5672
username: guest
password: guest
在启动后会看到这样的日志:
说明刷新的地址/bus/refresh是有Spring Cloud Bus来处理,之前的/refresh依然是由以前的逻辑处理。
所以要修改Gogs中的web hook的地址:(修改或者添加都可以)
查看RabbitMQ中的交换机:
再看队列:
接着,将itcast-microservice-item的端口改成8182,再启动一个itcast-microservice-item实例,进行测试。
发现,有2个队列,分别都绑定到springCloudBus的交换机。
接下里,修改配置文件的内容进行测试。
可以看到8181和8182这2个实例查询到的信息都是一样的。
接下来,修改配置文件内容将6666改成7777:
结果显示,都是获取到最新的数据。
在测试时,会发现,由于Gogs的web钩子推送到8181,所以8181的更新快一些,而8182更新就相对慢一些。
更新文件到Gogs,Gogs通过web钩子通知到8181的/bus/refresh,8181的实例将消息发送到springCloudBus的交换机,由于8181的队列页绑定到交换机,所以8081也获取到了更新的通知,然后去Config Server获取最新的数据。
在前面实现的架构中,发现8181这个实例不仅仅是提供了商品查询的服务,还负责发送更新的消息到RabbitMQ。
这其实是违反了微服务架构中的职责单一的原则。
其实这个架构是可以改进的,就是将原有的Config Server不仅仅是提供配置查询的服务,而且还要负责更新消息的发送。
server:
port: 6869 #服务端口
spring:
application:
name: itcasst-microservice-config-server #指定服务名
cloud:
config:
server:
git: #配置git仓库地址
uri: http://172.16.55.138:10080/zhangzhijun/itcast-config-server.git
#username: zhangzhijun
#password: 123456
rabbitmq: #RabbitMQ相关的配置
host: 127.0.0.1
port: 5672
username: guest
password: guest
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:itcast123@127.0.0.1:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ipAddress: 127.0.0.1
management:
security:
enabled: false #是否开启actuator安全认证