四、SpringCloud alibaba 之 OpenFeign
远程调用工具 | 描述 |
---|---|
HttpClient | HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 Http 协议的客户端编程工具包,并且它支持 HTTP 协议最新版本和建议。HttpClient 相比传统 JDK 自带的URLConnection,提升了易用性和灵活性,使客户端发送 HTTP 请求变得容易,提高了开发的效率。 |
Okhttp | 一个处理网络请求的开源项目,是安卓端最火的轻量级框架,由 Square 公司贡献,用于替代HttpUrlConnection 和 Apache HttpClient。OkHttp 拥有简洁的 API、高效的性能,并支持多种协议(HTTP/2 和 SPDY)。 |
HttpURLConnection | HttpURLConnection 是 Java 的标准类,它继承自 URLConnection,可用于向指定网站发送 |
RestTemplate |
Feign
@RequestMapping("/test")
public String order(){
// 根据服务Id获取注册到nacos中的服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("stock-service");
int index = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(index);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
// 拼接调用地址
String url="http://"+host+":"+port+"/stock/getStock";
// 远程调用
String result = restTemplate.getForObject(url, String.class);
return result;
}
自己写的调用负载均衡比较简单,此时可以引入Ribbon,可以采取http://{服务名称}/{请求路径}
注意:服务名调用方式需要RestTemplate被代理,也就是在注册RestTemplate时需要加上@LoadBalanced注解
4.1.3、OpenFeign介绍
Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket。Feign可帮助我们更加便捷、优雅地调用HTTP API。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
使其支持Spr ing MVC注解,另外还整合了Ribbon和Nacos
,从而使得Feign的使用更加方便。
Feign可以做到使用 HTTP 请求远程服务时就像调用本地方法
一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个 HTTP 请求。它像 Dubbo 一样,consumer 直接调用接口方法调用 provider,而不需要通过常规的 Http Client 构造请求再解析返回数据。它解决了让开发者调用远程接口就跟调用本地方法一样,无需关注与远程的交互细节,更无需关注分布式环境开发。
4.2、OpenFeign快速使用
4.2.1、引入OpenFeign依赖
OpenFeign依赖于springcloud,在引入openfeign之前,检查一下是否已经引入了SpringCloud的依赖。
<!--openfeign的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
4.2.2、编写OpenFeign接口
在消费者端使用OpenFeign,需要一个@注解及参数来定义一个接口。
FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。
@FeignClient标签的常用属性如下:
作用 | |
---|---|
name | 指定FeignClient的名称。name有两个作用:1、作为Bean的名称注册到spring容器中。2、如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name的第二个作用就失效了。 |
url | url一般用于调试,可以手动指定@FeignClient调用的地址 |
decode404 | 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException |
configuration | Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract |
fallback | 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口 |
fallbackFactory | 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码 |
path |
一般 建议在消费者项目
中创建一个feign的包
,用于存放当前项目中调用别的服务的Feign接口
。
{要调用的服务名}FeignService
。
package com.java.coder.feign.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* name: 指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name属性就失效了。
* path: 定义当前FeignClient的统一前缀, 一般是生产者类上@RequestMapping的value值
*/
@FeignClient(name = "stock-service",path = "/stock")
public interface StockFeignService {
/**
* openfeign中对注解的使用更加严格,PathVariable注解中的id必须写
* @param id
* @return
*/
@RequestMapping("/getStock/{id}")
String addOrder(@PathVariable("id") Integer id);
/**
* 入参和出参都是json的格式
* @param dto
* @return
*/
@RequestMapping("/qryStockById")
@ResponseBody
StockRespDto qryStockById(@RequestBody StockReqDto dto);
}
注意在Feign接口中声明的方法,必须与对应的服务提供方提供的方法声明一样。且在OpenFeign中注解的使用更加严格,比如:@PathVariable,在普通的springboot应用中,如果参数的名字与路径中的名字一样,那么@PathVariable中的name可以不用写,但是在OpenFeign中必须写
package com.java.coder.feign;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
// 启动Feign接口客户端
@EnableFeignClients
public class FeignApplication {
public static void main(String[] args) {
SpringApplication.run(FeignApplication.class,args);
}
}
package com.java.coder.feign.controller;
import com.java.coder.feign.feign.StockFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/feign")
@RestController
public class FeignController {
// 引入之前声明的Feign接口
@Autowired
private StockFeignService stockFeignService;
@RequestMapping("/add")
public String addOrder(){
// 直接通过Feign接口调用服务提供方
String forObject = stockFeignService.addOrder(1100);
return forObject;
}
@RequestMapping("/qryStockById")
public String qryStockById(){
StockReqDto stockReqDto=new StockReqDto();
stockReqDto.setStockId(10000000);
// 通过OpenFeign调用复杂参数接口
StockRespDto respDto = stockFeignService.qryStockById(stockReqDto);
log.info("respDto:{}",respDto);
return "调用成功";
}
}
@FeignClient(name="github-client",url = "https://api.github.com")
public interface ThirdFeignService {
@RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
String searchRepo(@RequestParam("q") String queryStr);
}
Feign 提供了很多的扩展机制,让用户可以更加灵活的使用。包括日志、超时时间、拦截器等。
4.3.1、日志配置
有时候我们遇到 Bug,比如接口调用失败、参数没收到等问题,或者想看看调用性能,就需要配置 Feign 的日志了,以此让 Feign 把请求信息输出来。
OpenFeign的日志等级有 4 种,分别是:
日志级别 | 备注 |
---|---|
NONE | 性能最佳,适用于生产,不记录任何日志(默认值)。 |
BASIC | 适用于生产环境追踪问题,仅记录请求方法、URL、响应状态代码以及执行时间。 |
HEADERS | 记录BASIC级别的基础上,记录请求和响应的header。 |
FULL |
(1)全局日志配置
全局日志配置就是此配置对当前项目中所有的远程微服务调用都生效,只需要在@CompontScan注解可以扫描到的地方设置一个配置类,且配置类被@Configuration注解标注。
package com.java.coder.feign.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 全局配置: 当使用@Configuration 会将配置作用所有的服务提供方
* 局部配置: 如果只想针对某一个服务进行配置,不要加@Configuration
*/
@Configuration
public class OpenFeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
此处的Logger是feign.Logger
(2)局部配置
局部日志配置有两种形式:
@FeignClient(name = "stock-service",path = "/stock", configuration= OpenFeignConfig.class)
public interface StockFeignService {
@RequestMapping("/getStock")
public String addOrder();
}
此时OpenFeignConfig
配置类中的相关配置,只有在调用StockFeignService
中声明的服务时才生效。
@FeignClient(name = "stock-service",path = "/stock", configuration= OpenFeignConfig.class)
FeignClientProperties.FeignClientConfiguration
feign:
client:
config:
stock-service: #服务的名称
loggerLevel: FULL
在yml配置文件中执行 Client 的日志级别才能正常输出日志,格式是"logging.level.feign接口包路径=debug
logging:
level:
com.tuling.mall.feigndemo.feign: debug
如果采用properties文件的话,配置的内容是
logging.level.com.java.coder.order.feign=debug
日志配置完后的运行结果如下
Spring Cloud 在 Feign 的基础上做了扩展,使用 Spring MVC 的注解来完成Feign的功能。原生的 Feign 是不支持 Spring MVC 注解的,如果你想在 Spring Cloud 中使用原生的注解方式来定义客户端也是可以的,通过配置契约来改变这个配置,Spring Cloud 中默认的是 SpringMvcContract。
Spring Cloud 1 早期版本就是用的原生Fegin. 随着netflix的停更替换成了Open feign
1)修改契约配置,支持Feign原生的注解
package com.java.coder.feign.config;
import feign.Contract;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenFeignConfig {
/**
* 配置openfeign的日志级别
* @return
*/
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
/**
* 修改契约配置,支持feign的原生注解
* @return
*/
@Bean
public Contract feignContract(){
return new Contract.Default();
}
}
注意:修改契约配置后,OrderFeignService 不再支持springmvc的注解,需要使用Feign原生的注解也可以通过配置文件配置。
4.3.3、自定义拦截器
定义拦截器
首先在消费者端
package com.java.coder.feign.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("service-name", "order-service");
// 可以设置一些token值
requestTemplate.header("service-code", "001");
}
}
让拦截器生效
package com.java.coder.feign.config;
import com.java.coder.feign.interceptor.FeignInterceptor;
import feign.Contract;
import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenFeignConfig {
/**
* 配置openfeign的日志级别
* @return
*/
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
/**
* 修改契约配置,支持feign的原生注解
* @return
*/
// @Bean
// public Contract feignContract(){
// return new Contract.Default();
// }
/**
* 超时时间配置
* @return
*/
// @Bean
// public Request.Options options() {
// return new Request.Options(5000,10000);
// }
/**
* 拦截器配置
* @return
*/
@Bean
public FeignInterceptor customFeignInterceptor(){
return new FeignInterceptor();
}
}
2、通过配置文件配置
feign:
client:
config:
stock-service: #服务的名称
loggerLevel: FULL
requestInterceptors[0]: com.java.coder.feign.interceptor.FeignInterceptor
package com.java.coder.stock.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
private String TRACE_ID = "TRACE_ID";
private String SERVICE_NAME = "service-name";
private String SERVICE_CODE = "service-code";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("service name:{}", request.getHeader(SERVICE_NAME));
log.info("service code:{}", request.getHeader(SERVICE_CODE));
return true;
}
}
2、把拦截器加入到拦截器链
package com.java.coder.stock.config;
import com.java.coder.stock.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置拦截器
*
* @param registry 相当于拦截器的注册中心
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//下面这句代码相当于添加一个拦截器 添加的拦截器就是我们刚刚创建的
registry.addInterceptor(new LogInterceptor())
//配置我们要拦截哪些路径 addPathPatterns("/**")表示拦截所有请求,包括我们的静态资源
.addPathPatterns("/**")
// excludePathPatterns()表示我们要放行哪些(表示不用经过拦截器)
// excludePathPatterns("/","/login")表示放行“/”与“/login”请求
// 如果有静态资源的时候可以在这个地方放行
.excludePathPatterns("/", "/login");
}
}
feign:
hystrix:
enabled: true
@Component
public class StockFeignServiceImpl implements StockFeignService {
@Override
public String getStock() {
System.out.println("调用失败");
return "调用失败";
}
}
3、在Feign接口上配置容错类
/**
* name: 指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name属性就失效了。
* path: 定义当前FeignClient的统一前缀, 一般是生产者类上@RequestMapping的value值
*/
@FeignClient(name = "stock-service",path = "/stock",fallback = StockFeignServiceImpl.class)
public interface StockFeignService {
/**
* openfeign中对注解的使用更加严格,PathVariable注解中的id必须写
* @return
*/
@RequestMapping("/getStock")
String getStock();
}
或者采取fallbackFactory配置也可以
@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
@RequestMapping(value="/custorm/getTest",
method = RequestMethod.POST,
headers = {"Content-Type=application/json;charset=UTF-8"})
List<String> test(@RequestParam("names") String[] names);
}
2、在方法参数中添加
@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
@RequestMapping(value="/custorm/getTest",method = RequestMethod.POST,
headers = {"Content-Type=application/json;charset=UTF-8"})
List<String> test(@RequestParam("names") String[] names,
@RequestHeader MultiValueMap<String, String> headers);
3、使用@Header注解
@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
@RequestMapping(value="/custorm/getTest",method = RequestMethod.POST)
@Headers({"Content-Type: application/json;charset=UTF-8"})
List<String> test(@RequestParam("names") String[] names);
}
RequestInterceptor
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate temp) {
temp.header(HttpHeaders.AUTHORIZATION, "XXXXX");
}
}
我们可以看到,实现动态代理的类就是ReflectiveFeign
的静态内部类FeignInvocationHandler
实现的。
feign.Client
接口的默认实现类lClient.Default
里面的convertAndSend
方法。
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final URL url = new URL(request.url());
final HttpURLConnection connection = this.getConnection(url);
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(options.isFollowRedirects());
connection.setRequestMethod(request.httpMethod().name());
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
if (request.body() != null) {
if (disableRequestBuffering) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
out.write(request.body());
} finally {
try {
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
return connection;
}
}
默认情况下是没有采取http连接池进行远程调用,为了性能考虑,建议采取连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
-
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
在项目中引入OkHttp依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>11.8</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>11.8</version>
</dependency>
在配置文件中开启OKHTTP
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。
因为我的负载均衡采用的是ribbon,因此真正执行远程调用的方法是FeignLoadBalancer的execute里面。
org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute
如果没有配置OKHttprequest.client()
方法返回的是Client.Default
。
如果配置了OKHttprequest.client()
方法返回的是OkHttpClient
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)