feign 实现签名、服务地址动态切换

2022-07-06 08:45:44

什么是feign

Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。

快速启动

1. 客户端

  • maven依赖:
<dependency><groupId>com.netflix.feign</groupId><artifactId>feign-core</artifactId><version>8.18.0</version></dependency>
  • feign声明服务方api
publicinterfaceHelloControllerApi{@RequestLine("GET /api/hello?name={name}")Stringhello(@Param(value="name")String name);}
  • 调用服务提供方api
publicclassHelloControllerApiTest{privateHelloControllerApi service;@Beforepublicvoidbefore(){
		service=Feign.builder().options(newRequest.Options(1000,3500)).retryer(newRetryer.Default(5000,5000,3)).target(HelloControllerApi.class,"http://127.0.0.1:8080");}@Testpublicvoidhello(){// 调用http://127.0.0.1:8080/api/hello?name=world 的http接口System.out.println(service.hello("world"));}}

2. 服务端

服务端是spring-web服务,当然可以基于其他形式web服务。

  • 服务代码:
@Controller@RequestMapping(value="api")publicclassHelloController{@RequestMapping(value="/hello", method={RequestMethod.GET})@ResponseBodypublicStringlist(@RequestParamString name){return"Hello "+ name;}}
  • 启动类
@SpringBootApplication(scanBasePackages={"com.vhicool.manager"})publicclassManagerApplication{publicstaticvoidmain(String[] args){SpringApplication.run(ManagerApplication.class, args);}}

实现签名验证

1. 客户端

  • 添加feign拦截器,并在请求heard中添加token
        service=Feign.builder().options(newRequest.Options(1000,3500)).retryer(newRetryer.Default(5000,5000,3)).requestInterceptor(newAuthRequestInterceptor(authKey)).target(HelloControllerApi.class,"http://127.0.0.1:8080");
publicclassAuthRequestInterceptorimplementsRequestInterceptor{privateString token;publicAuthRequestInterceptor(String token){this.token= token;}@Overridepublicvoidapply(RequestTemplate template){
		template.header("token", token);}}

2. 服务端

  • 服务端添加过滤器,对token进行验证
@ComponentpublicclassAuthFilterimplementsFilter{@Value("${token:test_token}")privateString token;@OverridepublicvoiddoFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain)throwsIOException,ServletException{String remoteToken=((HttpServletRequest) servletRequest).getHeader("token");Assert.isTrue(token.equals(remoteToken),"签名验证失败");
		filterChain.doFilter(servletRequest, servletResponse);}}

服务端自动生成feign

通过继承实现feign客户端和服务提供方api共用

在这里插入图片描述

spring-cloud-start-openfeign由于有支持spring-mvc controller主机,因此本例使用spring-cloud-start-openfeign依赖。实现原理为:1. 提取controller rest接口抽象接口,2. 在客户端由Feign接口继承,3. 在服务端由controller类实现处理逻辑。

在这里插入图片描述

1. 提取公共api

  • maven依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>
  • 公共服务接口(客户端和服务提供方,都需要继承改接口):
publicinterfaceIUserController{@RequestMapping(value="user/list-all", method={RequestMethod.GET})List<String>listAll(@RequestParamString name);}

2. 服务端实现公共api

  • maven依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.vhicool</groupId><artifactId>feign-api</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency>
  • controller
@Controller@RequestMappingpublicclassUserControllerimplementsIUserController{@Override@ResponseBodypublicList<String>listAll(String name){ArrayList<String> list=newArrayList<>();
		list.add("达菲");
		list.add("olu");
		list.add(name);return list;}}
  • token验证拦截器
@ComponentpublicclassAuthFilterimplementsFilter{@Value("${token:test_token}")privateString token;@OverridepublicvoiddoFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain)throwsIOException,ServletException{String remoteToken=((HttpServletRequest) servletRequest).getHeader("token");if(!token.equals(remoteToken)){((HttpServletResponse)servletResponse).setStatus(401);
			servletResponse.getWriter().write("签名验证失败");return;}
		filterChain.doFilter(servletRequest, servletResponse);}}
  • 启动类
@SpringBootApplication(scanBasePackages={"com.vhicool.manager"})publicclassManagerApplication{publicstaticvoidmain(String[] args){SpringApplication.run(ManagerApplication.class, args);}}

3. 客户端继承公共api

  • mavne依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.1.5.RELEASE</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>compile</scope></dependency><dependency><groupId>com.vhicool</groupId><artifactId>feign-api</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency>
  • 客户端调用类
@FeignClient(value="user", url="http://localhost:8080")publicinterfaceUserApiextendsIUserController{}
  • 启动类
@SpringBootApplication@EnableFeignClientspublicclassManagerApplication{publicstaticvoidmain(String[] args){SpringApplication.run(ManagerApplication.class, args);}}
  • feign公共配置
@ConfigurationpublicclassFeignConfiguration{/**
	 * 请求超时时间
	 * @return
	 */@BeanpublicRequest.Optionsoptions(){returnnewRequest.Options(2000,3500);}/**
	 * 拦截器
	 * @return
	 */@BeanpublicRequestInterceptorinterceptor(){returnnewAuthRequestInterceptor("test_token");}}publicclassAuthRequestInterceptorimplementsRequestInterceptor{privateString token;publicAuthRequestInterceptor(String token){this.token= token;}@Overridepublicvoidapply(RequestTemplate template){
		template.header("token", token);}}
  • 测试类
@RunWith(SpringRunner.class)@SpringBootTestpublicclassUserControllerTest{@AutowiredprivateUserApi userApi;@TestpublicvoidlistAll(){System.out.println(userApi.listAll("饼饼"));}}

客户端实现动态连接服务端url

背景

现在客户端调用服务端还是1对1的场景,在实际使用场景中,我们需要客户端可以连接不同的服务,例如:一个Manager 同时管理多个woker,每个worker对应一个业务环境(生产、测试、开发等)。用户使用Manager进行环境切换,后端也需要对应访问不同环境的worker服务。

方案(基于服务端自动生成feign api

步骤

1. 在manager处理请求时,获取当前业务环境,并设置为当前线程上下文环境(基于`ThreadLocal`的`com.alibaba.transmittable-thread-local`)
2. 在触发feign调用是,根据当前线程上下文环境,动态设置服务提供方url(feign拦截器`RequestInterceptor`)

代码

客户端(manager)

  • 新增maven依赖
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.10.2</version></dependency>
  • 设置当前线程所在环境
publicclassZBaseContextManager{privatestaticfinalTransmittableThreadLocal<String> ZBASE_CONTEXT=newTransmittableThreadLocal();/**
     * 设置当前线程环境
     * @param env
     */publicstaticvoidsetEnv(String env){
        ZBASE_CONTEXT.set(env);}/**
     * 获取当前线程环境
     * @return
     */publicstaticStringcurrentEnv(){return ZBASE_CONTEXT.get();}}
  • feign请求获取当前环境,并将url替换成对应环境worker的url
publicclassEnvRouterInterceptorimplementsRequestInterceptor{privatestaticfinalLogger LOG=LoggerFactory.getLogger(EnvRouterInterceptor.class);privateMap<String,String> envUrlMap;publicEnvRouterInterceptor(Map<String,String> envUrlMap){this.envUrlMap= envUrlMap;}@Overridepublicvoidapply(RequestTemplate template){String env=ZBaseContextManager.currentEnv();String url=this.envUrlMap.get(env);
		template.target(url);
		LOG.info("env : {} ,target url : {}", env, template.url());}}
  • 设置feign全局拦截器
@BeanpublicRequestInterceptorenvRouterInterceptor(){Map<String,String> envMap=newHashMap<>();
		envMap.put("dev","http://localhost:8080");
		envMap.put("fat","http://10.10.134.196:8080");returnnewEnvRouterInterceptor(envMap);}
  • 测试类
@RunWith(SpringRunner.class)@SpringBootTestpublicclassDynamicUserControllerTest{@AutowiredprivateUserApi userApi;@TestpublicvoidlistAllFat(){ZBaseContextManager.setEnv("fat");System.out.println(userApi.listAll("meimei"));}@TestpublicvoidlistAllDev(){ZBaseContextManager.setEnv("dev");System.out.println(userApi.listAll("meimei2"));}}

输出日志

env: fat ,target url: http://10.10.134.196:8080/user/list-all?name=meimeienv: dev ,target url: http://localhost:8080/user/list-all?name=meimei2
  • 作者:vhicool
  • 原文链接:https://blog.csdn.net/u011618288/article/details/121013193
    更新时间:2022-07-06 08:45:44