写在前面
源码 。
在前面的文章中我们实际上已经完成了优惠券模块微服务化的改造,但是其中还是有比较多可以优化和增强的地方,本文就先来对服务间的通信方式进行优化,具体就是使用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创建动态代理要使用的InvocationHandlerInvocationHandler 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<>());
}