💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] # 简介 HTTP客户端调用远程HTTP服务 在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求 ~~~ <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> ~~~ ## 请求 ~~~ @EnableFeignClients @SpringCloudApplication public class ConsumerApplication { ~~~ 配置请求的接口,要指定服务名,底层会调用Ribbon, 还有请求类型和路径 ~~~ @FeignClient("user-service") public interface UserClient { @GetMapping("user/{id}") User queryById(@PathVariable("id") Long id); } ~~~ 控制器中使用 自动注入 ~~~ @Autowired private UserClient userClient; ~~~ 然后直接调用方法就行 ~~~ return userClient.queryById(id); ~~~ ## hystix支持 默认情况是关闭的 开启 ~~~ ribbon: ConnectionTimeOut: 500 ReadTimeOut: 2000 feign: hystrix: enabled: true ~~~ 首先要定义一个类,实现刚才编写的接口,作为fallback的处理类 ~~~ @Component public class UserClientImplFallback implements UserClient { @Override public User queryById(Long id) { return new User(); } } ~~~ 然后在刚才的接口中,指定熔断的类 ~~~ @FeignClient(value = "user-service", fallback = UserClientImplFallback.class) public interface UserClient { @GetMapping("user/{id}") User queryById(@PathVariable("id") Long id); } ~~~ # 传递header RequestContextHolder.getRequestAttributes()该方法是从ThreadLocal变量里面取得相应信息的,当hystrix断路器的隔离策略为THREAD时,是无法取得ThreadLocal中的值。 解决方案: 1. hystrix隔离策略换为SEMAPHORE 2. 自定义策略,模仿Sleuth的trace传递 具体可参考:[http://www.itmuch.com/spring-cloud-sum/hystrix-threadlocal/](http://www.itmuch.com/spring-cloud-sum/hystrix-threadlocal/) ~~~ hystrix: command: default: execution: isolation: strategy: Semaphore ~~~ ## 转发单个 ~~~ package com.cloud.interceptor; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import static com.cloud.config.Constants.CLOUD_SESSION_ID; /** * feign转发header参数 */ @Configuration public class FeignRequestInterceptor implements RequestInterceptor { @Autowired HttpServletRequest httpServletRequest; public FeignRequestInterceptor() { } public void apply(RequestTemplate requestTemplate) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId(); String sessionId2 = httpServletRequest.getSession().getId(); String appkey0 = httpServletRequest.getHeader("appKey"); String appkey = request.getHeader("appKey"); // String traceId = request.getHeader("traceId"); // String requestIp = MyRequestUtil.getIpAddress(request); // String userAgent = request.getHeader("User-agent"); // String contentType = request.getHeader("Content-Type"); // // requestTemplate.header("Content-Type", new String[]{contentType}); // requestTemplate.header("User-agent", new String[]{userAgent}); // requestTemplate.header("User-IP", new String[]{requestIp}); // requestTemplate.header("traceId", new String[]{traceId}); // requestTemplate.header("appKey", new String[]{appkey}); requestTemplate.header(CLOUD_SESSION_ID, sessionId); } } ~~~ ## 转发所有 ~~~ package com.cloud.interceptor; import feign.RequestInterceptor; import feign.RequestTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; /** * 转发所有header */ // @Configuration // 注释掉, 不启用该拦截器; 如果启用, 直接打开注释即可 public class FeignRequestInterceptor implements RequestInterceptor { private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public void apply(RequestTemplate template) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String values = request.getHeader(name); template.header(name, values); } logger.info("feign interceptor header:{}",template); } // 转发参数 Enumeration<String> bodyNames = request.getParameterNames(); StringBuilder body = new StringBuilder(); if (bodyNames != null) { while (bodyNames.hasMoreElements()) { String name = bodyNames.nextElement(); String values = request.getParameter(name); body.append(name).append("=").append(values).append("&"); } } if (body.length() != 0) { body.deleteCharAt(body.length() - 1); template.body(body.toString()); //logger.info("feign interceptor body:{}",body.toString()); } } } ~~~ 最后需要注意的是,如果是使用多线程的情况下,则需要在主线程调用其他线程前将RequestAttributes对象设置为子线程共享 ~~~ ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); RequestContextHolder.setRequestAttributes(attribute, true); ~~~ ## 自定义feign隔离策略 ~~~ import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.HystrixThreadPoolProperties; import com.netflix.hystrix.strategy.HystrixPlugins; import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; import com.netflix.hystrix.strategy.properties.HystrixProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 自定义Feign的隔离策略: * 在转发Feign的请求头的时候, 如果开启了Hystrix, * Hystrix的默认隔离策略是Thread(线程隔离策略), 因此转发拦截器内是无法获取到请求的请求头信息的, * 可以修改默认隔离策略为信号量模式:hystrix.command.default.execution.isolation.strategy=SEMAPHORE, * 这样的话转发线程和请求线程实际上是一个线程, 这并不是最好的解决方法, 信号量模式也不是官方最为推荐的隔离策略; * 另一个解决方法就是自定义Hystrix的隔离策略: * 思路是将现有的并发策略作为新并发策略的成员变量,在新并发策略中, * 返回现有并发策略的线程池、Queue;将策略加到Spring容器即可; */ @Component public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategy.class); private HystrixConcurrencyStrategy delegate; public FeignHystrixConcurrencyStrategy() { try { this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); if (this.delegate instanceof FeignHystrixConcurrencyStrategy) { // Welcome to singleton hell... return; } HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook(); HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy(); this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy); HystrixPlugins.reset(); HystrixPlugins.getInstance().registerConcurrencyStrategy(this); HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); } catch (Exception e) { log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e); } } private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier, HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) { if (log.isDebugEnabled()) { log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy [" + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher [" + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]"); log.debug("Registering Sleuth Hystrix Concurrency Strategy."); } } @Override public <T> Callable<T> wrapCallable(Callable<T> callable) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); return new WrappedCallable<>(callable, requestAttributes); } @Override public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } @Override public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) { return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties); } @Override public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) { return this.delegate.getBlockingQueue(maxQueueSize); } @Override public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) { return this.delegate.getRequestVariable(rv); } static class WrappedCallable<T> implements Callable<T> { private final Callable<T> target; private final RequestAttributes requestAttributes; public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) { this.target = target; this.requestAttributes = requestAttributes; } @Override public T call() throws Exception { try { RequestContextHolder.setRequestAttributes(requestAttributes); return target.call(); } finally { RequestContextHolder.resetRequestAttributes(); } } } } ~~~ # 遇到的问题 **get参数问题** ~~~ @RequestMapping(value="/user/name", method=RequestMethod.GET) User findByUsername(final String userName, final String address); ~~~ 启动服务的时候,会报如下异常: ~~~ Caused by: java.lang.IllegalStateException: Method has too many Body parameters: public abstract com.chhliu.springboot.restful.vo.User com.chhliu.springboot.restful.feignclient.UserFeignClient.findByUsername(java.lang.String,java.lang.String) ~~~ 异常原因:当使用Feign时,如果发送的是get请求,那么需要在请求参数前加上@RequestParam注解修饰,Controller里面可以不加该注解修饰。 上面问题的解决方案如下: ~~~ @RequestMapping(value="/user/name", method=RequestMethod.GET) User findByUsername(@RequestParam("userName") final String userName, @RequestParam("address") final String address); ~~~ **post问题** `Request method 'POST' not supported` 错误代码示例: ~~~ @RequestMapping(value="/user/name", method=RequestMethod.GET) User findByUsername(final String userName, @RequestParam("address") final String address); ~~~ 注意:上面的userName参数没有用@RequestParam注解修饰,然后发送请求,会发现被调用的服务一直报Request method 'POST' not supported,我们明明使用的是GET方法,为什么被调用服务认为是POST方法了,原因是当userName没有被@RequestParam注解修饰时,会自动被当做request body来处理。只要有body,就会被feign认为是post请求,所以整个服务是被当作带有request parameter和body的post请求发送出去的。 # 请求压缩 支持对请求和响应进行GZIP压缩,以减少通信过程中性能损耗. 同时也可以设置对请求的数据类型,以及触发压缩的大小下限进行设置 ~~~ feign: compression: request: enabled: true # 开启请求压缩 mime-types: text/html,application/xml,application/json # 设置压缩的数据类型 min-request-size: 2048 # 设置触发压缩的大小下限 response: enabled: true # 开启响应压缩 ~~~ # 日志级别 `logging.level.xx=debug`来设置日志级别,然而这个对Fegin客户端而言不会产生效果. 因为@FeignClient注解修改的客户端被代理时,都会创建一个新的Fegin.Logger实例. 我们需要额外指定这个日志级别才可以 1. 设置com.jdxia包下的日志级别都为debug ~~~ logging: level: com.jdxia: debug # 将Feign接口的日志级别设置为DEBUG,因为Feign的Logger.Level只对DEBUG作出响应 ~~~ 2. 编写配置类,定义日志级别 ~~~ @Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ~~~ 这里是full,支持4个类型 * NONE:不记录任何日志(默认值) * BASIC:仅记录请求方法、URL、响应状态代码以及执行时间 * HEADERS:记录BASIC级别的基础上,记录请求和响应的header * FULL:记录请求和响应的header,body和元数据 3. 修改Feign的接口,指定其配置类 ~~~ @FeignClient(name = "microservice-provider-user", configuration = FeignConfig.class) public interface UserFeignClient { ~~~ # 构造多参数请求 **GET请求多参数的URL** `http://localhost:8001/get?id=1&username=张三` > 最直观的方法,URL有几个参数,Feign接口就有几个参数 ~~~ @FeignClient(name = "microservice-provider-user", configuration = FeignLogConfiguration.class) public interface UserFeignClient { @GetMapping(value = "/get") User findUserByCondi(@RequestParam("id") Long id, @RequestParam("username") String username); } ~~~ 使用 Map 构建。当目标URL参数非常多时,使用Map构建可简化Feign接口的编写 ~~~ @FeignClient(name = "microservice-provider-user", configuration = FeignLogConfiguration.class) public interface UserFeignClient { @GetMapping(value = "/get") User findUserByCondi(@RequestParam Map<String, Object> map); } ~~~ **POST请求包含多个参数** ~~~ @FeignClient(name = "microservice-provider-user", configuration = FeignLogConfiguration.class) public interface UserFeignClient { @PostMapping(value = "/post") User findUserByCondi(@RequestBody User user); } ~~~