SpringBoot-整合Open Feign

--------------------------------------------------

Spring Boot简单整合Open Feign
一、使用Open Feign
1、引入依赖
2、添加Open Feign
3、添加配置文件application.yml
二、Open Feign的调用
1、模拟一个服务的提供者(假设为student)
2、模拟一个服务的调用者(假设为classes)
一、使用Open Feign
1、引入依赖
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<!--可以删除version这一行,版本交给父依赖托管-->
<version>2.2.1.RELEASE</version>
</dependency>

<!--hystrix 豪猪哥,服务熔断-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<!--可以删除version这一行,版本交给父依赖托管-->
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<!--可以删除version这一行,版本交给父依赖托管-->
<version>2.2.1.RELEASE</version>
</dependency>

<!-- 配置这两个httpclient,目的是后续用对象的形式调用Get请求 -->
<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!-- 配置feign 发送请求使用 httpclient,而不是java原生 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>

2、添加Open Feign
在项目的启动类上添加Open Feign的启动注解==@EnableFeignClients和服务熔断的注解@EnableHystrix==
例如,第三行第四行

@SpringBootApplication()
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
public class ClassesApplication {
public static void main(String[] args) {
SpringApplication.run(ClassesApplication.class, args);
}
}
3、添加配置文件application.yml
在服务调用方配置文件如下

#feign客户端配置
feign:
#设置feign开启hystrix
hystrix:
enabled: true
#适配feign发送get请求
httpclient:
enabled: true
#ribbon配置
ribbon:
#ribbon的超时时间要大于hystrix的超时时间,否则 hystrix自定义的超时时间毫无意义
ReadTimeout: 5000
ConnectTimeout: 5000
#hystrix配置
hystrix:
command:
default:
execution:
isolation:
thread:
#feign整合hystrix 光设置Hystrix 超时没用的要配合ribbon超时
timeoutInMilliseconds: 3000
circuitBreaker:
#默认20 ,熔断的阈值,如何user服务报错满足3次,熔断器就会打开,就算order之后请求正确的数据也不行。
requestVolumeThreshold: 3
#默认5S , 等5S之后熔断器会处于半开状态,然后下一次请求的正确和错误讲决定熔断器是否真的关闭和是否继续打开
sleepWindowInMilliseconds: 8000

二、Open Feign的调用
假设当前场景有班级和学生两个对象,两个对象分别在ClassesApplication和SudentApplication两个服务中,现要求学生服务为班级服务提供查询所有学生的接口

1、模拟一个服务的提供者(假设为student)
1、学生服务的启动类,服务地址为localhost:9001,服务名称为service-student

@SpringBootApplication()
@EnableDiscoveryClient
@EnableFeignClients
public class SudentApplication{
public static void main(String[] args) {
SpringApplication.run(SudentApplication.class, args);
}
2、学生服务查询所有学生的接口

@RestController
@RequestMapping(value = "/api/v1/student")
public class StudentController{

//假设为学生接口服务层
@Autowired
private StudentService studentService;

//假设为查询所有学生的接口
@RequestMapping(value = "/list", method = RequestMethod.GET)
public List<Student> findAll() {
return student.findAll();
}
}

2、模拟一个服务的调用者(假设为classes)
1、班级服务的启动类,服务地址为localhost:9002,服务名称为service-classes

@SpringBootApplication()
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
public class ClassesApplication {
public static void main(String[] args) {
SpringApplication.run(ClassesApplication.class, args);
}
}

2、创建服务调用service

@FeignClient(name = "service-student", url = "localhost:9001/api/v1/student", fallback = "StudentFeignFallBack.class)
@Componet
public interface StudentFeignService{
//复制StudentController的查询所有学生的接口
@RequestMapping(value = "/list", method = RequestMethod.GET)
public List<Student> findAll();
}

@FeignClient即是我们常说的Feign注释,其中,name为调用的服务的服务名称,url为调用服务的服务地址,fallback为调用StudentFeignService方法失败后用来兜底的类

3、创建服务熔断类(兜底)

@Component
public class StudentFeignFallBack implements StudentFeignService{
/**
* 日志工具
*/
private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public List<Student> findAll(){
logger.error("获取学生列表失败", e);
retun null;
}
}

--------------------------------------------------

Spring Cloud OpenFeign Demo

之前项目中需要在Spring Cloud中使用OpenFeign的情况,Spring Cloud的版本是目前最新的Greenwich.SR2版本,对应的Spring boot是2.1.7.RELEASE。

在网上找了很多资料,大多言之不详,并且版本也比较低,不适合我的最新版本Spring Cloud的需求。 所以决定还是自己写个教程。

本教程要解决如下几个问题:

  1. 怎么配置OpenFeignServer
  2. 怎么配置OpenFeignClient
  3. 多个参数传递问题
  4. FeignClient的日志问题
  5. 多个FeignClient使用同一个name的问题

怎么配置OpenFeignServer

我们知道OpenFeign是用在Spring Cloud中的声明式的web service client。

OpenFeignServer就是一个普通的Rest服务,不同的是我们需要将他注册到eureka server上面,方便后面的OpenFeignClient调用。

启动类如下:

@SpringBootApplication
@EnableDiscoveryClient
public class OpenFeignServer {
        public static void main(String[] args) {
            SpringApplication.run(OpenFeignServer.class, args);
    }
}

我们定义了两个Rest服务:

OrderController:

@Slf4j
@RestController
@RequestMapping(path = "/order")
public class OrderController {

    /**
     * PostMapping with @RequestBody
     * @param user
     */
    @PostMapping("doOrder")
    public void doOrder(@RequestBody User user){
        log.info("do order !!!!");
    }
}

UserController:

@RestController
@RequestMapping(path = "/user")
public class UserController {

    /**
     * GetMapping example with @RequestParam
     * @param userId
     * @return userName
     */
    @GetMapping("getUserName")
    public String getUserName(@RequestParam("userId") String userId){
        if("100".equals(userId)) {
            return "张学友";
        }else{
            return "刘德华";
        }
    }

    /**
     * GetMapping example with @RequestParam and @SpringQueryMap
     * @param userId
     * @param user
     * @return userAge
     */
    @GetMapping("getUserAge")
    public String getUserAge(@RequestParam("userId") String userId, @SpringQueryMap User user){
        if("100".equals(userId)) {
            return "20";
        }else{
            return "18";
        }
    }
}

我们将其注册到eureka上面,名字为openfeign-server

spring:
  application:
    name: openfeign-server

怎么配置OpenFeignClient

OpenFeignClient的pom依赖如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

配置OpenFeignClient只需要使用@FeignClient来注解一个interface即可,如下所示:

@FeignClient(value = "openfeign-server")
@RequestMapping(path = "/user")
public interface UserClient {

    @GetMapping("getUserName")
    public String getUserName(@RequestParam("userId") String userId);

    @GetMapping("getUserAge")
    public String getUserAge(@RequestParam("userId") String userId, @SpringQueryMap User user);
}

其中@FeignClient中的value是要调用的服务的注册名,即OpenFeignServer在eureka的注册名。

FeignClient的 Request路径,方式和参数要和被调用的Rest服务保持一致。

这样我们就可以像下面一样来调用OpenFeignClient了:

@Slf4j
@RestController
public class UserController {

    @Autowired
    private UserClient userClient;

    @GetMapping("getUserName2")
    public void getUserName(){
        log.info(userClient.getUserName("100"));
    }
}

多个参数传递问题

一般我们会使用@GetMapping和@PostMapping两种方式来调用Rest服务。

而接收的参数则会使用@RequestParam和@RequestBody来获取。

首先我们讲一下@RequestBody,@RequestBody只能用在Post请求,并且一个Post请求只能有一个@RequestBody。 @RequestBody的参数可以包括复杂类型。

然后我们讲一下@RequestParam,@RequestParam可以用在Post和Get请求中,但是要注意:@RequestParam 的参数只能是基本类型或者Enum,或者List和Map(List和Map里面也只能是基本类型)。所以@RequestParam可以和@RequestBody一起使用。

如果我们是Get请求,但是又有复合类型怎么办? 比如我们想传递一个User对象。User对象里面只有普通的两个String属性。 这里我们可以使用@SpringQueryMap:

@GetMapping("getUserAge")
    public String getUserAge(@RequestParam("userId") String userId, @SpringQueryMap User user);

注意:@SpringQueryMap后面的参数只能是普通的POJO,不能是复合类型,否则解析不了。如果必须使用复合类型,那么使用@RequestBody吧。

FeignClient的日志问题

OpenFeign的Logger.Level有4种级别:

  • NONE 没有日志
  • BASIC 请求方法,请求URL,返回Code和执行时间
  • HEADERS 请求和返回的头部基本信息
  • FULL 请求和返回的头部,内容,元数据

要想使用这些级别,必须将OpenFeignClient的logger设置成debug级别:

#日志配置
logging:
  level:
    com:
      flydean: debug

同时我们在代码中配置OpenFeign的日志级别:

@Configuration
public class CustFeignLogConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

这样我们在日志里面就可以看到DEBUG的所有HTTP请求信息。

多个FeignClient使用同一个name的问题

其实这里我们的Server定义了两个Rest服务,User和Order。

上面我们讲到了可以这样定义UserClient:

@FeignClient(value = "openfeign-server")
@RequestMapping(path = "/user")
public interface UserClient {
    ...
}

如果我们同样的这样定义OrderClient:

@FeignClient(value = "openfeign-server")
@RequestMapping(path = "/order")
public interface OrderClient {
    ...
}

运行时候就会报错。 原因是两个FeignClient使用了同一个value!

那怎么解决这个问题呢?

/**
 * 因为@FeignClient的value不能重复,所以需要在这里以自定义的方式来创建
 * @author wayne
 * @version FeignClientController,  2019/9/5 7:07 下午
 */
@Data
@Component
@Import(FeignClientsConfiguration.class)
public class FeignClientController {

    private OrderClient orderClient;
    private UserClient userClient;

    public FeignClientController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
        this.orderClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                //默认是Logger.NoOpLogger
                .logger(new Slf4jLogger(OrderClient.class))
                //默认是Logger.Level.NONE
                .logLevel(Logger.Level.FULL)
                .target(OrderClient.class, "http://openfeign-server");

        this.userClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                //默认是Logger.NoOpLogger
                .logger(new Slf4jLogger(UserClient.class))
                //默认是Logger.Level.NONE
                .logLevel(Logger.Level.FULL)
                .target(UserClient.class, "http://openfeign-server");
    }
}

方法就是手动创建FeignClient, 上面的例子中,我们手动创建了OrderClient和UserClient两个FeignClient。

注意下面的代码片段,手动创建的FeignClient默认是没有logger和logLevel的。所以上面我们配置好的log信息对手动创建的FeignClient是无效的。 下面展示了如何手动添加:

//默认是Logger.NoOpLogger
                .logger(new Slf4jLogger(OrderClient.class))
                //默认是Logger.Level.NONE
                .logLevel(Logger.Level.FULL)

如何运行

本项目的模块都是以spring boot构建的,直接在编译器中运行Main方法即可启动。

  1. 启动openfeign-registry-server

openfeign-registry-server会启动eureka server,供后面的OpenFeignServer和OpenFeignClient注册。

  1. 启动openfeign-server
  2. 启动openfeign-client
  3. 测试openFeignClient

--------------------------------------------------

文章目录
前言
一、Feign是什么?
二、使用步骤
1.引入库
2.编写远程服务示例
3.本地编写访问接口
1.GET请求,使用RequestParam传参
2.DELETE请求,使用PathVariable传参
3.POST请求,使用RequestBody传参
4.PUT请求,使用RequestBody传参
5.POST请求,使用FORM表单传参
6.多个url参数使用map传递
7.form表单参数,以对象传递
8.form表单传送数据,多个参数一个一个传递
9.单文件上传-方式1
10.单文件上传-方式2
11.文件下载
12.多文件上传
12.总体的接口类
4.测试
总结
前言
大家都知道openfeigin是springcloud家族中的一个组件,实现远程服务的restful访问,需要配合springcloud的注册中心和负载均衡组件来使用,实现多个微服务间的访问。我在想在非微服务场景中使用openfeign呢?以前使用的Okhttp的封装工具类当然也是可以的,但openfeign更像是在调用本地接口,感觉更简单。当然,openfeign底层通信的http客户端也是okhttp啦。

一、Feign是什么?
Feign是一个声明式的Web Service客户端。它的出现使开发Web Service客户端变得很简单。使用Feign只需要创建一个接口加上对应的注解,比如:FeignClient注解。Feign有可插拔的注解,包括Feign注解和JAX-RS注解。
Feign也支持编码器和解码器,Spring Cloud Open Feign对Feign进行增强支持Spring MVC注解,可以像Spring Web一样使用HttpMessageConverters等。
Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程方法,更感知不到在访问HTTP请求。
feigin支持的功能:
1.可插拔的注解支持,包括Feign注解和JAX-RS注解。
2.支持可插拔的HTTP编码器和解码器(Gson,Jackson,Sax,JAXB,JAX-RS,SOAP)。
3.支持Hystrix和它的Fallback。
4.支持Ribbon的负载均衡。
5.支持HTTP请求和响应的压缩。
6.灵活的配置:基于 name 粒度进行配置
7.支持多种客户端:JDK URLConnection、apache httpclient、okhttp,ribbon)
8.支持日志
9.支持错误重试
10.url支持占位符
11.可以不依赖注册中心独立运行

二、使用步骤
1.引入库
这里用的gradle,如果使用maven请切换为maven的写法

api group: 'io.github.openfeign', name: 'feign-core', version: "11.1"
api group: 'io.github.openfeign', name: 'feign-jackson', version: "11.1"

2.编写远程服务示例
编写远程服务实例,以下实例包含了多种情况,请求方式包括GET、POST、DELETE、PUT,传递函数方式包括RequestParam、PathVariable、RequestBody。

/**
*
* 测试远程被调用的feign接口
* @author zhuquanwen
* @vesion 1.0
* @date 2021/4/13 9:26
* @since jdk1.8
*/
@RestController
@RequestMapping("/test/feign/remote")
public class RemoteFeignController extends BaseController {

@GetMapping("/t1")
public ResponseEntity t1(@RequestParam("name") String name) {
ResponseEntity response = getResponse();
response.setValue(name);
return response;
}

@DeleteMapping("/t2/{name}")
public ResponseEntity t2(@PathVariable("name") String name) {
ResponseEntity response = getResponse();
response.setValue(name);
return response;
}

@PostMapping("/t3")
public ResponseEntity t3(@RequestBody Map<String, Object> params) {
ResponseEntity response = getResponse();
response.setValue(params);
return response;
}

@PutMapping("/t4")
public ResponseEntity t4(@RequestBody Map<String, Object> params) {
ResponseEntity response = getResponse();
response.setValue(params);
return response;
}

@PostMapping("/t5")
public ResponseEntity t5(@RequestParam Map<String, Object> params) {
ResponseEntity response = getResponse();
response.setValue(params);
return response;
}
@PostMapping("/t6")
public ResponseEntity t6(RetrofitTestModel model) {
ResponseEntity response = getResponse();
response.setValue(model);
return response;
}

@PostMapping("/t7")
public ResponseEntity t7(String name) {
ResponseEntity response = getResponse();
response.setValue(name);
return response;
}

@PostMapping("/t8")
public ResponseEntity t8(@RequestHeader("name") String name) {
ResponseEntity response = getResponse();
response.setValue(name);
return response;
}

@PostMapping("/t9")
public ResponseEntity t9(@RequestHeader("name") String name, @RequestHeader("token") String token) {
ResponseEntity response = getResponse();
response.setValue(name + ";" + token);
return response;
}

@PostMapping("/t10")
public ResponseEntity t10(MultipartFile file1) {
ResponseEntity response = getResponse();
response.setValue(file1.toString());
return response;
}
// @PostMapping("/t11")
// public ResponseEntity t11(MultipartFile[] file1) {
// ResponseEntity response = getResponse();
// response.setValue(file1.toString());
// return response;
// }


@PostMapping("/t11")
public ResponseEntity t11() throws ServletException, IOException {
HttpServletRequest request = SpringUtils.getRequest();
Collection<Part> parts = request.getParts();
if (parts != null) {
for (Part part : parts) {
InputStream inputStream = part.getInputStream();
}
}
ResponseEntity response = getResponse();
response.setValue(parts.toString());
return response;
}

@GetMapping("/t12")
public void t12() throws Exception {
FileDownloadUtils.downFile(SpringUtils.getRequest(), SpringUtils.getResponse(),
"D:/test-sp/aaa.html", "aaa.html");
}

@Data
public static class RetrofitTestModel {
private String name;
}
}


3.本地编写访问接口
1.GET请求,使用RequestParam传参
@RequestLine("GET /test/feign/remote/t1?name={name}")
ResponseEntity remoteT1(@Param("name") String username);

2.DELETE请求,使用PathVariable传参
@RequestLine("DELETE /test/feign/remote/t2/{name}")
ResponseEntity remoteT2(@Param("name") String name);

3.POST请求,使用RequestBody传参
@RequestLine("POST /test/feign/remote/t3")
@Headers({"Content-Type: application/json","Accept: application/json"})
ResponseEntity remoteT3(Map<String, Object> params);

4.PUT请求,使用RequestBody传参
@RequestLine("PUT /test/feign/remote/t4")
@Headers({"Content-Type: application/json","Accept: application/json"})
ResponseEntity remoteT4(Map<String, Object> params);

5.POST请求,使用FORM表单传参
@RequestLine("POST /test/feign/remote/t5")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity remoteT5(@QueryMap Map<String, Object> params);

6.多个url参数使用map传递
/**多个url参数使用map传递*/
@RequestLine("POST /test/feign/remote/t5")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity remoteT5(@QueryMap Map<String, Object> params);

7.form表单参数,以对象传递
/**form表单参数,以对象传递*/
@RequestLine("POST /test/feign/remote/t6")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity<RemoteFeignController.RetrofitTestModel> remoteT6(RemoteFeignController.RetrofitTestModel model);

8.form表单传送数据,多个参数一个一个传递
@RequestLine("POST /test/feign/remote/t7")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity remoteT7(@Param("name") String name);

9.单文件上传-方式1
/**单文件上传*/
@RequestLine("POST /test/feign/remote/t10")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT10(@Param("file1") File file);

10.单文件上传-方式2
/**单文件上传*/
@RequestLine("POST /test/feign/remote/t10")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT10_2(@Param("file1") FormData data);

11.文件下载
/**文件下载*/
@RequestLine("GET /test/feign/remote/t12")
Response remoteT12();

12.多文件上传
/**多文件上传*/
@RequestLine("POST /test/feign/remote/t11")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT11(Map<String, ?> data);

12.总体的接口类
package com.iscas.biz.test.feign.client;
import com.iscas.templet.common.ResponseEntity;
import feign.*;
import okhttp3.MediaType;

import java.util.Map;

public interface FeignInterface {
/**url中参数 ?name=xxx*/
@RequestLine("GET /test/feign/remote/t1?name={name}")
ResponseEntity remoteT1(@Param("name") String username);

/**url路径中参数*/
@RequestLine("DELETE /test/feign/remote/t2/{name}")
ResponseEntity remoteT2(@Param("name") String name);

/**使用请求体传送json*/
@RequestLine("POST /test/feign/remote/t3")
@Headers({"Content-Type: application/json","Accept: application/json"})
ResponseEntity remoteT3(Map<String, Object> params);

/**使用请求体传送json,使用PUT请求*/
@RequestLine("PUT /test/feign/remote/t4")
@Headers({"Content-Type: application/json","Accept: application/json"})
ResponseEntity remoteT4(Map<String, Object> params);

/**多个url参数使用map传递*/
@RequestLine("POST /test/feign/remote/t5")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity remoteT5(@QueryMap Map<String, Object> params);

/**form表单参数,以对象传递*/
@RequestLine("POST /test/feign/remote/t6")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity<RemoteFeignController.RetrofitTestModel> remoteT6(RemoteFeignController.RetrofitTestModel model);

/**form表单传送数据,多个参数一个一个传递*/
@RequestLine("POST /test/feign/remote/t7")
@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/json"})
ResponseEntity remoteT7(@Param("name") String name);

/**单文件上传*/
@RequestLine("POST /test/feign/remote/t10")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT10(@Param("file1") File file);

/**单文件上传*/
@RequestLine("POST /test/feign/remote/t10")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT10_2(@Param("file1") FormData data);

/**多文件上传*/
@RequestLine("POST /test/feign/remote/t11")
@Headers({"Content-Type: multipart/form-data","Accept: application/json"})
ResponseEntity remoteT11(Map<String, ?> data);


/**文件下载*/
@RequestLine("GET /test/feign/remote/t12")
Response remoteT12();

}

拦截器:

package cn.ac.iscas.newframe.biz.test.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;

import java.util.Collection;
import java.util.Map;

/**
* feign的请求拦截器
*
* @author zhuquanwen
* @vesion 1.0
* @date 2021/8/31 15:28
* @since jdk1.8
*/
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Map<String, Collection<String>> headers = template.headers();
System.out.println(headers);
//增加一个header
template.header("custom-header", "lalala");
//增加一个参数
template.query("add-param", "heiheihei");
}
}


4.测试
注意下面target只不过FeignInterface.class就是上面定义的接口,http://localhost:7901/demo是远程服务的URL,/demo是服务的context-path。JacksonEncoder()和JacksonDecoder()是服务编解码的工具。

public static void main(String[] args) throws IOException {
FeignApi feignApi = Feign.builder()
.requestInterceptor(new FeignRequestInterceptor())
.encoder(new FormEncoder(new JacksonEncoder()))
.decoder(new JacksonDecoder())
// .decoder(new StringDecoder())
.options(new Request.Options(1000, 3500))
.retryer(new Retryer.Default(5000, 5000, 3))
.target(FeignApi.class, "http://localhost:7901/demo");
ResponseEntity res1 = feignApi.remoteT1("zhansan");
System.out.println(res1);
ResponseEntity res2 = feignApi.remoteT2("zhansan");
System.out.println(res2);
HashMap<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
ResponseEntity res3 = feignApi.remoteT3(map);
System.out.println(res3);
ResponseEntity res4 = feignApi.remoteT4(map);
System.out.println(res4);
ResponseEntity res5 = feignApi.remoteT5(map);
System.out.println(res5);
RemoteFeignController.RetrofitTestModel model = new RemoteFeignController.RetrofitTestModel();
model.setName("zhangsan");
ResponseEntity res6 = feignApi.remoteT6(model);
System.out.println(res6);
ResponseEntity res7 = feignApi.remoteT7("张三");
System.out.println(res7);
ResponseEntity res10 = feignApi.remoteT10(new File("D:/h2.mv.db"));
System.out.println(res10);
FormData formData = FormData.builder()
.data(Files.readAllBytes(new File("D:/h2.mv.db").toPath()))
.contentType("image/png")
.fileName("h2.mv.db")
.build();
ResponseEntity res10_2 = feignApi.remoteT10_2(formData);
System.out.println(res10_2);

ResponseEntity res11 = feignApi.remoteT11(new HashMap<String, File>(){{
put("file1", new File("D:/aaa.txt"));
put("file2", new File("D:/h2.mv.db"));
}});
System.out.println(res11);

Response response = feignApi.remoteT12();
System.out.println(response.body());
}

--------------------------------------------------

Spring Cloud OpenFeign
知识点:

OpenFeign介绍
使用方法
4个特点(Gzip、灵活Logger、替换HttpUrlConnect、超时控制)
原理分析(启动的EnbaleFeignClients,扫描FeignClient,注入Spring bean中的Factorybean 代理bean,调用的时候直接执行)
1、介绍
OpenFeign源于Netflix的Feign,是http通信的客户端。屏蔽了网络通信的细节,直接面向接口的方式开发,让开发者感知不到网络通信细节。 所有远程调用,都像调用本地方法一样完成!

OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。

OpenFeign 对 Ribbon进行了集成,利用 Ribbon 维护了一份可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。

Openfeign源于Netflix的Feign,http通信的客户端。屏蔽了网络通信的细节。让所有的调用都想本地调用一样完成。
支持Spring MVC注解。
集成了Ribbon实现了客户端的负载均衡。
1.1、OpenFeign 常用注解
注解 说明
@FeignClient 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。
@EnableFeignClients 该注解用于开启 OpenFeign 功能(一般使用在SpringBoot启动类上),当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。
@RequestMapping Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。
@GetMapping Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。
@PostMapping Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。
2、使用方法
创建项目provider-api,作为OpenFeign接口公共Api
引入pom依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

创建Api接口

public interface IPromotionService {
@GetMapping("/promotion")
public String promotion();
}

创建feignClient接口

@FeignClient(name = "marking-service") // 这里FeignClient注解会被扫描
public interface IPromotionServiceClient extends IPromotionService {
}

创建项目provider-service
引入pom依赖

<!-- eureka 客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- provider-api 接口依赖 -->
<dependency>
<groupId>com.fanger</groupId>
<artifactId>provider-api</artifactId>
</dependency>

创建API接口实现

@Slf4j
@RestController
@RequestMapping("/promotion")
public class PromotionController implements IPromotionService {
@GetMapping
public String promotion(){
return "promotion-info SUCCESS";
}
}

application.properties

server.port=9091
spring.application.name=provider-service
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

创建consumer-service
consumer-service是OpenFeign接口的调用方

引入pom依赖

<!-- eureka 客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- provider-api 接口依赖 -->
<dependency>
<groupId>com.fanger</groupId>
<artifactId>provider-api</artifactId>
</dependency>


启动类配置

// 开启OpenFeign,并配置扫描路径,provider-api中feignClient接口的类路径
@EnableFeignClients(basePackages = "com.fanger.mall.feignclient")
@SpringBootApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class, args);
}
}

application.properties

server.port=8080
spring.application.name=ConsumerService
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

Controller接口

@Slf4j
@RestController
public class PromotionController {

// 这里注入代理类
@Autowired
IPromotionService iPromotionService;

@PostMapping("/promotion")
public String login(){
// 这里直接类似本地调用的方式来执行
return iPromotionService.promotion();
}
}

3、OpenFeign的特性
3.1、Gzip压缩
开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点

#开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点
#开启gzip 压缩协议
feign.compression.request.enabled=false
#响应也开启gzip压缩传输协议
feign.compression.response.enabled=true
#最小压缩文件大小为2048m
feign.compression.request.min-request-size=2048
#请求数据格式
feign.compression.request.mime-types=text/xml, application/xml, application/json

3.2、Feign日志配置
a. 创建config类

@Configuration
public class FeignLogConfig {
@Bean
Logger.Level feignLogger(){
Logger.Level full = Logger.Level.FULL; //全部日志
Logger.Level none = Logger.Level.NONE; //不记录日志
Logger.Level basic = Logger.Level.BASIC; //除了headers日志
Logger.Level headers = Logger.Level.HEADERS; //headers 日志
return full;
}
}

b.需要记录日志处引用FeignLogConfig 类

@FeignClient(name = "eureka-product-service",configuration = FeignLogConfig.class)
public interface IHelloControllerFeign {

@GetMapping("/hello")
String sayHello();
}

c. application.properties 配置需要记录日志文件的日志级别

#设置feign日志级别
logging.level.[com.example.demo.service.IHelloControllerFeign]=debug

3.3、替换默认的底层通信
默认远程访问协议为jdk自带的sun.net.www.protocol.http.HttpURLConnection效率低,可改为okHttp

a.添加OKhttp依赖

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

b.application.properties 配置,关闭默认的sun.net.www.protocol.http.HttpURLConnection,开启okhttp

feign.httpclient.enable=false
feign.okhttp.enable=true

3.4、超时控制
OpenFeign默认超时时间为1s,超过1s就会返回错误页面。如果我们的接口处理业务确实超过1s,就需要对接口进行超时配置,如下:

ribbon: #设置feign客户端连接所用的超时时间,适用于网络状况正常情况下,两端连接所用时间
ReadTimeout: 1000 #指的是建立连接所用时间
ConnectTimeout: 1000 #指建立连接后从服务读取到可用资源所用时间

4、工作原理分析


Feign 的工作流程:

SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 这里以 DemoService 为例, 将这些类解析为 BeanDefinition 注册到 Spring 容器中
Sping 容器在为某些用的 Feign 接口的 Bean 注入 DemoService 时, Spring 会尝试从容器中查找 DemoService 的实现类
由于我们从来没有编写过 DemoService 的实现类, 上面步骤获取到的 DemoService 的实现类必然是 feign 框架通过扩展 spring 的 Bean 处理逻辑, 为 DemoService 创建一个动态接口代理对象, 这里我们将其称为 DemoServiceProxy 注册到spring 容器中。
Spring 最终在使用到 DemoService 的 Bean 中注入了 DemoServiceProxy 这一实例。
当业务请求真实发生时, 对于 DemoService 的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。

--------------------------------------------------

工作原理
在展开讲解工作原理前, 首先捋一下上文中, 我们完成 Feign 调用前所进行的操作:

添加了 Spring Cloud OpenFeign 的依赖
在 SpringBoot 启动类上添加了注解 @EnableFeignCleints
按照 Feign 的规则定义接口 DemoService, 添加@FeignClient 注解
在需要使用 Feign 接口 DemoService 的地方, 直接利用@Autowire 进行注入
使用接口完成对服务端的调用
可以根据上面使用 Feign 的步骤大致猜测出整体的工作流程:

SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 这里以 DemoService 为例, 将这些类解析为 BeanDefinition 注册到 Spring 容器中
Sping 容器在为某些用的 Feign 接口的 Bean 注入 DemoService 时, Spring 会尝试从容器中查找 DemoService 的实现类
由于我们从来没有编写过 DemoService 的实现类, 上面步骤获取到的 DemoService 的实现类必然是 feign 框架通过扩展 spring 的 Bean 处理逻辑, 为 DemoService 创建一个动态接口代理对象, 这里我们将其称为 DemoServiceProxy 注册到spring 容器中。
Spring 最终在使用到 DemoService 的 Bean 中注入了 DemoServiceProxy 这一实例。
当业务请求真实发生时, 对于 DemoService 的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。
上面整个流程可以进一步简化理解为:

我们定义的接口 DemoService 由于添加了注解 @FeignClient, 最终产生了一个虚假的实现类代理
使用这个接口的地方, 最终拿到的都是一个假的代理实现类 DemoServiceProxy
所有发生在 DemoServiceProxy 上的调用, 都被转交给 Feign 框架, 翻译成 HTTP 的形式发送出去, 并得到返回结果, 再翻译回接口定义的返回值形式。

posted @ 2022-12-06 14:18  hanease  阅读(3068)  评论(0编辑  收藏  举报