重新定义cloud 第四章feign

  • 消费者 调用 提供者
    • http client
    • jdk原生 URLConnection
    • Apache 的 Http Client
    • Netty 的异步 Http Client
    • Spring 的 RestTemplate
    • Spring Cloud Open Feign (最好)
  • cloud对feign增强,支持MVC注解

feign 概述

  • 声明式 的 web service 客户端

  • 声明式,模板化 的Http 客户端

  • FeignClient注解

  • 可插拔的注解,feign注解 和 JAX-RS注解

  • Spring Cloud Open Feign 对Feign增强

  • 可以像Spring Web一样 使用HttpMessageConverters

  • feign的优势

    • 可插拔的注解支持,Feign注解 和 JAX-RS注解
    • 可插拔的 HTTP 编码器 和 解码器
    • 支持 Hystrix的 FallBack
    • 支持 ribbon的负载均衡
    • 支持 HTTP 请求 和 响应 的 压缩

feign的入门案例

引入依赖和 开启feign

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Cloud OpenFeign的Starter的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>
@EnableFeignClients //当程序启动时,会进行包扫描 @FeignClient

编写 FeignClient 和 action

@FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)
public interface HelloFeignService {

    /**
     * content: {"message":"Validation Failed","errors":[{"resource":"Search","field":"q","code":"missing"}],
     * "documentation_url":"https://developer.github.com/v3/search"}
     * @param queryStr
     * @return
     */
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepo(@RequestParam("q") String queryStr);

}

@RestController
public class HelloFeignController {

    @Autowired
    private HelloFeignService helloFeignService;

    // 服务消费者对位提供的服务
    @GetMapping(value = "/search/github")
    public String searchGithubRepoByStr(@RequestParam("str") String queryStr) {
        return helloFeignService.searchRepo(queryStr);
    }

}

http://localhost:8011/search/github?str=spring-cloud-dubbo

https://api.github.com/search/repositories?q=spring-cloud-dubbo

{
	"total_count": 136,
	"incomplete_results": false,
	"items": [{
		"id": 133478014,
		"node_id": "MDEwOlJlcG9zaXRvcnkxMzM0NzgwMTQ=",
		"name": "spring-cloud-dubbo",
		"full_name": "SpringCloud/spring-cloud-dubbo",
		"private": false,
		"owner": {
			"login": "SpringCloud",
			"id": 15123112,
			"node_id": "MDEyOk9yZ2FuaXphdGlvbjE1MTIzMTEy",
			"avatar_url": "https://avatars3.githubusercontent.com/u/15123112?v=4",
			"gravatar_id": "",
			"url": "https://api.github.com/users/SpringCloud",
			"html_url": "https://github.com/SpringCloud",
			"type": "Organization",
			"site_admin": false
		},
}

feign 的工作原理

  • 当 feign接口的方法被调用时,通过JDK 的代理方式,生成 RequestTemplate

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface FeignClient {
        @AliasFor("name")
        String value() default ""; //指定feign名称
    
        /** @deprecated */
        @Deprecated
        String serviceId() default "";
    
        @AliasFor("value")
        String name() default "";
    
        String qualifier() default "";
    
        String url() default ""; //手动指定 调用的地址
    
        boolean decode404() default false; //404时为true,该字段为true,会调用 decoder进行解码
    
        Class<?>[] configuration() default {};//feign的配置类, Encoder Decoder LogLevel Contract
    
        Class<?> fallback() default void.class;//容错的处理类
    
        Class<?> fallbackFactory() default void.class;//工具类,生成fallback类示例,实现通用的容错
    
        String path() default "";//feign Client的统一前缀
    
        boolean primary() default true;
    }
    
    

feign 开启GZIP压缩

  • feign支持 请求和响应 进行gzip压缩
  • application.yml

    server:
      port: 8011
    spring:
      application:
        name: ch4-1-gzip
    
    logging:
         level:
           cn.springcloud.book.feign.service.HelloFeignService: debug
    
    
    feign:
        compression:
            request:
                enabled: true
                mime-types: text/xml,application/xml,application/json # 配置压缩支持的MIME TYPE
                min-request-size: 2048  # 配置压缩数据大小的下限
            response:
                enabled: true # 配置响应GZIP压缩
    
    compression 
    

英 /kəmˈpreʃn/ 美 /kəmˈpreʃn/ 全球(英国)
简明 新牛津 韦氏 例句 百科
n. 压缩,浓缩;压榨,压迫

compress
英 /kəmˈpres/ 美 /kəmˈpres/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
vi. 受压缩小
vt. 压缩,压紧;精简


- 开启gzip,feign之间通过 二进制协议 进行传输,接收:ResponseEntity<byte[]>

  ```java
  @FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)
  public interface HelloFeignService {
  
  
      @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
      ResponseEntity<byte[]> searchRepo(@RequestParam("q") String queryStr);
  
  }
  
  
  @Configuration
  public class HelloFeignServiceConfig {
  
      /**
       *
       * Logger.Level 的具体级别如下:
           NONE:不记录任何信息
           BASIC:仅记录请求方法、URL以及响应状态码和执行时间
           HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
           FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
       * @return
       */
      @Bean
    Logger.Level feignLoggerLevel() {
          return Logger.Level.FULL;
      }
  
  }
  
      @GetMapping(value = "/search/github")
      public ResponseEntity<byte[]> searchGithubRepoByStr(@RequestParam("str") String queryStr) {
          return helloFeignService.searchRepo(queryStr);
      }
  ```


### 单个feign配置 和通用feign

```yaml
feign:
  client:
    config:
      feignName: #配置名字
        connectionTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        retryer: #重试
        #还能配置错误编码器,拦截器,编码器,解码器
feign:
client:
  config:
    default:
      connectionTimeout: 5000
      readTimeout: 5000
      loggerLevel: basic
  • @EnableFeignClients(defaultConfiguration=DefaultFeignConfiguration.class)

feign client开启日志

@FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)


@Configuration
public class HelloFeignServiceConfig {

    /**
     *
     * Logger.Level 的具体级别如下:
         NONE:不记录任何信息
         BASIC:仅记录请求方法、URL以及响应状态码和执行时间
         HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
         FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

}


  • 或者用这个方法
logging:
     level:
       cn.springcloud.book.feign.service.HelloFeignService: debug
debug日志
 ---> GET https://api.github.com/search/repositories?q=spring-cloud HTTP/1.1

 <--- HTTP/1.1 200 OK (2249ms)
 content-type: application/json; charset=utf-8
 
 <--- END HTTP (164268-byte body)

feign的超时设置

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
  • feign两层

    • ribbon的调用 和 hystrix的调用。
    • 高版本的hystrix默认是关闭的。
  • feign.RetryableException:Read timed out execution

    • ribbon处理超时

      #application.properties 这两个配置可用,其他的我测试的都不可用。
      ribbon.ReadTimeout=6000 #请求处理的超时时间
      ribbon.ConnectTimeout=6000 #请求连接超时的时间
      #这两个配置后,12秒才会超时(无hystrix)。天更蓝了,所有的运行也都能成功了
      
      #hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
      #spring.cloud.loadbalancer.retry.enabled=false
      
      
  • HystrixRuntimeException:FeignDemo#demo()

    • Hystrix的超时

      feign:
        client:
          config: true
      hystrix:
        shareSecurityContext: true
        command:
          default:
            circuitBreaker:
              sleepWindowInMilliseconds: 100000
              forceClosed: true
            execution:
              isolation:
                thread:
                  timeoutInMilliseconds: 60000
      

feign的亲测可用的配置

ribbon:
  # 连接超时时间
  ConnectTimeout: 5000
  # 读取超时时间 如果 交给ribbon控制 ,超时时间就用这个。
  ReadTimeout: 6000 


feign:
  hystrix:
  	# 如果不开hystrix,上面的ribbon 6秒后,就直接报错
  	# java.net.SocketTimeoutException: Read timed out
    enabled: true

#  hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000
#  hystrix.command.default.execution.timeout.enable=true #开启feign的超时

hystrix:
  command:
    default:
      execution:
        timeout:
          # 为false则超时控制有ribbon控制,为true则hystrix
          enabled: true
        isolation:
          thread:
            # 超时时间 5s,要比其他组件时间长,不然重试机制就不起作用了
            timeoutInMilliseconds: 18000
            # 18秒后 才会进入断路器 (呵呵哒,第二天就变成了 ribbon的ReadTimeout了)

@produces(指定响应) 和 consumes(指定请求)

  • produces(指定响应)可能不算一个注解,因为什么呢,它是注解@requestMapping注解里面的属性项,它的作用是指定返回值类型,不但可以设置返回值类型还可以设定返回值的字符编码;
  • 还有一个属性与其对应,就是consumes(指定请求): 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
  • 有些项目需要加 produces (加在feign上 或者 提供者上 都行)。
  • 有些不加,feign也能收到 正确的响应。(不加就收到,那下面的都不用看了)
  • 总之 这两个属性都要加吧

feign 请求讲解

  • feign:我想接口接收到 json ,所以我 指定了produces

    • 也可以不加,不加的话对方要加(@RestController 是指定返回,有些项目还要加 produces)

    • 目标加:@PostMapping("/huaTest",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

    • 目标类上加:

      @RestController
      @RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
      
  • feign:我想发送的json被对方接收到,所及加了consumes ( 也可以不加,不加对方可以收到,@RequestBody)

    @GetMapping("/hello2")
    public ResultDTO hello2(){
        CarInfo carInfo=new CarInfo();
        carInfo.setGetCarCode("ddddddd-hua");
        ResultDTO resultDTO = auxiliaryFeign.huaTest(carInfo);
        System.out.println("我要打印了:"+resultDTO);
        return resultDTO;
    }

@FeignClient(name ="auxiliary", fallback = AuxiliaryHystrix.class)
public interface AuxiliaryFeign {
    
   @PostMapping(value = "/huaTest",
                consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
                produces = MediaType.APPLICATION_JSON_UTF8_VALUE
               )
    ResultDTO huaTest(@RequestBody CarInfo carInfo);
    
}

feign到的接口(提供者)讲解

@RestController
public class TestCotroller {
    
	@PostMapping("/huaTest",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResultDTO screenTest(@RequestBody CarInfo carInfo) {
        System.out.println("参数打印:"+carInfo);
        
        ResultDTO resultDTO = new ResultDTO();
        resultDTO.setCode(200);

        return resultDTO;
    }
    
}
  • 我想提供给对方json 用@RestController

  • 我想接收到的数据是json 用@RequestBody

  • 我想指定我的 返回 是json :

    • produces = MediaType.APPLICATION_JSON_UTF8_VALUE
  • 字符串为:application/json;charset=UTF-8

    • 和上面的并不冲突,这个主要是 feign好解析。有些项目,不指定这个 feign的返回为null(当然这是他没有在 feign上配置 produces 的原因)。

一个logback.xml文件,放在 resource下

<?xml version="1.0" encoding="UTF-8"?>

<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
<configuration scan="true" scanPeriod="10 seconds">
	<!--<include resource="org/springframework/boot/logging/logback/base.xml" 
		/> -->

	<!--定义日志文件的存储地址和前缀名 -->
	<property name="LOG_HOME" value="logs" />
	<property name="LOG_PREFIX" value="eureka-server" />

	<!-- 一般信息按照每天生成日志文件 -->
	<appender name="INFO_FILE"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<File>${LOG_HOME}/${LOG_PREFIX}-info.log</File>
		<rollingPolicy
			class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!-- 每天一归档 -->
			<fileNamePattern>${LOG_HOME}/${LOG_PREFIX}-info-%d{yyyyMMdd}.log.%i
			</fileNamePattern>
			<!-- 单个日志文件最多500MB, 30天的日志周期,最大不能超过20GB -->
			<maxFileSize>100MB</maxFileSize>
			<maxHistory>30</maxHistory>
			<totalSizeCap>20GB</totalSizeCap>
		</rollingPolicy>
		<encoder>
			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
			<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
				-%msg%n</Pattern>
		</encoder>
	</appender>

	<!--错误信息按照每天生成日志文件 -->
	<appender name="ERROR_FILE"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>ERROR</level>
		</filter>
		<File>${LOG_HOME}/${LOG_PREFIX}-error.log</File>
		<rollingPolicy
			class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!-- 每天一归档 -->
			<fileNamePattern>${LOG_HOME}/${LOG_PREFIX}-error-%d{yyyyMMdd}.log.%i
			</fileNamePattern>
			<!-- 单个日志文件最多500MB, 30天的日志周期,最大不能超过20GB -->
			<maxFileSize>100MB</maxFileSize>
			<maxHistory>30</maxHistory>
			<totalSizeCap>20GB</totalSizeCap>
		</rollingPolicy>
		<encoder>
			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
			<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
				-%msg%n</Pattern>
		</encoder>
	</appender>

	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
				%msg%n</pattern>
		</encoder>
	</appender>

	<!-- 日志输出级别 这样设置不打印日志 -->
	<root level="INFO">
		<appender-ref ref="STDOUT" />
		<appender-ref ref="INFO_FILE" />
		<appender-ref ref="ERROR_FILE" />
	</root>

</configuration>

feign的实战应用

  • feign默认用 JDK原生的 URL Connection 发送 请求
  • 可以用 Apache的 Http Client 和 okhttp替换

使用 apache httpclient替换

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Cloud OpenFeign的Starter的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 使用Apache HttpClient替换Feign原生httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

        <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>8.17.0</version>
        </dependency>

    </dependencies>
server:
  port: 8010
spring:
  application:
    name: ch4-3-httpclient

feign:
  httpclient:
      enabled: true

使用 okhttp替换feign默认的client

  • okhttp 具有以下特性

    • sydy 合并多个请求到同一个主机的请求
    • 使用连接池技术
    • 使用gzip压缩减少
    • 缓存响应 避免重复的网络请求
    
            <!-- Spring Cloud OpenFeign的Starter的依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
    
            <dependency>
                <groupId>io.github.openfeign</groupId>
                <artifactId>feign-okhttp</artifactId>
            </dependency>
    
    
    feign:
        httpclient:
             enabled: false
        okhttp:
             enabled: true
    
  • okHttpClient 是 okhttp的核心功能 执行者

  • new OkHttClient();

  • 也可以如下配置

    @Configuration
    @ConditionalOnClass(Feign.class)
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    public class FeignOkHttpConfig {
        @Bean
        public okhttp3.OkHttpClient okHttpClient(){
            return new okhttp3.OkHttpClient.Builder()
                     //设置连接超时
                    .connectTimeout(60, TimeUnit.SECONDS)
                    //设置读超时
                    .readTimeout(60, TimeUnit.SECONDS)
                    //设置写超时
                    .writeTimeout(60,TimeUnit.SECONDS)
                    //是否自动重连
                    .retryOnConnectionFailure(true)
                    .connectionPool(new ConnectionPool())
                    //构建OkHttpClient对象
                    .build();
        }
    
    }
    
  • http://localhost:8011/search/github?str=spring-cloud-openfeign

feign的多参数传递

  • 通过 feign 拦截器 拦截处理
    • 通过实现 feign 的 RequestInterceptor中的 apply方法 来 进行 统一 拦截 转换 处理 Feign中的 Get方法多参数传递的问题
配置
//交给spring ioc管理
@Component
public class FeignRequestInterceptor implements RequestInterceptor {//实现RequestInterceptor

    @Autowired//注入 objectMapper
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        // feign 不支持 GET 方法传 POJO, json body转query
        //如果请求模板,的方法 为 get ,并且 body()不为null
        if (template.method().equals("GET") && template.body() != null) {
            try {
                //读取 模板的 body()
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);//在把模板的body属性设置null

                //创建string  Collection<String>对象
                Map<String, Collection<String>> queries = new HashMap<>();
                //绑定这个 对象,路径为空, 最后一个参数为 Map
                buildQuery(jsonNode, "", queries);
                template.queries(queries);
            } catch (IOException e) {
                //提示:根据实践项目情况处理此处异常,这里不做扩展。
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        //不包含 叶子节点
        if (!jsonNode.isContainerNode()) {
            //如果为null,直接返回
            if (jsonNode.isNull()) {
                return;
            }
            //通过path 得到这个 Collection<String> 。path为"" ,Map也是空对象,肯定为空了
            Collection<String> values = queries.get(path);
            if (null == values) {
                //创建了一个List
                values = new ArrayList<>();
                //又把List放入。路径还是 ""
                queries.put(path, values);
            }
            //把Collection<String> 加入上 jsonNode(是template.body()),这次有东西了
            values.add(jsonNode.asText());
            return;
        }
        //包含叶子节点的
        if (jsonNode.isArray()) {   // 数组节点
            //数组节点,获取所有的对象
            Iterator<JsonNode> it = jsonNode.elements();
            //包含下一个
            while (it.hasNext()) {
                //绑定下一个,路径为"" ,对象还是 queries (Map<String, Collection<String>>) ,递归调用
                buildQuery(it.next(), path, queries);
            }
        } else {
            //如果不是数组节点
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();//得到这个 jsonNode的所有字段
            while (it.hasNext()) {
                //如果有下一个,得到下一个
                Map.Entry<String, JsonNode> entry = it.next();
                //如果 path 是 text
                if (StringUtils.hasText(path)) {
                    //绑定, 值+ .key + map
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                } else {  //否则绑定 根节点
                    buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
    
}
action
@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserFeignService userFeignService;

	/**
	 * 用于演示Feign的Get请求多参数传递
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/add", method = RequestMethod.POST)
	public String addUser( @RequestBody @ApiParam(name="用户",value="传入json格式",required=true) User user){
		return userFeignService.addUser(user);
	}

	/**
	 * 用于演示Feign的Post请求多参数传递
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/update", method = RequestMethod.POST)
	public String updateUser( @RequestBody @ApiParam(name="用户",value="传入json格式",required=true) User user){
		return userFeignService.updateUser(user);
	}

}




@FeignClient(name = "ch4-5-provider")
public interface UserFeignService {

    @RequestMapping(value = "/user/add", method = RequestMethod.GET)
    public String addUser(User user);

    @RequestMapping(value = "/user/update", method = RequestMethod.POST)
    public String updateUser(@RequestBody User user);

}
config目录下 swagger2Config
//上面的action加入了 @ApiParam(name="用户",value="传入json格式",required=true)

@Configuration
@EnableWebMvc
public class ApplicationExceptionAdapter extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("swagger-ui.html")
				.addResourceLocations("classpath:/META-INF/resources/");
		registry.addResourceHandler("/webjars/**")
				.addResourceLocations("classpath:/META-INF/resources/webjars/");
	}
}

Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors
                        .basePackage("cn.springcloud.book.feign.controller"))
                .paths(PathSelectors.any()).build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("Feign多参数传递问题").description("Feign多参数传递问题")
                .contact("Software_King@qq.com").version("1.0").build();
    }
}

提供者action
@RestController
@RequestMapping("/user")
public class UserController {

	@RequestMapping(value = "/add", method = RequestMethod.GET)
	public String addUser(User user , HttpServletRequest request){
		String token=request.getHeader("oauthToken");
		return "hello,"+user.getName();
	}

	@RequestMapping(value = "/update", method = RequestMethod.POST)
	public String updateUser( @RequestBody User user){
		return "hello,"+user.getName();
	}
	
}
测试
http://localhost:8011/swagger-ui.html



fegin 的文件上传

  • feign 提供了 子项目 feign-form 实现了上传所 需的 Encoder

  • eurekaServer

    server:
      port: 8761
    eureka:
      instance:
        hostname: localhost
      client:
        registerWithEureka: false
        fetchRegistry: false
        serviceUrl:
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
    
feign-file-server 提供者
server:
  port: 8012
spring:
  application:
    name: feign-file-server

eureka:
  server:
    enableSelfPreservation: false
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
  instance:
    prefer-ip-address: true
  • 提供者,只是打印了文件的名字,制定了请求的格式 为 文件

    @RestController
    public class FeignUploadController {
    
        @PostMapping(value = "/uploadFile/server", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        public String fileUploadServer(MultipartFile file ) throws Exception{
            return file.getOriginalFilename();
        }
    
    }
    
    
feign-upload-client 消费者
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- Spring Cloud OpenFeign的Starter的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- Feign文件上传依赖-->
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>3.0.3</version>
        </dependency>


        //文档编写
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.5.0</version>
        </dependency>

    </dependencies>
server:
  port: 8011
spring:
  application:
    name: feign-upload-client

eureka:
  client:
    service-url:
           defaultZone: http://localhost:8761/eureka
           
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SCFeignFileUploadApplication {}
action
@RestController
@Api(value="文件上传")
@RequestMapping("/feign")
public class FeignUploadController {

    @Autowired
    private FileUploadFeignService fileUploadFeignService;

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ApiOperation(value = "文件上传", notes = "请选择文件上传" )
    public String imageUpload(@ApiParam(value="文件上传",required = true)  MultipartFile file ) throws Exception{
        return fileUploadFeignService.fileUpload(file);
    }

}



@FeignClient(value = "feign-file-server", configuration = FeignMultipartSupportConfig.class)
public interface FileUploadFeignService {

    /***
     * 1.produces,consumes必填。 指定方法的返回是json,请求是file 。
     * 2.注意区分@RequestPart和RequestParam,不要将 @RequestPart(value = "file") 写成@RequestParam(value = "file")
     * @param file
     * @return
     * 
     */
    @RequestMapping(method = RequestMethod.POST, value = "/uploadFile/server",
            produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public  String fileUpload(@RequestPart(value = "file") MultipartFile file);

}
config包下,文档配置和 feign配置
@Configuration
@EnableWebMvc
public class ApplicationExceptionAdapter extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("swagger-ui.html")
				.addResourceLocations("classpath:/META-INF/resources/");
		registry.addResourceHandler("/webjars/**")
				.addResourceLocations("classpath:/META-INF/resources/webjars/");
	}
}

@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors
                        .basePackage("cn.springcloud.book.feign.controller"))
                .paths(PathSelectors.any()).build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("Feign文件上传").description("Feign文件上传")
                .contact("Software_King@qq.com").version("1.0").build();
    }
}


/**
 * Feign文件上传Configuration
 */
@Configuration
public class FeignMultipartSupportConfig {

    @Bean
    @Primary //主要的,如有多个,加载这个
    @Scope("prototype") //每次都加在一个
    public Encoder multipartFormEncoder() {
        return new SpringFormEncoder();
    }
}


http://localhost:8011/swagger-ui.html
http://localhost:8011/feign/upload
file 选择文件,form-data传输

解决feign首次请求失败问题

  • hystrix 默认超时 是 1秒
  • bean的装配 是 懒加载 机制
feign:
  hystrix:
    enabled: false #直接关闭 hystrix 不推荐
# hystrix的超时时间 改为5秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000

#禁用feign的超时时间
hystrix.command.default.execution.timeout.enable=false 

feign 返回图片流处理方式

  • feign返回一般为 字节数组

    public byte[] createImageCode(@RequestParam(“imagekey”) String imagekey);

  • 提供者不能返回字节数组 要返回Response

    public Response createImaeCode(@RequestParam(imagekey) String imageKey);

feign调用传送 token

  • 外部请求 到 A服务,A服务 可以拿到 token
  • A 调用 B服务时, Token就会丢失
  • 需要做: Feign调用的时候,向请求 头里面添加传送 token
  • 实现 Fegin提供的 Request Inter ceptor
  • 先获取 当前请求中的key 如:oauthToken,然后放在 Feign的请求 Header上
/**
 * Feign统一Token拦截器
 */
@Component
public class FeignTokenInterceptor implements RequestInterceptor {//feign统一token拦截器

    @Override
    public void apply(RequestTemplate requestTemplate) {
        if(null==getHttpServletRequest()){
            //此处省略日志记录
            return;
        }
        //将获取Token对应的值往下面传
        requestTemplate.header("oauthToken", getHeaders(getHttpServletRequest()).get("oauthToken"));
    }

    private HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Feign拦截器拦截请求获取Token对应的值
     * @param request
     * @return
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String key = enumeration.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }
}

	//传递到 提供者后
	@RequestMapping(value = "/add", method = RequestMethod.GET)
	public String addUser(User user , HttpServletRequest request){
		String token=request.getHeader("oauthToken");
		return "hello,"+user.getName();
	}
  • 最好的做法是 oauth-token 这样传送。 将获取Token对应的值往下面传,都用下划线传递。

  • http://localhost:8011/user/add 在hearder上参数也要这样发:oauth-token

    {
      "age": 2,
      "id": 0,
      "name": "string2"
    }
    

venus-cloud-feign设计与使用

  • MVC 不支持 实现接口中 方法参数上的注解(支持继承类,方法上的注解)
  • 通过 Feign拦截器的方法 解决了 Get请求多参数传递的 问题
  • cloud中国社区对feign进行了增强,为:venus-cloud-feign
  • https://github.com/springcloud/venus-cloud-feign
  1. Spring MVC Controller中方法不支持 继承 实现 Feign接口中 方法 参数上的 注解问题
    • 不支持 继承 实现 Feign接口中 方法参数上 的注解
  2. Feign 不支持 GET 方法 传递 POJO 的 问题
    • Spring MVC 支持 get 方法 直接 绑定 POJO
      • 要么POJO 拆散,一个一个单独的属性 放在 方法参数里传输
      • 要么 把方法参数 变成 Map传输
      • 违反 RestFul 规范 GET 传递 @RequestBody

编写服务器提供者

        <dependency>
            <groupId>cn.springcloud.feign</groupId>
            <artifactId>venus-cloud-starter-feign</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- Spring Cloud OpenFeign的Starter的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
@FeignClient(name = "ch4-6-provider")
public interface UserService {

    @RequestMapping(value = "/user/add", method = RequestMethod.GET)
    public String addUser(User user);

    @RequestMapping(value = "/user/update", method = RequestMethod.POST)
    public String updateUser(@RequestBody User user);

}

服务提供者的实现工程,引入上面的 项目

        <dependency>
            <groupId>cn.springcloud.book</groupId>
            <artifactId>ch4-6-provider-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
server:
  port: 8012
spring:
  application:
    name: ch4-6-provider

eureka:
    client:
      serviceUrl:
        defaultZone: http://localhost:8761/eureka/
   #eureka.instance.prefer-ip-address  表示将自己的IP注册到Eureka Server上,
   #如果不配置,会将当前服务提供者所在的主机名注册到Eureka Server上。
    instance:
      prefer-ip-address: true
@RestController //很奇怪的写法,直接继承 上个 feign
public class UserController implements UserService {

	@Override
	public String addUser(User user){
		return "hello,"+user.getName();
	}

	@Override
	public String updateUser(User user){
		return "hello,"+user.getName();
	}

}
// 覆盖了 feign 接口,没有在 Controller 上加 Spring MVC 相关的注解

创建消费者

        <dependency>
            <groupId>cn.springcloud.book</groupId>
            <artifactId>ch4-6-provider-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>cn.springcloud.feign</groupId>
            <artifactId>venus-cloud-starter-feign</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- Spring Cloud OpenFeign的Starter的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>


        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.5.0</version>
        </dependency>
config目录下配置不变
@Configuration
@EnableWebMvc
public class ApplicationExceptionAdapter extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("swagger-ui.html")
				.addResourceLocations("classpath:/META-INF/resources/");
		registry.addResourceHandler("/webjars/**")
				.addResourceLocations("classpath:/META-INF/resources/webjars/");
	}
}


@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors
                        .basePackage("cn.springcloud.book.feign.controller"))
                .paths(PathSelectors.any()).build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("Feign多参数传递问题").description("Feign多参数传递问题")
                .contact("Software_King@qq.com").version("1.0").build();
    }
}


controller
@RestController
@RequestMapping("/user")
public class UserController {
	@Autowired
	private UserService userService;

	@RequestMapping(value = "/add", method = RequestMethod.POST)
	public String addUser( @RequestBody @ApiParam(name="用户",value="传入json格式",required=true) User user){
		return userService.addUser(user);
	}

	@RequestMapping(value = "/update", method = RequestMethod.POST)
	public String updateUser( @RequestBody @ApiParam(name="用户",value="传入json格式",required=true) User user){
		return userService.updateUser(user);
	}

}
  • http://localhost:8011/swagger-ui.html

  • http://localhost:8011/user/add

    {
      "age": 2,
      "id": 0,
      "name": "string2ddd"
    }
    
    • RequestMethod.POST 请求

    • 使用的是提供者的action作为service注入: public class UserController implements UserService 
      
    • 通过 提供者client 的 feign get方法,进行访问

      • @FeignClient(name = “ch4-6-provider”)
      • public interface UserService {
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值