从本章开始,我将讲解Spring Cloud中的另一个组件—— Feign 。Feign是什么?能解决什么样的问题?
回顾一下我们之前使用RestTemplate + Ribbon + Eureka
的方式来进行服务间的调用,每次调用服务接口,都必须像下面这样写代码:
@RestController
public class ServiceBController {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@GetMapping(value = "/greeting/{name}")
public String greeting(@PathVariable("name") String name) {
RestTemplate restTemplate = getRestTemplate();
// ServiceA是服务提供方向Eureka注册的应用名
return restTemplate.getForObject("http://ServiceA/sayHello/" + name, String.class);
}
}
显然,这是一种低效的方式,针对服务提供方ServiceA的每一个对外接口,我们都要硬编码。理想的微服务架构中,ServiceA应该提供一个接口存根jar包,然后服务调用方直接引用一个jar包,并使用一些注解即可完成调用。
Feign要解决的就是这个问题,它就是一个 声明式服务调用框架 。
一、基本使用
我们来看下Feign的基本使用。我将创建以下应用:
- eureka-server:服务注册中心
- serviceA:服务提供者
- service-a-api:服务提供者API存根
- serviceB:服务调用者
更多Spring Cloud Feign的使用介绍,请参考Spring官方文档:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/。
1.1 eureka-server
eureka-server就是一个普通的Eureka注册中心。
启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
配置文件
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tpvlog</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>eureka-server</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<properties>
<spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
1.2 service-a-api
service-a-api定义了ServiceA对外的接口。
pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tpvlog</groupId>
<artifactId>service-a-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>service-a-api</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<distributionManagement>
<repository>
<id>nexus-releases</id>
<name>Nexus Release Repository</name>
<url>http://localhost:8081/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<name>Nexus Snapshot Repository</name>
<url>http://localhost:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
</dependencies>
</project>
服务接口
@RequestMapping("/user")
public interface ServiceAInterface {
@RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET)
String sayHello(@PathVariable("id") Long id,
@RequestParam("name") String name,
@RequestParam("age") Integer age);
@RequestMapping(value = "/", method = RequestMethod.POST)
String createUser(@RequestBody User user);
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
String updateUser(@PathVariable("id") Long id, @RequestBody User user);
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
String deleteUser(@PathVariable("id") Long id);
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
User getById(@PathVariable("id") Long id);
}
public class User {
private Long id;
private String name;
private Integer age;
//...
}
1.3 serviceA
ServiceA就是一个普通的Spring Boot应用。
启动类
@SpringBootApplication
@EnableEurekaClient
public class ServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAApplication.class, args);
}
}
配置文件
server:
port: 8088
spring:
application:
name: ServiceA
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka
pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tpvlog</groupId>
<artifactId>serviceA</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>serviceA</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<properties>
<spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.tpvlog</groupId>
<artifactId>service-a-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
服务接口实现
@RestController
public class ServiceAController implements ServiceAInterface {
public String sayHello(@PathVariable("id") Long id, @RequestParam("name") String name,
@RequestParam("age") Integer age) {
System.out.println("打招呼,id=" + id + ", name=" + name + ", age=" + age);
return "{'msg': 'hello, " + name + "'}";
}
public String createUser(@RequestBody User user) {
System.out.println("创建用户," + user);
return "{'msg': 'success'}";
}
public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
System.out.println("更新用户," + user);
return "{'msg': 'success'}";
}
public String deleteUser(@PathVariable("id") Long id) {
System.out.println("删除用户,id=" + id);
return "{'msg': 'success'}";
}
public User getById(@PathVariable("id") Long id) {
System.out.println("查询用户,id=" + id);
return new User(1L, "张三", 20);
}
}
1.4 serviceB
serviceB就是服务调用方,依赖了Feign实现声明式服务调用。
启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ServiceBApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceBApplication.class, args);
}
}
配置文件
server:
port: 9090
spring:
application:
name: ServiceB
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka
pom依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tpvlog</groupId>
<artifactId>serviceB</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>serviceB</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.tpvlog</groupId>
<artifactId>service-a-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-client</artifactId>
<version>2.2.5.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
服务调用
首先,定义一个Feign客户端,继承ServiceA接口存根:
@FeignClient("ServiceA") // ServiceA就是服务A的名称
public interface ServiceAClient extends ServiceAInterface {
}
@RestController
@RequestMapping("/ServiceB/user")
public class ServiceBController {
// 注入Feign客户端
@Autowired
private ServiceAClient serviceA;
@RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET)
public String greeting(@PathVariable("id") Long id,
@RequestParam("name") String name,
@RequestParam("age") Integer age) {
return serviceA.sayHello(id, name, age);
}
@RequestMapping(value = "/", method = RequestMethod.POST)
public String createUser(@RequestBody User user) {
return serviceA.createUser(user);
}
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
return serviceA.updateUser(id, user);
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public String deleteUser(@PathVariable("id") Long id) {
return serviceA.deleteUser(id);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getById(@PathVariable("id") Long id) {
return serviceA.getById(id);
}
}
上面的@FeignClient
注解的value表示要消费的服务名称,是在服务提供方的yml文件中配置的。Feign会自动生成访问后台服务的代理接口服务,后续我讲Feign原理时会专门讲解。
二、核心组件
Feign就跟Ribbon一样,内部都包含了很多的核心组件:
- 编码器(Encoder) :如果调用接口时,传递的参数是个对象,Feign会将这个对象进行编码,转换成JSON格式;
- 解码器(Decoder) :接受到响应后,将JSON转换为一个对象;
- Logger :负责日志打印,即打印这个接口调用的详细请求,包含请求、响应等等;
- Contract :契约组件,Feign使用时一般会用到SpringMVC相关的注解,这个组件就负责Feign的原生注解与SpringMVC注解之间的转化;
- Feign.Builder :FeignClient的一个实例构造器,这是Builder设计模式的典型实现;
- FeignClient :Feign客户端,里面包含了上述的一系列组件,可类比Ribbon客户端理解。
Spring Cloud对上述的这些Feign的组件都有默认的实现:
- Encoder:SpringEncoder;
- Decoder:ResponseEntityDecoder;
- Logger:Slf4jLogger;
- Contract:SpringMvcContract;
- Feign.Builder:HystrixFeign.Builder;
- FeignClient:LoadBalancerFeignClient;
2.1 自定义组件
除了这些默认组件外,我们也可以自定义实现:
@FeignClient(name = "ServiceA", configuration = FooConfiguration.class)
public interface ServiceAClient {
//..
}
public class FooConfiguration {
// 配置拦截器
@Bean
public RequestInterceptor requestInterceptor() {
return new MyRequestInterceptor();
}
// 配置日志级别:none,basic,headers,full
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
Spring会为每一个Feign客户端创建一个独立上下文ApplicationContext,默认这些客户端的组件都依赖
FeignClientsConfiguration
配置,我们可以通过自定义配置的方式替换掉部分默认配置给我们生成的Feign组件。
2.2 客户端配置
Feign使用时必然会涉及到很多参数的配置,这些参数大部分都有默认值,我们也可以通过下面的方式进行指定:
feign:
client:
config:
ServiceA: # 针对指定Feign客户端进行配置
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
decode404: false
feign:
client:
config:
default: # 全局默认配置
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
三、总结
最后,我用一张图简单说下Feign的核心工作流程,以便大家有一个初步印象。这张图并不完全准确,但可以帮助大家理解Feign,也能说明Feign的核心思想其实就是 动态代理 ,为我们后面研究它的源码打下基础: