图源:laiketui.com
虽然就像在本系列第一篇文章中演示的那样,我们可以用RestTemplate
对接口进行远程调用,并且可以实现负载均衡。但是这样存在一些问题,比如需要手动指定 URL 地址,以及显式实现接口返回 JSON 数据的解码。
实际上 Spring Cloud 框架中首选的 Http 调用客户端是 Feign,使用它可以简化很多工作。
下面我们看如何使用 Feign。
在子模块 shopping-order 中添加依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
在配置类上添加@EnableFeignClients
开启相关功能:
@EnableFeignClients
@MapperScan("org.example.shopping.order.mapper")
@SpringBootApplication
public class OrderApplication {
// ...
}
添加一个接口UserClient
用于远程调用:
@FeignClient("shopping-user")
public interface UserClient {
@GetMapping("/user/{id}")
Result<User> getUserInfo(@PathVariable Long id);
}
@FeignClient
注解的value
属性是接口对应的服务名,即在 Nacos 上注册的服务名。
@GetMapping
注解和@PathVariable
注解都是 Spring MVC 的注解,其用途也是类似的。在这里,@GetMapping
注解说明这个远程调用使用的是 Http GET 方法,@PathVariable
映射的是@GetMapping
中的路径参数{id}
。
接口方法getUserInfo
的返回类型Result
说明了远程调用返回的类型。
上面这个示例,实际上对应了对接口http://shopping-user/user/{id}
的调用。
使用 Feign 远程调用很容易,像使用本地服务一样调用即可:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class OrderService {
// ...
@Autowired
private UserClient userClient;
public Order findOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
Result<User> userInfo = userClient.getUserInfo(order.getUserId());
order.setUser(userInfo.getData());
return order;
}
}
重启子模块 shopping-order 后访问接口 http://localhost:8080/order/101,可以正常返回数据,并且观察控制台可以看到,多个 shopping-user 实例都会接收到请求,所以负载均衡依然是生效的。
与 RestTemplate
相比,Feign 是声明式的方式进行调用,更容易理解和调用。并且无需使用@LoadBalanced
注解,默认使用负载均衡方式进行调用。此外,如果返回类型包含泛型参数,Feign 也可以很好地处理 JSON 解码。
Feign可以支持多种自定义配置,如下表所示:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
Feign 的日志的级别分为四种:
可以通过配置文件或代码方式对配置进行修改,下面以修改日志级别为例进行说明。
修改全局日志级别:
feign:
client:
config:
default:
loggerLevel: BASIC
修改指定服务的日志级别:
feign:
client:
config:
default:
shopping-user:
loggerLevel: FULL
添加一个 Logger.Level
类型的 bean 定义:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level loggerLevel(){
return Logger.Level.BASIC;
}
}
修改全局日志级别:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
// ...
public class OrderApplication {
// ...
}
修改具体 FeignClient 的日志级别:
@FeignClient(value = "shopping-user", configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
Result<User> getUserInfo(@PathVariable Long id);
}
Feign 的底层 Http 调用并不是自己实现的,而是使用现成的 Http 客户端:
默认使用 JDK 自带的 URLConnection 实现 Http 调用,这个 Http 客户端不支持连接池,所以我们可以改用另外两种 Http 客户端进行替换,来改善性能。
下面使用 HttpClient 进行替换。
在子模块 shopping-order 中添加 HttpClient 的依赖:
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
在配置文件中添加相关连接池配置信息:
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
除了将底层 Http 客户端修改为 HttpClient 或 OKHttp 以外,还应当在生产或测试环境使用 BASIC 或 NONE 级别的日志,打印更多的日志信息同样会影响性能。
如果我们的项目只是一个小型项目,直接在子模块中创建 FeignClient 并没有什么问题,但如果是一个大型项目,在每个子模块中为同一个服务的远程调用创建 FeignClient 就显得很低效了,这没有体现重用的思想。
可以为微服务所提供的接口专门创建一个子模块,用于保存对应的 FeignClient,这样其他子模块需要调用接口时,只要直接从这个现成的子模块引用并使用 FeignClient 即可,不需要自行创建。
下面我们基于这个思路来改造当前项目。
改造前的项目代码对应的仓库是:ch6/shopping,改造后的项目仓库是ch6/shopping2。
先添加一个子模块 feign-api,用于存放 FeignClient。
- 添加子模块的方式可以参考这篇文章。
- 如果子模块的 pom.xml 文件是灰色带删除线的,可能是因为之前建立并删除过该模块,idea 会将该模块的 pom.xml 文件设置为被忽略的文件,此时应该在 file->settings->Maven->Ignored Files 设置中将对应的 pom.xml 文件取消忽略。
在 src/main/java 中添加包 org.example.shopping.feignapi 作为子模块的根包。
将 shopping-order 中的 FeignClient 接口移动到 feign-api:
因为缺少依赖和其它类的引用,所以这里都是红色的。
添加依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
还需要将实体类User
移动到 feign-api:
package org.example.shopping.feignapi.entity;
// ...
@Data
public class User {
private Long id;
private String userName;
private String address;
}
类似的,还要移动作为统一接口返回类型的Result
类和配置类DefaultFeignConfiguration
,这里不再赘述。
从 shopping-order 中删除已经以移动到 feign-api 模块中的类。
在 shopping-order 的 pom.xml 中添加对 feign-api 模块的依赖:
<dependency>
<groupId>org.examplegroupId>
<artifactId>feign-apiartifactId>
<version>1.0version>
dependency>
现在就可以在 shopping-order 中引用 feign-api 中的类了。之后只要处理出错的类引用,让其引用 feign-api 中的相应类即可。
现在启动子模块 shopping-order 会报错:
Consider defining a bean of type 'org.example.shopping.feignapi.client.UserClient' in your configuration.
这是因为默认情况下,使用@EnableFeignClients
注解可以扫描子模块包下的@FeignClient
标记的接口,并创建相应的 bean 用于接口调用。但现在 shopping-order 使用的是 feign-api 下的 FeignClient,而其包名是org.example.shopping.feignapi.client
,并非org.example.shopping.order
或其子包。所以自动扫描是扫描不到的,自然不会创建对应的 bean。
要让目标 FeignClient 能够被扫描到,需要:
@EnableFeignClients(basePackageClasses = UserClient.class)
或者:
@EnableFeignClients(basePackages = "org.example.shopping.feignapi.client")
后者会扫描并创建org.example.shopping.feignapi.client
包下的所有 FeignClient。
如果配置了 Feign 日志级别,但是没有输出日志信息,可以检查配置中是否有将 FeignClient 所在的包的日志级别设置为
DEBUG
:logging: level: org.example.shopping.feignapi: debug
上边介绍的方式主要是将各个子模块中的 FeignClient 抽取并集中到一个单独的 feign-api 模块中进行管理和引用。除了这种方式以外,还有一种用继承的方式进行约束和重用的方法。
改造前的代码仓库为ch6/shopping,改造后的代码仓库为ch6/shopping3。
在 shopping-user 子模块中定义 API 接口:
package org.example.shopping.user.feignapi;
// ...
public interface UserAPI {
@GetMapping("/user/{id}")
Result<User> getUserInfo(@Min(1) @NotNull @PathVariable Long id);
}
这个接口约束了接口地址、参数校验、参数类型等。
shopping-user 的 Controller 可以实现这个接口:
@RestController
@RequestMapping("")
@Validated
public class UserController implements UserAPI {
@Override
public Result<User> getUserInfo(Long id) {
return Result.success(userService.findUserById(id));
}
// ...
}
可以注意到因为接口方法有@GetMapping
注解定义路径,并且有 Hibernate Validation 的相关注解进行校验,所以这里不需要重复使用相应的注解。
在作为调用方的 shopping-order 模块中,需要添加对 shopping-user 模块的依赖:
<dependency>
<groupId>org.examplegroupId>
<artifactId>shopping-userartifactId>
<version>1.0version>
dependency>
现在 shopping-order 中的 FeignClient 只要简单继承 shopping-user 中的接口即可:
@FeignClient(value = "shopping-user")
public interface UserClient extends UserAPI {
}
这种方式的好处在于结构简单,并没有使用单独的模块管理 FeignClient,并且一定程度上简化并重用了代码。但缺点在于将接口的提供方和接口的调用方进行了紧耦合,并不利于代码扩展。
The End,谢谢阅读。
本文的完整示例代码可以从这里获取。