- 消费者 调用 提供者
- 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
- Spring MVC Controller中方法不支持 继承 实现 Feign接口中 方法 参数上的 注解问题
- 不支持 继承 实现 Feign接口中 方法参数上 的注解
- Feign 不支持 GET 方法 传递 POJO 的 问题
- Spring MVC 支持 get 方法 直接 绑定 POJO
- 要么POJO 拆散,一个一个单独的属性 放在 方法参数里传输
- 要么 把方法参数 变成 Map传输
- 违反 RestFul 规范 GET 传递 @RequestBody
- Spring MVC 支持 get 方法 直接 绑定 POJO
编写服务器提供者
<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 {
-