之前在SpringCloud中使用过@FeignClient的方式对服务进行调用,感觉使用起来还是很方便的,所以想要探索一下是否可以把@FeignClient用在K8s集群中进行服务间的调用;
feign是一个声明式web服务调用的客户端,创建一个接口并加上注解就能使用Feign了(同时支持JAX-RS类型的注解,可插入式的编码和解码),Spring Cloud Feign组件为他加入了spring mvc的注解(@RequestMappging, @RequestBody, @ResponseBody, @RequestParam, @PathVariable等)支持,以及在spring web开发过程中默认使用同样的HttpMessageConverters ,同时Spring Cloud整合了Ribbon和Eureka为使用feign的过程中提供了一个负载均衡的http客户端。
Feign示例(github-OpenFeign-feign):
//feign客户端声明
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
Listcontributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);}
//DTO定义
public static class Contributor {
String login;
int contributions;
}public static class Issue {
String title;
String body;
Listassignees;
int milestone;
Listlabels;
}//启动类
public class MyApp {
public static void main(String... args) {
//注册feign客户端
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
//注册feign客户端,并指定服务URL
.target(GitHub.class, "https://api.github.com");
//feign客户端进行服务调用
Listcontributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
SpringCloud @FeignClient示例(Declarative REST Client: Feign):
//FeignClient客户端声明(其中"stores"为SpringCloud中服务名称)
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
ListgetStores(); @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}//启动类
@EnableAutoConfiguration
@EnableEurekaClient
//启动FeignClient自动配置
@EnableFeignClients
public class Application {//自动注入FeignClient代理
@Resource
private StoreClient storeClient;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
//FeignClient进行服务调用
storeClient.update(1, new Store());
}}
//maven依赖
org.springframework.cloud
spring-cloud-starter-openfeign
2.1.1.RELEASE
结合Maven多module的特性,可以将Controller抽象出接口定义,即在单独的一个module(如api)中,定义Controller的接口,然后在其他模块(如web) 中去实现Controller的接口,而后将api打包后上传到maven私服供公司内其他服务依赖并调用;
例如Maven项目多module结构如下:
api:对外提供服务的参数、返回结果DTO定义,Controller接口定义
domain:domain对象定义(entity, vo, bo, dto等)
dao:数据访问层(数据库访问、缓存访问)
service:具体服务的定义
web:controller实现层的定义
即在api模块中进行controller接口定义:
@RequestMapping("/base")
public interface BaseController {@RequestMapping(value = "/reqParam", method = RequestMethod.POST)
@ResponseBody
Object reqParam(@SpringQueryMap ParamVo paramVo);@RequestMapping(value = "/reqBody", method = RequestMethod.POST)
@ResponseBody
Object reqBody(@RequestBody ParamVo paramVo);@RequestMapping(value = "/reqPath/{batteryCode}/{page}/{rows}", method = RequestMethod.POST)
@ResponseBody
Object reqPath(@PathVariable("batteryCode") String batteryCode, @PathVariable("page") Integer page, @PathVariable("rows") Integer rows);
}
在web模块中实现controller接口:
@Controller
@RequestMapping("/base")
public class BaseControllerImpl implements BaseController {
private static final Logger logger = LogManager.getLogger(BaseControllerImpl.class);@RequestMapping(value = "/reqParam", method = RequestMethod.POST)
@ResponseBody
public Object reqParam(@SpringQueryMap ParamVo paramVo) {
logger.info("reqParam param:{}", JsonUtils.toJson(paramVo));
return MxHttpRespUtility.successResp();
}@RequestMapping(value = "/reqBody", method = RequestMethod.POST)
@ResponseBody
public Object reqBody(@RequestBody ParamVo paramVo) {
logger.info("reqBody param:{}", JsonUtils.toJson(paramVo));
return MxHttpRespUtility.successResp();
}@RequestMapping(value = "/reqPath/{batteryCode}/{page}/{rows}", method = RequestMethod.POST)
@ResponseBody
public Object reqPath(@PathVariable("batteryCode") String batteryCode, @PathVariable("page") Integer page, @PathVariable("rows") Integer rows) {
logger.info("reqPath param: batteryCode={}, page={}, rows={}", batteryCode, page, rows);
return MxHttpRespUtility.successResp();
}
}
注:
@SpringQueryMap为Feign中注解,用于解析QeuryString 形式的参数,
即controller中方法参数为Object reqParam(ParamVo paramVo)形式(参数没有被@RequestParam, @RequestBody等所修饰)时,
需在controller接口定义的相应方法参数上添加@SpringQueryMap,如:Object reqParam(@SpringQueryMap ParamVo paramVo);
在其他服务中,依赖dev-web中api模块后,即可通过继承该controller接口的方式进行FeignClient调用:
//启动类添加注解(FeignClient自动配置)
@EnableFeignClients
//声明FeignClient,继承被调用服务的controller接口
//指定url后,即实际请求的路径即为url/requestPath(仅指定name则走SpringCloud的服务名称的负载均衡调用)
@FeignClient(name="base", url = "http://localhost:8089/dev-springboot-template")
public interface BaseControllerFeign extends BaseController {
}//自动注入FeignClient代理
@Resource
private BaseControllerFeign baseControllerFeign;
...
//FeignClient服务调用
Object result = baseControllerFeign.reqBody(paramVo);
在k8s(或Istio)中,我们可以通过指定url=serviceName.namespace的方式来进行k8s间服务的调用,即无需通过name指定服务名称,在K8s集群中可直接通过serviceName.namespace进行服务间的调用,由K8s(或Istio)集群环境负责服务的路由与负载均衡;
示例类图如下:
通过FeignClient的调用方式,可以将Controller抽象出接口定义,服务端来实现Controller的具体实现逻辑,而客户端依赖Controller接口定义并以此进行服务调用(由OpenFeign客户端对Controller中SpringMVC的注解进行解析并转换为相应的Http调用),此种方式中Controller接口定义为客户端和服务端二者之间连接的桥梁,通过Maven多module的形式使得Controller接口定义得以在多个应用间进行传递复用,而在K8s(Istio)中可以通过指定url=serviceName.namespace/contextPath来进行服务间的请求调用,由K8s(或Istio)集群环境负责服务的路由与负载均衡;
问题:FeignClient接口定义并不是完全符合SpringMVC规范,可能会对原Controller接口进行调整方能完全适配;例如@SpringQueryMap注解的使用,以及@PathVariable需要对应到每个参数上,例如原Controller实现定义如下:
@RequestMapping(value = "/reqPath/{batteryCode}/{page}/{rows}", method = RequestMethod.POST)
@ResponseBody
public Object reqPath(ParamVo paramVo) {
logger.info("reqPath param:{}", JsonUtils.toJson(paramVo));
return MxHttpRespUtility.successResp();
}
但是FeignClient需要修改如下形式才可正确调用:
@RequestMapping(value = "/reqPath/{batteryCode}/{page}/{rows}", method = RequestMethod.POST)
@ResponseBody
Object reqPath(@PathVariable("batteryCode") String batteryCode, @PathVariable("page") Integer page, @PathVariable("rows") Integer rows);
以上只是简单的了解,若想真正像使用Feign进行K8s间服务调用,还需对spring-cloud-starter-openfeign源码进行分析与重构,删除不必要的依赖...