springboot3 - feign实战 - feign原生实现 - MVC实现方式以及用接口实现代理转发

文章讲述了在SpringBoot项目中使用@RestController和@FeignClient时遇到的问题,版本不兼容导致的错误,以及如何通过升级依赖、MVC方式实现接口和使用SpringMvcContract进行请求映射、Hystrix容错处理和自定义编码解码来解决这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几年前突发奇想,将@RestController和@FeignClient放在了一下,这样测试的时候会方便很多,不用走业务就可以调用接口,然后误打误撞发现真的可以,
新项目使用springboot3,在pom.xml文件,引用dependency.version会自动提示版本号,所以就没有去查相应支持的版本,用的提示版本3.1.4,然后出现了很多问题

问题列表

  • 无法同时使用@RestController和@FeignClient,在springboot2.6中就已经不可以使用,
  • Method search not annotated with HTTP method type (ex. GET, POST)
  • @Param,@Header注解的使用方式
  • Feign自己实现方式导致找不到FeignContext - Consider defining a bean of type openfeign.FeignContext
  • Consider revisiting the conditions above or defining a bean of type ‘org.springframework.cloud.netflix.feign.FeignContext’ in your configuration.
  • No qualifying bean of type ‘org.springframework.cloud.openfeign.FeignContext’ available
  • NoSuchBeanDefinitionException: No qualifying bean of type

首先,检查你的版本,我当前用的是

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

这个就能解决很多问题了,然后用MVC实现的方式去写接口即可


用接口实现代理转发

@RestController和@FeignClient无法同时使用时,继承RequestMappingHandlerMapping,复写isHandler方法

public class FeignRequestMappingHandlerMapping extends RequestMappingHandlerMapping implements WebMvcRegistrations {
	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, RestController.class) ||
		        // 当前类上存在RequestMapping 但是不存在FeignClient注解
		        (AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)
		         && !AnnotatedElementUtils.hasAnnotation(beanType, FeignClient.class)
		        ));
	}
}
//注册
@Component
public class FeignWebMvcRegistrations implements WebMvcRegistrations {
	@Override
	public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
		return new FeignRequestMappingHandlerMapping();
	}
}

参考文章:当@FeignClient遇到@RequestMapping,spring容器启动报错
https://blog.csdn.net/sssdal19995/article/details/121023316


feign请求接口路径映射

因为springboot改版, @RestController/@RequestMapping和@FeignClient不可同时出现在同一个类中
当@RequestMapping与@FeignClient同时出现在一个类中时,两个注解中的path必须统一
这个path路径单独映射问题好像出现在2.5版本以后

@Component
public class FeignSpringMvcContract extends SpringMvcContract {
	private static final Log LOG = LogFactory.getLog(FeignSpringMvcContract.class);

	@Override
	protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
		CollectionFormat collectionFormat = findMergedAnnotation(clz, CollectionFormat.class);
		if (collectionFormat != null) {
			data.template().collectionFormat(collectionFormat.value());
		}
	}
}

调用方式

说明一下SpringMvcContract
它是Feign框架中的一个契约实现类,用SpringMVC的方式去做接口调用服务,说白了就是okhttp和httpclient的区别,或者jpa中jsql和hibernate,querydsl的区别

MvcContract例子
@RestController
@RequestMapping(path = "/provider")
@FeignClient(name = "ProviderUserServerApi", url = "${feign.userServerHost}/api", path = "/provider", fallback = UserServerApiHystrix.class)
@Schema(description = "用户(服务接口)")
public interface UserServerApi {

	@GetMapping(value = "/user/getUserByToken")
	@Headers({"Content-Type: application/json;charset=UTF-8"})
	FeignResult<UserDto> getUserByToken(@RequestHeader("token") String token);

	@GetMapping("/user/login/{code}")
	FeignResult<UserDto> login(@Valid @PathVariable("code") String code);

	@GetMapping("/user/testLogin/{userId}")
	FeignResult<UserDto> testLogin(@Valid @PathVariable("userId") Long userId);

	@GetMapping("/user/userInfoByToken")
	FeignResult<UserDto> userInfoByToken(@Valid @RequestParam("token") String token);

}
Feign原生例子
//Application启动类上添加注解: @ImportAutoConfiguration({FeignAutoConfiguration.class})

public interface UserServerApi {
	@RequestLine("GET /user/getUserByToken")
	@Headers({"Content-Type: application/json;charset=UTF-8", "token: ${token}"})
	FeignResult<ThirdPartyUser> getUserByToken(@Param String token);

	@RequestLine("GET /user/login/{code}")
	FeignResult<ThirdPartyUser> login(@Valid @Param("code") String code);

}

@Component
public class UserServerApiClient {
	public FeignResult<UserDto> getUserByToken(@RequestParam("token") String token) {
		UserServerApi api = Feign.builder()
				.encoder(new FastEncoder())
				.decoder(new FastDecoder())
				.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
				.target(UserServerApi.class, userServerHost + "/api/provider");
		return api.getUserByToken(token);
	}
}

动态URL方式

@FeignClient(name = "ProviderServerFeign", url = "EMPTY", fallback = ProviderServerHystrix.class)
public interface ProviderServerFeign {
	/**
	 * 服务状态,心跳
	 *
	 * @return 查询结果
	 */
	@ApiOperation(value = "服务状态")
	@GetMapping(value = "/live/serviceStatus")
	FeignResult<String> serviceStatus(URI uri);
}
//调用方式
providerServerFeign.serviceStatus(URI.create("http://" + serverAddress + "/api"));

偷懒小技巧

如果只是作为代理用可以省去参数验证什么的

@RestController
@RequestMapping(path = "/provider")
@FeignClient(name = "ProviderUserServerApi", url = "${feign.userServerHost}/api", path = "/provider", fallback = UserServerApiHystrix.class)
@Schema(description = "用户(服务接口)")
public interface UserServerApi {

	@GetMapping(value = "/user/getUserByToken")
	@Headers({"Content-Type: application/json;charset=UTF-8"})
	Object getUserByToken(@RequestHeader("token") Object token);

	@GetMapping("/user/login/{code}")
	Object login(@PathVariable Object code);

	@GetMapping("/user/testLogin/{userId}")
	Object testLogin(@PathVariable Object userId);

	@GetMapping("/user/userInfoByToken")
	Object userInfoByToken(@RequestParam Object token);

	@GetMapping("/user/userInfoByToken")
	Object addUser(@RequestBody Object vo);

}

编码解码

编码解码需要单独引用类,例如feign的Gson,但是因为解析问题,我借鉴了Gson的实现方式用fastjson实现了编解码类

//gson引用
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>12.4</version>
</dependency>
                
//fastjson实现方式
public class FastEncoder implements Encoder {
	public void encode(Object object, Type bodyType, RequestTemplate template) {
		template.body(JSON.toJSONString(object));
	}
}
public class FastDecoder implements Decoder {
	public Object decode(Response response, Type type) throws IOException {
		if (response.status() != 404 && response.status() != 204) {
			if (response.body() == null) {
				return null;
			} else {
				Reader reader = response.body().asReader(Util.UTF_8);
				Object var4;
				try {
					Response.Body body = response.body();
					InputStream inputStream = body.asInputStream();
					var4 = JSON.parseObject(inputStream, type);
				} catch (JsonIOException var8) {
					if (var8.getCause() != null && var8.getCause() instanceof IOException) {
						throw (IOException) IOException.class.cast(var8.getCause());
					}
					throw var8;
				} finally {
					Util.ensureClosed(reader);
				}
				return var4;
			}
		} else {
			return Util.emptyValueOf(type);
		}
	}
}

参考文章:Feign github
https://github.com/OpenFeign/feign


容错回调 Hystrix

UserServerApiHystrix继承UserServerApi,其他的我没有配置

public class UserServerApiHystrix implements UserServerApi {
	@Override
	public FeignResult<UserDto> getUserByToken(String token) {
		return FeignResult.fail();
	}
}

拦截器

可以添加一些信息或者修改信息

public class ProviderFeignRequestInterceptor implements RequestInterceptor {
	@Override
	public void apply(RequestTemplate template) {
		template.header("Content-Type", "application/json; charset=utf-8");
		template.header("ticket", "***");
		template.header("x-forwarded-for", AppSecUtils.getXRealIp());
	}
}

如果觉得本文还不错的话帮我点个赞

推荐文章:
在SpringCloud中使用Feign的两种方式
https://www.cnblogs.com/wugang/p/14477803.html

当@FeignClient遇到@RequestMapping,spring容器启动报错
https://blog.csdn.net/sssdal19995/article/details/121023316

Feign实现动态URL
https://cloud.tencent.com/developer/article/1990111

<think>好的,我现在需要解决用户的问题:在Spring Cloud项目中,将多个Feign远程调用改为本地调用时,如何在本地实现类中获取请求头信息。用户提到使用的是Java 1.8、Nacos和Gateway。 首先,回顾之前的对话,用户之前合并了模块,将Feign远程调用改为本地调用。现在的问题是,在本地实现类中需要获取请求头,比如认证信息或者其他HTTP头内容。因为原本的Feign客户端在远程调用时会自动传递请求头,但改为本地调用后,这些信息不会自动传递到本地方法中,需要手动处理。 接下来,我需要考虑如何在不使用远程调用的情况下,让本地实现类能够获取到原始的请求头信息。可能的思路是利用线程上下文或者请求上下文来传递这些信息。因为在Spring MVC中,请求是在一个线程中处理的,可以使用ThreadLocal来存储当前请求的信息。 首先想到的是使用ServletRequestAttributes来获取当前请求的上下文。在Spring中,可以通过RequestContextHolder获取当前请求的属性,包括请求头。但是需要注意,这种方法是否适用于Feign的本地调用,尤其是在多线程环境下是否会有问题。因为如果本地调用是在同一个请求线程中执行的,那么RequestContextHolder应该能够获取到当前的请求信息。 接下来,用户需要在本地实现类中获取请求头。例如,在之前的示例中,用户将ServiceAClient的Feign接口改为了本地实现LocalServiceAClient。现在需要在这个本地实现的方法中访问请求头,比如Authorization头或者其他自定义头。 可能的解决方案步骤: 1. 在本地实现类中,通过RequestContextHolder获取当前的ServletRequestAttributes,从而获取HttpServletRequest对象。 2. 从HttpServletRequest对象中获取所需的请求头信息。 3. 处理可能的空指针异常,比如在非Web请求的上下文中调用时,或者异步线程中无法获取请求上下文的情况。 4. 确保在Gateway中传递了必要的请求头,尤其是在路由到合并后的模块时,请求头没有被过滤或丢失。 另外,需要考虑用户提到的Nacos和Gateway的集成。在Gateway中,通常会有过滤器来处理请求,比如添加或转发请求头。需要确认Gateway的配置是否正确,确保请求头能够传递到下游服务,也就是合并后的模块中。例如,在Gateway的路由配置中,可能需要配置过滤器来保留或添加特定的头信息。 可能的代码实现: 在本地实现类中,注入HttpServletRequest对象,或者通过RequestContextHolder获取当前请求的上下文。例如: ```java @Service @Primary public class LocalServiceAClient implements ServiceAClient { @Override public String getA() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String authHeader = request.getHeader("Authorization"); // 使用authHeader处理逻辑 return "Local A with header: " + authHeader; } } ``` 但需要注意,RequestContextHolder是基于ThreadLocal的,如果在同一个线程中处理请求,则可行。但如果本地调用发生在异步线程中,可能会丢失上下文。这种情况下,需要手动传递请求头,或者在调用前将上下文信息保存到局部变量中。 另外,可能需要在Gateway中配置路由过滤器,确保特定的请求头被转发到下游服务。例如: ```yaml spring: cloud: gateway: routes: - id: abc-route uri: lb://merged-service predicates: - Path=/api/a/**,/api/b/**,/api/c/** filters: - AddRequestHeader=Authorization, {从Gateway获取并传递} ``` 或者使用默认的过滤器,确保所有请求头都被转发。但需要注意,默认情况下,Gateway可能会转发所有请求头,但某些敏感头(如Cookie、Authorization)可能需要显式配置。 此外,还需要考虑安全性和性能,确保在本地调用中正确处理请求头信息,避免泄露或错误处理。 总结步骤: 1. 在本地实现类中使用RequestContextHolder获取当前请求的HttpServletRequest。 2. 从HttpServletRequest中读取需要的请求头。 3. 确保Gateway配置正确转发相关请求头到合并后的服务。 4. 处理多线程或异步调用中的上下文传递问题。 5. 测试验证请求头是否正确获取。 可能出现的问题: - 当本地调用不在同一个请求线程时,RequestContextHolder可能无法获取到正确的请求上下文,导致空指针异常。 - Gateway可能默认不转发某些敏感头,需要显式配置。 - 需要确保在合并后的模块中,Spring的Web相关依赖正确引入,否则RequestContextHolder可能不可用。 因此,需要指导用户如何正确配置Gateway的过滤器,并确保在本地实现类中安全地获取请求头信息。同时,处理可能出现的异常情况,提供代码示例和配置示例。</think>在Spring Cloud微服务架构中,将Feign远程调用改为本地调用后获取请求头信息,可通过以下方案实现--- ### 一、请求头传递原理与实现步骤 #### 1. **请求头传递链路分析** - **网关层**:Gateway通过`Forwarded`头传递原始请求信息 - **服务层**:本地调用需通过线程上下文获取`HttpServletRequest` - **数据验证**:使用`curl -H "Authorization: Bearer token" http://gateway/api/a`测试头传递 #### 2. **本地实现类获取请求头**(核心代码) ```java @Service @Primary public class LocalServiceAClient implements ServiceAClient { // 方法一:直接获取当前请求对象 @Override public String getA() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder .currentRequestAttributes()) .getRequest(); String authHeader = request.getHeader("Authorization"); return "Process with header: " + authHeader; } // 方法二:通过@RequestHeader注解注入 public String getB(@RequestHeader("X-Custom-Header") String customHeader) { return "Custom Header Value: " + customHeader; } } ``` #### 3. **网关层关键配置** ```yaml spring: cloud: gateway: routes: - id: merged-service uri: lb://merged-service predicates: - Path=/api/** filters: - RewritePath=/api/(?<segment>.*), /$\{segment} # 路径重写 - AddRequestHeader=Original-Request, from-gateway # 透传原始请求头 ``` --- ### 二、线程上下文安全方案 #### 1. **异步场景解决方案** ```java // 在调用本地方法前保存上下文 public void asyncProcess() { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); new Thread(() -> { RequestContextHolder.setRequestAttributes(attributes); // 绑定到子线程 localService.getA(); RequestContextHolder.resetRequestAttributes(); // 清理线程副本 }).start(); } ``` #### 2. **ThreadLocal封装工具类** ```java public class HeaderHolder { private static final ThreadLocal<Map<String, String>> headerContext = new ThreadLocal<>(); public static void setHeaders(HttpServletRequest request) { Map<String, String> headers = Collections.list(request.getHeaderNames()) .stream() .collect(Collectors.toMap( name -> name, request::getHeader )); headerContext.set(headers); } public static String getHeader(String name) { return headerContext.get().get(name); } } // 在Controller层添加过滤器 @Bean public FilterRegistrationBean<HeaderFilter> headerFilter() { FilterRegistrationBean<HeaderFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new HeaderFilter()); registration.addUrlPatterns("/*"); return registration; } public class HeaderFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HeaderHolder.setHeaders((HttpServletRequest) req); chain.doFilter(req, res); HeaderHolder.remove(); // 防止内存泄漏 } } ``` --- ### 三、验证与异常处理 #### 1. **测试用例** ```java @SpringBootTest public class HeaderTest { @Autowired private ServiceAClient serviceAClient; @Test public void testHeaderPassing() { // 模拟请求上下文 MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "test-token"); RequestContextHolder.setRequestAttributes( new ServletRequestAttributes(request)); Assert.assertEquals("Process with header: test-token", serviceAClient.getA()); } } ``` #### 2. **常见问题处理** | 异常现象 | 解决方案 | |--------------------------|--------------------------------------------------------------------------| | `NullPointerException` | 检查是否在非Web线程中调用,使用`@Scope(value = "request", proxyMode = TARGET_CLASS)` | | 头信息丢失 | 在Gateway添加`PreserveHostHeader`过滤器 | | 异步线程头传递失败 | 使用`TransmittableThreadLocal`代替原生ThreadLocal[^1] | --- ### 四、架构影响评估 ```mermaid graph TD A[Gateway] -->|传递原始请求头| B(合并服务) B --> C{本地调用} C --> D[ServiceA] C --> E[ServiceB] C --> F[ServiceC] D --> G[读取Header] E --> G F --> G ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值