文章目录
前情提要:在 nacos上注册了 content-center和 user-center两个服务, content-center使用 Feign调用 user-center服务,使用 Ribbon做负载均衡
1.RestTemplate VS Feign
- Feign让我们的代码可读性、可维护性极佳,这是决胜点
- Feign 的唯一短板性能只有RestTemplate的一半,但是也有优化提升的空间
- 所以尽量使用 Feign
- 然而事无绝对,合理选择
2.Feign的组成
我们需要关注的几个部分
- Client 可以自定义请求Client,提高性能
- Logger 默认是不打印日志,所以这里需要配置
- RequestInterceptor 拦截器,很有用,例如给每个请求加Header
3.项目添加Feign
content-center 项目添加Feign组件,依然三板斧
3.1 加依赖
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2 加注解
启动类ContentApplication添加注解@EnableFeignClients
@SpringBootApplication
@MapperScan("com.zengchen.content.mapper")
@EnableFeignClients
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class, args);
}
}
3.3 写配置
没有必须写的配置
4.项目使用Feign
4.1 声明 FeignClient(UserCenterFeignClient)
新建个包feignclient下面新建接口UserCenterFeignClient
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "user-center")
public interface UserCenterFeignClient{
/**
* FeignClient的name + GetMapping的value
* 相当于 http://user-center/reciteHis/testAno
* 和RestTemplate里写的url一模一样
* @return Page
*/
@GetMapping(value = "/reciteHis/testAno")
Page memberRctHisAno();
}
4.2 使用UserCenterFeignClient
重构Controller里的方法,代码很简洁,并且从client接口里知晓每个方法是做什么的,可读性变强了
@Autowired
private UserCenterFeignClient userCenterFeignClient;
@GetMapping(value = "testRibbon")
public Object testRibbon() {
//1. 未使用Ribbon时
// List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
// String targetUrl = instances.stream()
// .map(instance -> instance.getUri().toString() + "/reciteHis/testAno")
// .findFirst()
// .orElseThrow(() -> new IllegalArgumentException("当前没有实例!"));
// 2.Ribbon + restTemplate
// String targetUrl= "http://user-center/reciteHis/testAno";
// Page page = restTemplate.getForObject(targetUrl, Page.class);
// 3.Feign
Page page = userCenterFeignClient.memberRctHisAno();
return ResponseVO.success(page);
}
4.3 测试
请求content-center的http://localhost:8082/reciteHis/testAno。使用Feign成功
5.Feign的日志级别
开发环境推荐使用FULL,生产环境推荐BASIC
5.1 java代码方式自定义日志级别
- 设置UserCenterFeignClient的项目(不是Feign的日志,注意这两个日志的概念区别)日志级别为debug
只有这个文件的日志级别设置为debug,Feign的日志才能打印出来,这是先决条件
logging:
file: D://springlog/content/log.log
level:
com.zengchen.content.feignclient.UserCenterFeignClient: debug
- 新建UserCenterFeignClientConfiguration
和Ribbon的java代码配置一样,写个配置类,返回日志级别。注意这里的Logger,得是feign包下面的Logger,别引用错了
如果你加了@Configuration,也会产生父子上下文问题,变成了Feign的全局配置
所以,要么别加@Configuration注解,要么把这个配置类放到一个启动类扫描不到的包里,like ribbon的配置类
这里推荐不加@Configuration注解的方式,因为简单啊。
那么ribbon的配置类问什么不采用这种不加注解的方式呢?因为ribbon的配置类必须加@Configuration,不加不行啊,所以ribbon没办法像feign一样
import feign.Logger;
import org.springframework.context.annotation.Bean;
//@Configuration
public class UserCenterFeignClientConfiguration {
@Bean
public Logger.Level loggerLevel(){
//全日志
return Logger.Level.FULL;
}
}
- UserCenterFeignClient使用UserCenterFeignClientConfiguration
configuration = UserCenterFeignClientConfiguration.class
@FeignClient(name = "user-center",configuration = UserCenterFeignClientConfiguration.class)
public interface UserCenterFeignClient {
/**
* FeignClient的name + GetMapping的value
* 相当于 http://user-center/reciteHis/testAno
* 和RestTemplate里写的url一模一样
* @return Page
*/
@GetMapping(value = "/reciteHis/testAno")
Page memberRctHisAno();
}
- 重启项目测试
请求 http://localhost:8081/poem/testRibbon,可以看到content-center里的日志,feign的日志都打印出来了
5.2 属性配置方式自定义日志级别
- 先把UserCenterFeignClient的注解注释掉
//@FeignClient(name = "user-center",configuration = UserCenterFeignClientConfiguration.class)
@FeignClient(name = "user-center")
public interface UserCenterFeignClient {
/**
* FeignClient的name + GetMapping的value
* 相当于 http://user-center/reciteHis/testAno
* 和RestTemplate里写的url一模一样
* @return Page
*/
@GetMapping(value = "/reciteHis/testAno")
Page memberRctHisAno();
}
- 写配置
logging:
file: D://springlog/content/log.log
level:
com.zengchen.content.feignclient.UserCenterFeignClient: debug
feign:
client:
config:
# 想要调用的微服务的名称
user-center:
loggerLevel: basic
- 重启项目测试
请求 http://localhost:8081/poem/testRibbon,可以看到content-center里feign的basic日志比full要少很多
6.Feign的全局配置
上小节的日志配置,都是只针对于user-center服务的起作用,如果content-center还要调用比如,短信服务,广告服务等等,就看不到feign的日志了,因为feign默认不打印日志,全局配置就是content-center调用其它所有的服务都起作用的配置
6.1 java代码方式
利用父子上下文ComponentScan的bug
这虽然是一种方式,但是这是一种病态的方式,强烈不建议使用- 启动类@EnableFeignClients的defaultConfiguration属性
注释application.yml里的配置
#feign:
# client:
# config:
# # 想要调用的微服务的名称
# user-center:
# loggerLevel: basic
给@EnableFeignClients添加
defaultConfiguration = UserCenterFeignClientConfiguration.class属性,这样UserCenterFeignClientConfiguration就变成了全局的配置,不再仅限于调用user-center的时候
@SpringBootApplication
@MapperScan("com.zengchen.content.mapper")
@EnableFeignClients(defaultConfiguration = UserCenterFeignClientConfiguration.class)
//@EnableFeignClients(basePackages = "com.zengchen.user.client")
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class, args);
}
}
- 重启项目测试
请求 http://localhost:8081/poem/testRibbon,又变成full日志了
6.2 属性配置方式
- 注释java代码全局配置
@SpringBootApplication
@MapperScan("com.zengchen.content.mapper")
@EnableFeignClients//(defaultConfiguration = UserCenterFeignClientConfiguration.class)
//@EnableFeignClients(basePackages = "com.zengchen.user.client")
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class, args);
}
}
- 修改属性配置
把原来的user-center改成default就行
feign:
client:
config:
# 全局配置
default:
loggerLevel: basic
- 重启项目测试
请求 http://localhost:8081/poem/testRibbon,又变回basic日志了
6.3 java代码方式 vs 属性配置方式
这两种方式所支持的配置项是不同的 !!
- java代码方式支持的配置项
- 属性配置方式支持的配置项
7.配置实践总结
7.1 Ribbon配置 vs Feign配置
7.2 Feign代码方式 vs 属性方式
我测试了一下,Ribbon代码方式比属性方式优先级高,Feign代码方式比属性方式优先级低
7.3 最佳配置搭配推荐
- 尽量使用属性配置,属性配置实现不了再考虑用代码配置
- 同一个微服务尽量保持配置方式单一性,不要两种配置方式混用,会增加定位代码问题的复杂性
8.多参数请求构造
参考大目老师手记:https://www.imooc.com/article/289000
9.Feign请求非注册服务的接口
这种方式不会使用到Ribbon,所以也叫Feign脱离Ribbon的使用方式
- 重新定义一个 TestImoocFeignClient
@FeignClient(name = “getImooc”,url = “www.baidu.com”),name随便自己定义,url就是你想请求的链接。关键就是这个url参数
//@FeignClient(name = "getImooc",url = "localhost:8082/reciteHis/1")
//@FeignClient(name = "getImooc",url = "www.sogou.com")
//@FeignClient(name = "getImooc",url = "www.imooc.com")
@FeignClient(name = "getImooc",url = "www.baidu.com")
public interface TestImoocFeignClient {
@GetMapping(value = "")
String index();
}
- 新建 TestImoocController 使用TestImoocFeignClient里的index方法
@RestController
public class TestImoocController {
@Autowired
TestImoocFeignClient testImoocFeignClient;
@GetMapping(value = "getImooc")
public String imoocIndex(){
return this.testImoocFeignClient.index();
}
}
- 重启测试
访问 http://localhost:8081/getImooc,FeignClient里的url=www.baidu.com,所以我们要访问的是baidu
- 其它url测试结果
TestImoocFeignClient的@FeignClient url参数,我换成 www.imooc.com,或者www.csdn.net,或者www.so.com,都会报错 301 Moved Permanently错误,只有www.baidu.com得到了预期返回。
修改user-center的配置,把user-center变成非nacos注册服务时,用Feign访问user-center的请求,是可以访问的
@FeignClient(name = "getImooc",url = "localhost:8082/reciteHis/1")
10.Feign性能优化
Feign的性能只有RestTemplate的50%,但是不用为Feign担心,一般项目的瓶颈不会发生在Feign上,优化仅是为了更好!
10.1 优化原理
第二节 Feign的组成里有Client组件,就是优化这个Client
- 不和Ribbon配合使用的时候,Client 是默认的Feign.client.default ,这个default里使用的是HttpURLConnection,这个HttpURLConnection,不支持连接池,所以性能不高
- 和Ribbon配合使用的时候,Client 使用的是LoadBalancerFeignClient,支持代理模式,默认的也是Feign.client.default
从上图可以看出,还有两个构造client的类,一个是HttpClient,一个是OKHttp。优化就是用它们替换default
10.2 使用HttpClient优化
- 引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 写配置
feign:
client:
config:
# 全局配置
default:
loggerLevel: basic
httpclient:
# 让feign使用apache httpclient做请求,而不是默认的httpurlconnection
enabled: true
# feign 最大连接数
max-connections: 200
# feign 单个路径的最大连接数
max-connections-per-route: 50
10.3 使用OKHttp优化
- 引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<!--<version>10.3.0</version>-->
</dependency>
- 写配置
feign:
client:
config:
# 全局配置
default:
loggerLevel: basic
okhttp:
enabled: true
httpclient:
# 让feign使用apache httpclient做请求,而不是默认的httpurlconnection
# enabled: true
# feign 最大连接数
max-connections: 200
# feign 单个路径的最大连接数
max-connections-per-route: 50
max-connections和max-connections-per-route使用的是httpclient里面的
11.Feign常见问题总结
参考大目老师手记:https://www.imooc.com/article/289005
12.Feign的继承特性
https://cloud.spring.io/spring-cloud-openfeign/reference/html/#spring-cloud-feign-inheritance
12.1 项目拆分成三个模块
将user-center,content-center都拆分为多模块项目。
这个可以参照我的另一篇博文 :
https://blog.csdn.net/hantangduhey/article/details/99306490
12.2 利用Feign的继承特性重构项目
这一步之前,要确保服务提供方user-center已经完成项目多模块改造
12.2.1 重构服务提供方 user-center
- 将请求路径 http://user-center/reciteHis/testAno 的原实现Controller方法注释掉
// @GetMapping(value = "/reciteHis/testAno")
// public Page<ReciteHisOT> memberRctHisAno() {
// log.info("接收title={}",httpServletRequest.getParameter("title"));
// Page<ReciteHisOT> page = new Page<>(1, 2);
// page.setRecords(reciteHisService.queryReciteHisPage(page,"3"));
// log.info("我是{},我被调用了",httpServletRequest.getRequestURL());
// return page;
// }
- 在user-client模块里新建UserCenterFeignClientService接口
package com.zengchen.user.client.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zengchen.user.common.ReciteHisOT;
import org.springframework.web.bind.annotation.GetMapping;
public interface UserCenterFeignClientService {
@GetMapping(value = "/reciteHis/testAno")
Page<ReciteHisOT> memberRctHisAno();
}
包结构:
- 返回对象ReciteHisOT放到user-common模块里,user-client模块引用user-common依赖,user-client的pom.xml加入:
<dependency>
<groupId>com.zc</groupId>
<artifactId>user-common</artifactId>
</dependency>
- user-server引入user-common和user-client模块,user-server的pom.xml加入:
<!--引入另外两个模块-->
<dependency>
<groupId>com.zc</groupId>
<artifactId>user-client</artifactId>
</dependency>
<dependency>
<groupId>com.zc</groupId>
<artifactId>user-common</artifactId>
</dependency>
- user-server新建UserCenterFeignClientController实现user-client模块里的UserCenterFeignClientService接口
@RestController
public class UserCenterFeignClientController implements UserCenterFeignClientService {
private static final Logger log = LoggerFactory.getLogger(UserCenterFeignClientController.class);
@Autowired
private ReciteHisService reciteHisService;
@Autowired
HttpServletRequest httpServletRequest; // 是线程安全的
@Override
public Page<ReciteHisOT> memberRctHisAno() {
Page<ReciteHisOT> page = new Page<>(1, 2);
page.setRecords(reciteHisService.queryReciteHisPage(page,"3"));
log.info("我是{},我被调用了",httpServletRequest.getRequestURL());
return page;
}
}
- 启动user-center项目测试访问路径 http://localhost:8082/reciteHis/testAno,接口可以访问
12.2.2 重构服务消费方 content-center
- install 服务提供方 user-center
install -DskipTests
- content-center引入user-client模块,content-server的pom.xml里引入依赖
<dependency>
<groupId>com.zc</groupId>
<artifactId>user-client</artifactId>
</dependency>
- 修改UserCenterFeignClient,注释掉所有代码,让它继承user-client模块里的接口UserCenterFeignClientService
package com.zengchen.content.feignclient;
import com.zengchen.user.client.service.UserCenterFeignClientService;
import org.springframework.cloud.openfeign.FeignClient;
//@FeignClient(name = "user-center",configuration = UserCenterFeignClientConfiguration.class)
@FeignClient(name = "user-center")
public interface UserCenterFeignClient extends UserCenterFeignClientService {
/**
* FeignClient的name + GetMapping的value
* 相当于 http://user-center/reciteHis/testAno
* 和RestTemplate里写的url一模一样
* @return Page
*/
// @GetMapping(value = "/reciteHis/testAno")
// Page<ReciteHisOT> memberRctHisAno();
}
- controller代码不做修改,保持原样
@GetMapping(value = "testRibbon")
public Object testRibbon() {
//1. 未使用Ribbon时
// List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
// String targetUrl = instances.stream()
// .map(instance -> instance.getUri().toString() + "/reciteHis/testAno")
// .findFirst()
// .orElseThrow(() -> new IllegalArgumentException("当前没有实例!"));
// 2.Ribbon + restTemplate
// String targetUrl= "http://user-center/reciteHis/testAno";
// Page page = restTemplate.getForObject(targetUrl, Page.class);
// 3.Feign
Page<ReciteHisOT> page = userCenterFeignClient.memberRctHisAno();
return ResponseVO.success(page);
}
12.3 测试继承模式
content-center调用user-center的 /reciteHis/testAno
启动两个服务,请求content-center的 http://localhost:8081/poem/testRibbon,仍然可以调用user成功!
UserCenterFeignClient extends UserCenterFeignClientService 这就是Feign继承特性的体现!
12.4 Feign的继承特性引发的理念分歧
12.4.1 继承模式的优点
- 代码量少
feignClient和cotroller,一个继承service,一个实现service,实现了代码的重用,feignClient里面是没有代码的 - 维护方便
非继承模式情况下,如果接口返回对象改动了,两边都需要去修改返回对象
继承模式就没有这个问题,因为东西都在user-center这边,一个微服务改就行了 - 面向契约
契约在代码层面的体现就是接口,即我们刚才写的UserCenterFeignClientService类,提供的服务都统一定义在servier接口里,提供方和消费方都遵守这个接口,就是契约了
12.4.2 继承模式的缺点
缺点虽然只有一个,但却是个大方向的缺点,content-center需要依赖user-client和user-common两个user服务的模块,这是啥?这增加了两个服务的耦合度啊,与微服务的设计理念简直背道而驰,南辕北辙!!!因此官方文档也发出了不推荐大家使用的声明,就是下面那段我画了红框的
12.4.3 用还是不用,你们怎么选择呢?
反正我是不会用的,我学习的是微服务,简单灵活,能够独立部署运行的微服务。我的代码已经改了,我就保持这样了