前面的案例中我们已经搭建好了SpringCloud的基本架构。目前主要就是三个服务。一个Nacos服务,目前作为我们的注册中心,一个用户服务,一个订单服务。上个章节中,我们已经成功的将用户服务和订单服务注册到了Nacos中,并且模拟了多节点部署的情况下,Nacos对于服务实例的管理。
一、注册中心作用
服务注册中心是服务实现服务化管理的核心组件,类似于目录服务的作用,主要用来存储服务信息,譬如提供者 url 串、路由信息等。服务注册中心是微服务架构中最基础的设施之一。
注册中心可以说是微服务架构中的"通讯录",它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用。
简单理解就是:在没有注册中心时候,服务间调用需要知道被当服务调方的具体地址(写死的 ip:port)。更换部署地址,就不得不修改调用当中指定的地址。而有了注册中心之后,每个服务在调用别人的时候只需要知道服务名称(软编码)就好,地址都会通过注册中心根据服务名称获取到具体的服务地址进行调用。
注册中心的主要功能
- 服务注册表:注册中心的核心功能,记录各个服务提供者的信息,比如服务名、IP、端口等。维护服务订阅者信息。
- 服务注册、注销接口:供服务提供方进行服务注册和服务注销。
- 健康检查和服务摘除:提供健康信息上报接口,供注册服务上传健康信息进行服务保活。同时对于失效节点能够及时摘除并同步给服务订阅者。
- 服务订阅和变更通知:供服务消费者使用
一般情况下我们在部署微服务的时候,每个节点都会部署多个,在服务之间相互调用的时候,都是通过注册中心上的服务名称进行调用,当有服务宕机时,注册中心可以通知消费者,选择可用的节点进行调用,这样就最大程度的保证了服务的可用性。
二、OpenFeign简介
在单体应用时代,我们各个服务之间要想完成调用,一般会使用Http的相关工具类来完成,但是随着微服务的兴起,服务数量的不断增多,继续使用Http工具类无疑会极大的增加我们的工作量,而且也不够优雅。而SpringCloud OpenFeign就是给我们提供了一种声明式的服务调用客户端,使用OpenFeign可以让服务之间的调用变得简单。
OpenFeign到底能干啥:
- OpenFeign的设计宗旨式简化Java Http客户端的开发。Feign在restTemplate的基础上做了进一步的封装,由其来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的协助下,我们只需创建一个接口并使用注解的方式进行配置(类似于Dao接口上面的Mapper注解)即可完成对服务提供方的接口绑定,大大简化了Spring cloud Ribbon的开发,自动封装服务调用客户端的开发量。
- OpenFeign集成了Ribbon,利用ribbon维护了服务列表,并且通过ribbon实现了客户端的负载均衡。与ribbon不同的是,通过OpenFeign只需要定义服务绑定接口且以申明式的方法,优雅而简单的实现了服务调用。
还有一点要注意一下, openFeign 和 feign其实是有区别的,简单来说feign一般是老版本的springcloud在用,后来不维护了,就又出了openfeign,也优化了一些用法。 详细的区别大家也可以去网上看看。
三、接口开发
我们先在userservice服务和 orderservice 服务下都添加几个接口。方便观察调用的结果。
我们先把之前讲过的统一结果封装的几个类集成到common模块下,这块如果大家不太清楚,可以去找找我之前的文章: 《SpringBoot统一结果封装》。核心的几个类如下:
然后我们在userservice下创建一个UserController。 先简单开发一个接口。
java
package com.lsqingfeng.springcloud.user.controller;
import com.lsqingfeng.springcloud.common.base.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @className: UserController
* @description:
* @author: sh.Liu
* @date: 2022-03-29 17:25
*/
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("hello")
public Result hello(String name){
return Result.success(name);
}
}
同样的,我们在orderservice模块下也创建一个Controller,开发一个接口。
java
package com.lsqingfeng.springcloud.order.controller;
import com.lsqingfeng.springcloud.common.base.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @className: OrderController
* @description:
* @author: sh.Liu
* @date: 2022-03-29 17:54
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("getOrder")
public Result getOrder(String orderNo){
return Result.success(orderNo);
}
}
启动两个程序,我们来测试一下这两个接口。
user接口:
order接口:
四、集成OpenFeign
首先来说明一下,OpenFeign是不属于SpringCloud Alibaba这套体系中的,它是一套独立的技术架构,从SpringCloud的目录中我们也可以看出,他其实是和SpringCloud Alibaba是平级的。但是这并不影响我们在SpringCloud Alibaba的架构中使用它,并且在使用上也是完全兼容的。
OpenFein本身的作用其实就是服务之间的调用,这种调用当然也可以选择其他的方式,比如SpringCloud Alibaba体系中的 dubbo做RPC调用, Dubbo本身在SpringCloud出现之前就已经存在了,并且活跃了很长时间,所以在服务调用这一领域我们可以使用OpenFeign,也可以使用dubbo,但是据我观察周围还是使用OpenFeign的比较多。而Dubbo往往还是和Zookeeper一起做分布式RPC调用来用。后面如果有时间我们也会集成dubbo来实现服务之间的调用。
好了大致的背景介绍完了,接下来我们就来实现一个小需求: orderservice通过OpenFeign去调用userservice中的接口。
4.1 引入依赖
由于我们的依赖都是在parent这个模块中统一管理的。现在我们需要使用SpringCloud的相关依赖了,所以在里边添加:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
版本信息: <spring.cloud-version>2021.0.1</spring.cloud-version>
完整pom如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lsqingfeng.springcloud</groupId>
<artifactId>spring-cloud-learning</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>spring-cloud-learning-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>spring-cloud-learning-parent</name>
<description>项目父pom,用于被继承</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring.boot.version>2.6.5</spring.boot.version>
<spring.cloud.alibaba.version> 2021.0.1.0</spring.cloud.alibaba.version>
<spring.cloud-version>2021.0.1</spring.cloud-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<lombok.version>1.18.20</lombok.version>
<nacos.version>0.2.1</nacos.version>
<modelmapper.version>2.3.0</modelmapper.version>
<springfox-swagger2.version>2.9.2</springfox-swagger2.version>
<xiaoymin.swagger.version>1.9.6</xiaoymin.swagger.version>
</properties>
<repositories>
<!-- maven仓库采用阿里云-->
<repository>
<id>maven-ali</id>
<url>http://maven.aliyun.com/nexus/content/groups/public//</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<!-- springBoot 版本依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- springCloud-alibaba版本依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- springCloud相关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.lsqingfeng.springcloud</groupId>
<artifactId>spring-cloud-learning-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- 实体拷贝工具 -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${modelmapper.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>${xiaoymin.swagger.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后在我们的消费者端配置依赖。我们的消费者也就是服务的调用者,就是orderservice.
xml
<!--openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
4.2 开发OpenFeign客户端接口
依赖准备好了以后,我们现在orderservice的启动类上添加一个注解: @EnableFeignClients
然后我们创建一个包,专门来放feign客户端接口。就叫client.然后在里边创建一个接口。UserFeignClient。接口里的内容要和我们想调用的接口的Controller一模一样,只不过只写接口,不用写实现。如果有pojo类型的参数或返回值,也直接把对应的类拷贝过来。如果太多,把他们放到单独的一个模块中,然后依赖进来。
由于我们想调用的UserController中只有一个方法,就直接拿过来就行了。同时在接口上添加feinClient注解。
java
package com.lsqingfeng.springcloud.order.client;
import com.lsqingfeng.springcloud.common.base.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @interface: UserFeignClient
* @description:
* @author: sh.Liu
* @date: 2022-03-29 19:10
*/
@Component
@FeignClient(name="userservice")
public interface UserFeignClient {
/**
* userController中的hello
* @param name
* @return
*/
@GetMapping("hello")
Result hello(String name);
}
这里注意,@FeignClient中的参数name就是我们要调用的服务的名称。也就是对应服务的spring.application.name值。这个值也注册到nacos中的名称也是一样的。
4.3 在消费者中使用
客户端准备好了,就可以和我们正常依赖注入的方式一样使用了。我们在OrderController中依赖对应的客户端完成对用户模块接口的调用。
java
package com.lsqingfeng.springcloud.order.client;
import com.lsqingfeng.springcloud.common.base.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @interface: UserFeignClient
* @description:
* @author: sh.Liu
* @date: 2022-03-29 19:10
*/
@Component
@FeignClient(name="userservice")
public interface UserFeignClient {
/**
* userController中的hello
* @param name
* @return
*/
@GetMapping("/user/hello")
Result hello(String name);
}
然后我们重新编译运行项目。运行orderservice的时候发现报了一个错。
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderController': Unsatisfied dependency expressed through field 'userFeignClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.lsqingfeng.springcloud.order.client.UserFeignClient': Unexpected exception during bean creation; nested exception is java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
这个问题是由于:SpringCloud Feign在Hoxton.M2 RELEASED版本之后不再使用Ribbon而是使用spring-cloud-loadbalancer,所以不引入spring-cloud-loadbalancer会报错。
解决方法:
加入spring-cloud-loadbalancer依赖
java
<!--loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
再次启动项目成功,我们调用订单的接口看看效果:
再次出现错误,这次是405, 心态要炸了,这个主要是由于OpenFein在调用的时候对于参数缺少注解导致的。我们在openFein接口的参数里加上 @RequestParam
java
package com.lsqingfeng.springcloud.order.client;
import com.lsqingfeng.springcloud.common.base.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @interface: UserFeignClient
* @description:
* @author: sh.Liu
* @date: 2022-03-29 19:10
*/
@Component
@FeignClient(name="userservice")
public interface UserFeignClient {
/**
* userController中的hello
* @param name
* @return
*/
@GetMapping("/user/hello")
Result hello(@RequestParam("name") String name);
}
这个只能说这是属于OpenFeign不够完美的地方,很多注解不能省略。再次调用。有结果了:
这样我们就实现了就像调用自己项目内部的接口一样的方式去调用其他服务的接口,这完全依赖于注册中心的帮助。我们加个断点,看的更加清晰一下。
好了这样就实现了OpenFeign的调用。大家赶快好好理解理解吧。代码已经上传到了git上,有不清楚的地方可以把代码下载下来研究。