springcloud之通过openfeign优化服务调用方式
写在前面
源码 。
在前面的文章中我们实际上已经完成了优惠券模块微服务化的改造,但是其中还是有比较多可以优化和增强的地方,本文就先来对服务间的通信方式进行优化,具体就是使用openfeign来替换调原来的webclient。下面我们就开始吧!
1:为什么要替换webclient
使用webclient进行服务间调用的方式可能如下:
webClientBuilder.build()
// 声明这是一个POST方法
.post()
// 声明服务名称和访问路径
.uri("http://coupon-calculation-serv/calculator/simulate")
// 传递请求参数的封装
.bodyValue(order)
.retrieve()
// 声明请求返回值的封装类型
.bodyToMono(SimulationResponse.class)
// 使用阻塞模式来获取结果
.block()
这段代码有如下的不足:
1:和业务代码耦合,如请求地址,请求方式这些其实和业务是没有任何关系的,不符合指责隔离的原则
2:每个接口调用都需要写类似的重复代码,编码的效率低
针对以上的问题,springcloud给出的解决方案是openfeign ,可以认为openfeign是一种rpc框架允许我们通过好像调用一个本地的方法一样来调用远端的服务。
2:实战改造
2.1:引入openfeign依赖
首先我们需要在coupon-customer-impl的pom中引入openfeign的基础依赖:
<!-- OpenFeign组件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2:定义服务的service
我们以调用template服务为例来进行改造,因此首先在coupon-customer-impl模块中定义如下的service:
@FeignClient(value = "coupon-template-serv-feign", path = "/template")
public interface TemplateService {
// 读取优惠券
@GetMapping("/getTemplate")
CouponTemplateInfo getTemplate(@RequestParam("id") Long id);
// 批量获取
@GetMapping("/getBatch")
Map<Long, CouponTemplateInfo> getTemplateInBatch(@RequestParam("ids") Collection<Long> ids);
}
在注解@FeignClient中定义了要访问的服务名称以及要web接口的基础路径这样就不用重复在方法上配置了
,通过注解@XxxMapping定义的接口的访问路径信息,通过方法的参数来定义入参信息,这样发起服务调用的完整信息就都全了。
2.3:改造接口调用
我们来修改接口/coupon-customer/simulateOrder 来执行试算,当前代码如下:
public SimulationResponse simulateOrderPrice(SimulationOrder order) {
...
return webClientBuilder.build().post()
// .uri("http://coupon-calculation-serv/calculator/simulate")
.uri("http://coupon-calculation-serv-feign/calculator/simulate")
.bodyValue(order)
.retrieve()
.bodyToMono(SimulationResponse.class)
.block();
}
修改为openfeign后如下:
@Autowired
private CalculationService calculationService;
public SimulationResponse simulateOrderPrice(SimulationOrder order) {
List<CouponInfo> couponInfos = Lists.newArrayList();
...
System.out.println("calculate by openfeign...");
return calculationService.simulate(order);
}
最后还需要在main函数上增加注解@EnableFeignClients(basePackages = { "dongshi.daddy" })
来设置需要扫描的openfeign服务接口所在的包路径。具体的大家可自行测试。效果是一样的。
3:openfeign原理分析
实战重要,但原理更重要,所以一起来看一波原理吧!
当我们在main上增加了@EnableFeignClients(basePackages = { "dongshi.daddy" })
注解后,就会扫描指定包路径下标注了@FeignClient
注解的接口,使用jdk的动态代理技术生成动态代理类,之后会将这个生成的动态代理类放到spring容器中,最后注入到需要的类中,这个过程如下:
看到这里不知道你有没有疑问,这个扫描包的过程是怎么开始的,其实秘密藏在@EnableFeignClients注解中,该注解如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
...
}
注意在注解上使用了@Import注解,spring会调用类FeignClientsRegistrar的registerBeanDefinitions方法,如下:
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
...
// 注册feign客户端(重要!!!)
registerFeignClients(metadata, registry);
}
registerFeignClients方法如下:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 最终存储所有openfeign的接口
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
...
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
...
}
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
...
// 注册feign客户端
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
registerFeignClients方法如下:
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
...
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
...
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
...
// 获取基于jdk的动态代理类
return factoryBean.getObject();
});
...
}
factoryBean.getObject方法最终调用到如下方法:
feign.ReflectiveFeign#newInstance
public <T> T newInstance(Target<T> target) {
// 解析openfeign方法为MethodHandler,作为方法代理
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
...
}
// 封装methodToHandler创建动态代理要使用的InvocationHandler
InvocationHandler handler = factory.create(target, methodToHandler);
// 生成动态代理
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
...
// 返回动态代理
return proxy;
}
到这里就成功获取动态代理类了。总结这个过程如下:
1:项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。
2:扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。
3:解析 FeignClient 注解:FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
4:构建动态代理对象:ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到 FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。
最后上述流程中解析接口中方法和注解信息为MethodHandler的过程在如下方法中完成:
// org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationOnMethod
// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
// 省略部分代码...
// 如果方法上没有使用RequestMapping注解,则不进行解析
// 其实GetMapping、PostMapping等注解都属于RequestMapping注解
if (!RequestMapping.class.isInstance(methodAnnotation)
&& !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}
// 获取RequestMapping注解实例
RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型
RequestMethod[] methods = methodMapping.method();
// 如果没有定义methods属性则默认当前方法是个GET方法
if (methods.length == 0) {
methods = new RequestMethod[] { RequestMethod.GET };
}
checkOne(method, methods, "method");
data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
// 解析Path属性,即方法上写明的请求路径
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// 如果path没有以斜杠开头,则补上/
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue, true);
if (data.template().decodeSlash() != decodeSlash) {
data.template().decodeSlash(decodeSlash);
}
}
}
// 解析RequestMapping中定义的produces属性
parseProduces(data, method, methodMapping);
// 解析RequestMapping中定义的consumer属性
parseConsumes(data, method, methodMapping);
// 解析RequestMapping中定义的headers属性
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<>());
}
写在后面
参考文章列表
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!