文章目录
什么是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
public interface HelloControllerApi {
@RequestLine("GET /api/hello?name={name}")
String hello(@Param(value = "name") String name);
}
- 调用服务提供方api
public class HelloControllerApiTest {
private HelloControllerApi service;
@Before
public void before(){
service = Feign.builder()
.options(new Request.Options(1000, 3500))
.retryer(new Retryer.Default(5000, 5000, 3))
.target(HelloControllerApi.class, "http://127.0.0.1:8080");
}
@Test
public void hello(){
// 调用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")
public class HelloController {
@RequestMapping(value = "/hello", method = {RequestMethod.GET})
@ResponseBody
public String list(@RequestParam String name) {
return "Hello " + name;
}
}
- 启动类
@SpringBootApplication(scanBasePackages = {"com.vhicool.manager"})
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class, args);
}
}
实现签名验证
1. 客户端
- 添加feign拦截器,并在请求heard中添加token
service = Feign.builder()
.options(new Request.Options(1000, 3500))
.retryer(new Retryer.Default(5000, 5000, 3))
.requestInterceptor(new AuthRequestInterceptor(authKey))
.target(HelloControllerApi.class, "http://127.0.0.1:8080");
public class AuthRequestInterceptor implements RequestInterceptor {
private String token;
public AuthRequestInterceptor(String token) {
this.token = token;
}
@Override
public void apply(RequestTemplate template) {
template.header("token", token);
}
}
2. 服务端
- 服务端添加过滤器,对token进行验证
@Component
public class AuthFilter implements Filter {
@Value("${token:test_token}")
private String token;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, 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>
- 公共服务接口(客户端和服务提供方,都需要继承改接口):
public interface IUserController {
@RequestMapping(value = "user/list-all", method = {RequestMethod.GET})
List<String> listAll(@RequestParam String 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
@RequestMapping
public class UserController implements IUserController {
@Override
@ResponseBody
public List<String> listAll(String name) {
ArrayList<String> list = new ArrayList<>();
list.add("达菲");
list.add("olu");
list.add(name);
return list;
}
}
- token验证拦截器
@Component
public class AuthFilter implements Filter {
@Value("${token:test_token}")
private String token;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, 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"})
public class ManagerApplication {
public static void main(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")
public interface UserApi extends IUserController {
}
- 启动类
@SpringBootApplication
@EnableFeignClients
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class, args);
}
}
- feign公共配置
@Configuration
public class FeignConfiguration {
/**
* 请求超时时间
* @return
*/
@Bean
public Request.Options options() {
return new Request.Options(2000, 3500);
}
/**
* 拦截器
* @return
*/
@Bean
public RequestInterceptor interceptor() {
return new AuthRequestInterceptor("test_token");
}
}
public class AuthRequestInterceptor implements RequestInterceptor {
private String token;
public AuthRequestInterceptor(String token) {
this.token = token;
}
@Override
public void apply(RequestTemplate template) {
template.header("token", token);
}
}
- 测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
@Autowired
private UserApi userApi;
@Test
public void listAll() {
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>
- 设置当前线程所在环境
public class ZBaseContextManager {
private static final TransmittableThreadLocal<String> ZBASE_CONTEXT = new TransmittableThreadLocal();
/**
* 设置当前线程环境
* @param env
*/
public static void setEnv(String env) {
ZBASE_CONTEXT.set(env);
}
/**
* 获取当前线程环境
* @return
*/
public static String currentEnv() {
return ZBASE_CONTEXT.get();
}
}
- feign请求获取当前环境,并将url替换成对应环境worker的url
public class EnvRouterInterceptor implements RequestInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(EnvRouterInterceptor.class);
private Map<String, String> envUrlMap;
public EnvRouterInterceptor(Map<String, String> envUrlMap) {
this.envUrlMap = envUrlMap;
}
@Override
public void apply(RequestTemplate template) {
String env = ZBaseContextManager.currentEnv();
String url = this.envUrlMap.get(env);
template.target(url);
LOG.info("env : {} ,target url : {}", env, template.url());
}
}
- 设置feign全局拦截器
@Bean
public RequestInterceptor envRouterInterceptor() {
Map<String, String> envMap = new HashMap<>();
envMap.put("dev", "http://localhost:8080");
envMap.put("fat", "http://10.10.134.196:8080");
return new EnvRouterInterceptor(envMap);
}
- 测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicUserControllerTest {
@Autowired
private UserApi userApi;
@Test
public void listAllFat() {
ZBaseContextManager.setEnv("fat");
System.out.println(userApi.listAll("meimei"));
}
@Test
public void listAllDev() {
ZBaseContextManager.setEnv("dev");
System.out.println(userApi.listAll("meimei2"));
}
}
输出日志
env : fat ,target url : http://10.10.134.196:8080/user/list-all?name=meimei
env : dev ,target url : http://localhost:8080/user/list-all?name=meimei2
- 推荐:
Feign异常传递