一、OpenFeign介绍
OpenFeign是⼀种声明式,模版化的HTTP客户端。使⽤OpenFeign进⾏远程调⽤时,开发者完全感知不到这是在进⾏远程调⽤,⽽是像在调⽤本地⽅法⼀样。使⽤⽅式是注解+接⼝形式,把需要调⽤的远程接⼝封装到接⼝当中,映射地址为远程接⼝的地址。在启动SpringCloud应⽤时,Feign会扫描标有@FeignClient注解的接⼝,⽣成代理并且注册到Spring容器当中。⽣成代理时Feign会为每个接⼝⽅法创建⼀个RequestTemplate对象,该对象封装HTTP请求需要的全部信息,请求参数名、请求⽅法等信息都是在这个过程中确定的,模版化就体现在这⾥。
二、OpenFeign的使用
- 搭建前置环境,在pom.xml文件中引入依赖,可以选择使用注册中心或者配置中心
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 配置中⼼依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<!-- 注册中⼼依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 健康检查,将服务注册到consul需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- openfeign,在需要远程调⽤的服务中引⼊ -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
1.使用注册中心
- 使⽤注册中⼼,将服务注册到consul(nacos),调⽤者拿到被调⽤服务的地址端⼝进⾏调⽤
spring.cloud.consul.host=192.168.137.1 #consul地址
spring.cloud.consul.port=8080 #端⼝号
spring.cloud.consul.discovery.service-name=service-test-01 #服务名称
spring.cloud.consul.discovery.health-check-interval=1m #健康检查间隔时间
server.port=10000 #服务端⼝号
- 在配置类上开启服务发现以及允许远程调⽤
@EnableDiscoveryClient //开启服务发现
@EnableFeignClients //开启服务调⽤,只需要在调⽤⽅开启即可
- 服务运⾏之后可以在consul的UI界⾯看到运⾏的服务,consul会定时检查服务的健康状态
- 创建远程调用接口
@FeignClient(\"serviceName\")
public interface Service2Remote {
/** 这⾥有⾃定义解码器对远程调⽤的结果进⾏解析,拿到真正的返回类型,所以接⼝返回值类型和远程接⼝返回类型保持⼀致 **/
@PostMapping(\"/page\")
List<QuestionResp> pageQuestion(PageQuestionReq req);
}
- 简单使用
@RestController
@RequestMapping(\"/service/remote\")
public class RemoteController {
@Autowired
private Service2Remote service2Remote;
@PostMapping(\"/getQuestionList\")
public List<QuestionResp> getQuestionList(@RequestBody PageQuestionReq req){
List<QuestionResp> result = service2Remote.pageQuestion(req);
//对拿到的数据进⾏处理...
return result;
}
}
2.使用配置中心
- 将请求的URL写在配置中⼼进⾏读取修改配置⽂件
spring.cloud.consul.config.format=KEY_VALUE #consul⽀持yaml格式和Key-value形式
spring.cloud.consul.config.enabled=true #开启配置
spring.cloud.consul.config.prefixes=glab/plat/wt/application/test #consul配置存放的外层⽂件夹⽬录
spring.cloud.consul.config.default-context=config #⽗级⽂件夹
spring.cloud.consul.config.watch.delay=1000 #轮询时间
spring.cloud.consul.discovery.enabled=false #关闭注册
remote.url=www.baidu.com #请求地址
- 创建远程调用接口
@FeignClient(name = \"service2RemoteByUrl\",url = \"${remote.url}\") //name需要配置,URL从配置中⼼读取
public interface Service2RemoteByUrl {
@PostMapping(\"/page\")
List<QuestionResp> pageQuestion(PageQuestionReq req);
}
3.自定义解码器(编码器)
//⾃定义解码器实现Decoder接⼝,重写decode⽅法即可,根据具体需求进⾏编写
//如果是⾃定义编码器,需要实现Encoder接⼝,重写encode⽅法
public class FeignDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException,DecodeException, FeignException {
if (response.body() == null){
throw new DecodeException(ErrorEnum.EXECUTE_ERR.getErrno(),\"没有获取到有效结果值\",response.request());
}
// 拿到值
String result = Util.toString(response.body().asReader(Util.UTF_8));
Map<String,Object> resMap = null;
try {
resMap = JSON.parseObject(result, Map.class);
} catch (Exception e) {
//返回结果是字符串
return result;
}
}
4.远程调用携带Cookie
- 由于feign调⽤是新创建⼀个Request,因此在请求时不会携带⼀些原本就有的信息,例如Cookie,因此需要⾃定义RequestInterceptor对Request进⾏额外设置,⼀般情况下,写⼊Cookie是⽐较常⻅的做法,如下设置
@Configuration
public class BeanConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return template -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//此处可以根据业务⽽具体定制携带规则
String data = request.getParameter(\"data\");
String code = null;
try {
//这⾥需要转码,否则会报错
code = URLEncoder.encode(data, \"UTF-8\");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
template.query(\"data\",code);
//请求头中携带Cookie
String cookie = request.getHeader(\"Cookie\");
template.header(\"Cookie\",cookie);
};
}
@Bean
public Decoder decoder(){
return new FeignDecoder();
}
}
三、调用流程解析
//在使⽤EnableFeignClients开启feign功能时,点击进⼊会看到该注解是通过ImportFeignClientsRegistrar类⽣效的,其中有个⽅法
//registerBeanDefinitions执⾏两条语句
registerDefaultConfiguration(metadata, registry); //加载默认配置信息
registerFeignClients(metadata, registry); //注册扫描标有FeignClient的接⼝
//关注registerFeignClients⽅法
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); //在basePackage路径下扫描并添加标有FeignClient的接⼝
}
for (BeanDefinition candidateComponent : candidateComponents) { //遍历
if (candidateComponent instanceof AnnotatedBeanDefinition) {
registerClientConfiguration(registry, name, attributes.get(\"configuration\")); //
registerFeignClient(registry, annotationMetadata, attributes); //注册到Spring容器当中,⽅法详细在FeignClientsRegistrar类当中
}
}
//在对feign调⽤时进⾏断点调试
//在⽣成Feign远程接⼝的代理类时,调⽤处理器是Feign提供的FeignInvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (\"equals\".equals(method.getName())) {
//equals,hashCode,toString三个⽅法直接本地执⾏
} else if (\"hashCode\".equals(method.getName())) {
return hashCode();
} else if (\"toString\".equals(method.getName())) {
return toString();
}
//执⾏⽅法对应的⽅法处理器MethodHandler,这个接⼝是Feign提供的,与InvocationHandler⽆任何关系,只有⼀个invoke⽅法
return dispatch.get(method).invoke(args);
}
//点进上⾯的invoke⽅法
public Object invoke(Object[] argv) throws Throwable {
//创建⼀个request模版
RequestTemplate template = buildTemplateFromArgs.create(argv);
while (true) {
try {
return executeAndDecode(template, options); //创建request执⾏并且解码
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template); //创建Request并增强
Response response = client.execute(request, options); //执⾏调用请求,不再继续分析了
response = response.toBuilder().request(request).requestTemplate(template).build();
//如果有重写解码器,使⽤⾃定义的解码器,feign默认使⽤SpringEncoder
if (decoder != null)
return decoder.decode(response, metadata.returnType());
}
Request targetRequest(RequestTemplate template) {
//如果⾃定义了RequestInterceptor,在这⾥可以对Request进⾏增强
for (RequestInterceptor interceptor : requestInterceptors) {
//执⾏⾃定义的apply⽅法
interceptor.apply(template);
}
//创建Request
return target.apply(template);
}
四、补充
- 关于Client接⼝的实现类,使⽤注册中⼼和使⽤配置中⼼其流程稍有区别
//使⽤配置中⼼拿url⽅式进⾏调⽤,使⽤的是Client的默认内部实现类 Default ,其中Default使⽤的是HttpURLConnection进⾏Http请求的
HttpURLConnection connection = convertAndSend(request, options);
//如果使⽤的是服务发现,使⽤的使⽤Client的实现类FeignBlockingLoadBalancerClient,它会去根据配置的服务名去注册中⼼查找服务的IP地址和端⼝号,执⾏使⽤的仍然是默认实现类Default,通过HttpURLConnection请求
//FeignBlockingLoadBalancerClient,根据服务名称查找服务IP地址、端⼝ 88⾏
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
//具体实现⽅法,BlockingLoadBalancerClient类中 145⾏
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
//还有其他实现Client接⼝的客户端,例如ApacheHttpClient,ApacheHttpClient带有连接池功能,具有优秀的HTTP连接复⽤能⼒,需要通过引⼊依赖来使⽤
来源:https://www.cnblogs.com/hujh2022/p/16365366.html
本站部分图文来源于网络,如有侵权请联系删除。